diff --git a/.agents/skills/c-04-retrieval-strategy-router/codegraphcontext-high-level-methods.md b/.agents/skills/c-04-retrieval-strategy-router/codegraphcontext-high-level-methods.md index 664f30aa..14b47cf0 100644 --- a/.agents/skills/c-04-retrieval-strategy-router/codegraphcontext-high-level-methods.md +++ b/.agents/skills/c-04-retrieval-strategy-router/codegraphcontext-high-level-methods.md @@ -34,7 +34,7 @@ and complexity signals. | Where is this symbol? | `cgc_symbol_search` | `find name ` | | What does this function/method call? | `cgc_callees` | `analyze calls ` | | Who calls this function/method? | `cgc_callers` | `analyze callers ` | -| Which files import this module string? | `cgc_dependencies` | `analyze dependencies ` | +| Which files import this module string? | `cgc_dependencies` | `analyze deps ` | | Which functions are most complex? | `cgc_complexity` | `analyze complexity [function]` | | Do I need the interactive graph view? | `cgc_visualize` | `visualize` | diff --git a/.agents/skills/c-09-git-worktree-manager/SKILL.md b/.agents/skills/c-09-git-worktree-manager/SKILL.md index 4ef741c4..e19c7902 100644 --- a/.agents/skills/c-09-git-worktree-manager/SKILL.md +++ b/.agents/skills/c-09-git-worktree-manager/SKILL.md @@ -30,6 +30,7 @@ worktree_closeout_preview(contract_path="", code_c worktree_closeout_apply(contract_path="", intent_note="", code_commit_message="", memory_commit_message="", ledger_commit_message="") worktree_integrate(contract_path="", strategy="ff-only") worktree_cleanup(contract_path="") +task_reopen(contract_path="", dry_run=true) lifecycle_finalize_task(contract_path="", task_doc_path="", master_doc_path="", subtask_number="", dry_run=true) ``` @@ -254,6 +255,20 @@ edge is finalized separately. Cleanup is idempotent. If the worktrees or merged branches are already gone, it reports the already-clean state instead of failing. If Git refuses to delete an unmerged branch, cleanup leaves that branch in place and reports it for developer review. +## Reopening A Completed Leaf + +Reopening reuses the EXACT same leaf id — never mint a suffixed leaf (`…-r1`). +`task_reopen(contract_path=…)` is a state reset, not a worktree creator: it refuses +anything but a fully landed leaf (closeout, integration, and cleanup completed, worktrees +gone), then resets the contract's review/closeout/integration state, clears the stale +lifecycle binding, marks `cleanup: reopened`, and puts the leaf's task document back to +`planning` (master index entry flipped, audit decision appended). Preview with +`dry_run=true` first. Afterwards: edit the doc's steps via `task_doc` (add, change, or +untick work), then run a NORMAL `worktree_start` with the same leaf id — it recreates the +worktrees off the current source tips, promotes/mints a fresh lifecycle, and restamps the +doc's `lifecycleId`, so doc, chat, and dashboard bindings hold by construction. Implementation +then proceeds as usual, including closeout → integrate → finalize. + ## Boundaries 1. The `c-09-git-worktree-manager` skill may create or reuse worktrees, root series contracts, and leaf enclosure contracts. diff --git a/.claude/skills/c-04-retrieval-strategy-router/codegraphcontext-high-level-methods.md b/.claude/skills/c-04-retrieval-strategy-router/codegraphcontext-high-level-methods.md index 664f30aa..14b47cf0 100644 --- a/.claude/skills/c-04-retrieval-strategy-router/codegraphcontext-high-level-methods.md +++ b/.claude/skills/c-04-retrieval-strategy-router/codegraphcontext-high-level-methods.md @@ -34,7 +34,7 @@ and complexity signals. | Where is this symbol? | `cgc_symbol_search` | `find name ` | | What does this function/method call? | `cgc_callees` | `analyze calls ` | | Who calls this function/method? | `cgc_callers` | `analyze callers ` | -| Which files import this module string? | `cgc_dependencies` | `analyze dependencies ` | +| Which files import this module string? | `cgc_dependencies` | `analyze deps ` | | Which functions are most complex? | `cgc_complexity` | `analyze complexity [function]` | | Do I need the interactive graph view? | `cgc_visualize` | `visualize` | diff --git a/.claude/skills/c-09-git-worktree-manager/SKILL.md b/.claude/skills/c-09-git-worktree-manager/SKILL.md index 4ef741c4..e19c7902 100644 --- a/.claude/skills/c-09-git-worktree-manager/SKILL.md +++ b/.claude/skills/c-09-git-worktree-manager/SKILL.md @@ -30,6 +30,7 @@ worktree_closeout_preview(contract_path="", code_c worktree_closeout_apply(contract_path="", intent_note="", code_commit_message="", memory_commit_message="", ledger_commit_message="") worktree_integrate(contract_path="", strategy="ff-only") worktree_cleanup(contract_path="") +task_reopen(contract_path="", dry_run=true) lifecycle_finalize_task(contract_path="", task_doc_path="", master_doc_path="", subtask_number="", dry_run=true) ``` @@ -254,6 +255,20 @@ edge is finalized separately. Cleanup is idempotent. If the worktrees or merged branches are already gone, it reports the already-clean state instead of failing. If Git refuses to delete an unmerged branch, cleanup leaves that branch in place and reports it for developer review. +## Reopening A Completed Leaf + +Reopening reuses the EXACT same leaf id — never mint a suffixed leaf (`…-r1`). +`task_reopen(contract_path=…)` is a state reset, not a worktree creator: it refuses +anything but a fully landed leaf (closeout, integration, and cleanup completed, worktrees +gone), then resets the contract's review/closeout/integration state, clears the stale +lifecycle binding, marks `cleanup: reopened`, and puts the leaf's task document back to +`planning` (master index entry flipped, audit decision appended). Preview with +`dry_run=true` first. Afterwards: edit the doc's steps via `task_doc` (add, change, or +untick work), then run a NORMAL `worktree_start` with the same leaf id — it recreates the +worktrees off the current source tips, promotes/mints a fresh lifecycle, and restamps the +doc's `lifecycleId`, so doc, chat, and dashboard bindings hold by construction. Implementation +then proceeds as usual, including closeout → integrate → finalize. + ## Boundaries 1. The `c-09-git-worktree-manager` skill may create or reuse worktrees, root series contracts, and leaf enclosure contracts. diff --git a/.codex/skills/c-04-retrieval-strategy-router/codegraphcontext-high-level-methods.md b/.codex/skills/c-04-retrieval-strategy-router/codegraphcontext-high-level-methods.md index 664f30aa..14b47cf0 100644 --- a/.codex/skills/c-04-retrieval-strategy-router/codegraphcontext-high-level-methods.md +++ b/.codex/skills/c-04-retrieval-strategy-router/codegraphcontext-high-level-methods.md @@ -34,7 +34,7 @@ and complexity signals. | Where is this symbol? | `cgc_symbol_search` | `find name ` | | What does this function/method call? | `cgc_callees` | `analyze calls ` | | Who calls this function/method? | `cgc_callers` | `analyze callers ` | -| Which files import this module string? | `cgc_dependencies` | `analyze dependencies ` | +| Which files import this module string? | `cgc_dependencies` | `analyze deps ` | | Which functions are most complex? | `cgc_complexity` | `analyze complexity [function]` | | Do I need the interactive graph view? | `cgc_visualize` | `visualize` | diff --git a/.codex/skills/c-09-git-worktree-manager/SKILL.md b/.codex/skills/c-09-git-worktree-manager/SKILL.md index 4ef741c4..e19c7902 100644 --- a/.codex/skills/c-09-git-worktree-manager/SKILL.md +++ b/.codex/skills/c-09-git-worktree-manager/SKILL.md @@ -30,6 +30,7 @@ worktree_closeout_preview(contract_path="", code_c worktree_closeout_apply(contract_path="", intent_note="", code_commit_message="", memory_commit_message="", ledger_commit_message="") worktree_integrate(contract_path="", strategy="ff-only") worktree_cleanup(contract_path="") +task_reopen(contract_path="", dry_run=true) lifecycle_finalize_task(contract_path="", task_doc_path="", master_doc_path="", subtask_number="", dry_run=true) ``` @@ -254,6 +255,20 @@ edge is finalized separately. Cleanup is idempotent. If the worktrees or merged branches are already gone, it reports the already-clean state instead of failing. If Git refuses to delete an unmerged branch, cleanup leaves that branch in place and reports it for developer review. +## Reopening A Completed Leaf + +Reopening reuses the EXACT same leaf id — never mint a suffixed leaf (`…-r1`). +`task_reopen(contract_path=…)` is a state reset, not a worktree creator: it refuses +anything but a fully landed leaf (closeout, integration, and cleanup completed, worktrees +gone), then resets the contract's review/closeout/integration state, clears the stale +lifecycle binding, marks `cleanup: reopened`, and puts the leaf's task document back to +`planning` (master index entry flipped, audit decision appended). Preview with +`dry_run=true` first. Afterwards: edit the doc's steps via `task_doc` (add, change, or +untick work), then run a NORMAL `worktree_start` with the same leaf id — it recreates the +worktrees off the current source tips, promotes/mints a fresh lifecycle, and restamps the +doc's `lifecycleId`, so doc, chat, and dashboard bindings hold by construction. Implementation +then proceeds as usual, including closeout → integrate → finalize. + ## Boundaries 1. The `c-09-git-worktree-manager` skill may create or reuse worktrees, root series contracts, and leaf enclosure contracts. diff --git a/.cursor/skills/c-04-retrieval-strategy-router/codegraphcontext-high-level-methods.md b/.cursor/skills/c-04-retrieval-strategy-router/codegraphcontext-high-level-methods.md index 664f30aa..14b47cf0 100644 --- a/.cursor/skills/c-04-retrieval-strategy-router/codegraphcontext-high-level-methods.md +++ b/.cursor/skills/c-04-retrieval-strategy-router/codegraphcontext-high-level-methods.md @@ -34,7 +34,7 @@ and complexity signals. | Where is this symbol? | `cgc_symbol_search` | `find name ` | | What does this function/method call? | `cgc_callees` | `analyze calls ` | | Who calls this function/method? | `cgc_callers` | `analyze callers ` | -| Which files import this module string? | `cgc_dependencies` | `analyze dependencies ` | +| Which files import this module string? | `cgc_dependencies` | `analyze deps ` | | Which functions are most complex? | `cgc_complexity` | `analyze complexity [function]` | | Do I need the interactive graph view? | `cgc_visualize` | `visualize` | diff --git a/.cursor/skills/c-09-git-worktree-manager/SKILL.md b/.cursor/skills/c-09-git-worktree-manager/SKILL.md index 4ef741c4..e19c7902 100644 --- a/.cursor/skills/c-09-git-worktree-manager/SKILL.md +++ b/.cursor/skills/c-09-git-worktree-manager/SKILL.md @@ -30,6 +30,7 @@ worktree_closeout_preview(contract_path="", code_c worktree_closeout_apply(contract_path="", intent_note="", code_commit_message="", memory_commit_message="", ledger_commit_message="") worktree_integrate(contract_path="", strategy="ff-only") worktree_cleanup(contract_path="") +task_reopen(contract_path="", dry_run=true) lifecycle_finalize_task(contract_path="", task_doc_path="", master_doc_path="", subtask_number="", dry_run=true) ``` @@ -254,6 +255,20 @@ edge is finalized separately. Cleanup is idempotent. If the worktrees or merged branches are already gone, it reports the already-clean state instead of failing. If Git refuses to delete an unmerged branch, cleanup leaves that branch in place and reports it for developer review. +## Reopening A Completed Leaf + +Reopening reuses the EXACT same leaf id — never mint a suffixed leaf (`…-r1`). +`task_reopen(contract_path=…)` is a state reset, not a worktree creator: it refuses +anything but a fully landed leaf (closeout, integration, and cleanup completed, worktrees +gone), then resets the contract's review/closeout/integration state, clears the stale +lifecycle binding, marks `cleanup: reopened`, and puts the leaf's task document back to +`planning` (master index entry flipped, audit decision appended). Preview with +`dry_run=true` first. Afterwards: edit the doc's steps via `task_doc` (add, change, or +untick work), then run a NORMAL `worktree_start` with the same leaf id — it recreates the +worktrees off the current source tips, promotes/mints a fresh lifecycle, and restamps the +doc's `lifecycleId`, so doc, chat, and dashboard bindings hold by construction. Implementation +then proceeds as usual, including closeout → integrate → finalize. + ## Boundaries 1. The `c-09-git-worktree-manager` skill may create or reuse worktrees, root series contracts, and leaf enclosure contracts. diff --git a/.github-vscode/skills/c-04-retrieval-strategy-router/codegraphcontext-high-level-methods.md b/.github-vscode/skills/c-04-retrieval-strategy-router/codegraphcontext-high-level-methods.md index 664f30aa..14b47cf0 100644 --- a/.github-vscode/skills/c-04-retrieval-strategy-router/codegraphcontext-high-level-methods.md +++ b/.github-vscode/skills/c-04-retrieval-strategy-router/codegraphcontext-high-level-methods.md @@ -34,7 +34,7 @@ and complexity signals. | Where is this symbol? | `cgc_symbol_search` | `find name ` | | What does this function/method call? | `cgc_callees` | `analyze calls ` | | Who calls this function/method? | `cgc_callers` | `analyze callers ` | -| Which files import this module string? | `cgc_dependencies` | `analyze dependencies ` | +| Which files import this module string? | `cgc_dependencies` | `analyze deps ` | | Which functions are most complex? | `cgc_complexity` | `analyze complexity [function]` | | Do I need the interactive graph view? | `cgc_visualize` | `visualize` | diff --git a/.github-vscode/skills/c-09-git-worktree-manager/SKILL.md b/.github-vscode/skills/c-09-git-worktree-manager/SKILL.md index 4ef741c4..e19c7902 100644 --- a/.github-vscode/skills/c-09-git-worktree-manager/SKILL.md +++ b/.github-vscode/skills/c-09-git-worktree-manager/SKILL.md @@ -30,6 +30,7 @@ worktree_closeout_preview(contract_path="", code_c worktree_closeout_apply(contract_path="", intent_note="", code_commit_message="", memory_commit_message="", ledger_commit_message="") worktree_integrate(contract_path="", strategy="ff-only") worktree_cleanup(contract_path="") +task_reopen(contract_path="", dry_run=true) lifecycle_finalize_task(contract_path="", task_doc_path="", master_doc_path="", subtask_number="", dry_run=true) ``` @@ -254,6 +255,20 @@ edge is finalized separately. Cleanup is idempotent. If the worktrees or merged branches are already gone, it reports the already-clean state instead of failing. If Git refuses to delete an unmerged branch, cleanup leaves that branch in place and reports it for developer review. +## Reopening A Completed Leaf + +Reopening reuses the EXACT same leaf id — never mint a suffixed leaf (`…-r1`). +`task_reopen(contract_path=…)` is a state reset, not a worktree creator: it refuses +anything but a fully landed leaf (closeout, integration, and cleanup completed, worktrees +gone), then resets the contract's review/closeout/integration state, clears the stale +lifecycle binding, marks `cleanup: reopened`, and puts the leaf's task document back to +`planning` (master index entry flipped, audit decision appended). Preview with +`dry_run=true` first. Afterwards: edit the doc's steps via `task_doc` (add, change, or +untick work), then run a NORMAL `worktree_start` with the same leaf id — it recreates the +worktrees off the current source tips, promotes/mints a fresh lifecycle, and restamps the +doc's `lifecycleId`, so doc, chat, and dashboard bindings hold by construction. Implementation +then proceeds as usual, including closeout → integrate → finalize. + ## Boundaries 1. The `c-09-git-worktree-manager` skill may create or reuse worktrees, root series contracts, and leaf enclosure contracts. diff --git a/.hermes/skills/c-04-retrieval-strategy-router/codegraphcontext-high-level-methods.md b/.hermes/skills/c-04-retrieval-strategy-router/codegraphcontext-high-level-methods.md index 664f30aa..14b47cf0 100644 --- a/.hermes/skills/c-04-retrieval-strategy-router/codegraphcontext-high-level-methods.md +++ b/.hermes/skills/c-04-retrieval-strategy-router/codegraphcontext-high-level-methods.md @@ -34,7 +34,7 @@ and complexity signals. | Where is this symbol? | `cgc_symbol_search` | `find name ` | | What does this function/method call? | `cgc_callees` | `analyze calls ` | | Who calls this function/method? | `cgc_callers` | `analyze callers ` | -| Which files import this module string? | `cgc_dependencies` | `analyze dependencies ` | +| Which files import this module string? | `cgc_dependencies` | `analyze deps ` | | Which functions are most complex? | `cgc_complexity` | `analyze complexity [function]` | | Do I need the interactive graph view? | `cgc_visualize` | `visualize` | diff --git a/.hermes/skills/c-09-git-worktree-manager/SKILL.md b/.hermes/skills/c-09-git-worktree-manager/SKILL.md index 4ef741c4..e19c7902 100644 --- a/.hermes/skills/c-09-git-worktree-manager/SKILL.md +++ b/.hermes/skills/c-09-git-worktree-manager/SKILL.md @@ -30,6 +30,7 @@ worktree_closeout_preview(contract_path="", code_c worktree_closeout_apply(contract_path="", intent_note="", code_commit_message="", memory_commit_message="", ledger_commit_message="") worktree_integrate(contract_path="", strategy="ff-only") worktree_cleanup(contract_path="") +task_reopen(contract_path="", dry_run=true) lifecycle_finalize_task(contract_path="", task_doc_path="", master_doc_path="", subtask_number="", dry_run=true) ``` @@ -254,6 +255,20 @@ edge is finalized separately. Cleanup is idempotent. If the worktrees or merged branches are already gone, it reports the already-clean state instead of failing. If Git refuses to delete an unmerged branch, cleanup leaves that branch in place and reports it for developer review. +## Reopening A Completed Leaf + +Reopening reuses the EXACT same leaf id — never mint a suffixed leaf (`…-r1`). +`task_reopen(contract_path=…)` is a state reset, not a worktree creator: it refuses +anything but a fully landed leaf (closeout, integration, and cleanup completed, worktrees +gone), then resets the contract's review/closeout/integration state, clears the stale +lifecycle binding, marks `cleanup: reopened`, and puts the leaf's task document back to +`planning` (master index entry flipped, audit decision appended). Preview with +`dry_run=true` first. Afterwards: edit the doc's steps via `task_doc` (add, change, or +untick work), then run a NORMAL `worktree_start` with the same leaf id — it recreates the +worktrees off the current source tips, promotes/mints a fresh lifecycle, and restamps the +doc's `lifecycleId`, so doc, chat, and dashboard bindings hold by construction. Implementation +then proceeds as usual, including closeout → integrate → finalize. + ## Boundaries 1. The `c-09-git-worktree-manager` skill may create or reuse worktrees, root series contracts, and leaf enclosure contracts. diff --git a/.openclaw/workspace/skills/c-04-retrieval-strategy-router/codegraphcontext-high-level-methods.md b/.openclaw/workspace/skills/c-04-retrieval-strategy-router/codegraphcontext-high-level-methods.md index 664f30aa..14b47cf0 100644 --- a/.openclaw/workspace/skills/c-04-retrieval-strategy-router/codegraphcontext-high-level-methods.md +++ b/.openclaw/workspace/skills/c-04-retrieval-strategy-router/codegraphcontext-high-level-methods.md @@ -34,7 +34,7 @@ and complexity signals. | Where is this symbol? | `cgc_symbol_search` | `find name ` | | What does this function/method call? | `cgc_callees` | `analyze calls ` | | Who calls this function/method? | `cgc_callers` | `analyze callers ` | -| Which files import this module string? | `cgc_dependencies` | `analyze dependencies ` | +| Which files import this module string? | `cgc_dependencies` | `analyze deps ` | | Which functions are most complex? | `cgc_complexity` | `analyze complexity [function]` | | Do I need the interactive graph view? | `cgc_visualize` | `visualize` | diff --git a/.openclaw/workspace/skills/c-09-git-worktree-manager/SKILL.md b/.openclaw/workspace/skills/c-09-git-worktree-manager/SKILL.md index 4ef741c4..e19c7902 100644 --- a/.openclaw/workspace/skills/c-09-git-worktree-manager/SKILL.md +++ b/.openclaw/workspace/skills/c-09-git-worktree-manager/SKILL.md @@ -30,6 +30,7 @@ worktree_closeout_preview(contract_path="", code_c worktree_closeout_apply(contract_path="", intent_note="", code_commit_message="", memory_commit_message="", ledger_commit_message="") worktree_integrate(contract_path="", strategy="ff-only") worktree_cleanup(contract_path="") +task_reopen(contract_path="", dry_run=true) lifecycle_finalize_task(contract_path="", task_doc_path="", master_doc_path="", subtask_number="", dry_run=true) ``` @@ -254,6 +255,20 @@ edge is finalized separately. Cleanup is idempotent. If the worktrees or merged branches are already gone, it reports the already-clean state instead of failing. If Git refuses to delete an unmerged branch, cleanup leaves that branch in place and reports it for developer review. +## Reopening A Completed Leaf + +Reopening reuses the EXACT same leaf id — never mint a suffixed leaf (`…-r1`). +`task_reopen(contract_path=…)` is a state reset, not a worktree creator: it refuses +anything but a fully landed leaf (closeout, integration, and cleanup completed, worktrees +gone), then resets the contract's review/closeout/integration state, clears the stale +lifecycle binding, marks `cleanup: reopened`, and puts the leaf's task document back to +`planning` (master index entry flipped, audit decision appended). Preview with +`dry_run=true` first. Afterwards: edit the doc's steps via `task_doc` (add, change, or +untick work), then run a NORMAL `worktree_start` with the same leaf id — it recreates the +worktrees off the current source tips, promotes/mints a fresh lifecycle, and restamps the +doc's `lifecycleId`, so doc, chat, and dashboard bindings hold by construction. Implementation +then proceeds as usual, including closeout → integrate → finalize. + ## Boundaries 1. The `c-09-git-worktree-manager` skill may create or reuse worktrees, root series contracts, and leaf enclosure contracts. diff --git a/.pi/skills/c-04-retrieval-strategy-router/codegraphcontext-high-level-methods.md b/.pi/skills/c-04-retrieval-strategy-router/codegraphcontext-high-level-methods.md index 664f30aa..14b47cf0 100644 --- a/.pi/skills/c-04-retrieval-strategy-router/codegraphcontext-high-level-methods.md +++ b/.pi/skills/c-04-retrieval-strategy-router/codegraphcontext-high-level-methods.md @@ -34,7 +34,7 @@ and complexity signals. | Where is this symbol? | `cgc_symbol_search` | `find name ` | | What does this function/method call? | `cgc_callees` | `analyze calls ` | | Who calls this function/method? | `cgc_callers` | `analyze callers ` | -| Which files import this module string? | `cgc_dependencies` | `analyze dependencies ` | +| Which files import this module string? | `cgc_dependencies` | `analyze deps ` | | Which functions are most complex? | `cgc_complexity` | `analyze complexity [function]` | | Do I need the interactive graph view? | `cgc_visualize` | `visualize` | diff --git a/.pi/skills/c-09-git-worktree-manager/SKILL.md b/.pi/skills/c-09-git-worktree-manager/SKILL.md index 4ef741c4..e19c7902 100644 --- a/.pi/skills/c-09-git-worktree-manager/SKILL.md +++ b/.pi/skills/c-09-git-worktree-manager/SKILL.md @@ -30,6 +30,7 @@ worktree_closeout_preview(contract_path="", code_c worktree_closeout_apply(contract_path="", intent_note="", code_commit_message="", memory_commit_message="", ledger_commit_message="") worktree_integrate(contract_path="", strategy="ff-only") worktree_cleanup(contract_path="") +task_reopen(contract_path="", dry_run=true) lifecycle_finalize_task(contract_path="", task_doc_path="", master_doc_path="", subtask_number="", dry_run=true) ``` @@ -254,6 +255,20 @@ edge is finalized separately. Cleanup is idempotent. If the worktrees or merged branches are already gone, it reports the already-clean state instead of failing. If Git refuses to delete an unmerged branch, cleanup leaves that branch in place and reports it for developer review. +## Reopening A Completed Leaf + +Reopening reuses the EXACT same leaf id — never mint a suffixed leaf (`…-r1`). +`task_reopen(contract_path=…)` is a state reset, not a worktree creator: it refuses +anything but a fully landed leaf (closeout, integration, and cleanup completed, worktrees +gone), then resets the contract's review/closeout/integration state, clears the stale +lifecycle binding, marks `cleanup: reopened`, and puts the leaf's task document back to +`planning` (master index entry flipped, audit decision appended). Preview with +`dry_run=true` first. Afterwards: edit the doc's steps via `task_doc` (add, change, or +untick work), then run a NORMAL `worktree_start` with the same leaf id — it recreates the +worktrees off the current source tips, promotes/mints a fresh lifecycle, and restamps the +doc's `lifecycleId`, so doc, chat, and dashboard bindings hold by construction. Implementation +then proceeds as usual, including closeout → integrate → finalize. + ## Boundaries 1. The `c-09-git-worktree-manager` skill may create or reuse worktrees, root series contracts, and leaf enclosure contracts. diff --git a/README.md b/README.md index fb8ea420..b428d0f1 100644 --- a/README.md +++ b/README.md @@ -198,9 +198,9 @@ ar-coordination/ ## Status -Agents Remember is at `2.9.3` and actively developed. The core path — by-path onboarding, drift checks, and approval-gated updates — is in real use and stable enough to rely on. The public contracts listed under [Stability](#stability) are held stable across minor releases and change only on a major bump; the internals beneath them and the optional semantic/relationship providers may still evolve, so pin a version and read the notes for your target version in [GitHub Releases](https://github.com/Foxfire1st/agents-remember/releases) — the repository's canonical changelog — before upgrading. The Claude Code path is the most exercised; other harnesses are supported but less battle-tested. +Agents Remember is at `3.0.0rc1` and actively developed. The core path — by-path onboarding, drift checks, and approval-gated updates — is in real use and stable enough to rely on. The public contracts listed under [Stability](#stability) are held stable across minor releases and change only on a major bump; the internals beneath them and the optional semantic/relationship providers may still evolve, so pin a version and read the notes for your target version in [GitHub Releases](https://github.com/Foxfire1st/agents-remember/releases) — the repository's canonical changelog — before upgrading. The Claude Code path is the most exercised; other harnesses are supported but less battle-tested. -Where the journey is going: the next arc makes the working session itself observable and steerable. The session job lifecycle is becoming a first-class, machine-readable entity — system-managed lifecycle state and events, durable approval gates, and a projection layer any frontend can consume — culminating in a browser cockpit ([#2](https://github.com/Foxfire1st/agents-remember/issues/2), [#43](https://github.com/Foxfire1st/agents-remember/issues/43)) for watching and acting on parallel agent sessions across repos. That direction is worth breaking changes: the architecture supports the dashboard, not the other way around, up to and including a 3.0 jump. +The 3.0 arc: the working session itself is now observable and steerable — a system-managed session job lifecycle with durable approval gates and an event/projection layer, served as the mission-control browser cockpit directly from the MCP package (`agents-remember dashboard`; [#2](https://github.com/Foxfire1st/agents-remember/issues/2), [#43](https://github.com/Foxfire1st/agents-remember/issues/43)). The `rc` tag means the cockpit surface is still settling toward the final 3.0.0 contract; the architecture beneath it is the one described above. ## Stability diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index 02b6fdae..0bc877fc 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -8,6 +8,19 @@ "name": "agents-remember-dashboard", "version": "0.0.0", "dependencies": { + "@codemirror/lang-css": "^6.3.1", + "@codemirror/lang-html": "^6.4.11", + "@codemirror/lang-javascript": "^6.2.5", + "@codemirror/lang-json": "^6.0.2", + "@codemirror/lang-markdown": "^6.5.0", + "@codemirror/lang-python": "^6.2.1", + "@codemirror/language": "^6.12.4", + "@codemirror/merge": "^6.12.2", + "@codemirror/state": "^6.7.0", + "@codemirror/view": "^6.43.4", + "@headless-tree/core": "^1.7.0", + "@headless-tree/react": "^1.7.0", + "@lezer/highlight": "^1.2.3", "@react-three/fiber": "^9.6.1", "@tanstack/react-virtual": "^3.14.4", "@testing-library/react": "^16.3.2", @@ -20,6 +33,7 @@ "react-aria-components": "^1.18.0", "react-dom": "^19.0.0", "react-markdown": "^10.1.0", + "react-resizable-panels": "^3.0.6", "remark-gfm": "^4.0.1", "three": "^0.184.0", "zustand": "^5.0.0" @@ -158,6 +172,160 @@ "sisteransi": "^1.0.5" } }, + "node_modules/@codemirror/autocomplete": { + "version": "6.20.3", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.3.tgz", + "integrity": "sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-css": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz", + "integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.2", + "@lezer/css": "^1.1.7" + } + }, + "node_modules/@codemirror/lang-html": { + "version": "6.4.11", + "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.11.tgz", + "integrity": "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/lang-css": "^6.0.0", + "@codemirror/lang-javascript": "^6.0.0", + "@codemirror/language": "^6.4.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/css": "^1.1.0", + "@lezer/html": "^1.3.12" + } + }, + "node_modules/@codemirror/lang-javascript": { + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.5.tgz", + "integrity": "sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.6.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/javascript": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-json": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.2.tgz", + "integrity": "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/json": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-markdown": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.5.0.tgz", + "integrity": "sha512-0K40bZ35jpHya6FriukbgaleaqzBLZfOh7HuzqbMxBXkbYMJDxfF39c23xOgxFezR+3G+tR2/Mup+Xk865OMvw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.7.1", + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.3.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.2.1", + "@lezer/markdown": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-python": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.2.1.tgz", + "integrity": "sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.3.2", + "@codemirror/language": "^6.8.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.1", + "@lezer/python": "^1.1.4" + } + }, + "node_modules/@codemirror/language": { + "version": "6.12.4", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.4.tgz", + "integrity": "sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.5.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.7.tgz", + "integrity": "sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.42.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/merge": { + "version": "6.12.2", + "resolved": "https://registry.npmjs.org/@codemirror/merge/-/merge-6.12.2.tgz", + "integrity": "sha512-V8JvyAPjHbPupqP7BeMcsdsYCbyPij74jxIbaIJDORI+VZzW44zFmon8bF+oxGWvOKhcRmkiUMXd8MxHr3YA2w==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/highlight": "^1.0.0", + "style-mod": "^4.1.0" + } + }, + "node_modules/@codemirror/state": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.7.0.tgz", + "integrity": "sha512-Zbl9NyscLMZkfXPQnNAIIAFftidrA1UbcJEIMp24C0Bukc2I5T8wJS0wsXYsnDOqCFJUeJ1BITGNs5CqPDSmSg==", + "license": "MIT", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.43.4", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.4.tgz", + "integrity": "sha512-YImu23iyKfncJzT7sRy+rEqEhSc8RhOHqDxwy4WzXRKJwYm6iwf/9OJk5ctCAdZ6yi2ZqaGEvmf55fSVqMDrgg==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.7.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, "node_modules/@csstools/color-helpers": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", @@ -962,6 +1130,29 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@headless-tree/core": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@headless-tree/core/-/core-1.7.0.tgz", + "integrity": "sha512-LxcX7LNepwfOPrZcs4PNfDwCzbi326uCAX5jYBfw94jI+pZQA7ANaE/No3LW0XFgcBcQtK/G2bInbVEhLF1C/Q==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/lukasbach" + } + }, + "node_modules/@headless-tree/react": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@headless-tree/react/-/react-1.7.0.tgz", + "integrity": "sha512-hcnG4mpTP98KVWZYqKsXNOoiv7ZQqZVM2jWwzEWtF+VDSz+1O/juMI+/Q0G4lZM+ez07/tmBBWjvZub/mLAUXQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/lukasbach" + }, + "peerDependencies": { + "@headless-tree/core": "*", + "react": "*", + "react-dom": "*" + } + }, "node_modules/@hono/node-server": { "version": "1.19.14", "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", @@ -1075,6 +1266,101 @@ "dev": true, "license": "MIT" }, + "node_modules/@lezer/common": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz", + "integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==", + "license": "MIT" + }, + "node_modules/@lezer/css": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.4.tgz", + "integrity": "sha512-N+tn9tej2hPvyKgHEApMOQfHczDJCwxrRFS3SPn9QjYN+uwHvEDnCgKRrb3mxDYxRS8sKMM8fhC3+lc04Abz5Q==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.0" + } + }, + "node_modules/@lezer/highlight": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", + "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.3.0" + } + }, + "node_modules/@lezer/html": { + "version": "1.3.13", + "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.13.tgz", + "integrity": "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/javascript": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz", + "integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.1.3", + "@lezer/lr": "^1.3.0" + } + }, + "node_modules/@lezer/json": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz", + "integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz", + "integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/markdown": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.6.4.tgz", + "integrity": "sha512-N0SxazMj4k65DBfaf1azqtMZd6u7MqluP84/NZnB/io8Td9aleFmAhz9hcbvSfsxT5tdYlJ5qgv5aMJGY4zEtA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.5.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@lezer/python": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.19.tgz", + "integrity": "sha512-MhQIURHRytsNzP/YXnqpYKW6la6voAH3kyplTOOiCdjyFY6cWWGFVmYVdHIPrElqSDf4iCDktQCockB9FxuhzQ==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.3.tgz", + "integrity": "sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==", + "license": "MIT" + }, "node_modules/@modelcontextprotocol/sdk": { "version": "1.29.0", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", @@ -3944,6 +4230,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/crelt": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.7.tgz", + "integrity": "sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==", + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -7752,6 +8044,16 @@ "react": ">=18" } }, + "node_modules/react-resizable-panels": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-3.0.6.tgz", + "integrity": "sha512-b3qKHQ3MLqOgSS+FRYKapNkJZf5EQzuf6+RLiq1/IlTHw99YrZ2NJZLk4hQIzTnnIkRg2LUqyVinu6YWWpUYew==", + "license": "MIT", + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/react-stately": { "version": "3.47.0", "resolved": "https://registry.npmjs.org/react-stately/-/react-stately-3.47.0.tgz", @@ -8324,6 +8626,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-mod": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", + "license": "MIT" + }, "node_modules/style-to-js": { "version": "1.1.21", "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", @@ -9119,6 +9427,12 @@ } } }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", diff --git a/dashboard/package.json b/dashboard/package.json index 0efe8be1..b7310193 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -16,6 +16,19 @@ "e2e": "playwright test" }, "dependencies": { + "@codemirror/lang-css": "^6.3.1", + "@codemirror/lang-html": "^6.4.11", + "@codemirror/lang-javascript": "^6.2.5", + "@codemirror/lang-json": "^6.0.2", + "@codemirror/lang-markdown": "^6.5.0", + "@codemirror/lang-python": "^6.2.1", + "@codemirror/language": "^6.12.4", + "@codemirror/merge": "^6.12.2", + "@codemirror/state": "^6.7.0", + "@codemirror/view": "^6.43.4", + "@headless-tree/core": "^1.7.0", + "@headless-tree/react": "^1.7.0", + "@lezer/highlight": "^1.2.3", "@react-three/fiber": "^9.6.1", "@tanstack/react-virtual": "^3.14.4", "@testing-library/react": "^16.3.2", @@ -28,6 +41,7 @@ "react-aria-components": "^1.18.0", "react-dom": "^19.0.0", "react-markdown": "^10.1.0", + "react-resizable-panels": "^3.0.6", "remark-gfm": "^4.0.1", "three": "^0.184.0", "zustand": "^5.0.0" diff --git a/dashboard/public/assets/sc2-siege-tank-boomerang.mp4 b/dashboard/public/assets/sc2-siege-tank-boomerang.mp4 new file mode 100644 index 00000000..74ab4152 Binary files /dev/null and b/dashboard/public/assets/sc2-siege-tank-boomerang.mp4 differ diff --git a/dashboard/public/assets/sc2-siegetank-blueprint-video.mp4 b/dashboard/public/assets/sc2-siegetank-blueprint-video.mp4 deleted file mode 100644 index f7adf06b..00000000 Binary files a/dashboard/public/assets/sc2-siegetank-blueprint-video.mp4 and /dev/null differ diff --git a/dashboard/src/cockpit/Cockpit.test.tsx b/dashboard/src/cockpit/Cockpit.test.tsx index 2dea18fe..976c9b6d 100644 --- a/dashboard/src/cockpit/Cockpit.test.tsx +++ b/dashboard/src/cockpit/Cockpit.test.tsx @@ -1,8 +1,10 @@ import { cleanup, fireEvent, render } from "@testing-library/react"; -import { afterEach, describe, expect, it } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { sessionStore } from "../data/sessions"; import { dashboardStore } from "../data/store"; import { GALLERY } from "../dev/fixtures"; +import type { LifecycleProjection, TaskDocNode, WorkspaceProjection } from "../types/projection"; import { CockpitShell } from "./Cockpit"; function seed(stateName: string) { @@ -11,7 +13,110 @@ function seed(stateName: string) { dashboardStore.getState().applySnapshot(fixture.projection); } -afterEach(cleanup); +// Mock the lazy Terminal so toggling the rail to chat never pulls xterm (a canvas probe) into jsdom. +vi.mock("../panels/Terminal", () => ({ + Terminal: ({ sessionId }: { sessionId: string }) =>
, +})); + +function taskDoc(over: Partial & Pick): TaskDocNode { + return { + lifecycleId: "ROOT", + repository: "repo-a", + title: "doc", + status: "inProgress", + createdAt: "2026-06-20T09:00:00+00:00", + stepsDone: 0, + stepsTotal: 0, + steps: [], + objective: "", + requirements: [], + codeExamples: [], + decisions: [], + openQuestions: [], + references: [], + subTasks: [], + sections: [], + ...over, + } as TaskDocNode; +} + +// A lifecycle-bound master with one authored, drillable leaf — the drilled-leaf fixture for fix 1. +function seedDrillableMaster() { + const lc: LifecycleProjection = { + id: "ROOT", + state: "running", + phase: "build", + fleeting: false, + repoId: "repo-a", + tokens: 0, + startedAt: "2026-06-20T09:00:00+00:00", + lastEventTs: "2026-06-20T09:00:30+00:00", + inferred: false, + actions: [], + tokenSeries: [], + }; + const master = taskDoc({ + id: "master-x", + kind: "master", + title: "Ops Master", + docPath: "/tasks/repo-a/ops/task.json", + objective: "Master objective.", + subTasks: [ + { + number: "1", + name: "Leaf One", + file: "01_leaf.md", + status: "inProgress", + scope: "", + createdAt: "2026-06-20T09:00:00+00:00", + }, + ], + }); + const leaf = taskDoc({ + id: "leaf-one", + kind: "subTask", + title: "Leaf One", + docPath: "/tasks/repo-a/ops/01_leaf.json", + objective: "Leaf objective.", + }); + const projection: WorkspaceProjection = { + version: 2, + generatedAt: "2026-06-20T09:01:00+00:00", + lifecycles: [lc], + enclosures: [], + providers: [], + activeWorktreeGroups: [], + metrics: { + lifecycleCount: 1, + runningCount: 1, + blockedCount: 0, + pausedCount: 0, + totalTokens: 0, + stalenessHistogram: {}, + }, + analytics: { + driftSnapshots: [], + stalestSidecars: [], + setupSummaries: [], + setupProgress: [], + routeCoverage: [], + toolReports: [], + ledgers: [], + taskDocuments: [master, leaf], + series: [], + attentionQueue: [], + engineProcesses: [], + }, + }; + dashboardStore.getState().applySnapshot(projection); +} + +afterEach(() => { + cleanup(); + sessionStore.setState({ sessions: [], activeId: null, count: 0 }); + dashboardStore.getState().reset(); + window.localStorage.clear(); // the rail toggle now persists its choice; isolate it between tests +}); describe("CockpitShell full-bleed machine-map views (5f S1)", () => { it("rails the Operations view but goes full-bleed (no rails) for the Engine Room", () => { @@ -46,6 +151,115 @@ describe("CockpitShell full-bleed machine-map views (5f S1)", () => { }); }); +describe("right-rail River⇄Chat toggle (L5 S2)", () => { + it("swaps the rail--right content between the Event River and the single-instance chat", () => { + seed("engine-fleet"); + const { container, getByTestId } = render(); + + const railRight = container.querySelector(".rail--right"); + expect(railRight).not.toBeNull(); + // Default = the Event River; the chat surface is not mounted. + expect(railRight?.querySelector('[data-testid="event-river"]')).not.toBeNull(); + expect(railRight?.querySelector('[data-testid="rail-chat"]')).toBeNull(); + + // Toggle to Chat: the river is gone, the single-instance chat is mounted in its place. + fireEvent.click(getByTestId("rail-toggle-chat")); + expect(railRight?.querySelector('[data-testid="rail-chat"]')).not.toBeNull(); + expect(railRight?.querySelector('[data-testid="event-river"]')).toBeNull(); + + // Toggle back to River restores it. + fireEvent.click(getByTestId("rail-toggle-river")); + expect(railRight?.querySelector('[data-testid="event-river"]')).not.toBeNull(); + expect(railRight?.querySelector('[data-testid="rail-chat"]')).toBeNull(); + }); + + it("remembers the rail choice across a window refresh (localStorage)", () => { + seed("engine-fleet"); + const first = render(); + // Default = River, then switch to Chat. + expect(first.container.querySelector('.rail--right [data-testid="event-river"]')).not.toBeNull(); + fireEvent.click(first.getByTestId("rail-toggle-chat")); + expect(window.localStorage.getItem("cockpit.rail-chat")).toBe("1"); + first.unmount(); + + // A fresh mount (the window refresh) restores Chat from localStorage — the river is not shown. + const second = render(); + const railRight = second.container.querySelector(".rail--right"); + expect(railRight?.querySelector('[data-testid="rail-chat"]')).not.toBeNull(); + expect(railRight?.querySelector('[data-testid="event-river"]')).toBeNull(); + }); +}); + +describe("Operations rails are resizable + persisted", () => { + it("renders a resize gutter on each rail and applies the persisted widths to the grid", () => { + window.localStorage.setItem("cockpit.rail-left-w", "430"); + window.localStorage.setItem("cockpit.rail-right-w", "250"); + seed("engine-fleet"); + const { container, getByTestId } = render(); + + // Each rail owns a drag gutter, and the railed grid uses the stored widths (centre takes the rest). + expect(getByTestId("rail-resize-left")).not.toBeNull(); + expect(getByTestId("rail-resize-right")).not.toBeNull(); + const body = container.querySelector(".shell__body") as HTMLElement; + expect(body.style.gridTemplateColumns).toBe("430px minmax(380px, 1fr) 250px"); + }); + + it("nudges a rail width with the keyboard and persists the new width", () => { + seed("engine-fleet"); // no stored width -> default 340 + const { getByTestId } = render(); + + fireEvent.keyDown(getByTestId("rail-resize-left"), { key: "ArrowRight" }); + expect(window.localStorage.getItem("cockpit.rail-left-w")).toBe("364"); // 340 + 24 + + // The right rail's gutter is mirror-imaged: ArrowLeft grows it. + fireEvent.keyDown(getByTestId("rail-resize-right"), { key: "ArrowLeft" }); + expect(window.localStorage.getItem("cockpit.rail-right-w")).toBe("324"); // 300 + 24 + }); +}); + +describe("rail chat keys by the drilled leaf, not the master (L5 fix 1)", () => { + it("keys the rail chat by the leaf once a master's sub-task is drilled open", () => { + seedDrillableMaster(); + const { getByText, getByTestId } = render(); + + // Select the master, then toggle the rail to the chat surface. + fireEvent.click(getByText("Ops Master")); + fireEvent.click(getByTestId("rail-toggle-chat")); + + // Master overview shown (no leaf drilled): the rail is NOT blocked — it offers the + // create-from-anywhere empty state, and the heading carries no leaf id yet (not the master's). + expect(getByTestId("rail-chat-empty")).not.toBeNull(); + expect(getByTestId("rail-chat-heading").textContent).not.toContain("master-x"); + + // Drill into the master's sub-task → the rail keys by THAT leaf id, not the master. + fireEvent.click(getByTestId("subtask-open-1")); + const heading = getByTestId("rail-chat-heading"); + expect(heading.textContent).toContain("leaf-one"); + expect(heading.textContent).not.toContain("master-x"); + }); +}); + +describe("Operations drill survives a view switch (DetailPanel mount preservation)", () => { + it("keeps the drilled sub-task open after switching to another tab and back", () => { + seedDrillableMaster(); + const { getByText, getByRole, getByTestId, queryByTestId } = render(); + + // Select the master, then drill into its sub-task → the leaf reader (a breadcrumb back to the + // master) replaces the sub-task index. + fireEvent.click(getByText("Ops Master")); + fireEvent.click(getByTestId("subtask-open-1")); + expect(getByTestId("series-breadcrumb")).not.toBeNull(); + expect(queryByTestId("subtask-open-1")).toBeNull(); // we're in the reader, not the index + + // Leave Operations for another tab, then come back: the drill is preserved (the panel was hidden, + // not unmounted), so it does NOT reset to the master overview. + fireEvent.click(getByRole("radio", { name: "Memory" })); + fireEvent.click(getByRole("radio", { name: "Operations" })); + expect(getByTestId("series-breadcrumb")).not.toBeNull(); + expect(queryByTestId("subtask-open-1")).toBeNull(); + }); +}); + describe("Chats persistence across view switches (6e hardening)", () => { it("keeps mounted (hidden) on other views and shows the same node on Chats", () => { seed("engine-fleet"); diff --git a/dashboard/src/cockpit/Cockpit.tsx b/dashboard/src/cockpit/Cockpit.tsx index 2e54be6b..8bd6bf47 100644 --- a/dashboard/src/cockpit/Cockpit.tsx +++ b/dashboard/src/cockpit/Cockpit.tsx @@ -1,4 +1,9 @@ -import { useEffect, useState } from "react"; +import { + useEffect, + useState, + type KeyboardEvent as ReactKeyboardEvent, + type PointerEvent as ReactPointerEvent, +} from "react"; import { motion } from "motion/react"; import { css, cva, cx } from "../../styled-system/css"; @@ -6,19 +11,33 @@ import { selectQueue } from "../data/selectors"; import type { ConnState } from "../data/store"; import { dashboardStore, useDashboard } from "../data/store"; import { connectEvents, connectState } from "../data/stream"; -import { lifecycleIdForSelection, lifecycleSelectionKey } from "../data/taskIdentity"; +import { + lifecycleIdForSelection, + lifecycleSelectionKey, + masterFolderForSelection, +} from "../data/taskIdentity"; import { ModeBar } from "../grammar/ModeBar"; import { AttentionQueue } from "../panels/AttentionQueue"; import { Chats } from "../panels/Chats"; +import { ChangeSetViewer, type ChangeSetTarget } from "../panels/changeset/ChangeSetViewer"; import { DetailPanel } from "../panels/DetailPanel"; import { EngineRoom } from "../panels/EngineRoom"; import { useShouldAnimate } from "../panels/engine-room/useShouldAnimate"; import { EventRiver } from "../panels/EventRiver"; +import { FileViewer } from "../panels/file-viewer/FileViewer"; import { Hangar } from "../panels/Hangar"; import { HighlightComposer } from "../panels/HighlightComposer"; import { LifecycleList } from "../panels/LifecycleList"; import { MemoryMirror } from "../panels/MemoryMirror"; +import { RailChat } from "../panels/RailChat"; +import { usePersistedFlag, usePersistedNumber } from "../panels/file-viewer/usePersistedFlag"; import { Topology } from "../panels/Topology"; +import type { EngineProcessNode, TaskDocNode } from "../types/projection"; + +// A stable empty array so the `analytics?.taskDocuments ?? …` selector never returns a fresh reference +// (which would churn the zustand snapshot and re-render every tick). +const EMPTY_TASK_DOCS: TaskDocNode[] = []; +const EMPTY_ENGINE_PROCESSES: EngineProcessNode[] = []; // The cockpit shell (model C, slice 5c): persistent command chrome that never hides the alarms — // a top status bar, a left rail (attention queue + lifecycle list = the master-caution, always @@ -28,10 +47,11 @@ import { Topology } from "../panels/Topology"; // Slice 5f S1 (§4.1): the "machine map" views (Engine Room / Topology) and the Chats terminal // (slice 6e) drop the rails and span the full body width; the top-bar caution stays visible so an // alarm is never hidden. -type View = "operations" | "engine" | "memory" | "topology" | "hangar" | "chats"; +type View = "operations" | "files" | "engine" | "memory" | "topology" | "hangar" | "chats"; const VIEWS: { id: View; label: string }[] = [ { id: "operations", label: "Operations" }, + { id: "files", label: "File Viewer" }, { id: "engine", label: "Engine Room" }, { id: "memory", label: "Memory" }, { id: "topology", label: "Topology" }, @@ -100,6 +120,37 @@ const effectsToggle = cva({ }, }, }); +// The right-rail River⇄Chat switch (slice L5): a small two-segment control sitting above the rail +// content. React-state driven (railView) so the rail swaps the Event River for the single-instance chat. +const railToggle = css({ + display: "flex", + flexShrink: 0, + gap: "0.25rem", + paddingBottom: "0.1rem", +}); +const railToggleButton = cva({ + base: { + flex: "1", + font: "inherit", + fontSize: "0.7rem", + letterSpacing: "0.06em", + paddingBlock: "0.12rem", + borderRadius: "2px", + borderWidth: "1px", + borderStyle: "solid", + cursor: "pointer", + background: "transparent", + transition: "color 0.15s ease, border-color 0.15s ease", + _hover: { borderColor: "muted" }, + _focusVisible: { outline: "1px solid token(colors.amber)", outlineOffset: "1px" }, + }, + variants: { + on: { + true: { color: "amber", borderColor: "amber" }, + false: { color: "muted", borderColor: "grid" }, + }, + }, +}); const caution = cva({ base: { letterSpacing: "0.06em", color: "muted" }, variants: { @@ -141,6 +192,7 @@ const bodyGrid = cva({ }, }); const rail = css({ + position: "relative", // anchors the absolutely-positioned drag handle on the rail's inner edge display: "flex", flexDirection: "column", gap: "0.7rem", @@ -148,6 +200,85 @@ const rail = css({ minHeight: "0", overflow: "hidden", // the rail itself does not scroll — each panel scrolls on its own }); +// Operations rails are draggable (like the File Viewer's split): each rail owns a thin gutter on its +// inner edge that drags its pixel width, persisted so the layout survives a reload. Bounds keep a rail +// from collapsing to nothing or eating the centre column. The handle sits flush inside the rail edge so +// the rail's own `overflow:hidden` never clips it. +const RAIL_MIN = 220; +const RAIL_MAX = 560; +const RAIL_STEP = 24; // keyboard nudge per ArrowLeft/ArrowRight +const railHandle = cva({ + base: { + position: "absolute", + top: "0", + bottom: "0", + width: "7px", + zIndex: "3", + cursor: "col-resize", + background: "transparent", + transition: "background 0.15s ease", + _hover: { background: "amber" }, + _focusVisible: { outline: "1px solid token(colors.amber)", outlineOffset: "-1px" }, + }, + variants: { + side: { + left: { right: "0" }, // left rail: gutter on its right (centre-facing) edge + right: { left: "0" }, // right rail: gutter on its left (centre-facing) edge + }, + }, +}); + +function clampRail(n: number): number { + return Math.max(RAIL_MIN, Math.min(RAIL_MAX, Math.round(n))); +} + +// One rail's resize gutter. Pointer drag adjusts the rail width live (left rail grows rightward, right +// rail grows leftward); Arrow keys nudge it. The new width flows straight to the persisted state so it +// both re-lays-out and survives a reload. +function RailResizeHandle({ + side, + width, + onResize, +}: { + side: "left" | "right"; + width: number; + onResize: (next: number) => void; +}) { + const onPointerDown = (event: ReactPointerEvent) => { + event.preventDefault(); + const startX = event.clientX; + const startWidth = width; + event.currentTarget.setPointerCapture(event.pointerId); + const move = (moveEvent: globalThis.PointerEvent) => { + const delta = moveEvent.clientX - startX; + onResize(clampRail(side === "left" ? startWidth + delta : startWidth - delta)); + }; + const up = () => { + window.removeEventListener("pointermove", move); + window.removeEventListener("pointerup", up); + }; + window.addEventListener("pointermove", move); + window.addEventListener("pointerup", up); + }; + const onKeyDown = (event: ReactKeyboardEvent) => { + const dir = event.key === "ArrowRight" ? 1 : event.key === "ArrowLeft" ? -1 : 0; + if (!dir) return; + event.preventDefault(); + onResize(clampRail(width + (side === "left" ? dir : -dir) * RAIL_STEP)); + }; + return ( +
+ ); +} const viewport = css({ display: "flex", flexDirection: "column", @@ -166,6 +297,15 @@ const chatsLayer = css({ minHeight: "0", minWidth: "0", }); +// The File Viewer (slice L2) is kept mounted across view switches too (hidden via display, never +// unmounted) so its repo/scope selection, open file, expanded trees, and view-mode survive a tab +// switch instead of resetting — same rationale as `chatsLayer`, and the same layout, so it reuses it. +const filesLayer = chatsLayer; +// Operations' DetailPanel is kept mounted across view switches for the SAME reason: its drill state +// (the opened sub-task `openSlug`) lives inside DetailPanel, so unmounting it on a tab switch reset the +// view back to the master overview on return. Hidden-not-unmounted preserves the drilled leaf (and the +// rail's reported leaf key with it). Same layout as the other persistent layers, so it reuses it. +const operationsLayer = chatsLayer; // Cockpit wires the live SSE streams, then renders the presentational shell. The shell is split // out so the dev gallery (/dev/bench) renders the exact same surface against fixture state. @@ -186,18 +326,43 @@ export function Cockpit() { export function CockpitShell() { const [view, setView] = useState("operations"); const [selectedId, setSelectedId] = useState(null); + // The Change-Set Viewer (L4) is a task-scoped TAKEOVER: when set, it replaces the railed body + // full-bleed; the screen's back link clears it, restoring the rails + Operations. A mode-bar + // switch or an open() also clears it (the takeover is transient, not a standing tab). + const [changeSet, setChangeSet] = useState(null); + // The right rail toggles between the Event River (default) and the single-instance leaf chat (L5). + // Persisted to localStorage (same pattern as the effects toggle) so the choice survives a window refresh. + const [chatRail, setChatRail] = usePersistedFlag("cockpit.rail-chat", false); + const railView: "river" | "chat" = chatRail ? "chat" : "river"; + const setRailView = (next: "river" | "chat") => setChatRail(next === "chat"); + // Operations rail widths (px), draggable + persisted. They drive the railed grid's outer columns; the + // centre takes the rest. Defaults roughly match the old fixed fr layout. + const [leftRailWidth, setLeftRailWidth] = usePersistedNumber("cockpit.rail-left-w", 340); + const [rightRailWidth, setRightRailWidth] = usePersistedNumber("cockpit.rail-right-w", 300); const animate = useShouldAnimate(); const selectedLifecycleId = useDashboard((s) => lifecycleIdForSelection(selectedId, s.lifecycles, s.analytics), ); + // The leaf the detail panel is actually SHOWING (a drilled sub-task or a directly-opened leaf doc), + // reported up from DetailPanel — its durable QUALIFIED LEAF ID, so the rail chat + "attach to leaf" + // key by the real leaf, not the master/series (L5 fix 1). Lifted here so it survives DetailPanel + // unmount (a full-bleed view switch) and reaches both the rail and the Chats page. + const [viewedLeafKey, setViewedLeafKey] = useState(undefined); + const taskDocuments = useDashboard((s) => s.analytics?.taskDocuments ?? EMPTY_TASK_DOCS); + const engineProcesses = useDashboard((s) => s.analytics?.engineProcesses ?? EMPTY_ENGINE_PROCESSES); + // The master folder of the current selection — pre-drills the leaf-attach picker to the task in context. + const contextMaster = useDashboard((s) => + masterFolderForSelection(selectedId, s.lifecycles, s.analytics), + ); // The machine-map views + the Chats terminal span full width: the rails hide and the view's own // layout breathes. - const fullBleed = view === "engine" || view === "topology" || view === "chats"; + const fullBleed = view === "files" || view === "engine" || view === "topology" || view === "chats"; // Open a node AND surface it in Operations: the attention queue / topology / hangar all jump // into the detail view, so a cross-view click lands where you can inspect it. const open = (id: string) => { + setChangeSet(null); // leaving the change-set takeover for a selected node setSelectedId( id.startsWith("taskdoc:") || id.startsWith("series:") || id.startsWith("lifecycle:") ? id @@ -206,6 +371,12 @@ export function CockpitShell() { setView("operations"); }; + // Mode-bar switches exit the takeover too (it is not one of the standing views). + const changeView = (next: View) => { + setChangeSet(null); + setView(next); + }; + // Gated fade-in when the rails return (reduced-motion / data-effects=off → no tween). Leaving to // a full-bleed view unmounts them for a clean expand; the determinism path stays snapshot-stable. const railEnter = animate ? { initial: { opacity: 0 }, animate: { opacity: 1 } } : {}; @@ -214,7 +385,29 @@ export function CockpitShell() {