Compare commits
86 Commits
1de5a9aac4
...
7f1518382c
| Author | SHA1 | Date | |
|---|---|---|---|
| 7f1518382c | |||
| 55f203710b | |||
| 839b06cbaa | |||
| 6756cc3a69 | |||
| c7d6bb84e5 | |||
| 40d58a7090 | |||
| f216601aba | |||
| 42789b471c | |||
| c1f3ba6b89 | |||
| 3c0cf73141 | |||
| fd9613222e | |||
| df6d7ee9c3 | |||
| ccf43f8466 | |||
| 17c2f38a8f | |||
| a6cc7e6629 | |||
| c049f9ba6d | |||
| f952c1cfb4 | |||
| e592496c61 | |||
| a0c1408279 | |||
| abbc8e16f0 | |||
| c0fb243760 | |||
| 28616777af | |||
| e57e410c4d | |||
| 550e0159ed | |||
| 3d662bc40e | |||
| f265098eed | |||
| 9aa6e7d1ab | |||
| 43f273de03 | |||
| 9e8d77fc70 | |||
| 716eb053d1 | |||
| 8f3f5bff2c | |||
| 3064c984b5 | |||
| 24a5bba852 | |||
| 1cee15479d | |||
| 30a07b7f36 | |||
| c57d13e61b | |||
| 24d6a62a54 | |||
| f2f74a8aab | |||
| c73690b38c | |||
| 0dd8d268a6 | |||
| 446134020f | |||
| 7a97a5fe9d | |||
| 4eefc0f87e | |||
| b660e790c6 | |||
| 7e8e03d4a1 | |||
| 994ee5d918 | |||
| 2f3f54b01a | |||
| c119c426d3 | |||
| e0f0f3c8b3 | |||
| f7141b85e0 | |||
| 489059019d | |||
| 3b4182cd08 | |||
| 32e93fe075 | |||
| 3a35968338 | |||
| 393cde9259 | |||
| 52683a5646 | |||
| 176e50450b | |||
| 5db02079d0 | |||
| 7f8f8ae656 | |||
| 9717d64458 | |||
| 1b7de88a59 | |||
| 7f386f749e | |||
| f662ada9f6 | |||
| 198708866a | |||
| 37c7f5b79c | |||
| ab85388122 | |||
| 7463f8ec53 | |||
| eec805287b | |||
| 733aa96c87 | |||
| e5c95acd33 | |||
| e87b14fa1e | |||
| 2cc0ea9f31 | |||
| 5fcdd2a3ed | |||
| dbc0ed2673 | |||
| 02f61ca02f | |||
| ff7814e3c6 | |||
| 074105c37c | |||
| ffd81de815 | |||
| 4ce4ee6532 | |||
| fe11d3691c | |||
| 5cca1692c9 | |||
| 609c2c4b42 | |||
| 32f34f2fab | |||
| 5f59b0bf57 | |||
| 66b7b97839 | |||
| 9a3693d2f0 |
|
|
@ -1,30 +0,0 @@
|
|||
[scope]
|
||||
allow = [
|
||||
{ target = ".", permission = "write", recursive = true },
|
||||
]
|
||||
|
||||
[session]
|
||||
record_event_trace = true
|
||||
|
||||
[worker]
|
||||
reasoning = "high"
|
||||
|
||||
[model]
|
||||
ref = "codex-oauth/gpt-5.5"
|
||||
|
||||
[compaction]
|
||||
compact_threshold = 200000
|
||||
compact_request_threshold = 240000
|
||||
compact_worker_max_input_tokens = 100000
|
||||
|
||||
[memory]
|
||||
extract_threshold = 50000
|
||||
|
||||
consolidation_threshold_files = 5
|
||||
consolidation_threshold_bytes = 50000
|
||||
|
||||
[web]
|
||||
enabled = true
|
||||
[web.search]
|
||||
provider = "brave"
|
||||
api_key_env = "BRAVE_SEARCH_API_KEY"
|
||||
|
|
@ -1,3 +1,23 @@
|
|||
[backend]
|
||||
provider = "builtin:yoi_local"
|
||||
root = ".yoi/tickets"
|
||||
|
||||
[roles.intake]
|
||||
profile = "builtin:default"
|
||||
workflow = "ticket-intake-workflow"
|
||||
|
||||
[roles.orchestrator]
|
||||
profile = "builtin:default"
|
||||
workflow = "ticket-orchestrator-routing"
|
||||
|
||||
[roles.coder]
|
||||
profile = "builtin:default"
|
||||
workflow = "multi-agent-workflow"
|
||||
|
||||
[roles.reviewer]
|
||||
profile = "builtin:default"
|
||||
workflow = "multi-agent-workflow"
|
||||
|
||||
[roles.investigator]
|
||||
profile = "builtin:default"
|
||||
workflow = "ticket-preflight-workflow"
|
||||
|
|
|
|||
|
|
@ -2,14 +2,15 @@
|
|||
id: 20260527-000017-tui-spawned-pod-panel
|
||||
slug: tui-spawned-pod-panel
|
||||
title: TUI: spawned child Pod の一覧と一時 attach
|
||||
status: pending
|
||||
status: closed
|
||||
kind: task
|
||||
priority: P2
|
||||
labels: [migrated]
|
||||
created_at: 2026-05-27T00:00:17Z
|
||||
updated_at: 2026-06-05T04:03:38Z
|
||||
updated_at: 2026-06-07T03:14:39Z
|
||||
assignee: null
|
||||
legacy_ticket: tickets/tui-spawned-pod-panel.md
|
||||
workflow_state: done
|
||||
---
|
||||
|
||||
## Migration reference
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
Closed as intentionally not planned.
|
||||
|
||||
The old migrated spawned-Pod panel idea has been superseded by the workspace panel, Pod list/open/attach behavior, Ticket role launching, and the local role session registry. The remaining direction is not to revive this standalone spawned-child panel ticket. Future panel work should be tracked through the newer workspace panel / orchestration tickets.
|
||||
|
|
@ -17,4 +17,23 @@ Current need is not a TUI panel for spawned Pods. The priority is Ticket-driven
|
|||
This ticket is not closed as technically invalid; it is moved out of the active multi-agent implementation path. Revisit only if direct child Pod visibility/attach UI becomes a concrete UX requirement.
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: hare at: 2026-06-07T03:14:39Z from: intake to: done reason: closed field: workflow_state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Ticket closed; workflow_state set to done.
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: close author: hare at: 2026-06-07T03:14:39Z status: closed -->
|
||||
|
||||
## Closed
|
||||
|
||||
Closed as intentionally not planned.
|
||||
|
||||
The old migrated spawned-Pod panel idea has been superseded by the workspace panel, Pod list/open/attach behavior, Ticket role launching, and the local role session registry. The remaining direction is not to revive this standalone spawned-child panel ticket. Future panel work should be tracked through the newer workspace panel / orchestration tickets.
|
||||
|
||||
---
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
# Delegation intent: workspace panel layout/display tuning
|
||||
|
||||
## Classification
|
||||
|
||||
`implementation-ready` as a focused UX/layout pass after the first-pass panel implementation.
|
||||
|
||||
The main requested change is row readability: Ticket titles and Pod names/ids are variable-length and currently lead the row, which makes status/action scanning hard. Rework rendering into aligned columns with short fields first and long free-text identifiers last.
|
||||
|
||||
## Intent
|
||||
|
||||
Tune `yoi panel` row rendering and immediate display affordances without changing Ticket backend/action semantics.
|
||||
|
||||
Primary direction:
|
||||
|
||||
- Ticket/action rows: short aligned columns first, long Ticket title last.
|
||||
- Pod rows: short aligned columns first, variable-length Pod id/name last.
|
||||
- Ticket row and Pod row schemas do not need to be identical, but each row family should align its own columns consistently.
|
||||
|
||||
## Worktree / branch
|
||||
|
||||
- worktree: `/home/hare/Projects/yoi/.worktree/workspace-panel-layout-display-tuning`
|
||||
- branch: `work/workspace-panel-layout-display-tuning`
|
||||
|
||||
This ticket may read tracked `.yoi/tickets` records/design artifacts. Do not read or edit `.yoi/memory/`.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Rework row rendering into stable aligned columns rather than sentence-like rows.
|
||||
- Ticket/action row layout should move the long Ticket title to the end.
|
||||
- Put short, comparable fields first.
|
||||
- Suggested fields/order: selection marker, priority bucket, action, derived status, phase, short id/slug, title.
|
||||
- Exact widths can be tuned, but status/action/phase should line up across Ticket rows.
|
||||
- Pod row layout should move variable-length Pod name/id to the end.
|
||||
- Put short, comparable fields first.
|
||||
- Suggested fields/order: selection marker, status, action, role/kind if cheaply available, Pod name/id.
|
||||
- Long Pod names must not break status/action alignment.
|
||||
- Preserve existing color/style semantics unless a small adjustment improves readability.
|
||||
- Improve labels only where it directly helps the aligned layout; avoid broad copy rewrites.
|
||||
- Preserve `yoi panel` behavior in both Ticket-enabled and no-Ticket workspaces.
|
||||
- Preserve Ticket action dispatch, Intake launch, Orchestrator lifecycle, Pod open/direct-send, and composer target behavior.
|
||||
- Do not move authority into rendering code; keep the thin ViewModel boundary.
|
||||
- Do not reintroduce `--multi`.
|
||||
- Avoid broad refactors or new scheduler/action semantics.
|
||||
|
||||
## Current code map
|
||||
|
||||
- `crates/tui/src/multi_pod.rs`
|
||||
- `panel_row_line(...)` currently renders Ticket/action rows as `<title> [priority] Action: status ...`.
|
||||
- `row_line(...)` currently renders Pod rows as `<pod name> [status] action ...`.
|
||||
- `panel_priority_style(...)`, `row_status_label(...)`, section/header rendering, and tests are nearby.
|
||||
- `crates/tui/src/workspace_panel.rs`
|
||||
- `PanelRow`, `TicketPanelEntry`, `ActionPriority`, `NextUserAction`, `TicketPanelPhase`, and `ticket_subtitle(...)` provide the display fields.
|
||||
- Extend display-ready fields only if needed; keep this as UI data, not authority.
|
||||
- Existing tests in `crates/tui/src/multi_pod.rs` and `workspace_panel.rs`
|
||||
- Add/adjust unit tests for row rendering/alignment and truncation behavior.
|
||||
|
||||
## Suggested display shape
|
||||
|
||||
Ticket/action row example:
|
||||
|
||||
```text
|
||||
▶ decision Review implementation reported review workspace-panel-composer-targets Workspace panel composer targets
|
||||
ready Go ready for Go preflight ticket-slug Long Ticket title...
|
||||
```
|
||||
|
||||
Pod row example:
|
||||
|
||||
```text
|
||||
live idle send pod companion
|
||||
live running open pod very-long-background-worker-name...
|
||||
```
|
||||
|
||||
Exact wording and widths can differ, but the visual rule is fixed: short comparable columns first, long names/titles last.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Backend/Ticket action semantic changes.
|
||||
- New scheduler/queue behavior.
|
||||
- Orchestrator/Intake protocol changes.
|
||||
- Replacing the single-Pod TUI.
|
||||
- Reintroducing `--multi`.
|
||||
- Large module refactors.
|
||||
|
||||
## Validation
|
||||
|
||||
Run at least:
|
||||
|
||||
- targeted TUI tests for Ticket row rendering/alignment;
|
||||
- targeted TUI tests for Pod row rendering/alignment;
|
||||
- `cargo test -p tui workspace_panel`;
|
||||
- `cargo test -p tui multi_pod`;
|
||||
- `cargo test -p yoi panel`;
|
||||
- `cargo check --workspace --all-targets`;
|
||||
- `cargo fmt --check`;
|
||||
- `git diff --check`;
|
||||
- `cargo build -p yoi`;
|
||||
- `target/debug/yoi ticket doctor`.
|
||||
|
||||
Run `nix build .#yoi --no-link` if feasible.
|
||||
|
||||
## Completion report
|
||||
|
||||
Report:
|
||||
|
||||
- worktree path / branch;
|
||||
- commit hash;
|
||||
- final Ticket row column schema and widths/truncation behavior;
|
||||
- final Pod row column schema and widths/truncation behavior;
|
||||
- tests updated/added;
|
||||
- validation results;
|
||||
- remaining UX/display tuning items, if any.
|
||||
|
|
@ -2,12 +2,12 @@
|
|||
id: 20260606-060548-workspace-panel-layout-display-tuning
|
||||
slug: workspace-panel-layout-display-tuning
|
||||
title: Workspace panel layout and display tuning
|
||||
status: open
|
||||
status: closed
|
||||
kind: task
|
||||
priority: P2
|
||||
labels: [tui, ticket, orchestration, panel, ux]
|
||||
created_at: 2026-06-06T06:05:48Z
|
||||
updated_at: 2026-06-06T06:06:30Z
|
||||
updated_at: 2026-06-06T21:16:52Z
|
||||
assignee: null
|
||||
legacy_ticket: null
|
||||
---
|
||||
|
|
@ -27,6 +27,14 @@ Refine the workspace panel layout, labels, ordering, and displayed detail so the
|
|||
- Follow existing TUI visual conventions/components.
|
||||
- Preserve the single `yoi panel` route and no-Ticket Pod-centric behavior.
|
||||
- Keep the UI intermediate representation thin; do not move authority into rendering code.
|
||||
- Rework row rendering into stable, aligned columns rather than sentence-like rows.
|
||||
- Ticket/action rows should put short, fixed-ish fields first and move the long Ticket title to the end.
|
||||
- Example column order: marker/selection, priority/action/status/phase/id-or-slug, then title.
|
||||
- The exact set can be tuned, but the long free-text title must not be the leading column.
|
||||
- Pod rows should likewise put short, alignable state/action fields first and move the variable-length Pod id/name to the end.
|
||||
- Example column order: marker/selection, status/action/role-or-kind, then Pod id/name.
|
||||
- Long Pod identifiers should not break status/action alignment.
|
||||
- Ticket rows and Pod rows may use different schemas, but each row family should align its own columns consistently.
|
||||
- Improve Ticket/action row labels, status markers, and key hints.
|
||||
- Improve detail pane content for selected Ticket/action rows:
|
||||
- current phase;
|
||||
|
|
@ -52,6 +60,9 @@ Refine the workspace panel layout, labels, ordering, and displayed detail so the
|
|||
|
||||
- `yoi panel` remains functional in both Ticket-enabled and no-Ticket workspaces.
|
||||
- The primary row list makes user-action-required items visually distinguishable from passive background Pods.
|
||||
- Ticket/action rows have aligned columns with the long Ticket title at the end, not the beginning.
|
||||
- Pod rows have aligned columns with variable-length Pod id/name at the end, not the beginning.
|
||||
- Short status/action fields stay visually comparable across rows.
|
||||
- The active composer target is unambiguous.
|
||||
- Common diagnostics are concise and actionable.
|
||||
- Layout/display tests or snapshot-like unit tests cover key row/detail rendering decisions where practical.
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
Implemented workspace panel aligned row layout.
|
||||
|
||||
Final Ticket/action row schema:
|
||||
|
||||
```text
|
||||
<marker><priority> <action> <status> <phase> <slug-or-id> <title>
|
||||
```
|
||||
|
||||
Column behavior:
|
||||
- marker: width 2;
|
||||
- priority: width 11;
|
||||
- action: width 7;
|
||||
- status: width 24;
|
||||
- phase: width 12;
|
||||
- slug-or-id: width 32;
|
||||
- title: flexible remaining width.
|
||||
|
||||
Fixed columns are padded/truncated before the title. The long Ticket title is last and truncates with `…` when needed.
|
||||
|
||||
Final Pod row schema:
|
||||
|
||||
```text
|
||||
<marker><status> <action> <kind> <pod-name>
|
||||
```
|
||||
|
||||
Column behavior:
|
||||
- marker: width 2;
|
||||
- status: width 18;
|
||||
- action: width 8;
|
||||
- kind: width 3, currently `pod`;
|
||||
- pod name: flexible remaining width.
|
||||
|
||||
Fixed Pod columns are padded/truncated before the Pod name. Long Pod names no longer shift status/action alignment.
|
||||
|
||||
Tests added/updated:
|
||||
- `panel_ticket_rows_use_aligned_columns_before_title`
|
||||
- `panel_ticket_title_truncates_after_stable_columns`
|
||||
- `panel_pod_rows_use_aligned_columns_before_pod_name`
|
||||
- `panel_pod_name_truncates_after_status_action_and_kind`
|
||||
- adjusted action-before-pod ordering test to locate Ticket rows by slug because title is no longer the leading field.
|
||||
|
||||
Validation after merge:
|
||||
- `cargo test -p tui panel_`
|
||||
- `cargo test -p tui workspace_panel`
|
||||
- `cargo test -p tui multi_pod`
|
||||
- `cargo test -p yoi panel`
|
||||
- `cargo check --workspace --all-targets`
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check HEAD~1..HEAD`
|
||||
- `cargo build -p yoi`
|
||||
- `target/debug/yoi ticket doctor`
|
||||
- `nix build .#yoi --no-link --print-out-paths`
|
||||
|
||||
External review approved with no requested changes.
|
||||
|
||||
Remaining UX/display tuning:
|
||||
- Broader detail-pane/timeline/copy tuning remains optional follow-up; this ticket intentionally stayed focused on aligned row columns.
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
<!-- event: create author: yoi ticket at: 2026-06-06T06:05:48Z -->
|
||||
|
||||
## Created
|
||||
|
||||
Created by LocalTicketBackend create.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: plan author: hare at: 2026-06-06T06:06:30Z -->
|
||||
|
||||
## Plan
|
||||
|
||||
Created after closing the first-pass workspace orchestration panel implementation.
|
||||
|
||||
The first pass deliberately prioritized end-to-end behavior over visual/layout polish. This ticket owns the next tuning pass: row labels, key hints, detail pane content, composer target visibility, concise diagnostics, and optional phase/dependency/timeline display, while preserving existing TUI conventions and the thin ViewModel/action-dispatch boundaries.
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: decision author: hare at: 2026-06-06T08:36:49Z -->
|
||||
|
||||
## Decision
|
||||
|
||||
User layout direction:
|
||||
|
||||
Rows should be column-aligned instead of leading with long identifiers/titles.
|
||||
|
||||
- Ticket/action rows: move the long Ticket title to the end. Put short, alignable fields first, such as priority/action/status/phase/id-or-slug, then the title.
|
||||
- Pod rows: move variable-length Pod id/name to the end. Put short, alignable fields first, such as status/action/role-or-kind, then the Pod id/name.
|
||||
- Ticket and Pod rows do not need to share exactly the same schema, but each row family should keep stable aligned columns so status/action can be visually scanned.
|
||||
|
||||
This should be handled in the layout/display tuning ticket, not by changing backend/action semantics.
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: plan author: hare at: 2026-06-06T09:32:01Z -->
|
||||
|
||||
## Plan
|
||||
|
||||
Preflight result: `implementation-ready` as a focused layout/display pass.
|
||||
|
||||
Implementation should rework panel row rendering into aligned columns with short comparable fields first and long variable text last:
|
||||
- Ticket/action rows: move Ticket title to the end; align priority/action/status/phase/id-or-slug first.
|
||||
- Pod rows: move Pod id/name to the end; align status/action/kind first.
|
||||
|
||||
This should not change Ticket backend semantics, action dispatch, Orchestrator lifecycle, Intake handoff, no-Ticket behavior, or the thin ViewModel boundary.
|
||||
|
||||
Detailed delegation intent is recorded in `artifacts/delegation-intent.md`.
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: review author: hare at: 2026-06-06T21:16:52Z status: approve -->
|
||||
|
||||
## Review: approve
|
||||
|
||||
External reviewer approved the implementation with no requested changes.
|
||||
|
||||
Review summary:
|
||||
- Ticket/action rows use stable fixed-width columns first and place the variable Ticket title last.
|
||||
- Pod rows use stable fixed-width columns first and place the variable Pod name/id last.
|
||||
- Long titles/names truncate after stable columns and do not shift status/action alignment.
|
||||
- Color/style semantics are preserved closely.
|
||||
- Changes are scoped to rendering/helpers/tests; no Ticket action dispatch, Intake launch, Orchestrator lifecycle, Pod open/direct-send, composer target, authority, or I/O semantics changed.
|
||||
- No `--multi` route was reintroduced.
|
||||
- Tests cover row rendering, alignment, and truncation.
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: close author: hare at: 2026-06-06T21:16:52Z status: closed -->
|
||||
|
||||
## Closed
|
||||
|
||||
Implemented workspace panel aligned row layout.
|
||||
|
||||
Final Ticket/action row schema:
|
||||
|
||||
```text
|
||||
<marker><priority> <action> <status> <phase> <slug-or-id> <title>
|
||||
```
|
||||
|
||||
Column behavior:
|
||||
- marker: width 2;
|
||||
- priority: width 11;
|
||||
- action: width 7;
|
||||
- status: width 24;
|
||||
- phase: width 12;
|
||||
- slug-or-id: width 32;
|
||||
- title: flexible remaining width.
|
||||
|
||||
Fixed columns are padded/truncated before the title. The long Ticket title is last and truncates with `…` when needed.
|
||||
|
||||
Final Pod row schema:
|
||||
|
||||
```text
|
||||
<marker><status> <action> <kind> <pod-name>
|
||||
```
|
||||
|
||||
Column behavior:
|
||||
- marker: width 2;
|
||||
- status: width 18;
|
||||
- action: width 8;
|
||||
- kind: width 3, currently `pod`;
|
||||
- pod name: flexible remaining width.
|
||||
|
||||
Fixed Pod columns are padded/truncated before the Pod name. Long Pod names no longer shift status/action alignment.
|
||||
|
||||
Tests added/updated:
|
||||
- `panel_ticket_rows_use_aligned_columns_before_title`
|
||||
- `panel_ticket_title_truncates_after_stable_columns`
|
||||
- `panel_pod_rows_use_aligned_columns_before_pod_name`
|
||||
- `panel_pod_name_truncates_after_status_action_and_kind`
|
||||
- adjusted action-before-pod ordering test to locate Ticket rows by slug because title is no longer the leading field.
|
||||
|
||||
Validation after merge:
|
||||
- `cargo test -p tui panel_`
|
||||
- `cargo test -p tui workspace_panel`
|
||||
- `cargo test -p tui multi_pod`
|
||||
- `cargo test -p yoi panel`
|
||||
- `cargo check --workspace --all-targets`
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check HEAD~1..HEAD`
|
||||
- `cargo build -p yoi`
|
||||
- `target/debug/yoi ticket doctor`
|
||||
- `nix build .#yoi --no-link --print-out-paths`
|
||||
|
||||
External review approved with no requested changes.
|
||||
|
||||
Remaining UX/display tuning:
|
||||
- Broader detail-pane/timeline/copy tuning remains optional follow-up; this ticket intentionally stayed focused on aligned row columns.
|
||||
|
||||
|
||||
---
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
# Delegation intent: remove obsolete TUI `:ticket` commands
|
||||
|
||||
## Classification
|
||||
|
||||
`implementation-ready` cleanup.
|
||||
|
||||
The workspace panel now owns the user-facing Ticket/Intake/Orchestrator UX. The old single-Pod TUI `:ticket ...` commands are no longer needed and should be removed rather than kept as a fallback.
|
||||
|
||||
## Intent
|
||||
|
||||
Remove the obsolete TUI command family:
|
||||
|
||||
```text
|
||||
:ticket intake ...
|
||||
:ticket route ...
|
||||
:ticket investigate ...
|
||||
:ticket implement ...
|
||||
:ticket review ...
|
||||
```
|
||||
|
||||
Users should use:
|
||||
|
||||
- `yoi panel` for workspace Ticket/Intake/Orchestrator UI;
|
||||
- `yoi ticket ...` for direct Ticket CLI operations;
|
||||
- Ticket tools/workflows where appropriate.
|
||||
|
||||
## Worktree / branch
|
||||
|
||||
- worktree: `/home/hare/Projects/yoi/.worktree/remove-tui-ticket-commands`
|
||||
- branch: `work/remove-tui-ticket-commands`
|
||||
|
||||
This ticket may read tracked `.yoi/tickets` records/design artifacts. Do not read or edit `.yoi/memory/`.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Remove `:ticket` command registration/parsing from TUI command registry.
|
||||
- Remove TUI parser tests that expect `:ticket ...` success and replace with unknown-command / unsupported behavior coverage where appropriate.
|
||||
- Remove `CommandAction::TicketRole` and pending local action plumbing if it is only used by `:ticket`.
|
||||
- Remove single-Pod runtime handling that launches Ticket role Pods from `:ticket` commands.
|
||||
- Remove now-unused imports such as `TicketRole`, `TicketRef`, or `launch_ticket_role_pod` from single-Pod command handling if applicable.
|
||||
- Keep shared `client::ticket_role` launcher code because `yoi panel` still uses it for Orchestrator/Intake.
|
||||
- Keep `yoi panel` behavior unchanged.
|
||||
- Keep `yoi ticket ...` CLI unchanged.
|
||||
- Keep Ticket tools/workflows unchanged.
|
||||
- Update active docs/help that still present `:ticket ...` as a supported route.
|
||||
- Do not mass-rewrite historical closed Ticket records/artifacts that mention `:ticket`.
|
||||
- Do not reintroduce `--multi`.
|
||||
|
||||
## Current code map
|
||||
|
||||
- `crates/tui/src/command.rs`
|
||||
- command registration, `ticket_command(...)`, `CommandAction::TicketRole`, parser tests.
|
||||
- `crates/tui/src/single_pod.rs`
|
||||
- pending local action handling and `handle_ticket_role_command(...)` path.
|
||||
- `crates/tui/src/app.rs` / nearby files
|
||||
- check for pending local action storage if command action removal requires cleanup.
|
||||
- `docs/development/work-items.md` and active docs
|
||||
- remove user-facing `:ticket ...` instructions or replace with `yoi panel` / `yoi ticket ...`.
|
||||
|
||||
## Validation
|
||||
|
||||
Run at least:
|
||||
|
||||
- `cargo test -p tui command` or targeted command parser tests;
|
||||
- `cargo test -p tui workspace_panel`;
|
||||
- `cargo test -p tui multi_pod`;
|
||||
- `cargo test -p yoi panel`;
|
||||
- `cargo test -p client ticket_role` if shared launcher imports/usage are touched;
|
||||
- `cargo check --workspace --all-targets`;
|
||||
- `cargo fmt --check`;
|
||||
- `git diff --check`;
|
||||
- `cargo build -p yoi`;
|
||||
- `target/debug/yoi ticket doctor`.
|
||||
|
||||
Run `nix build .#yoi --no-link` if feasible.
|
||||
|
||||
Also check active references:
|
||||
|
||||
```bash
|
||||
rg -n ":ticket|ticket intake|ticket route|ticket implement|ticket review" docs crates/tui/src .yoi/workflow AGENTS.md README.md
|
||||
```
|
||||
|
||||
Remaining matches should be code comments/tests intentionally covering unsupported behavior or historical closed records outside the active-reference search.
|
||||
|
||||
## Completion report
|
||||
|
||||
Report:
|
||||
|
||||
- worktree path / branch;
|
||||
- commit hash;
|
||||
- removed command/runtime paths;
|
||||
- docs/tests updated;
|
||||
- confirmation that panel/CLI/shared launcher remain intact;
|
||||
- validation results;
|
||||
- remaining historical references, if any.
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
---
|
||||
id: 20260606-210832-remove-tui-ticket-commands
|
||||
slug: remove-tui-ticket-commands
|
||||
title: Remove obsolete TUI :ticket commands
|
||||
status: closed
|
||||
kind: task
|
||||
priority: P2
|
||||
labels: [tui, ticket, cleanup, panel]
|
||||
created_at: 2026-06-06T21:08:32Z
|
||||
updated_at: 2026-06-06T21:28:39Z
|
||||
assignee: null
|
||||
legacy_ticket: null
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
The old TUI `:ticket ...` command surface was an MVP/fallback for launching fixed Ticket-role Pods before the workspace panel existed. The panel now owns the Ticket/Intake/Orchestrator UX:
|
||||
|
||||
- `yoi panel` is the workspace entrypoint.
|
||||
- Ticket Intake is a composer target.
|
||||
- Orchestrator lifecycle and Intake handoff are integrated.
|
||||
- Ticket Go/Defer actions are available from panel rows.
|
||||
- No-Ticket workspaces remain Pod-centric.
|
||||
|
||||
Keeping `:ticket ...` now creates a second user-facing route for the same role-launch concepts and conflicts with the direction that the panel should be the single workspace control surface.
|
||||
|
||||
## Goal
|
||||
|
||||
Remove the obsolete TUI `:ticket ...` command family and update active docs/tests so users are directed to `yoi panel`, `yoi ticket ...`, Ticket tools, and workflows instead.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Remove parsing and handling for TUI `:ticket ...` commands:
|
||||
- `:ticket intake ...`
|
||||
- `:ticket route ...`
|
||||
- `:ticket investigate ...`
|
||||
- `:ticket implement ...`
|
||||
- `:ticket review ...`
|
||||
- Remove the associated TUI-local `CommandAction::TicketRole` / pending-ticket-role command path if it becomes unused.
|
||||
- Remove single-Pod runtime handling that launches Ticket roles from `:ticket` commands.
|
||||
- Keep shared role-launcher code in `client` because `yoi panel` still uses it.
|
||||
- Do not remove `yoi ticket ...` CLI.
|
||||
- Do not remove Ticket tools or workflows.
|
||||
- Do not change `yoi panel` behavior.
|
||||
- Update active docs/help text that presents `:ticket ...` as a supported route.
|
||||
- Historical closed Ticket records may still mention `:ticket`; do not mass-rewrite old history.
|
||||
- Add/adjust tests so `:ticket` is unknown/unsupported and no active parser tests expect it.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Removing the Ticket role launcher.
|
||||
- Removing panel Intake/Orchestrator/Go behavior.
|
||||
- Removing `yoi ticket ...`.
|
||||
- Rewriting historical closed Ticket threads/artifacts.
|
||||
- Layout/display tuning.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- `:ticket ...` is no longer accepted by TUI command parsing.
|
||||
- There is no runtime path in the single-Pod TUI that launches Ticket roles from `:ticket`.
|
||||
- Active docs/help no longer direct users to `:ticket`.
|
||||
- `yoi panel` and panel Intake launch still work.
|
||||
- `yoi ticket ...` still works.
|
||||
- Tests cover removal or unknown-command behavior.
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
Removed the obsolete single-Pod TUI `:ticket ...` command surface.
|
||||
|
||||
Changes:
|
||||
- Removed TUI command registration/parsing/handling for `:ticket intake`, `:ticket route`, `:ticket investigate`, `:ticket implement`, and `:ticket review`.
|
||||
- Removed `CommandAction::TicketRole`, `TicketRoleCommand`, pending command-action plumbing, and the single-Pod runtime Ticket role launch path.
|
||||
- Kept shared `client::ticket_role` launcher code because `yoi panel` uses it for Orchestrator/Intake launches.
|
||||
- Kept `yoi panel`, `yoi ticket ...`, Ticket tools, and workflows intact.
|
||||
- Updated active docs to direct users to `yoi panel`, Ticket tools/workflows, and `yoi ticket ...` instead of `:ticket ...`.
|
||||
- Left an explicit unsupported note for single-Pod `:ticket ...` command mode.
|
||||
- Updated parser tests so `ticket intake ...` is unknown/unsupported.
|
||||
- Did not reintroduce `--multi`.
|
||||
|
||||
Validation after merge:
|
||||
- `cargo test -p tui command`
|
||||
- `cargo test -p tui workspace_panel`
|
||||
- `cargo test -p tui multi_pod`
|
||||
- `cargo test -p yoi panel`
|
||||
- `cargo test -p client ticket_role`
|
||||
- `cargo check --workspace --all-targets`
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check HEAD~1..HEAD`
|
||||
- `cargo build -p yoi`
|
||||
- `target/debug/yoi ticket doctor`
|
||||
- `nix build .#yoi --no-link --print-out-paths`
|
||||
|
||||
Remaining active-reference matches are intentional: `yoi ticket review`, panel/client `ticket_role` usage, unsupported-note/test coverage, historical report text, and non-command identifiers such as `ticket_enabled` / `builtin:ticket`.
|
||||
|
||||
External review approved with no requested changes.
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
<!-- event: create author: yoi ticket at: 2026-06-06T21:08:32Z -->
|
||||
|
||||
## Created
|
||||
|
||||
Created by LocalTicketBackend create.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: plan author: hare at: 2026-06-06T21:09:49Z -->
|
||||
|
||||
## Plan
|
||||
|
||||
Preflight result: `implementation-ready` cleanup.
|
||||
|
||||
The workspace panel now owns the Ticket/Intake/Orchestrator user-facing route, so the old single-Pod TUI `:ticket ...` command family should be removed rather than kept as fallback. Keep the shared role launcher because `yoi panel` uses it; remove only the TUI command surface/runtime handling and active docs/tests.
|
||||
|
||||
Detailed delegation intent is recorded in `artifacts/delegation-intent.md`.
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: review author: hare at: 2026-06-06T21:28:39Z status: approve -->
|
||||
|
||||
## Review: approve
|
||||
|
||||
External reviewer approved the implementation with no requested changes.
|
||||
|
||||
Review summary:
|
||||
- TUI `ticket` command registration/parser/handler was removed.
|
||||
- `CommandAction::TicketRole`, `TicketRoleCommand`, pending local action plumbing, and the single-Pod local role-launch path were removed.
|
||||
- Shared `client::ticket_role` code remains intact for `yoi panel`.
|
||||
- `yoi panel`, `yoi ticket ...`, Ticket tools, and workflows remain intact.
|
||||
- Active docs now direct users to `yoi panel`, Ticket tools/workflows, and `yoi ticket ...`; the remaining `:ticket ...` mention is an explicit unsupported note.
|
||||
- Parser coverage treats `ticket intake ...` as unknown.
|
||||
- No `--multi` reintroduction.
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: close author: hare at: 2026-06-06T21:28:39Z status: closed -->
|
||||
|
||||
## Closed
|
||||
|
||||
Removed the obsolete single-Pod TUI `:ticket ...` command surface.
|
||||
|
||||
Changes:
|
||||
- Removed TUI command registration/parsing/handling for `:ticket intake`, `:ticket route`, `:ticket investigate`, `:ticket implement`, and `:ticket review`.
|
||||
- Removed `CommandAction::TicketRole`, `TicketRoleCommand`, pending command-action plumbing, and the single-Pod runtime Ticket role launch path.
|
||||
- Kept shared `client::ticket_role` launcher code because `yoi panel` uses it for Orchestrator/Intake launches.
|
||||
- Kept `yoi panel`, `yoi ticket ...`, Ticket tools, and workflows intact.
|
||||
- Updated active docs to direct users to `yoi panel`, Ticket tools/workflows, and `yoi ticket ...` instead of `:ticket ...`.
|
||||
- Left an explicit unsupported note for single-Pod `:ticket ...` command mode.
|
||||
- Updated parser tests so `ticket intake ...` is unknown/unsupported.
|
||||
- Did not reintroduce `--multi`.
|
||||
|
||||
Validation after merge:
|
||||
- `cargo test -p tui command`
|
||||
- `cargo test -p tui workspace_panel`
|
||||
- `cargo test -p tui multi_pod`
|
||||
- `cargo test -p yoi panel`
|
||||
- `cargo test -p client ticket_role`
|
||||
- `cargo check --workspace --all-targets`
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check HEAD~1..HEAD`
|
||||
- `cargo build -p yoi`
|
||||
- `target/debug/yoi ticket doctor`
|
||||
- `nix build .#yoi --no-link --print-out-paths`
|
||||
|
||||
Remaining active-reference matches are intentional: `yoi ticket review`, panel/client `ticket_role` usage, unsupported-note/test coverage, historical report text, and non-command identifiers such as `ticket_enabled` / `builtin:ticket`.
|
||||
|
||||
External review approved with no requested changes.
|
||||
|
||||
|
||||
---
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
# Delegation intent: explicit Ticket workflow state
|
||||
|
||||
## Classification
|
||||
|
||||
`implementation-ready` after `typed-ticket-thread-event-log`.
|
||||
|
||||
The typed thread event foundation is complete. This ticket should add explicit durable workflow state to Ticket records and update the panel/queue action to use it instead of inferred phase/action/status heuristics.
|
||||
|
||||
## Intent
|
||||
|
||||
Replace inferred panel Ticket state with explicit Ticket workflow fields and simplify the panel main list to display durable state directly.
|
||||
|
||||
Durable workflow state:
|
||||
|
||||
```text
|
||||
intake -> ready -> queued -> inprogress -> done
|
||||
```
|
||||
|
||||
The only normal human gate is `ready -> queued`, exposed as `Queue` in the panel. Review/rework/validation remain within `inprogress`; transient activity should not be stored in Ticket frontmatter.
|
||||
|
||||
## Worktree / branch
|
||||
|
||||
- worktree: `/home/hare/Projects/yoi/.worktree/explicit-ticket-workflow-state`
|
||||
- branch: `work/explicit-ticket-workflow-state`
|
||||
|
||||
This ticket may read tracked `.yoi/tickets` records/design artifacts. Do not read or edit `.yoi/memory/`.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Add typed workflow fields to Ticket frontmatter/model parsing/writing:
|
||||
- `workflow_state: intake | ready | queued | inprogress | done`;
|
||||
- `attention_required: null | "..."` or compatible optional string overlay;
|
||||
- `queued_by: null | "user"` or compatible optional string;
|
||||
- `queued_at: null | timestamp` or compatible optional timestamp/string.
|
||||
- Preserve existing Tickets through safe defaults/migration behavior.
|
||||
- Existing open Tickets without `workflow_state` should not be guessed from title/labels/thread as authoritative state.
|
||||
- Use a conservative default such as `intake` or `ready` only if documented and tested; if the current status is closed, map to `done` where appropriate.
|
||||
- Use the typed thread event APIs from `typed-ticket-thread-event-log` for state changes where practical:
|
||||
- `ready -> queued` should update frontmatter and append `state_changed` as one logical mutation;
|
||||
- the event should record actor/reason and remain concise.
|
||||
- Update `yoi panel` to display workflow state directly.
|
||||
- Simplify panel main-list rows:
|
||||
- Ticket rows: `<sel> <state> <slug-or-id> <title>`;
|
||||
- Pod rows: `<sel> <pod-state> <pod-name>`;
|
||||
- remove permanent priority/action/status/phase columns from main rows.
|
||||
- Move row-specific operations such as Queue/Defer/Open/Send to selected-row actionbar/key hints instead of always-visible row columns.
|
||||
- Keep composer/status bar concise and stable; move verbose target/help/diagnostics to actionbar/detail/diagnostic area.
|
||||
- Remove or demote heuristics that infer phase/action from labels, title text, `readiness`, `needs_preflight`, or thread event presence.
|
||||
- Rename panel `Go`/`ApproveIntake` queue-like behavior to `Queue`.
|
||||
- Queue action semantics:
|
||||
- only valid when current `workflow_state == ready`;
|
||||
- re-check state before mutation;
|
||||
- transition to `queued`;
|
||||
- set `queued_by` / `queued_at` if adopted;
|
||||
- append typed `state_changed` event;
|
||||
- notify Orchestrator if reachable, but notification failure must not roll back a successful Ticket transition.
|
||||
- `Defer` may remain as a safe status/pending action if currently useful, but it should not pretend to be workflow state unless explicitly modeled.
|
||||
- Intake readiness:
|
||||
- add API/tool/helper path for Intake/Orchestrator to mark a Ticket `ready` using explicit state and, where practical, an `intake_summary`/`state_changed` event;
|
||||
- do not dump full Intake transcripts into Ticket thread.
|
||||
- Done/close flow should set or derive `workflow_state = done` consistently.
|
||||
- No-Ticket workspace Pod-centric panel behavior must remain unchanged.
|
||||
- Do not store transient `activity` in frontmatter.
|
||||
- Do not reintroduce `--multi` or `:ticket`.
|
||||
|
||||
## Current code map
|
||||
|
||||
- `crates/ticket/src/lib.rs`
|
||||
- Ticket item/frontmatter parsing/writing, `Ticket` model, backend APIs.
|
||||
- Newly available typed event APIs: `TicketStateChange`, `TicketIntakeSummary`, `add_state_changed`, `add_intake_summary`, `set_state_field`.
|
||||
- `crates/ticket/src/tool.rs`
|
||||
- Built-in Ticket tool behavior. Add workflow state support only if needed for Intake/Orchestrator/Panel flows.
|
||||
- `crates/yoi/src/ticket_cli.rs`
|
||||
- Direct CLI. Update display/create behavior only if needed; keep CLI stable.
|
||||
- `crates/tui/src/workspace_panel.rs`
|
||||
- Current inferred `TicketPanelPhase`, `NextUserAction`, row derivation heuristics.
|
||||
- `crates/tui/src/multi_pod.rs`
|
||||
- Panel row rendering, Queue/Defer action dispatch, status/action bar text, Orchestrator notification.
|
||||
- `crates/client/src/ticket_role.rs`
|
||||
- Intake/Orchestrator role launch context if state-ready/intake-summary integration needs launch-prompt changes.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Scheduler/lease/queue system.
|
||||
- Persisting live Pod activity into Tickets.
|
||||
- Reintroducing human approve/reject gates for every review loop.
|
||||
- Reintroducing `--multi` or `:ticket`.
|
||||
- Broad layout redesign beyond the required state-only row/statusbar simplification.
|
||||
|
||||
## Validation
|
||||
|
||||
Run at least:
|
||||
|
||||
- `cargo test -p ticket workflow` or targeted workflow-state tests;
|
||||
- `cargo test -p ticket`;
|
||||
- `cargo test -p tui workspace_panel`;
|
||||
- `cargo test -p tui multi_pod`;
|
||||
- `cargo test -p yoi panel`;
|
||||
- `cargo test -p yoi ticket` if CLI behavior changes;
|
||||
- `cargo test -p pod ticket --lib` if tools change;
|
||||
- `cargo check --workspace --all-targets`;
|
||||
- `cargo fmt --check`;
|
||||
- `git diff --check`;
|
||||
- `cargo build -p yoi`;
|
||||
- `target/debug/yoi ticket doctor`.
|
||||
|
||||
Run `nix build .#yoi --no-link` if feasible.
|
||||
|
||||
## Completion report
|
||||
|
||||
Report:
|
||||
|
||||
- worktree path / branch;
|
||||
- commit hash;
|
||||
- final frontmatter fields and default/migration behavior;
|
||||
- state transition API usage;
|
||||
- Queue semantics and Orchestrator notification behavior;
|
||||
- panel row/statusbar simplification;
|
||||
- heuristics removed/demoted;
|
||||
- tests updated/added;
|
||||
- validation results;
|
||||
- remaining follow-up items, if any.
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
---
|
||||
id: 20260606-215403-explicit-ticket-workflow-state
|
||||
slug: explicit-ticket-workflow-state
|
||||
title: Replace inferred panel Ticket state with explicit workflow state
|
||||
status: closed
|
||||
kind: task
|
||||
priority: P1
|
||||
labels: [ticket, tui, orchestration, panel, state]
|
||||
created_at: 2026-06-06T21:54:03Z
|
||||
updated_at: 2026-06-07T00:08:04Z
|
||||
assignee: null
|
||||
legacy_ticket: null
|
||||
workflow_state: done
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
The first-pass workspace panel derives Ticket phase/action/status from labels, title text, readiness fields, and thread events such as `plan`, `implementation_report`, and `review`. That was useful for bootstrapping the panel, but it makes the panel infer workflow state instead of reading explicit Ticket state.
|
||||
|
||||
The desired model is simpler and more durable: Ticket records should carry a small explicit workflow state, while transient execution activity remains derived from live Pod/session state and is not persisted into Ticket frontmatter.
|
||||
|
||||
## Goal
|
||||
|
||||
Replace panel-inferred Ticket workflow state with explicit Ticket workflow fields and update the panel/Orchestrator/Intake flows to use those fields.
|
||||
|
||||
## Target state model
|
||||
|
||||
Use a small durable state machine:
|
||||
|
||||
```text
|
||||
intake -> ready -> queued -> inprogress -> done
|
||||
```
|
||||
|
||||
Meanings:
|
||||
|
||||
- `intake`: Intake Pod and user are clarifying/materializing the Ticket.
|
||||
- `ready`: Intake is complete; the Ticket is ready for a human to queue.
|
||||
- `queued`: Human queued the Ticket; Orchestrator may schedule it when resources/priority allow.
|
||||
- `inprogress`: Orchestrator/coder/reviewer/validator work is underway. Review/rework loops stay inside this state.
|
||||
- `done`: completed/closed.
|
||||
|
||||
`blocked` is not a workflow state. Blocking/user attention is an overlay such as `attention_required`.
|
||||
|
||||
`review` is not a workflow state. Review/rework/validation are runtime activity or thread events inside `inprogress`.
|
||||
|
||||
## Durable fields
|
||||
|
||||
Add explicit current-state fields to Ticket frontmatter, for example:
|
||||
|
||||
```yaml
|
||||
workflow_state: intake | ready | queued | inprogress | done
|
||||
attention_required: null | "..."
|
||||
queued_by: null | "user"
|
||||
queued_at: null | "2026-..."
|
||||
```
|
||||
|
||||
Exact field names can be refined during implementation, but the semantics should remain:
|
||||
|
||||
- `workflow_state` is the durable Ticket workflow state.
|
||||
- `attention_required` is a durable human-attention overlay, not a separate workflow state.
|
||||
- `queued_by` / `queued_at` are durable facts recorded when the user queues a ready Ticket.
|
||||
|
||||
Do not persist `activity` in Ticket frontmatter. Current activity such as implementing/reviewing/validating should be displayed by combining Ticket state with live Pod/session/role-launch metadata and latest thread events.
|
||||
|
||||
## Panel display simplification
|
||||
|
||||
Once explicit workflow state exists, the panel row should stop showing multiple near-synonymous columns such as priority/action/status/phase. For Ticket rows, the main list should show only the durable state plus identity/title:
|
||||
|
||||
```text
|
||||
<sel> <state> <slug-or-id> <title>
|
||||
```
|
||||
|
||||
Actions such as `Queue`, `Defer`, or `Open` should move to the actionbar/key-hint area for the selected row, not occupy a permanent row column. Ticket priority and other metadata can appear in the detail pane when useful, not as primary list columns.
|
||||
|
||||
Pod rows should follow the same principle:
|
||||
|
||||
```text
|
||||
<sel> <pod-state> <pod-name>
|
||||
```
|
||||
|
||||
Pod operations such as send/open should also be shown as selected-row key hints/actions, not as a permanent noisy row column.
|
||||
|
||||
The panel composer/status area should also be simplified. Do not put verbose target/help/diagnostic text in the status bar. Keep the status bar concise and stable; put transient guidance/errors in the actionbar or detail/diagnostic area instead.
|
||||
|
||||
## Flow
|
||||
|
||||
- `intake -> ready` is completed through Intake Pod conversation and Ticket materialization.
|
||||
- `ready -> queued` is the normal human panel action.
|
||||
- `queued -> inprogress` is performed by Orchestrator scheduling.
|
||||
- `inprogress -> done` is performed by Orchestrator/review/close flow when complete.
|
||||
|
||||
The panel action currently called `Go` should become `Queue`, because the user is queuing a ready Ticket rather than approving implementation details.
|
||||
|
||||
## Thread/event-log relationship
|
||||
|
||||
Current workflow state should live in frontmatter, but every workflow state transition should be explainable through a concise append-only thread event. The thread should become a typed event log, not a freeform conversation transcript.
|
||||
|
||||
This is split into companion ticket `typed-ticket-thread-event-log` so this Ticket can focus on current-state fields and panel semantics while the companion defines/implements the event-log API.
|
||||
|
||||
Desired split:
|
||||
|
||||
- `item.md` frontmatter: current workflow state authority.
|
||||
- `item.md` body: current Ticket snapshot.
|
||||
- `thread.md`: typed append-only events such as `state_changed`, `intake_summary`, `decision`, `implementation_report`, `review`, and `close`.
|
||||
- Pod/session logs: full conversations/runtime transcript.
|
||||
|
||||
State mutations should eventually use backend APIs that update frontmatter and append a `state_changed` event as one logical operation. Intake should write a concise `intake_summary` instead of copying the full Intake conversation into the Ticket thread.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Add explicit workflow state fields to the Ticket model/parser/writer and tool/CLI surfaces as needed.
|
||||
- Migrate or default existing Tickets safely without relying on labels/title/thread-event heuristics as authoritative state.
|
||||
- Update `yoi panel` to display `workflow_state` directly.
|
||||
- Simplify Ticket rows to state + identity/title only: remove permanent priority/action/status/phase columns from the main row.
|
||||
- Simplify Pod rows to pod-state + pod-name only: remove permanent action/kind columns from the main row.
|
||||
- Move row-specific operations such as Queue/Defer/Open/Send to selected-row actionbar/key hints rather than always-visible row columns.
|
||||
- Keep composer/status bar text concise; avoid verbose target/help/diagnostic clutter in the status bar.
|
||||
- Put transient guidance/errors in the actionbar or diagnostic/detail area instead of the status bar.
|
||||
- Remove or demote current panel heuristics that infer phase/action from labels, title text, `readiness`, `needs_preflight`, or thread event presence.
|
||||
- Rename panel `Go` action to `Queue` and make it transition `ready -> queued`.
|
||||
- Queue action must re-check current Ticket state before mutation.
|
||||
- Queue action records a durable typed `state_changed` / decision event and sets `queued_by` / `queued_at` if those fields are adopted.
|
||||
- Orchestrator should treat `queued` as schedulable and set `inprogress` when it starts work, with a typed state transition event once the companion event-log API exists.
|
||||
- Intake should set `workflow_state = ready` when the Ticket is fully materialized and ready to queue.
|
||||
- Done/close flow should set `workflow_state = done` or derive it consistently from close status.
|
||||
- Do not store transient `activity` in Ticket frontmatter.
|
||||
- Preserve no-Ticket workspace Pod-centric panel behavior.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Building the full typed thread event-log API; companion ticket `typed-ticket-thread-event-log` owns that, though this ticket should align with it.
|
||||
- Persisting live Pod activity into Tickets.
|
||||
- Reintroducing human approve/reject gates for every review loop.
|
||||
- Reintroducing `--multi` or `:ticket`.
|
||||
- Layout-only tuning unrelated to explicit state, except the required state-only row/statusbar simplification described above.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- New/updated Tickets can carry explicit workflow state.
|
||||
- Panel rows show workflow state from Ticket fields rather than inferred phase/status.
|
||||
- Ticket rows use a minimal `state + slug/id + title` shape; priority/action/status/phase are not permanent main-list columns.
|
||||
- Pod rows use a minimal `pod-state + pod-name` shape; operations are shown as selected-row key hints/actions, not permanent main-list columns.
|
||||
- Composer/status bar text is concise and does not contain verbose target/help/diagnostic clutter.
|
||||
- Ready Tickets show `Queue` rather than `Go` as the selected-row actionbar/key-hint action.
|
||||
- Queue action transitions only `ready -> queued` and rejects stale/invalid states.
|
||||
- Review/rework activity does not create a separate workflow state; it remains `inprogress` plus runtime/thread detail.
|
||||
- No persistent `activity` field is required for current Pod activity.
|
||||
- Existing tests cover default/migration behavior, panel display, Queue dispatch, and stale-state rejection.
|
||||
- Companion ticket `typed-ticket-thread-event-log` exists and captures the append-only state transition / Intake summary event-log work if not implemented in the same change series.
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
Implemented explicit Ticket workflow state.
|
||||
|
||||
Final frontmatter fields:
|
||||
- `workflow_state: intake | ready | queued | inprogress | done`
|
||||
- `attention_required: null | "..."`
|
||||
- `queued_by: null | "..."`
|
||||
- `queued_at: null | "..."`
|
||||
|
||||
State/default behavior:
|
||||
- Closed Tickets default/derive to `done` where appropriate.
|
||||
- Existing non-closed Tickets without explicit workflow state use conservative defaults without treating labels/title/thread heuristics as workflow-state authority.
|
||||
- Transient activity such as reviewing/reworking/validating is not persisted in frontmatter.
|
||||
|
||||
Workflow transition APIs/tools:
|
||||
- `mark_intake_ready` / `TicketIntakeReady` performs `intake -> ready`, appending typed `intake_summary` and `state_changed` events.
|
||||
- `queue_ready` remains the dedicated panel Queue path for `ready -> queued`, sets queued metadata, and appends typed `state_changed`.
|
||||
- `set_workflow_state` / `TicketWorkflowState` is bounded to role-side transitions `queued -> inprogress` and `inprogress -> done`.
|
||||
- Generic `set_state_field(..., "workflow_state", ...)` is rejected to prevent bypass.
|
||||
- Backward/skip transitions such as `ready -> inprogress`, `queued -> done`, and `done -> intake` are rejected.
|
||||
|
||||
Panel changes:
|
||||
- Panel rows display explicit workflow state directly.
|
||||
- Ticket rows are simplified to state + slug/id + title.
|
||||
- Pod rows are simplified to pod-state + pod-name.
|
||||
- Row operations move to selected-row actionbar/key hints instead of permanent action/status/phase columns.
|
||||
- Queue replaces the previous Go/ApproveIntake wording and is only valid for current `workflow_state == ready`.
|
||||
- Queue notifies Orchestrator when reachable; notification failure does not roll back a successful Ticket transition.
|
||||
- No-Ticket Pod-centric panel behavior is preserved.
|
||||
|
||||
Role prompt/tool behavior:
|
||||
- Intake/Orchestrator role text now uses `workflow_state` / `Queue` vocabulary.
|
||||
- Intake is instructed to set `workflow_state = ready` through typed Ticket tools after materializing a Ticket.
|
||||
- Orchestrator treats `queued` as schedulable and moves to `inprogress` when starting.
|
||||
|
||||
Validation after merge:
|
||||
- `cargo test -p ticket workflow --lib`
|
||||
- `cargo test -p ticket`
|
||||
- `cargo test -p tui workspace_panel --lib`
|
||||
- `cargo test -p tui multi_pod --lib`
|
||||
- `cargo test -p yoi panel`
|
||||
- `cargo test -p yoi ticket`
|
||||
- `cargo test -p pod ticket --lib`
|
||||
- `cargo test -p client ticket_role`
|
||||
- `cargo check --workspace --all-targets`
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check HEAD~1..HEAD`
|
||||
- `cargo build -p yoi`
|
||||
- `target/debug/yoi ticket doctor`
|
||||
- `nix build .#yoi --no-link --print-out-paths`
|
||||
|
||||
External review approved after transition-graph enforcement was added.
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
<!-- event: create author: yoi ticket at: 2026-06-06T21:54:03Z -->
|
||||
|
||||
## Created
|
||||
|
||||
Created by LocalTicketBackend create.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: plan author: hare at: 2026-06-06T21:54:48Z -->
|
||||
|
||||
## Plan
|
||||
|
||||
Created from design discussion about removing inferred panel Ticket state.
|
||||
|
||||
Decision recorded:
|
||||
- stop treating panel heuristics from labels/title/thread events as authoritative workflow state;
|
||||
- add an explicit durable Ticket workflow state: `intake -> ready -> queued -> inprogress -> done`;
|
||||
- `intake -> ready` is completed through Intake Pod conversation/materialization;
|
||||
- `ready -> queued` is the normal human panel action, replacing the current `Go` action with `Queue`;
|
||||
- review/rework stay inside `inprogress`;
|
||||
- blocked/user-attention is an overlay such as `attention_required`, not a workflow state;
|
||||
- transient activity should be derived from live Pod/session/thread state and not persisted in Ticket frontmatter.
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: decision author: hare at: 2026-06-06T22:04:15Z -->
|
||||
|
||||
## Decision
|
||||
|
||||
Updated per user direction:
|
||||
|
||||
- Ticket row should not expose priority/action/status/phase as separate permanent columns; they are redundant once explicit workflow state exists.
|
||||
- Ticket rows should be essentially `state + slug/id + title`.
|
||||
- Pod rows should likewise be essentially `pod-state + pod-name`.
|
||||
- Operations such as Queue/Defer/Open/Send should move to selected-row actionbar/key hints instead of row columns.
|
||||
- Composer/status bar should be kept concise; verbose target/help/diagnostic text should move to actionbar or detail/diagnostic areas.
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: decision author: hare at: 2026-06-06T22:14:29Z -->
|
||||
|
||||
## Decision
|
||||
|
||||
Updated based on thread/event-log discussion and split out companion work.
|
||||
|
||||
Decision:
|
||||
- Do not delete Ticket threads.
|
||||
- Do not use threads as freeform AI conversation dumps.
|
||||
- Use `item.md` frontmatter for current workflow state, `item.md` body for the current Ticket snapshot, `thread.md` as a concise typed append-only event log, and Pod/session logs for full conversations.
|
||||
- Workflow state transitions should eventually update frontmatter and append a `state_changed` event as one logical backend operation.
|
||||
- Intake should write a bounded `intake_summary` when materializing/marking a Ticket ready, not copy the full Intake conversation.
|
||||
|
||||
Created companion ticket `typed-ticket-thread-event-log` for the typed thread event model/API so `explicit-ticket-workflow-state` can stay focused on current-state fields and panel semantics.
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: plan author: hare at: 2026-06-06T22:49:29Z -->
|
||||
|
||||
## Plan
|
||||
|
||||
Preflight result: `implementation-ready` after `typed-ticket-thread-event-log`.
|
||||
|
||||
This ticket should add explicit durable Ticket workflow fields and update the panel to stop using inferred state/action/status columns. The core workflow is `intake -> ready -> queued -> inprogress -> done`; `ready -> queued` is the normal human panel action and should be shown as `Queue`, not `Go`.
|
||||
|
||||
Implementation should use the newly-added typed thread event APIs for state transitions where practical, simplify panel rows to state + identity/title, and keep transient Pod activity out of Ticket frontmatter.
|
||||
|
||||
Detailed delegation intent is recorded in `artifacts/delegation-intent.md`.
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: review author: hare at: 2026-06-07T00:08:04Z status: approve -->
|
||||
|
||||
## Review: approve
|
||||
|
||||
External reviewer approved current HEAD after two requested-changes cycles.
|
||||
|
||||
Review summary:
|
||||
- Explicit workflow fields and panel display are implemented.
|
||||
- Ticket rows are state + id/title oriented and no longer use inferred priority/action/status/phase as main-list authority.
|
||||
- Queue re-checks current state and transitions ready -> queued through the dedicated backend path.
|
||||
- Typed tools expose bounded Intake and Orchestrator workflow transitions.
|
||||
- Workflow transition graph is centrally enforced:
|
||||
- IntakeReady only permits intake -> ready.
|
||||
- Queue path owns ready -> queued.
|
||||
- Role-side TicketWorkflowState permits queued -> inprogress and inprogress -> done.
|
||||
- Backward/skip/bypass transitions are rejected.
|
||||
- Handoff prompt uses workflow_state / Queue vocabulary instead of old readiness/Go wording.
|
||||
- No transient activity is persisted in frontmatter.
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: hare at: 2026-06-07T00:08:04Z from: intake to: done reason: closed field: workflow_state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Ticket closed; workflow_state set to done.
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: close author: hare at: 2026-06-07T00:08:04Z status: closed -->
|
||||
|
||||
## Closed
|
||||
|
||||
Implemented explicit Ticket workflow state.
|
||||
|
||||
Final frontmatter fields:
|
||||
- `workflow_state: intake | ready | queued | inprogress | done`
|
||||
- `attention_required: null | "..."`
|
||||
- `queued_by: null | "..."`
|
||||
- `queued_at: null | "..."`
|
||||
|
||||
State/default behavior:
|
||||
- Closed Tickets default/derive to `done` where appropriate.
|
||||
- Existing non-closed Tickets without explicit workflow state use conservative defaults without treating labels/title/thread heuristics as workflow-state authority.
|
||||
- Transient activity such as reviewing/reworking/validating is not persisted in frontmatter.
|
||||
|
||||
Workflow transition APIs/tools:
|
||||
- `mark_intake_ready` / `TicketIntakeReady` performs `intake -> ready`, appending typed `intake_summary` and `state_changed` events.
|
||||
- `queue_ready` remains the dedicated panel Queue path for `ready -> queued`, sets queued metadata, and appends typed `state_changed`.
|
||||
- `set_workflow_state` / `TicketWorkflowState` is bounded to role-side transitions `queued -> inprogress` and `inprogress -> done`.
|
||||
- Generic `set_state_field(..., "workflow_state", ...)` is rejected to prevent bypass.
|
||||
- Backward/skip transitions such as `ready -> inprogress`, `queued -> done`, and `done -> intake` are rejected.
|
||||
|
||||
Panel changes:
|
||||
- Panel rows display explicit workflow state directly.
|
||||
- Ticket rows are simplified to state + slug/id + title.
|
||||
- Pod rows are simplified to pod-state + pod-name.
|
||||
- Row operations move to selected-row actionbar/key hints instead of permanent action/status/phase columns.
|
||||
- Queue replaces the previous Go/ApproveIntake wording and is only valid for current `workflow_state == ready`.
|
||||
- Queue notifies Orchestrator when reachable; notification failure does not roll back a successful Ticket transition.
|
||||
- No-Ticket Pod-centric panel behavior is preserved.
|
||||
|
||||
Role prompt/tool behavior:
|
||||
- Intake/Orchestrator role text now uses `workflow_state` / `Queue` vocabulary.
|
||||
- Intake is instructed to set `workflow_state = ready` through typed Ticket tools after materializing a Ticket.
|
||||
- Orchestrator treats `queued` as schedulable and moves to `inprogress` when starting.
|
||||
|
||||
Validation after merge:
|
||||
- `cargo test -p ticket workflow --lib`
|
||||
- `cargo test -p ticket`
|
||||
- `cargo test -p tui workspace_panel --lib`
|
||||
- `cargo test -p tui multi_pod --lib`
|
||||
- `cargo test -p yoi panel`
|
||||
- `cargo test -p yoi ticket`
|
||||
- `cargo test -p pod ticket --lib`
|
||||
- `cargo test -p client ticket_role`
|
||||
- `cargo check --workspace --all-targets`
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check HEAD~1..HEAD`
|
||||
- `cargo build -p yoi`
|
||||
- `target/debug/yoi ticket doctor`
|
||||
- `nix build .#yoi --no-link --print-out-paths`
|
||||
|
||||
External review approved after transition-graph enforcement was added.
|
||||
|
||||
|
||||
---
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
# Delegation intent: typed Ticket thread event log
|
||||
|
||||
## Classification
|
||||
|
||||
`implementation-ready` foundational backend/API slice before `explicit-ticket-workflow-state`.
|
||||
|
||||
The explicit workflow-state ticket needs a reliable audit/event path for state transitions. This ticket should first make the existing Ticket thread format more typed and useful without rewriting history or changing panel workflow semantics yet.
|
||||
|
||||
## Intent
|
||||
|
||||
Turn `thread.md` into a concise typed append-only event log for durable Ticket events, especially state transitions and Intake summaries. Keep current state in `item.md` frontmatter; keep full conversations in Pod/session logs.
|
||||
|
||||
## Worktree / branch
|
||||
|
||||
- worktree: `/home/hare/Projects/yoi/.worktree/typed-ticket-thread-event-log`
|
||||
- branch: `work/typed-ticket-thread-event-log`
|
||||
|
||||
This ticket may read tracked `.yoi/tickets` records/design artifacts. Do not read or edit `.yoi/memory/`.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Add/formalize typed event kinds for at least:
|
||||
- `state_changed`;
|
||||
- `intake_summary`.
|
||||
- Preserve existing event kinds and historical compatibility:
|
||||
- `create`;
|
||||
- `comment`;
|
||||
- `plan`;
|
||||
- `decision`;
|
||||
- `implementation_report`;
|
||||
- `review`;
|
||||
- `status_changed`;
|
||||
- `close`.
|
||||
- Add typed Rust data/API for appending a `state_changed` event with fields such as:
|
||||
- `from`;
|
||||
- `to`;
|
||||
- `actor` or `author`;
|
||||
- `reason`;
|
||||
- optional concise `summary` / refs body.
|
||||
- Add typed Rust data/API for appending an `intake_summary` event with a bounded Markdown body and/or structured heading metadata.
|
||||
- Keep `thread.md` append-only.
|
||||
- Do not use thread events as the current-state authority; this ticket may add event APIs, but `explicit-ticket-workflow-state` owns current workflow frontmatter fields.
|
||||
- If a minimal state-transition helper is added, keep it forward-compatible with frontmatter state updates; do not invent a separate scheduler/state machine.
|
||||
- Update parser/doctor to understand the new event kinds and required attributes where practical.
|
||||
- `state_changed` should require `from`, `to`, `author/actor`, and `at` or equivalent.
|
||||
- `intake_summary` should require normal event metadata and a non-empty body where practical.
|
||||
- Keep existing Ticket tools/CLI working.
|
||||
- Avoid mass rewriting historical `thread.md` records.
|
||||
- Update docs/comments that describe thread role/event meanings.
|
||||
|
||||
## Current code map
|
||||
|
||||
- `crates/ticket/src/lib.rs`
|
||||
- `TicketEventKind` currently supports `StatusChanged` and `Other`.
|
||||
- `LocalTicketBackend::append_thread_event(...)` writes HTML comment event metadata.
|
||||
- `parse_thread(...)`, `parse_event_comment(...)`, `doctor_thread_events(...)` parse/validate thread events.
|
||||
- `LocalTicketBackend::add_event(...)`, `review(...)`, `status(...)`, and `close(...)` are existing mutation paths.
|
||||
- `crates/ticket/src/tool.rs`
|
||||
- `TicketComment` currently exposes only `comment`, `plan`, `decision`, `implementation_report` roles. Consider whether to expose new event kinds now or keep them backend-only for first slice.
|
||||
- `crates/yoi/src/ticket_cli.rs`
|
||||
- Direct CLI paths for comment/review/status/close; update only if new event API needs direct maintainer exposure.
|
||||
- `docs/development/work-items.md`
|
||||
- Update thread semantics if touched.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Implementing explicit `workflow_state` frontmatter fields; next ticket owns that.
|
||||
- Replacing Pod/session logs.
|
||||
- Capturing full Intake conversations in Ticket thread.
|
||||
- Rewriting all historical thread files.
|
||||
- Building scheduler/lease/queue behavior.
|
||||
- Panel layout changes.
|
||||
|
||||
## Validation
|
||||
|
||||
Run at least:
|
||||
|
||||
- `cargo test -p ticket thread` or targeted new thread event tests;
|
||||
- `cargo test -p ticket`;
|
||||
- `cargo test -p yoi ticket` if CLI/tool-visible behavior changes;
|
||||
- `cargo test -p pod ticket --lib` if Ticket tools change;
|
||||
- `cargo check --workspace --all-targets`;
|
||||
- `cargo fmt --check`;
|
||||
- `git diff --check`;
|
||||
- `cargo build -p yoi`;
|
||||
- `target/debug/yoi ticket doctor`.
|
||||
|
||||
Run `nix build .#yoi --no-link` if feasible.
|
||||
|
||||
## Completion report
|
||||
|
||||
Report:
|
||||
|
||||
- worktree path / branch;
|
||||
- commit hash;
|
||||
- new event kinds and metadata fields;
|
||||
- backend APIs added;
|
||||
- parser/doctor behavior;
|
||||
- tool/CLI/doc updates, if any;
|
||||
- historical compatibility behavior;
|
||||
- validation results;
|
||||
- whether `explicit-ticket-workflow-state` can proceed.
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
---
|
||||
id: 20260606-221301-typed-ticket-thread-event-log
|
||||
slug: typed-ticket-thread-event-log
|
||||
title: Typed Ticket thread event log for workflow state changes
|
||||
status: closed
|
||||
kind: task
|
||||
priority: P1
|
||||
labels: [ticket, orchestration, state, audit]
|
||||
created_at: 2026-06-06T22:13:01Z
|
||||
updated_at: 2026-06-06T22:48:18Z
|
||||
assignee: null
|
||||
legacy_ticket: null
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
The explicit workflow state design moves current Ticket workflow state into frontmatter, but frontmatter alone only records the current value. The system also needs a small append-only audit trail explaining why state changed and which actor changed it.
|
||||
|
||||
The current Ticket thread is too freeform to be a reliable workflow record. It tends to become a place for arbitrary comments and AI-generated prose, while still not clearly recording state transitions or Intake decisions.
|
||||
|
||||
## Goal
|
||||
|
||||
Make `thread.md` a concise typed event log for durable Ticket events, especially workflow state changes and Intake summaries, rather than a general conversation transcript.
|
||||
|
||||
## Authority split
|
||||
|
||||
- `item.md` frontmatter: current mechanical state authority.
|
||||
- `item.md` body: current human-readable Ticket snapshot: background, goal, requirements, non-goals, acceptance criteria.
|
||||
- `thread.md`: append-only typed event log explaining state transitions, important decisions, implementation reports, reviews, and close events.
|
||||
- Pod/session logs: full conversational/runtime transcript. Do not copy full Pod conversations into Ticket thread.
|
||||
|
||||
## Event model
|
||||
|
||||
Add or formalize typed thread events for at least:
|
||||
|
||||
```text
|
||||
create
|
||||
state_changed
|
||||
intake_summary
|
||||
decision
|
||||
implementation_report
|
||||
review
|
||||
close
|
||||
```
|
||||
|
||||
`comment` may remain for manual escape hatches if needed, but Orchestrator/Intake/panel workflow should not rely on freeform comments as the primary record.
|
||||
|
||||
## State transition logging
|
||||
|
||||
Every workflow state mutation should append a concise `state_changed` event containing at least:
|
||||
|
||||
```text
|
||||
from
|
||||
to
|
||||
actor
|
||||
reason
|
||||
summary or refs
|
||||
```
|
||||
|
||||
State transitions should be performed through backend APIs that update frontmatter and append the thread event as one logical mutation. Avoid separate call sites that can update frontmatter without an event or append an event without updating current state.
|
||||
|
||||
## Intake logging
|
||||
|
||||
Intake should not dump the full Intake Pod conversation into `thread.md`. When Intake materializes a Ticket or marks it `ready`, it should write a concise `intake_summary` event containing:
|
||||
|
||||
- accepted user intent;
|
||||
- important clarified decisions;
|
||||
- requirements / acceptance criteria summary;
|
||||
- known non-goals;
|
||||
- unresolved questions, if any.
|
||||
|
||||
The Ticket body should hold the current snapshot. The thread should hold the reason/history for how that snapshot became accepted.
|
||||
|
||||
## Constraints
|
||||
|
||||
- Keep events concise and bounded.
|
||||
- Do not persist hidden reasoning.
|
||||
- Do not paste raw transcripts by default.
|
||||
- Do not use thread events as the source of current workflow state; current state lives in frontmatter.
|
||||
- Do not store transient Pod activity as Ticket state.
|
||||
- Preserve existing historical thread records; do not mass-rewrite old history unless a migration explicitly requires it.
|
||||
|
||||
## Relationship to explicit workflow state
|
||||
|
||||
This ticket is a companion/prerequisite to `explicit-ticket-workflow-state`. That ticket defines the current-state fields and panel semantics. This ticket defines the audit/event-log mechanics so state transitions remain explainable without relying on noisy freeform thread prose.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Building a scheduler/lease system.
|
||||
- Replacing Pod/session logs.
|
||||
- Capturing full Intake conversations in Ticket records.
|
||||
- Rewriting all historical threads.
|
||||
- Changing public UI layout by itself.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- A typed event model exists for `state_changed` and `intake_summary` at minimum.
|
||||
- Workflow state changes have an API path that updates current state and appends a `state_changed` event together.
|
||||
- Intake-ready/materialization flow has a bounded `intake_summary` event path.
|
||||
- Panel/Orchestrator state changes do not rely on freeform comments for auditability.
|
||||
- Existing Ticket doctor/lint checks accept the new event types and reject malformed required fields where practical.
|
||||
- Documentation explains the split between current state, current Ticket snapshot, append-only thread events, and Pod/session transcripts.
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
Implemented typed Ticket thread event logging for workflow audit events.
|
||||
|
||||
Changes:
|
||||
- Added event kinds:
|
||||
- `state_changed`
|
||||
- `intake_summary`
|
||||
- Preserved existing event kinds and historical compatibility.
|
||||
- Added typed backend data/API:
|
||||
- `TicketStateChange`
|
||||
- `TicketIntakeSummary`
|
||||
- `TicketBackend::add_state_changed(...)`
|
||||
- `TicketBackend::add_intake_summary(...)`
|
||||
- `TicketBackend::set_state_field(...)` as a frontmatter-field update plus `state_changed` event helper for the next workflow-state slice.
|
||||
- Parser now understands quoted event attributes and exposes typed metadata such as `from`, `to`, `reason`, `state_field`, and full attributes.
|
||||
- Doctor validates required fields for `state_changed` and `intake_summary` where practical.
|
||||
- `TicketShow` tool output includes new event metadata fields/attributes.
|
||||
- Thread event append now prevalidates and prerenders metadata before opening/appending `thread.md`, preventing failed appends from corrupting the log.
|
||||
- Create event author validation happens before writing a ticket record.
|
||||
- Documentation now describes `thread.md` as append-only audit history, not current-state authority.
|
||||
|
||||
Validation after merge:
|
||||
- `cargo test -p ticket thread`
|
||||
- `cargo test -p ticket`
|
||||
- `cargo test -p yoi ticket`
|
||||
- `cargo test -p pod ticket --lib`
|
||||
- `cargo check --workspace --all-targets`
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check HEAD~1..HEAD`
|
||||
- `cargo build -p yoi`
|
||||
- `target/debug/yoi ticket doctor`
|
||||
- `nix build .#yoi --no-link --print-out-paths`
|
||||
|
||||
External review approved after fixing prevalidation/partial-append safety.
|
||||
|
||||
`explicit-ticket-workflow-state` can proceed next.
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
<!-- event: create author: yoi ticket at: 2026-06-06T22:13:01Z -->
|
||||
|
||||
## Created
|
||||
|
||||
Created by LocalTicketBackend create.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: plan author: hare at: 2026-06-06T22:14:29Z -->
|
||||
|
||||
## Plan
|
||||
|
||||
Created as a companion split from `explicit-ticket-workflow-state`.
|
||||
|
||||
This ticket owns making Ticket `thread.md` a concise typed append-only event log for workflow state transitions and Intake summaries, rather than a freeform transcript/comment sink. It should define/implement events such as `state_changed` and `intake_summary`, and provide backend APIs that keep frontmatter current state and thread transition events in sync.
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: plan author: hare at: 2026-06-06T22:16:04Z -->
|
||||
|
||||
## Plan
|
||||
|
||||
Preflight result: `implementation-ready` as the foundational backend/API slice before `explicit-ticket-workflow-state`.
|
||||
|
||||
This ticket should formalize Ticket `thread.md` as a concise typed append-only event log by adding state-transition and Intake-summary event types/APIs while preserving existing historical thread compatibility. It should not add workflow_state frontmatter yet; that is the next ticket.
|
||||
|
||||
Detailed delegation intent is recorded in `artifacts/delegation-intent.md`.
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: review author: hare at: 2026-06-06T22:48:18Z status: approve -->
|
||||
|
||||
## Review: approve
|
||||
|
||||
External reviewer approved the implementation after one requested-changes cycle.
|
||||
|
||||
Review summary:
|
||||
- `state_changed` and `intake_summary` are typed event kinds.
|
||||
- Existing event kinds and historical compatibility are preserved.
|
||||
- Thread event metadata is prevalidated/prerendered before append, so failed appends do not corrupt `thread.md`.
|
||||
- Create event author validation happens before writing a ticket record.
|
||||
- Parser/doctor understand the new event kinds and validate required fields where practical.
|
||||
- No premature `workflow_state` authority or scheduler/state-machine behavior was introduced.
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: close author: hare at: 2026-06-06T22:48:18Z status: closed -->
|
||||
|
||||
## Closed
|
||||
|
||||
Implemented typed Ticket thread event logging for workflow audit events.
|
||||
|
||||
Changes:
|
||||
- Added event kinds:
|
||||
- `state_changed`
|
||||
- `intake_summary`
|
||||
- Preserved existing event kinds and historical compatibility.
|
||||
- Added typed backend data/API:
|
||||
- `TicketStateChange`
|
||||
- `TicketIntakeSummary`
|
||||
- `TicketBackend::add_state_changed(...)`
|
||||
- `TicketBackend::add_intake_summary(...)`
|
||||
- `TicketBackend::set_state_field(...)` as a frontmatter-field update plus `state_changed` event helper for the next workflow-state slice.
|
||||
- Parser now understands quoted event attributes and exposes typed metadata such as `from`, `to`, `reason`, `state_field`, and full attributes.
|
||||
- Doctor validates required fields for `state_changed` and `intake_summary` where practical.
|
||||
- `TicketShow` tool output includes new event metadata fields/attributes.
|
||||
- Thread event append now prevalidates and prerenders metadata before opening/appending `thread.md`, preventing failed appends from corrupting the log.
|
||||
- Create event author validation happens before writing a ticket record.
|
||||
- Documentation now describes `thread.md` as append-only audit history, not current-state authority.
|
||||
|
||||
Validation after merge:
|
||||
- `cargo test -p ticket thread`
|
||||
- `cargo test -p ticket`
|
||||
- `cargo test -p yoi ticket`
|
||||
- `cargo test -p pod ticket --lib`
|
||||
- `cargo check --workspace --all-targets`
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check HEAD~1..HEAD`
|
||||
- `cargo build -p yoi`
|
||||
- `target/debug/yoi ticket doctor`
|
||||
- `nix build .#yoi --no-link --print-out-paths`
|
||||
|
||||
External review approved after fixing prevalidation/partial-append safety.
|
||||
|
||||
`explicit-ticket-workflow-state` can proceed next.
|
||||
|
||||
|
||||
---
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
---
|
||||
id: 20260607-001651-workspace-panel-remove-direct-pod-send
|
||||
slug: workspace-panel-remove-direct-pod-send
|
||||
title: Remove workspace panel direct Pod send
|
||||
status: closed
|
||||
kind: task
|
||||
priority: P1
|
||||
labels: [tui, panel, companion, cleanup]
|
||||
workflow_state: done
|
||||
created_at: 2026-06-07T00:16:51Z
|
||||
updated_at: 2026-06-07T02:00:58Z
|
||||
assignee: null
|
||||
legacy_ticket: null
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
The panel currently uses `Companion` as the default composer target, but implementation-wise this is selected-Pod direct send. That is no longer desired.
|
||||
|
||||
The panel should not provide an arbitrary direct-message path to selected Pods. Users can attach/open Pods to inspect details, and Ticket Intake remains the path for new work requests. A real workspace Companion Pod will become the foreground management chat in a follow-up ticket.
|
||||
|
||||
## Goal
|
||||
|
||||
Remove selected-Pod direct send from `yoi panel` and stop presenting it as Companion behavior.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Remove or disable the panel code path that sends composer text directly to the selected Pod via `Method::Run`.
|
||||
- Remove UI labels/key hints that imply selected-Pod direct send is supported.
|
||||
- Preserve Pod attach/open behavior for inspection.
|
||||
- Preserve Ticket Intake composer target and launch behavior.
|
||||
- Preserve Orchestrator lifecycle and Ticket action dispatch.
|
||||
- No-Ticket workspaces should remain useful for Pod discovery/attach/open even without direct send.
|
||||
- If composer input cannot yet be routed to a real Companion in this ticket, show a clear bounded diagnostic instead of falling back to selected-Pod direct send.
|
||||
- Do not reintroduce `--multi` or `:ticket`.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Implementing real Companion Pod lifecycle; `workspace-panel-companion-pod-lifecycle` owns that.
|
||||
- Companion prompt/profile/tool policy; `companion-status-context-tool-policy` owns that.
|
||||
- Removing attach/open.
|
||||
- Removing Ticket Intake.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- Panel composer text is no longer sent directly to arbitrary selected Pods.
|
||||
- Existing tests that assumed selected-Pod direct send are updated or removed.
|
||||
- Pod attach/open still works.
|
||||
- Ticket Intake still works.
|
||||
- UI/key hints do not advertise direct Pod send.
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
Implemented and merged.
|
||||
|
||||
Summary:
|
||||
- Removed the workspace panel path that treated the `Companion` composer target as direct selected-Pod `Method::Run` sending.
|
||||
- Non-empty Companion submit now preserves the draft and reports that the real Companion lifecycle is not connected yet.
|
||||
- Preserved Pod open/attach behavior through empty Enter / `o`.
|
||||
- Preserved Ticket Intake composer launch behavior.
|
||||
- Updated Pod row action/key-hint behavior away from direct-send semantics.
|
||||
- Updated focused tests for the new behavior.
|
||||
|
||||
Merged implementation:
|
||||
- Child commit: `393cde9 tui: remove panel direct pod send`
|
||||
- Main merge commit: `merge: remove panel direct pod send`
|
||||
|
||||
Validation after merge:
|
||||
- `cargo test -p tui multi_pod --lib`
|
||||
- `cargo test -p tui workspace_panel --lib`
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check`
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
<!-- event: create author: "yoi ticket" at: 2026-06-07T00:16:51Z -->
|
||||
|
||||
## Created
|
||||
|
||||
Created by LocalTicketBackend create.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: plan author: hare at: 2026-06-07T01:32:12Z -->
|
||||
|
||||
## Plan
|
||||
|
||||
## Preflight / implementation intent
|
||||
|
||||
Classification: implementation-ready.
|
||||
|
||||
Intent:
|
||||
- Remove the workspace panel path that treats `Companion` composer input as a direct `Method::Run` send to the selected Pod.
|
||||
- Keep Pod open/attach, Ticket Intake launch, Orchestrator lifecycle, Queue/Ticket actions, and Pod discovery intact.
|
||||
- Until the real Companion Pod lifecycle exists, Companion composer submit should produce a bounded diagnostic instead of falling back to selected-Pod direct send.
|
||||
|
||||
Requirements:
|
||||
- `Enter` on non-empty Companion composer input must not send to `selected_pod.socket_path`.
|
||||
- UI/status/key hints must stop advertising selected-Pod direct send.
|
||||
- Pod rows should advertise/open attach behavior, not direct-send behavior.
|
||||
- Ticket Intake target must still build and launch Intake requests.
|
||||
- Existing no-Ticket panel remains useful for discovering and opening Pods.
|
||||
|
||||
Invariants / boundaries:
|
||||
- Do not implement real Companion Pod lifecycle here; that belongs to `workspace-panel-companion-pod-lifecycle`.
|
||||
- Do not change Companion tool/profile policy here; that belongs to `companion-status-context-tool-policy`.
|
||||
- Do not remove Pod attach/open.
|
||||
- Do not reintroduce single-Pod `:ticket` or old `--multi` semantics.
|
||||
- Do not change Ticket workflow-state semantics.
|
||||
|
||||
Current code map:
|
||||
- `crates/tui/src/multi_pod.rs`: `MultiPodApp::prepare_send`, `send_run_and_confirm`, Enter handling, status/help text, and tests currently embody selected-Pod direct send.
|
||||
- `crates/tui/src/workspace_panel.rs`: `NextUserAction::SendToPod`, `pod_row`, and key hints currently advertise direct-send behavior for Pod rows.
|
||||
- `crates/tui/src/multi_pod.rs`: `prepare_intake_launch` / `launch_intake_with_handoff` must remain intact for Ticket Intake.
|
||||
|
||||
Critical risks:
|
||||
- Accidentally removing/weakening open/attach while deleting send behavior.
|
||||
- Leaving stale status-bar/help text that still claims Enter sends to selected Pods.
|
||||
- Conflating this cleanup with real Companion lifecycle and creating a fake/partial Companion implementation.
|
||||
- Breaking Ticket Intake Enter behavior while removing Companion direct send.
|
||||
|
||||
Validation:
|
||||
- Update focused `tui` unit tests that currently assert direct-send eligibility and non-empty Enter direct-send behavior.
|
||||
- Run focused tests around `multi_pod` and `workspace_panel`, plus formatting/checks as appropriate for the diff.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: intake_summary author: INSOMNIA at: 2026-06-07T01:32:18Z -->
|
||||
|
||||
## Intake summary
|
||||
|
||||
Implementation intent is clear: remove selected-Pod direct send from the workspace panel, keep Pod open/attach and Ticket Intake, and show a bounded diagnostic for Companion composer submit until the real Companion Pod lifecycle is implemented.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: INSOMNIA at: 2026-06-07T01:32:18Z from: intake to: ready reason: intake_ready field: workflow_state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Ticket intake complete; workflow_state intake -> ready.
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: hare at: 2026-06-07T01:33:27Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
## Delegation
|
||||
|
||||
Implementation delegated to child Pod `remove-direct-pod-send-coder-20260607` in worktree `.worktree/workspace-panel-remove-direct-pod-send` on branch `work/workspace-panel-remove-direct-pod-send`.
|
||||
|
||||
Scope:
|
||||
- Remove selected-Pod direct send from `yoi panel` Companion composer behavior.
|
||||
- Keep Pod open/attach and Ticket Intake launch working.
|
||||
- Show a bounded diagnostic for Companion composer submit until real Companion lifecycle exists.
|
||||
- Update focused `multi_pod` / `workspace_panel` tests and run focused validation.
|
||||
|
||||
The child should commit implementation work in the child worktree and report diff/tests. Merge, review, Ticket closure, and cleanup remain with the parent/human workflow.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: review author: INSOMNIA at: 2026-06-07T02:00:40Z status: approve -->
|
||||
|
||||
## Review: approve
|
||||
|
||||
Approved after reviewing the child implementation and post-merge validation.
|
||||
|
||||
Review notes:
|
||||
- The workspace panel no longer sends non-empty Companion composer input to the selected Pod via direct `Method::Run`.
|
||||
- Non-empty Companion submit now keeps the draft and reports that the real Companion lifecycle is not connected yet.
|
||||
- Empty Enter and `o` still open/attach the selected Pod.
|
||||
- Ticket Intake Enter still builds the Intake launch request and is not treated as direct selected-Pod send.
|
||||
- Pod rows now advertise open behavior rather than send behavior.
|
||||
|
||||
Validation run after merge:
|
||||
- `cargo test -p tui multi_pod --lib`
|
||||
- `cargo test -p tui workspace_panel --lib`
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check`
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: hare at: 2026-06-07T02:00:58Z from: ready to: done reason: closed field: workflow_state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Ticket closed; workflow_state set to done.
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: close author: hare at: 2026-06-07T02:00:58Z status: closed -->
|
||||
|
||||
## Closed
|
||||
|
||||
Implemented and merged.
|
||||
|
||||
Summary:
|
||||
- Removed the workspace panel path that treated the `Companion` composer target as direct selected-Pod `Method::Run` sending.
|
||||
- Non-empty Companion submit now preserves the draft and reports that the real Companion lifecycle is not connected yet.
|
||||
- Preserved Pod open/attach behavior through empty Enter / `o`.
|
||||
- Preserved Ticket Intake composer launch behavior.
|
||||
- Updated Pod row action/key-hint behavior away from direct-send semantics.
|
||||
- Updated focused tests for the new behavior.
|
||||
|
||||
Merged implementation:
|
||||
- Child commit: `393cde9 tui: remove panel direct pod send`
|
||||
- Main merge commit: `merge: remove panel direct pod send`
|
||||
|
||||
Validation after merge:
|
||||
- `cargo test -p tui multi_pod --lib`
|
||||
- `cargo test -p tui workspace_panel --lib`
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check`
|
||||
|
||||
---
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
---
|
||||
id: 20260607-012131-workspace-panel-local-role-session-registry
|
||||
slug: workspace-panel-local-role-session-registry
|
||||
title: Workspace panel local role session registry
|
||||
status: closed
|
||||
kind: task
|
||||
priority: P1
|
||||
labels: [tui, panel, ticket, pod, orchestration]
|
||||
workflow_state: done
|
||||
created_at: 2026-06-07T01:21:31Z
|
||||
updated_at: 2026-06-07T02:34:48Z
|
||||
assignee: null
|
||||
legacy_ticket: null
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
The panel now has two Ticket/Intake entry paths:
|
||||
|
||||
- A user instruction can start a pre-Ticket Intake session from the panel composer.
|
||||
- An already-existing Ticket in `workflow_state = intake` can later need an Intake session.
|
||||
|
||||
For now, `ticket-*` Pods being visible in the panel is an acceptable interim access path. Longer term, the panel needs an explicit local model for role sessions and Ticket claims.
|
||||
|
||||
Ticket metadata and thread files are git-managed project records. Local Pod names, session/socket/runtime state, and per-machine assignment state must not be written into Ticket frontmatter or other git-tracked Ticket records.
|
||||
|
||||
Intake sessions are not necessarily 1:1 with Tickets. A pre-Ticket Intake session may have no Ticket yet, may materialize one Ticket, or may split the request into multiple Tickets. Conversely, an existing Ticket may be clarified by a later Intake session.
|
||||
|
||||
## Goal
|
||||
|
||||
Introduce a local, workspace-scoped role session registry and Ticket claim index for panel orchestration state, without storing local Pod assignment details in git-tracked Ticket metadata.
|
||||
|
||||
## Target model
|
||||
|
||||
- Ticket project records keep durable workflow state such as `workflow_state`, `attention_required`, and review/summary events.
|
||||
- Local Pod/session assignment lives in a user-data workspace overlay, not under git-tracked `.yoi/tickets` files.
|
||||
- A Ticket may have at most one active local Pod claim at a time.
|
||||
- A Pod/session may relate to zero or more Tickets.
|
||||
- Pre-Ticket Intake sessions are represented as local role sessions even before any Ticket exists.
|
||||
- Existing-Ticket Intake uses the Ticket claim index to prevent a second Pod from attaching to an already-claimed Ticket.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Define a workspace-scoped local storage location for panel role/session overlay data under the user data directory.
|
||||
- Model local role sessions keyed by Pod/session identity, including at least role, pod name, origin, created/updated timestamps, and related Ticket refs.
|
||||
- Model Ticket claims keyed by stable Ticket id, with at most one active local Pod claim per Ticket.
|
||||
- Do not write local Pod names, socket paths, runtime status, or local claim state into git-tracked Ticket frontmatter/thread files.
|
||||
- Allow one Intake Pod/session to relate to multiple Tickets.
|
||||
- Allow a pre-Ticket Intake session to exist with no related Ticket.
|
||||
- When starting Intake for an existing Ticket, claim before spawning/restoring so double-spawn races are avoided.
|
||||
- If a Ticket already has a local claim, do not start a second Pod automatically.
|
||||
- If the claimed Pod is live, offer/open/attach that Pod.
|
||||
- If the claimed Pod is restorable, offer restore/open.
|
||||
- If the claim is stale, show an explicit reclaim action/diagnostic rather than silently starting a second Pod.
|
||||
- Panel should join Ticket state, local overlay, and Pod metadata for display/actions.
|
||||
- Do not introduce polling that automatically starts Intake for newly-created `workflow_state = intake` Tickets.
|
||||
- Keep the current `ticket-*` Pod visibility as an acceptable interim access path until the registry UI is implemented.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Storing local Pod assignment in Ticket metadata.
|
||||
- Assuming Intake Pod and Ticket are 1:1.
|
||||
- Automatically starting Intake by polling Ticket files.
|
||||
- Replacing the Orchestrator scheduling model.
|
||||
- Full cross-machine coordination; this is a local runtime overlay.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- Local role session / Ticket claim data is stored outside git-tracked Ticket records.
|
||||
- A Ticket cannot be claimed by two active local Pods through panel actions.
|
||||
- A single Intake Pod can be associated with multiple Tickets in the local model.
|
||||
- A pre-Ticket Intake session can be represented before any Ticket exists.
|
||||
- Panel can distinguish at least: no claim, live/restorable claimed Pod, and stale claim.
|
||||
- Existing tests or new tests cover the non-1:1 Intake/Ticket relation and one-active-claim-per-Ticket invariant.
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
Implemented, externally reviewed, merged, and validated.
|
||||
|
||||
Summary:
|
||||
- Added a local workspace-scoped panel role session registry under user data.
|
||||
- Added local Ticket claim files to enforce at most one active local Pod claim per Ticket.
|
||||
- Supported pre-Ticket Intake sessions and 0..N related Tickets per role session.
|
||||
- Preserved `ticket-*` Pod heuristic visibility while adding local claim status display.
|
||||
- Existing-Ticket Intake now claims before launch and refuses to silently start a second Pod when a live/restorable/stale claim exists.
|
||||
- Local Pod assignment is not written into git-tracked Ticket metadata/thread.
|
||||
- Preserved the already-merged no-direct-selected-Pod-send behavior.
|
||||
|
||||
Merged implementation:
|
||||
- `4890590 tui: add panel role session registry`
|
||||
- `2f3f54b fixup! tui: add panel role session registry`
|
||||
- Merge commit: `merge: panel role session registry`
|
||||
|
||||
Review:
|
||||
- Initial review requested changes for direct-send semantic conflict, incomplete registry schema, and launch failure/recording behavior.
|
||||
- Fixup addressed blockers.
|
||||
- Follow-up review approved with only non-blocking follow-ups: stale-lock recovery and temp claim file housekeeping.
|
||||
|
||||
Validation after merge:
|
||||
- `cargo test -p tui role_session_registry --lib`
|
||||
- `cargo test -p tui workspace_panel --lib`
|
||||
- `cargo test -p tui multi_pod --lib`
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check`
|
||||
- `target/debug/yoi ticket doctor`
|
||||
|
|
@ -0,0 +1,209 @@
|
|||
<!-- event: create author: LocalTicketBackend at: 2026-06-07T01:21:31Z -->
|
||||
|
||||
## Created
|
||||
|
||||
Created by LocalTicketBackend create.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: plan author: hare at: 2026-06-07T01:42:08Z -->
|
||||
|
||||
## Plan
|
||||
|
||||
## Preflight / implementation intent
|
||||
|
||||
Classification: implementation-ready with coordination risk.
|
||||
|
||||
Intent:
|
||||
- Add a local, workspace-scoped role session registry and Ticket claim index for `yoi panel` orchestration state.
|
||||
- Store registry data under the configured user data directory, not under git-tracked `.yoi/tickets` files and not under the runtime directory.
|
||||
- Model `Ticket -> at most one active local Pod claim` while allowing `Pod/session -> 0..N related Tickets`.
|
||||
- Represent pre-Ticket Intake sessions even before a Ticket exists.
|
||||
- Join Ticket state, local registry data, and Pod metadata in panel state/actions where practical, without introducing automatic polling/spawn.
|
||||
|
||||
Requirements:
|
||||
- Define a stable workspace key/path for registry storage under `manifest::paths::data_dir()` or an equivalent existing user-data boundary.
|
||||
- Keep local Pod names, socket paths, runtime status, and local claim state out of git-tracked Ticket frontmatter/thread files.
|
||||
- Make claim creation race-aware: when starting Intake for an existing Ticket, claim before spawn/restore; if a claim exists, do not start a second Pod silently.
|
||||
- Treat stale claims explicitly with diagnostics/reclaim affordance rather than auto-replacing them.
|
||||
- Intake is not 1:1 with Ticket: support no related Ticket and multiple related Tickets in the local model.
|
||||
- Preserve the current interim access path where `ticket-*` Pods are visible in the panel.
|
||||
|
||||
Code map:
|
||||
- `crates/manifest/src/paths.rs`: existing data/config/runtime directory boundary; registry should use durable user data, not runtime.
|
||||
- `crates/tui/src/multi_pod.rs`: Intake launch path (`prepare_intake_launch` / `finish_intake_launch`), panel snapshot construction, selected actions.
|
||||
- `crates/tui/src/workspace_panel.rs`: Ticket rows, related Pod display, `workflow_state = intake` row behavior, Panel view model.
|
||||
- `crates/client/src/ticket_role.rs`: Ticket role launch context and Pod naming; useful integration point but should not write git-tracked Ticket assignment.
|
||||
- Existing Pod list/status model is in `crates/tui/src/multi_pod.rs` / `workspace_panel.rs`; use it for liveness/restorability rather than persisting live status as authority.
|
||||
|
||||
Boundaries / non-goals:
|
||||
- Do not store local Pod assignment in Ticket metadata/thread.
|
||||
- Do not assume Intake Pod and Ticket are 1:1.
|
||||
- Do not add polling that auto-starts Intake for `workflow_state = intake` Tickets.
|
||||
- Do not replace Orchestrator scheduling.
|
||||
- Do not implement the real Companion Pod lifecycle here.
|
||||
- Do not remove selected-Pod direct-send here; `workspace-panel-remove-direct-pod-send` owns that and is being implemented in parallel.
|
||||
|
||||
Coordination note:
|
||||
- `workspace-panel-remove-direct-pod-send` is active in branch `work/workspace-panel-remove-direct-pod-send` and may touch `multi_pod.rs` / `workspace_panel.rs`. Keep this registry implementation as additive/localized as possible and report any likely merge conflicts.
|
||||
|
||||
Validation:
|
||||
- Add focused unit tests for registry serialization/path behavior, one-active-claim-per-Ticket invariant, and non-1:1 Intake/Ticket relation.
|
||||
- Run focused `cargo test -p tui workspace_panel --lib` and relevant new registry tests; run `cargo test -p tui multi_pod --lib` if integration touches panel runtime paths; run formatting/checks appropriate for the diff.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: intake_summary author: INSOMNIA at: 2026-06-07T01:42:13Z -->
|
||||
|
||||
## Intake summary
|
||||
|
||||
Implementation intent is clear: add a user-data workspace-scoped local role/session registry and Ticket claim index, enforce one active local Pod claim per Ticket, allow one Intake session to relate to multiple Tickets or none, and keep local Pod assignment out of git-tracked Ticket records.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: INSOMNIA at: 2026-06-07T01:42:13Z from: intake to: ready reason: intake_ready field: workflow_state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Ticket intake complete; workflow_state intake -> ready.
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: hare at: 2026-06-07T01:42:51Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
## Delegation
|
||||
|
||||
Implementation delegated to child Pod `panel-role-session-registry-coder-20260607` in worktree `.worktree/workspace-panel-local-role-session-registry` on branch `work/workspace-panel-local-role-session-registry`.
|
||||
|
||||
Scope:
|
||||
- Add a durable user-data workspace-scoped local role session registry and Ticket claim index.
|
||||
- Enforce one active local Pod claim per Ticket while allowing a role session/Intake Pod to relate to zero or more Tickets.
|
||||
- Keep local Pod assignment out of git-tracked Ticket metadata/thread.
|
||||
- Preserve no-polling semantics and the interim `ticket-*` Pod visibility path.
|
||||
|
||||
Coordination note: `workspace-panel-remove-direct-pod-send` is active in parallel and may touch `multi_pod.rs` / `workspace_panel.rs`; the child was instructed to keep the registry implementation additive/localized and report likely conflicts.
|
||||
|
||||
The child should commit implementation work in the child worktree and report diff/tests. Merge, review, Ticket closure, and cleanup remain with the parent/human workflow.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: hare at: 2026-06-07T02:04:23Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
## Review delegation
|
||||
|
||||
External review delegated to `panel-role-session-registry-reviewer-20260607` with read-only scope over the repository.
|
||||
|
||||
Review focus:
|
||||
- Validate the local user-data registry and Ticket claim model against the ticket intent.
|
||||
- Check one-active-claim-per-Ticket behavior, non-1:1 Intake/Ticket support, claim-before-spawn behavior, stale claim handling, and workspace-key/path choices.
|
||||
- Confirm local Pod assignment is not written to git-tracked Ticket metadata/thread.
|
||||
- Explicitly review semantic/merge conflicts with current `develop`, because `workspace-panel-remove-direct-pod-send` has already landed and touched `multi_pod.rs` / `workspace_panel.rs` after this implementation branch was created.
|
||||
|
||||
The reviewer is instructed not to modify files and to return approve/request-changes guidance with evidence and merge-readiness notes.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: review author: panel-role-session-registry-reviewer-20260607 at: 2026-06-07T02:11:38Z status: request_changes -->
|
||||
|
||||
## Review: request changes
|
||||
|
||||
Request changes.
|
||||
|
||||
Blockers:
|
||||
|
||||
1. Semantic conflict with current `develop`: direct Pod send is reintroduced. `workspace-panel-remove-direct-pod-send` has landed, but this branch still carries the old direct-send path and adds back `DirectSendRequest`, `prepare_send`, `finish_send`, `DirectSendError`, `NextUserAction::SendToPod`, and Pod-row direct-send behavior. The registry branch must be rebased/resolved so it preserves current panel behavior: no selected-Pod direct send, Pod rows open/attach only.
|
||||
|
||||
2. The role session record is missing required fields. The Ticket requires local role sessions to include at least role, pod name, origin, created/updated timestamps, and related Ticket refs. Implemented `RoleSessionRecord` only has pod_name, role, optional session_id, and ticket_ids. Add origin, created_at, updated_at, and keep related Ticket refs compatible with the non-1:1 Intake/Ticket model.
|
||||
|
||||
3. Launch failure / registry recording can lose local coordination state. Existing-Ticket path claims before launch, but `launch_intake_with_handoff` releases the claim on any launch error. `launch_ticket_role_pod_with_options` can fail after `spawn_pod` succeeds during connect, pre-run/send, or run-acceptance, leaving a live/restorable/stale Pod with no claim and allowing a later second launch. Successful pre-Ticket Intake registry recording is also best-effort only, so a successful launch can silently fail to be represented.
|
||||
|
||||
Non-blocking concerns:
|
||||
- Claim creation uses `create_new`, which is good for one-claim invariant, but the file is visible before JSON content is fully written; transient partial reads can produce poor diagnostics.
|
||||
- `role-sessions.json` updates are read-modify-write without locking, so concurrent `record_session` calls can lose refs.
|
||||
- Workspace key/path behavior is likely acceptable for local user-data but should be a conscious decision; symlinked/moved workspaces create distinct registries, and full workspace_root is stored in JSON.
|
||||
|
||||
Positive evidence:
|
||||
- Registry files are under user data dir, not `.yoi/tickets`.
|
||||
- The implementation does not modify git-tracked Ticket files for local Pod assignment.
|
||||
- Normal existing-Ticket Intake checks existing claim and refuses a second Pod, with status derived from current Pod list.
|
||||
- Panel display preserves interim `ticket-*` heuristic while adding claim Pod names.
|
||||
- Pre-Ticket Intake names are unique and do not require Ticket id.
|
||||
|
||||
Merge readiness: not merge-ready. Needs rebase/semantic conflict fix against current `develop`, schema additions, and failure-behavior hardening, followed by focused tests and build validation.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: review author: panel-role-session-registry-reviewer-20260607 at: 2026-06-07T02:34:09Z status: approve -->
|
||||
|
||||
## Review: approve
|
||||
|
||||
Approved after fixup commit `2f3f54b`.
|
||||
|
||||
Reviewer verified:
|
||||
- The selected-Pod direct-send path is not reintroduced; no `DirectSendRequest`, `prepare_send`, `finish_send`, `DirectSendError`, or `NextUserAction::SendToPod` panel path remains.
|
||||
- Pod rows use open/attach-style behavior and Companion non-empty submit remains rejected with the Companion lifecycle diagnostic.
|
||||
- `RoleSessionRecord` includes `role`, `pod_name`, `origin`, `created_at`, `updated_at`, `related_tickets`, and optional `session_id`.
|
||||
- `RoleSessionOrigin` distinguishes pre-Ticket Intake from existing-Ticket claims.
|
||||
- Related Tickets are represented as a vector, preserving 0..N Ticket relationships.
|
||||
- Existing-Ticket claim is created before launch and is not broadly released after launch failures that may have spawned a Pod.
|
||||
- Pre-Ticket successful launch surfaces registry recording failure as a warning/diagnostic.
|
||||
- Claim files avoid visible partial JSON by writing a temp file then linking into place.
|
||||
- `role-sessions.json` updates use a local lock guard.
|
||||
- Live/restorable/stale status is derived from current Pod list/metadata at panel render time.
|
||||
- Local Pod assignment is stored under user data panel workspace storage, not `.yoi/tickets`.
|
||||
- `git merge-tree --write-tree develop work/workspace-panel-local-role-session-registry` exits 0, and no semantic conflict with current direct-send removal was found.
|
||||
|
||||
Non-blocking follow-ups:
|
||||
- Registry lock has no stale-lock recovery.
|
||||
- Temp claim files may remain after a crash before link/cleanup, though they are not read as claim JSON.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: hare at: 2026-06-07T02:34:48Z from: ready to: done reason: closed field: workflow_state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Ticket closed; workflow_state set to done.
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: close author: hare at: 2026-06-07T02:34:48Z status: closed -->
|
||||
|
||||
## Closed
|
||||
|
||||
Implemented, externally reviewed, merged, and validated.
|
||||
|
||||
Summary:
|
||||
- Added a local workspace-scoped panel role session registry under user data.
|
||||
- Added local Ticket claim files to enforce at most one active local Pod claim per Ticket.
|
||||
- Supported pre-Ticket Intake sessions and 0..N related Tickets per role session.
|
||||
- Preserved `ticket-*` Pod heuristic visibility while adding local claim status display.
|
||||
- Existing-Ticket Intake now claims before launch and refuses to silently start a second Pod when a live/restorable/stale claim exists.
|
||||
- Local Pod assignment is not written into git-tracked Ticket metadata/thread.
|
||||
- Preserved the already-merged no-direct-selected-Pod-send behavior.
|
||||
|
||||
Merged implementation:
|
||||
- `4890590 tui: add panel role session registry`
|
||||
- `2f3f54b fixup! tui: add panel role session registry`
|
||||
- Merge commit: `merge: panel role session registry`
|
||||
|
||||
Review:
|
||||
- Initial review requested changes for direct-send semantic conflict, incomplete registry schema, and launch failure/recording behavior.
|
||||
- Fixup addressed blockers.
|
||||
- Follow-up review approved with only non-blocking follow-ups: stale-lock recovery and temp claim file housekeeping.
|
||||
|
||||
Validation after merge:
|
||||
- `cargo test -p tui role_session_registry --lib`
|
||||
- `cargo test -p tui workspace_panel --lib`
|
||||
- `cargo test -p tui multi_pod --lib`
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check`
|
||||
- `target/debug/yoi ticket doctor`
|
||||
|
||||
---
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
---
|
||||
id: 20260607-031439-ticket-init-role-profile-scaffold
|
||||
slug: ticket-init-role-profile-scaffold
|
||||
title: Scaffold explicit Ticket role profiles in init config
|
||||
status: closed
|
||||
kind: task
|
||||
priority: P1
|
||||
labels: [ticket, config, init, profiles, panel]
|
||||
workflow_state: done
|
||||
created_at: 2026-06-07T03:14:39Z
|
||||
updated_at: 2026-06-07T04:05:15Z
|
||||
assignee: null
|
||||
legacy_ticket: null
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
Fallback audit found that `.yoi/ticket.config.toml` can currently contain only backend settings. In that case Ticket config parsing succeeds, but fixed Ticket role profiles fall back to `inherit`, which top-level Panel/CLI Ticket role launch rejects. This causes Panel Intake / Orchestrator launch failures such as:
|
||||
|
||||
```text
|
||||
Ticket role profile 'inherit' cannot be used for top-level launch execution; configure a concrete role profile selector
|
||||
```
|
||||
|
||||
The desired policy is to distinguish explicit builtin preset selection from implicit runtime fallback. It is acceptable for init/scaffold to generate a working config that explicitly names builtin presets. It is not acceptable for runtime to silently fill missing role config with builtin/default/inherit.
|
||||
|
||||
## Goal
|
||||
|
||||
Make project init/scaffold generate a complete, explicit Ticket role configuration suitable for Panel Intake and Orchestrator launch.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Locate or introduce the init/scaffold path that creates `.yoi/ticket.config.toml` for a workspace.
|
||||
- Generate explicit backend config:
|
||||
- `[backend] provider = "builtin:yoi_local"`
|
||||
- `root = ".yoi/tickets"`
|
||||
- Generate explicit fixed role tables for at least:
|
||||
- `[roles.intake]`
|
||||
- `[roles.orchestrator]`
|
||||
- `[roles.coder]`
|
||||
- `[roles.reviewer]`
|
||||
- `[roles.investigator]`
|
||||
- Each role table must include a concrete `profile`, not `inherit`.
|
||||
- Minimal initial implementation may explicitly use `builtin:default` if role-specific builtin profiles are not ready yet.
|
||||
- If role-specific builtin profiles are introduced, use explicit selectors such as `builtin:ticket-intake`, `builtin:ticket-orchestrator`, etc.
|
||||
- Each role table should include explicit `workflow` for readability and reproducibility:
|
||||
- intake: `ticket-intake-workflow`
|
||||
- orchestrator: `ticket-orchestrator-routing`
|
||||
- coder/reviewer: `multi-agent-workflow` or the agreed workflow for those roles
|
||||
- investigator: the agreed read/investigation workflow
|
||||
- Generated config must pass Ticket role launch validation for Panel Intake and workspace Orchestrator.
|
||||
- Do not implement runtime fallback from missing role profiles to builtin presets in this ticket.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Making builtin role profiles an implicit runtime fallback.
|
||||
- Allowing `inherit` for top-level Ticket role launch.
|
||||
- Redesigning provider/model fallback policy.
|
||||
- Implementing full Companion lifecycle.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- A newly initialized workspace has explicit concrete Ticket role profiles in `.yoi/ticket.config.toml`.
|
||||
- Panel Intake launch planning does not fail because generated role profile is `inherit` or missing.
|
||||
- Workspace Orchestrator launch planning does not fail because generated role profile is `inherit` or missing.
|
||||
- Tests cover generated config shape and successful role-launch validation against the scaffolded config.
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
Implemented, reviewed, merged, validated, and applied to this workspace config.
|
||||
|
||||
Summary:
|
||||
- Added a narrow `yoi ticket init` command.
|
||||
- The command creates `.yoi/ticket.config.toml` only when missing and refuses to overwrite an existing config with an actionable diagnostic.
|
||||
- Added `ticket_config_scaffold()` for the generated config body.
|
||||
- Generated config includes explicit backend config and fixed role tables for intake, orchestrator, coder, reviewer, and investigator.
|
||||
- Each generated role uses explicit `profile = "builtin:default"` and explicit default workflow.
|
||||
- No runtime fallback was added or loosened.
|
||||
- The generated config validates for Ticket role launch planning under the strict validation implemented by `ticket-role-launch-config-strict-validation`.
|
||||
- This repository's `.yoi/ticket.config.toml` was updated to the scaffolded explicit role-profile shape for dogfooding.
|
||||
|
||||
Implementation:
|
||||
- Child commit: `f265098 ticket: add ticket config init scaffold`
|
||||
- Merge commit: `merge: ticket init scaffold`
|
||||
|
||||
Review:
|
||||
- External reviewer `ticket-init-scaffold-reviewer-20260607` approved with no blockers.
|
||||
|
||||
Validation after merge:
|
||||
- `cargo test -p ticket config --lib`
|
||||
- `cargo test -p client ticket_role --lib`
|
||||
- `cargo test -p yoi ticket`
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check`
|
||||
- `target/debug/yoi ticket doctor`
|
||||
|
|
@ -0,0 +1,202 @@
|
|||
<!-- event: create author: LocalTicketBackend at: 2026-06-07T03:14:39Z -->
|
||||
|
||||
## Created
|
||||
|
||||
Created by LocalTicketBackend create.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: plan author: hare at: 2026-06-07T03:43:33Z -->
|
||||
|
||||
## Plan
|
||||
|
||||
## Preflight / implementation intent
|
||||
|
||||
Classification: implementation-ready after `ticket-role-launch-config-strict-validation` landed.
|
||||
|
||||
Intent:
|
||||
- Add a Ticket-focused scaffold/init path that writes an explicit `.yoi/ticket.config.toml` with concrete fixed role profiles.
|
||||
- This is explicit config generation, not runtime fallback. The generated file may choose builtin presets, but the runtime must continue requiring explicit role config.
|
||||
|
||||
Current state:
|
||||
- There is no top-level `yoi init` command in `crates/yoi/src/main.rs`.
|
||||
- `yoi ticket` currently has no `init` command.
|
||||
- `yoi ticket` already documents fallback backend behavior when config is absent, but role launch validation now requires explicit role config for Panel Intake/Orchestrator launches.
|
||||
|
||||
Implementation direction:
|
||||
- Introduce a narrow `yoi ticket init` scaffold command rather than a broad product-level init redesign.
|
||||
- The command should create `.yoi/ticket.config.toml` if missing, and optionally ensure the local Ticket root `.yoi/tickets` exists.
|
||||
- Default generated config should explicitly include:
|
||||
- `[backend] provider = "builtin:yoi_local"`, `root = ".yoi/tickets"`;
|
||||
- fixed `[roles.*]` tables for intake, orchestrator, coder, reviewer, investigator;
|
||||
- each role has a concrete `profile`, initially `builtin:default` unless role-specific builtin profiles are introduced in the same small diff;
|
||||
- each role has explicit `workflow` matching the role defaults.
|
||||
- Do not overwrite an existing `.yoi/ticket.config.toml` unless an explicit force flag is intentionally added and tested. A minimal first pass can fail with an actionable diagnostic when the config already exists.
|
||||
- Generated config should pass the strict role launch validation added by the previous ticket.
|
||||
|
||||
Suggested command shape:
|
||||
- `yoi ticket init`
|
||||
- Optional flags only if small and useful:
|
||||
- `--force` is not required for first pass;
|
||||
- `--print`/dry-run is not required for first pass.
|
||||
|
||||
Code map:
|
||||
- `crates/yoi/src/ticket_cli.rs`: add parse/run/help for `init` and tests.
|
||||
- `crates/ticket/src/config.rs`: add reusable scaffold string/helper if that keeps config generation testable near config types.
|
||||
- `crates/client/src/ticket_role.rs`: use existing launch validation tests to verify generated config is launch-ready.
|
||||
- `.yoi/ticket.config.toml`: update this repository's config through the new intended shape or keep repository config update in main workspace if child worktree excludes `.yoi`; the implementation should at least make the scaffold generate that shape.
|
||||
|
||||
Non-goals:
|
||||
- Do not loosen runtime validation.
|
||||
- Do not add implicit builtin/default fallback.
|
||||
- Do not implement broad global `yoi init` unless the existing CLI structure makes it clearly smaller than `yoi ticket init`.
|
||||
- Do not design role-specific builtin profiles unless it is trivial and scoped; `builtin:default` as explicit scaffold output is acceptable for now.
|
||||
|
||||
Validation:
|
||||
- Tests should cover generated config shape, refusal to overwrite existing config, and successful role launch validation against generated config.
|
||||
- Run focused `cargo test -p ticket config --lib`, `cargo test -p client ticket_role --lib`, and `cargo test -p yoi ticket` or equivalent ticket CLI tests.
|
||||
- Run `cargo fmt --check` and `git diff --check`.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: intake_summary author: INSOMNIA at: 2026-06-07T03:43:38Z -->
|
||||
|
||||
## Intake summary
|
||||
|
||||
Implementation-ready after strict role launch validation: add a narrow `yoi ticket init` scaffold path that writes explicit backend and fixed role profile config to `.yoi/ticket.config.toml`, using concrete selectors such as explicit `builtin:default` for now, without adding runtime fallback.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: INSOMNIA at: 2026-06-07T03:43:38Z from: intake to: ready reason: intake_ready field: workflow_state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Ticket intake complete; workflow_state intake -> ready.
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: hare at: 2026-06-07T03:44:16Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
## Delegation
|
||||
|
||||
Implementation delegated to child Pod `ticket-init-scaffold-coder-20260607` in worktree `.worktree/ticket-init-role-profile-scaffold` on branch `work/ticket-init-role-profile-scaffold`.
|
||||
|
||||
Scope:
|
||||
- Add a narrow `yoi ticket init` scaffold command.
|
||||
- Generate explicit backend and fixed role profile/workflow config in `.yoi/ticket.config.toml`.
|
||||
- Use concrete role profiles in the generated config; explicit `builtin:default` is acceptable for the first pass.
|
||||
- Do not loosen strict role launch validation or add runtime fallback.
|
||||
- Do not overwrite existing config in the first pass.
|
||||
|
||||
The child should commit implementation work in the child worktree and report diff/tests. Merge, review, Ticket closure, and cleanup remain with the parent/human workflow.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: hare at: 2026-06-07T03:58:12Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
## Implementation report
|
||||
|
||||
Coder Pod `ticket-init-scaffold-coder-20260607` completed implementation in `.worktree/ticket-init-role-profile-scaffold`.
|
||||
|
||||
Commit:
|
||||
- `f265098 ticket: add ticket config init scaffold`
|
||||
|
||||
Reported summary:
|
||||
- Added narrow `yoi ticket init`.
|
||||
- It creates `.yoi/ticket.config.toml` only when missing and ensures `.yoi/tickets` exists.
|
||||
- Existing `.yoi/ticket.config.toml` is not overwritten and fails with an actionable diagnostic.
|
||||
- Added reusable `ticket_config_scaffold()` in `crates/ticket/src/config.rs`.
|
||||
- Generated config includes backend provider/root plus fixed roles intake/orchestrator/coder/reviewer/investigator.
|
||||
- Each generated role uses explicit `profile = "builtin:default"` and explicit default `workflow`.
|
||||
- Added tests for config shape, CLI init/no-overwrite behavior, CLI help, and launch planning for Intake/Orchestrator from the scaffold.
|
||||
|
||||
Reported validation:
|
||||
- `cargo test -p ticket config --lib`
|
||||
- `cargo test -p client ticket_role --lib`
|
||||
- `cargo test -p yoi ticket`
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check`
|
||||
- `nix build .#yoi`
|
||||
|
||||
External review will be delegated before merge.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: review author: ticket-init-scaffold-reviewer-20260607 at: 2026-06-07T04:04:17Z status: approve -->
|
||||
|
||||
## Review: approve
|
||||
|
||||
Approved.
|
||||
|
||||
Evidence:
|
||||
- Runtime fallback was not loosened. Strict role launch validation still requires configured role table/profile and rejects top-level `inherit` before launch.
|
||||
- `ticket_config_scaffold()` writes explicit backend config and all fixed role tables: intake, orchestrator, coder, reviewer, investigator.
|
||||
- Each generated role has explicit `profile = "builtin:default"` and explicit default `workflow`.
|
||||
- Launch validation coverage plans both Intake and Orchestrator from the scaffolded config.
|
||||
- `yoi ticket init` refuses to overwrite existing `.yoi/ticket.config.toml`, uses create-new behavior, and reports an actionable diagnostic.
|
||||
- Command shape is scoped to `yoi ticket init`; no broad product init or runtime implicit fallback was introduced.
|
||||
|
||||
Reviewer validation:
|
||||
- `cargo test -p ticket config --lib`
|
||||
- `cargo test -p client ticket_role --lib`
|
||||
- `cargo test -p yoi ticket`
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check develop...HEAD`
|
||||
- `nix build --no-link .#yoi`
|
||||
|
||||
Merge readiness:
|
||||
- Branch is behind current `develop` only by Ticket metadata changes.
|
||||
- Implementation changes are limited to `crates/client/src/ticket_role.rs`, `crates/ticket/src/config.rs`, and `crates/yoi/src/ticket_cli.rs`.
|
||||
- `git merge-tree` showed no conflicts.
|
||||
|
||||
Dogfooding note:
|
||||
- The main workspace's existing `.yoi/ticket.config.toml` remains backend-only, so a separate main-workspace config update is needed after merge if this workspace should use explicit Ticket role profiles.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: hare at: 2026-06-07T04:05:15Z from: ready to: done reason: closed field: workflow_state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Ticket closed; workflow_state set to done.
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: close author: hare at: 2026-06-07T04:05:15Z status: closed -->
|
||||
|
||||
## Closed
|
||||
|
||||
Implemented, reviewed, merged, validated, and applied to this workspace config.
|
||||
|
||||
Summary:
|
||||
- Added a narrow `yoi ticket init` command.
|
||||
- The command creates `.yoi/ticket.config.toml` only when missing and refuses to overwrite an existing config with an actionable diagnostic.
|
||||
- Added `ticket_config_scaffold()` for the generated config body.
|
||||
- Generated config includes explicit backend config and fixed role tables for intake, orchestrator, coder, reviewer, and investigator.
|
||||
- Each generated role uses explicit `profile = "builtin:default"` and explicit default workflow.
|
||||
- No runtime fallback was added or loosened.
|
||||
- The generated config validates for Ticket role launch planning under the strict validation implemented by `ticket-role-launch-config-strict-validation`.
|
||||
- This repository's `.yoi/ticket.config.toml` was updated to the scaffolded explicit role-profile shape for dogfooding.
|
||||
|
||||
Implementation:
|
||||
- Child commit: `f265098 ticket: add ticket config init scaffold`
|
||||
- Merge commit: `merge: ticket init scaffold`
|
||||
|
||||
Review:
|
||||
- External reviewer `ticket-init-scaffold-reviewer-20260607` approved with no blockers.
|
||||
|
||||
Validation after merge:
|
||||
- `cargo test -p ticket config --lib`
|
||||
- `cargo test -p client ticket_role --lib`
|
||||
- `cargo test -p yoi ticket`
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check`
|
||||
- `target/debug/yoi ticket doctor`
|
||||
|
||||
---
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
---
|
||||
id: 20260607-031439-ticket-role-launch-config-strict-validation
|
||||
slug: ticket-role-launch-config-strict-validation
|
||||
title: Strictly validate Ticket role launch config
|
||||
status: closed
|
||||
kind: task
|
||||
priority: P1
|
||||
labels: [ticket, config, validation, panel, profiles]
|
||||
workflow_state: done
|
||||
created_at: 2026-06-07T03:14:39Z
|
||||
updated_at: 2026-06-07T03:42:39Z
|
||||
assignee: null
|
||||
legacy_ticket: null
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
Fallback audit found that Ticket backend config and Ticket role launch config are currently conflated. A backend-only `.yoi/ticket.config.toml` can be parsed and treated as usable by the panel, while missing role profiles are filled by role defaults that currently use `inherit`. Top-level Ticket role launches then fail late because `inherit` is not supported there.
|
||||
|
||||
The desired policy is fail-closed runtime validation: role launch must require explicit concrete role profile configuration. Init/scaffold may write builtin selectors explicitly, but runtime must not silently invent them.
|
||||
|
||||
## Goal
|
||||
|
||||
Add strict validation for Ticket role launch configuration so Panel Intake / Orchestrator launch availability reflects whether role config is actually executable.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Separate Ticket backend availability from Ticket role launch availability.
|
||||
- Backend-only config may be usable for listing/showing Tickets.
|
||||
- Role launch actions require fixed role config validation.
|
||||
- Add a role-launch validation path that rejects:
|
||||
- missing role table for the target fixed role;
|
||||
- missing role `profile`;
|
||||
- `profile = "inherit"` for top-level Ticket role launch;
|
||||
- role profile selectors that cannot be resolved for the launch path, where resolvability can be checked at this layer.
|
||||
- Panel Intake and workspace Orchestrator lifecycle must use the strict role-launch validation before presenting/attempting launch.
|
||||
- Diagnostics must be bounded and actionable, e.g. tell the user to run init/scaffold or set `[roles.<role>].profile` explicitly.
|
||||
- Keep `TicketRoleLaunchPlan::spawn_config` rejection of `inherit` as a final defensive check, but normal Panel/launcher paths should fail earlier with a clearer diagnostic.
|
||||
- Preserve explicit concrete builtin selectors when they are present in config; the target is not to ban builtin, only implicit fallback.
|
||||
- Do not silently convert missing roles to `builtin:default`, `default`, or `inherit` at runtime.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Implementing init/scaffold generation; that belongs to `ticket-init-role-profile-scaffold`.
|
||||
- Removing low-risk backend defaults such as the local Ticket backend root.
|
||||
- Redesigning model/provider fallback policy.
|
||||
- Allowing top-level Ticket role launch to resolve `inherit`.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- Backend-only `.yoi/ticket.config.toml` is not considered sufficient for Panel Intake / Orchestrator role launch.
|
||||
- Missing or `inherit` role profile produces an early actionable diagnostic before spawn.
|
||||
- Explicit concrete role profile config allows launch planning to proceed.
|
||||
- Tests cover backend-only config, partial role config, explicit `inherit`, and full concrete role config.
|
||||
- Existing Ticket backend listing/show behavior remains available when only backend config is present, unless a separate design intentionally changes that.
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
Implemented, reviewed, merged, and validated.
|
||||
|
||||
Summary:
|
||||
- Added strict Ticket role launch readiness validation while preserving backend config loading/list/show behavior.
|
||||
- Role launch planning now rejects missing role table, missing role `profile`, explicit top-level `profile = "inherit"`, and unresolvable concrete selectors before spawn.
|
||||
- `plan_ticket_role_launch(_with_config)` uses launch-specific validation instead of implicit role defaults.
|
||||
- No implicit builtin/default/inherit fallback was added.
|
||||
- `TicketRoleLaunchPlan::spawn_config` still defensively rejects `inherit`.
|
||||
- Diagnostics are bounded/actionable for Panel/launcher surfaces.
|
||||
- Init/scaffold generation was intentionally not implemented here; `ticket-init-role-profile-scaffold` remains the next task.
|
||||
|
||||
Implementation:
|
||||
- Child commit: `30a07b7 ticket: validate role launch config`
|
||||
- Merge commit: `merge: role launch config validation`
|
||||
|
||||
Review:
|
||||
- External reviewer `role-launch-config-validation-reviewer-20260607` approved with no blockers.
|
||||
|
||||
Validation after merge:
|
||||
- `cargo test -p ticket config --lib`
|
||||
- `cargo test -p client ticket_role --lib`
|
||||
- `cargo test -p tui multi_pod --lib`
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check`
|
||||
- `target/debug/yoi ticket doctor`
|
||||
|
|
@ -0,0 +1,194 @@
|
|||
<!-- event: create author: LocalTicketBackend at: 2026-06-07T03:14:39Z -->
|
||||
|
||||
## Created
|
||||
|
||||
Created by LocalTicketBackend create.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: plan author: hare at: 2026-06-07T03:20:38Z -->
|
||||
|
||||
## Plan
|
||||
|
||||
## Preflight / implementation intent
|
||||
|
||||
Classification: implementation-ready, should run before `ticket-init-role-profile-scaffold`.
|
||||
|
||||
Intent:
|
||||
- Add an explicit runtime validation boundary for Ticket role launch config.
|
||||
- Keep backend availability separate from role-launch readiness: backend-only config may list/show Tickets, but Panel Intake / Orchestrator launch must fail early if fixed role profile config is missing or non-executable.
|
||||
- Preserve the policy distinction between explicit builtin selectors in config and implicit runtime fallback.
|
||||
|
||||
Requirements:
|
||||
- Reject role launch readiness for:
|
||||
- missing role table for the target role;
|
||||
- missing role profile for the target role;
|
||||
- `profile = "inherit"` for top-level launch;
|
||||
- unresolvable concrete selector where this layer can validate it.
|
||||
- Do not silently convert missing roles to `builtin:default`, `default`, or `inherit` at runtime.
|
||||
- Keep backend defaults/list/show behavior working where possible.
|
||||
- Panel Intake and workspace Orchestrator launch paths should use the stricter validation before spawn and emit bounded actionable diagnostics.
|
||||
- Keep `TicketRoleLaunchPlan::spawn_config` `inherit` rejection as a final defensive check.
|
||||
|
||||
Current code map:
|
||||
- `crates/ticket/src/config.rs`
|
||||
- `TicketConfig::load_workspace` returns `default_for_workspace` on missing config.
|
||||
- `RawTicketConfig::resolve` starts with `TicketRoleProfiles::default()` and overlays provided roles.
|
||||
- `TicketRoleConfig::default_for_role` uses `ProfileSelectorRef::inherit()`.
|
||||
- Tests around missing config/default roles need adjustment or expansion.
|
||||
- `crates/client/src/ticket_role.rs`
|
||||
- `plan_ticket_role_launch(_with_config)` reads `config.role(context.role)` and currently does not distinguish explicit vs default role config.
|
||||
- `spawn_config` rejects `inherit` late.
|
||||
- `crates/tui/src/multi_pod.rs`
|
||||
- Panel Intake / Orchestrator role launch surfaces should receive clearer diagnostics.
|
||||
|
||||
Implementation direction:
|
||||
- Preserve enough metadata in parsed Ticket config to know whether a role was explicitly configured, or add a dedicated validation API that can report missing/implicit role defaults.
|
||||
- Prefer a typed validation error with role name and actionable remediation text.
|
||||
- Tests should cover backend-only config, partial role config, explicit `inherit`, and full concrete role config.
|
||||
|
||||
Non-goals:
|
||||
- Do not implement init/scaffold generation here; `ticket-init-role-profile-scaffold` follows after this lands.
|
||||
- Do not add implicit builtin fallback.
|
||||
- Do not allow top-level `inherit` launch.
|
||||
- Do not redesign global provider/model/profile fallback policy.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: intake_summary author: INSOMNIA at: 2026-06-07T03:20:49Z -->
|
||||
|
||||
## Intake summary
|
||||
|
||||
Implementation-ready: add strict Ticket role launch config validation before spawn, keep backend availability separate from role-launch readiness, reject missing role profile and top-level `inherit`, and avoid implicit builtin/default fallback. This should land before init/scaffold generation.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: INSOMNIA at: 2026-06-07T03:20:49Z from: intake to: ready reason: intake_ready field: workflow_state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Ticket intake complete; workflow_state intake -> ready.
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: hare at: 2026-06-07T03:21:41Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
## Delegation
|
||||
|
||||
Implementation delegated to child Pod `role-launch-config-validation-coder-20260607` in worktree `.worktree/ticket-role-launch-config-strict-validation` on branch `work/ticket-role-launch-config-strict-validation`.
|
||||
|
||||
Scope:
|
||||
- Add strict runtime validation for Ticket role launch config.
|
||||
- Keep backend availability separate from role-launch readiness.
|
||||
- Reject missing role config and top-level `inherit` before spawn with actionable diagnostics.
|
||||
- Do not implement init/scaffold generation here; `ticket-init-role-profile-scaffold` follows after this lands.
|
||||
|
||||
The child should commit implementation work in the child worktree and report diff/tests. Merge, review, Ticket closure, and cleanup remain with the parent/human workflow.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: hare at: 2026-06-07T03:38:25Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
## Implementation report
|
||||
|
||||
Coder Pod `role-launch-config-validation-coder-20260607` completed implementation in `.worktree/ticket-role-launch-config-strict-validation`.
|
||||
|
||||
Commit:
|
||||
- `30a07b7 ticket: validate role launch config`
|
||||
|
||||
Reported summary:
|
||||
- Added explicit launch-readiness metadata for fixed Ticket roles without breaking backend-only config loading.
|
||||
- Added typed validation failures for missing role table, missing `profile`, top-level `inherit`, and profile selector resolution failure.
|
||||
- Updated `plan_ticket_role_launch(_with_config)` to validate launch readiness before plan/spawn.
|
||||
- Kept `TicketRoleLaunchPlan::spawn_config` defensive rejection of `inherit`.
|
||||
- Added focused tests for backend-only configs, partial roles, missing profile, explicit inherit, unresolvable selectors, and successful concrete `builtin:default` launch planning.
|
||||
|
||||
Reported validation:
|
||||
- `cargo test -p ticket config --lib`
|
||||
- `cargo test -p client ticket_role --lib`
|
||||
- `cargo test -p tui multi_pod --lib`
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check`
|
||||
- `nix build .#yoi`
|
||||
|
||||
External review delegated to `role-launch-config-validation-reviewer-20260607`.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: review author: role-launch-config-validation-reviewer-20260607 at: 2026-06-07T03:42:05Z status: approve -->
|
||||
|
||||
## Review: approve
|
||||
|
||||
Approved.
|
||||
|
||||
Reviewer found no blockers and judged the implementation merge-ready.
|
||||
|
||||
Evidence:
|
||||
- Strict launch readiness is separated from backend config loading: backend-only config still loads, while `role_launch_config` rejects missing role table, missing role `profile`, and explicit top-level `inherit`.
|
||||
- No implicit launch fallback was added: role launch planning uses launch-specific validation instead of internal default role config access.
|
||||
- Early validation happens before plan/spawn, including profile selector resolution where possible.
|
||||
- `TicketRoleLaunchPlan::spawn_config` still defensively rejects `inherit`.
|
||||
- Backend-only `.yoi/ticket.config.toml` still parses/exposes backend root while role launch fails with `MissingRoleTable`.
|
||||
- Diagnostics are bounded/actionable on panel surfaces.
|
||||
- Explicit concrete `builtin:default` launch planning works; unresolvable selectors fail before spawn.
|
||||
- Init/scaffold generation was not implemented in this ticket.
|
||||
|
||||
Reviewer validation:
|
||||
- `cargo test -p ticket config --lib`
|
||||
- `cargo test -p client ticket_role --lib`
|
||||
- `cargo test -p tui multi_pod --lib`
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check`
|
||||
- `nix build .#yoi`
|
||||
|
||||
Merge readiness:
|
||||
- `git merge-tree --write-tree develop HEAD` exited 0.
|
||||
- Worktree status clean.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: hare at: 2026-06-07T03:42:39Z from: ready to: done reason: closed field: workflow_state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Ticket closed; workflow_state set to done.
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: close author: hare at: 2026-06-07T03:42:39Z status: closed -->
|
||||
|
||||
## Closed
|
||||
|
||||
Implemented, reviewed, merged, and validated.
|
||||
|
||||
Summary:
|
||||
- Added strict Ticket role launch readiness validation while preserving backend config loading/list/show behavior.
|
||||
- Role launch planning now rejects missing role table, missing role `profile`, explicit top-level `profile = "inherit"`, and unresolvable concrete selectors before spawn.
|
||||
- `plan_ticket_role_launch(_with_config)` uses launch-specific validation instead of implicit role defaults.
|
||||
- No implicit builtin/default/inherit fallback was added.
|
||||
- `TicketRoleLaunchPlan::spawn_config` still defensively rejects `inherit`.
|
||||
- Diagnostics are bounded/actionable for Panel/launcher surfaces.
|
||||
- Init/scaffold generation was intentionally not implemented here; `ticket-init-role-profile-scaffold` remains the next task.
|
||||
|
||||
Implementation:
|
||||
- Child commit: `30a07b7 ticket: validate role launch config`
|
||||
- Merge commit: `merge: role launch config validation`
|
||||
|
||||
Review:
|
||||
- External reviewer `role-launch-config-validation-reviewer-20260607` approved with no blockers.
|
||||
|
||||
Validation after merge:
|
||||
- `cargo test -p ticket config --lib`
|
||||
- `cargo test -p client ticket_role --lib`
|
||||
- `cargo test -p tui multi_pod --lib`
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check`
|
||||
- `target/debug/yoi ticket doctor`
|
||||
|
||||
---
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
---
|
||||
id: 20260607-033536-ticket-lifecycle-pod-feature
|
||||
slug: ticket-lifecycle-pod-feature
|
||||
title: Ticket lifecycle pod feature
|
||||
status: closed
|
||||
kind: task
|
||||
priority: P1
|
||||
labels: [ticket, pod-feature, tools, orchestration, workflow]
|
||||
workflow_state: done
|
||||
created_at: 2026-06-07T03:35:36Z
|
||||
updated_at: 2026-06-07T04:03:33Z
|
||||
assignee: null
|
||||
legacy_ticket: null
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
Panel/Orchestrator queue automation needs Ticket lifecycle operations to be available to Pods through the feature/tool system. This should not be modeled as an "Orchestrator feature": features should represent domain capabilities, not roles. Orchestrator, Intake, Companion, coder, and reviewer profiles may each be granted different subsets of the Ticket domain capability by policy/profile configuration.
|
||||
|
||||
The immediate motivation is queued Ticket handling: when a Ticket enters `workflow_state = queued`, the Orchestrator should be able to inspect Ticket state, record decisions/progress/blockers, and transition lifecycle state according to the Ticket workflow contract before starting worktree/Pod side effects. Prompt instructions are acceptable for sequencing initially; the first step is to make the Ticket lifecycle capability a proper `pod::feature` domain feature.
|
||||
|
||||
## Goal
|
||||
|
||||
Implement a `pod::feature`-based Ticket lifecycle domain feature that registers typed Ticket lifecycle tools for Pods, without making the feature role-specific.
|
||||
|
||||
## Domain model
|
||||
|
||||
- Feature identity should be Ticket-domain oriented, e.g. `ticket_lifecycle` / `ticket` rather than `orchestrator`.
|
||||
- Role/profile policy decides which tools or permission level a Pod receives.
|
||||
- Orchestrator may receive mutating lifecycle tools.
|
||||
- Companion should receive read/status-only Ticket tools by default.
|
||||
- Intake may receive create/update/intake-ready tools needed to materialize Tickets.
|
||||
- Coder/reviewer may receive report/review/comment tools as appropriate.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Add or extend a `pod::feature` module for Ticket lifecycle/tool registration.
|
||||
- Register typed Ticket tools through the feature system rather than ad-hoc role-specific wiring.
|
||||
- Cover at least the lifecycle operations needed by Panel/Orchestrator automation:
|
||||
- Ticket list/show read access;
|
||||
- append comment/decision/implementation report events;
|
||||
- append review events where granted;
|
||||
- `intake -> ready` intake-ready operation where granted;
|
||||
- `queued -> inprogress` and `inprogress -> done` workflow-state transitions where granted;
|
||||
- close Ticket where granted.
|
||||
- Keep transition enforcement in the Ticket backend/tool implementation, not only in prompts.
|
||||
- Do not make this an Orchestrator-only feature. The same domain feature must be grantable with different permissions to different role profiles.
|
||||
- Provide a clear policy/config surface for read-only vs mutating Ticket lifecycle access, or use the existing feature/tool permission machinery if it already supports this distinction.
|
||||
- Ensure tool descriptions/prompts communicate the sequencing contract:
|
||||
- queued Ticket implementation side effects should occur only after `queued -> inprogress` acceptance;
|
||||
- prompt sequencing is acceptable initially, but tool behavior must still enforce valid workflow transitions.
|
||||
- Preserve current typed Ticket backend invariants and doctor validation.
|
||||
- Keep local Pod/session assignment out of git-tracked Ticket metadata; use the existing local role session registry for local runtime association when needed.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Implementing full Orchestrator queue automation in this ticket.
|
||||
- Implementing workflow state-machine-based dynamic tool gating.
|
||||
- Replacing prompt/workflow sequencing with a complete operation-state enforcement runtime.
|
||||
- Making `StartTicketWork` / `AcceptQueuedTicket` a composite tool unless it naturally falls out as a small wrapper; composite orchestration can remain a follow-up.
|
||||
- Adding Companion Bash or broad workspace mutation authority.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- Ticket lifecycle tools are contributed through a Ticket-domain `pod::feature`.
|
||||
- The feature can be granted to different roles/profiles with different authority levels; it is not hard-coded as an Orchestrator feature.
|
||||
- Orchestrator-capable profiles can receive the Ticket lifecycle tools needed to inspect queued Tickets and perform `queued -> inprogress` / `inprogress -> done` transitions.
|
||||
- Read-only/status roles can receive Ticket read tools without mutating lifecycle authority.
|
||||
- Existing Ticket transition constraints remain enforced by typed backend/tool paths.
|
||||
- Tests cover feature registration and at least one read-only vs mutating permission/grant distinction, or document why the existing feature permission tests already cover it.
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
Implemented, reviewed, merged, and validated.
|
||||
|
||||
Summary:
|
||||
- Refined the existing Ticket-domain `builtin:ticket` feature rather than introducing an Orchestrator-specific feature.
|
||||
- Added `TicketFeatureAccess::{ReadOnly, Lifecycle}`.
|
||||
- Added explicit Ticket tool partitions:
|
||||
- read-only/status tools: `TicketList`, `TicketShow`, `TicketDoctor`;
|
||||
- mutating/lifecycle tools: create/comment/review/intake-ready/workflow-state/status/close.
|
||||
- Feature descriptors and installation now expose only the tools allowed by the configured access level.
|
||||
- Preserved the existing full lifecycle/default behavior for existing call sites.
|
||||
- Updated `TicketWorkflowState` tool description with the queued acceptance sequencing contract.
|
||||
- Preserved typed backend/tool transition enforcement.
|
||||
|
||||
Implementation:
|
||||
- Child commit: `3d662bc pod: split ticket feature access levels`
|
||||
- Merge commit: `merge: ticket lifecycle feature`
|
||||
|
||||
Review:
|
||||
- External reviewer `ticket-lifecycle-feature-reviewer-20260607` approved with no blockers.
|
||||
|
||||
Validation after merge:
|
||||
- `cargo test -p pod ticket --lib`
|
||||
- `cargo test -p ticket tool --lib`
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check`
|
||||
- `cargo check --workspace --all-targets`
|
||||
|
|
@ -0,0 +1,208 @@
|
|||
<!-- event: create author: LocalTicketBackend at: 2026-06-07T03:35:36Z -->
|
||||
|
||||
## Created
|
||||
|
||||
Created by LocalTicketBackend create.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: plan author: hare at: 2026-06-07T03:47:42Z -->
|
||||
|
||||
## Plan
|
||||
|
||||
## Preflight / implementation intent
|
||||
|
||||
Classification: implementation-ready and safe to run in parallel with `ticket-init-role-profile-scaffold`.
|
||||
|
||||
Parallelism note:
|
||||
- `ticket-init-role-profile-scaffold` is expected to touch `crates/yoi/src/ticket_cli.rs`, `crates/ticket/src/config.rs`, and maybe role-launch tests.
|
||||
- This ticket should primarily touch `crates/pod/src/feature/builtin/ticket.rs`, `crates/ticket/src/tool.rs`, feature registration tests, and possibly role/profile integration tests.
|
||||
- Coordinate carefully if `ticket/src/config.rs` is touched, but the expected overlap is low.
|
||||
|
||||
Current code map / discovery:
|
||||
- A built-in Ticket feature already exists at `crates/pod/src/feature/builtin/ticket.rs` with feature id `builtin:ticket`.
|
||||
- It currently declares/registers all `ticket::tool::TICKET_TOOL_NAMES` through the feature registry and requires `HostAuthority::TicketBackend`.
|
||||
- `ticket::tool::TICKET_TOOL_NAMES` already includes lifecycle tools such as `TicketIntakeReady` and `TicketWorkflowState`.
|
||||
- The missing piece is not creating Ticket tools from nothing; it is shaping this existing Ticket-domain feature into an explicit lifecycle capability with authority subsets/grants suitable for roles such as read-only Companion vs mutating Orchestrator/Intake/coder/reviewer.
|
||||
|
||||
Intent:
|
||||
- Keep this as a Ticket-domain feature, not an Orchestrator feature.
|
||||
- Extend/refine the existing built-in Ticket feature so it can expose read-only vs mutating/lifecycle tool subsets according to profile/policy/grant selection.
|
||||
- Preserve backend/tool transition enforcement; prompts can specify sequencing, but invalid workflow transitions must still fail in typed Ticket backend/tool paths.
|
||||
|
||||
Requirements for first implementation:
|
||||
- Do not duplicate Ticket tools outside the feature system.
|
||||
- Do not create an Orchestrator-specific feature id.
|
||||
- Provide a clear API/config surface for Ticket feature authority level or tool subset. Suggested initial shape: a `TicketFeatureAccess` / `TicketToolSet` enum or builder with at least:
|
||||
- read-only/status: `TicketList`, `TicketShow`, maybe `TicketDoctor` if considered read-only diagnostic;
|
||||
- lifecycle/mutating: comment/review/intake-ready/workflow-state/status/close/create according to the existing tool set.
|
||||
- Feature descriptor should declare only tools that the configured feature instance may register, and installation should register only that subset.
|
||||
- Tests should prove read-only installation does not expose mutating tools, while lifecycle/mutating installation exposes the needed lifecycle tools.
|
||||
- Tool descriptions for `TicketWorkflowState` should communicate the queued acceptance sequencing contract: implementation side effects should happen only after `queued -> inprogress` acceptance.
|
||||
- Preserve existing `builtin:ticket` behavior where needed, or provide an intentional default that does not silently grant broad mutation to roles that should be read-only. If compatibility choices are ambiguous, prefer a small explicit API and report the integration follow-up.
|
||||
|
||||
Non-goals:
|
||||
- Do not implement full Orchestrator queue automation.
|
||||
- Do not implement workflow state-machine dynamic tool gating.
|
||||
- Do not implement `StartTicketWork` / `AcceptQueuedTicket` unless it is a trivial wrapper; it is acceptable to leave composite orchestration as follow-up.
|
||||
- Do not add Companion Bash or broad write authority.
|
||||
- Do not alter local role session registry semantics.
|
||||
|
||||
Validation:
|
||||
- Add/update focused tests in `pod::feature::builtin::ticket` and/or `ticket::tool`.
|
||||
- Run at minimum `cargo test -p pod ticket --lib` and relevant `cargo test -p ticket tool --lib` if ticket tool partitions are added.
|
||||
- Run `cargo fmt --check` and `git diff --check`; broader `cargo check --workspace --all-targets` if feature API surfaces change broadly.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: intake_summary author: INSOMNIA at: 2026-06-07T03:47:55Z -->
|
||||
|
||||
## Intake summary
|
||||
|
||||
Implementation-ready: refine the existing built-in Ticket-domain feature (`builtin:ticket`) into a lifecycle capability with explicit read-only vs mutating/lifecycle tool subsets. This can run in parallel with ticket init scaffold because expected code overlap is low.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: INSOMNIA at: 2026-06-07T03:47:55Z from: intake to: ready reason: intake_ready field: workflow_state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Ticket intake complete; workflow_state intake -> ready.
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: hare at: 2026-06-07T03:48:50Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
## Delegation
|
||||
|
||||
Implementation delegated to child Pod `ticket-lifecycle-feature-coder-20260607` in worktree `.worktree/ticket-lifecycle-pod-feature` on branch `work/ticket-lifecycle-pod-feature`.
|
||||
|
||||
Scope:
|
||||
- Refine the existing built-in Ticket-domain feature (`builtin:ticket`) rather than creating an Orchestrator-specific feature.
|
||||
- Add explicit read-only/status vs mutating/lifecycle tool subset support.
|
||||
- Keep Ticket transition enforcement in typed backend/tool paths.
|
||||
- Do not implement full Orchestrator automation or composite `StartTicketWork` in this ticket.
|
||||
|
||||
Parallel note:
|
||||
- This is running alongside `ticket-init-role-profile-scaffold`; expected code overlap is low. The child was instructed to keep changes localized and report any conflict risk.
|
||||
|
||||
The child should commit implementation work in the child worktree and report diff/tests. Merge, review, Ticket closure, and cleanup remain with the parent/human workflow.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: hare at: 2026-06-07T03:58:12Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
## Implementation report
|
||||
|
||||
Coder Pod `ticket-lifecycle-feature-coder-20260607` completed implementation in `.worktree/ticket-lifecycle-pod-feature`.
|
||||
|
||||
Commit:
|
||||
- `3d662bc pod: split ticket feature access levels`
|
||||
|
||||
Reported summary:
|
||||
- Added `TicketFeatureAccess::{ReadOnly, Lifecycle}` for `builtin:ticket`.
|
||||
- Preserved default full lifecycle behavior for `ticket_tools_feature(...)`.
|
||||
- Added `ticket_tools_feature_with_access(...)` and constructors/accessors for explicit access selection.
|
||||
- Feature descriptors declare only tools allowed by the configured access level.
|
||||
- Feature installation registers only the configured subset.
|
||||
- Added Ticket tool name partitions:
|
||||
- `TICKET_READ_ONLY_TOOL_NAMES`: `TicketList`, `TicketShow`, `TicketDoctor`;
|
||||
- `TICKET_MUTATING_TOOL_NAMES`: create/comment/review/intake-ready/workflow-state/status/close.
|
||||
- Updated `TicketWorkflowState` tool description with the `queued -> inprogress` acceptance contract.
|
||||
- Added tests proving read-only install excludes mutating tools and lifecycle install exposes lifecycle tools.
|
||||
|
||||
Reported validation:
|
||||
- `cargo test -p pod ticket --lib`
|
||||
- `cargo test -p ticket tool --lib`
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check`
|
||||
- `cargo check --workspace --all-targets`
|
||||
- `nix build .#yoi`
|
||||
|
||||
Reported follow-up:
|
||||
- Call sites still use the default full lifecycle access. Companion/status-specific callers can opt into read-only access once integration points are selected.
|
||||
- Host authority remains `HostAuthority::TicketBackend`; read-only boundary is enforced by subset exposure rather than a separate read-only backend authority.
|
||||
|
||||
External review will be delegated before merge.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: review author: ticket-lifecycle-feature-reviewer-20260607 at: 2026-06-07T04:02:48Z status: approve -->
|
||||
|
||||
## Review: approve
|
||||
|
||||
Approved.
|
||||
|
||||
Evidence reviewed:
|
||||
- Feature identity remains Ticket-domain (`builtin:ticket`), not Orchestrator-specific.
|
||||
- `TicketFeatureAccess::ReadOnly` maps only to read/status tools (`TicketList`, `TicketShow`, `TicketDoctor`).
|
||||
- Feature descriptor and installation register only the tools allowed by the selected access level.
|
||||
- Lifecycle/default access preserves existing full Ticket tool behavior.
|
||||
- Tests cover read-only descriptor/install and lifecycle install behavior.
|
||||
- `TicketWorkflowStateTool` still delegates to backend workflow-state enforcement; allowed transitions remain enforced by typed Ticket backend/tool paths.
|
||||
- Updated queued acceptance wording is guidance only, not a replacement for backend transition validation.
|
||||
- Keeping `HostAuthority::TicketBackend` for read-only access is acceptable for this ticket because the boundary here is model-visible tool exposure, not a separate storage-level read-only backend authority.
|
||||
|
||||
Reviewer validation:
|
||||
- `cargo test -p pod ticket --lib`
|
||||
- `cargo test -p ticket tool --lib`
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check`
|
||||
- `cargo check --workspace --all-targets`
|
||||
- `nix build .#yoi`
|
||||
- `git merge-tree --write-tree develop HEAD`
|
||||
|
||||
Merge readiness:
|
||||
- Worktree clean.
|
||||
- Branch contains implementation commit `3d662bc pod: split ticket feature access levels`.
|
||||
- Merge-tree against current `develop` is clean.
|
||||
- No requested changes.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: hare at: 2026-06-07T04:03:33Z from: ready to: done reason: closed field: workflow_state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Ticket closed; workflow_state set to done.
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: close author: hare at: 2026-06-07T04:03:33Z status: closed -->
|
||||
|
||||
## Closed
|
||||
|
||||
Implemented, reviewed, merged, and validated.
|
||||
|
||||
Summary:
|
||||
- Refined the existing Ticket-domain `builtin:ticket` feature rather than introducing an Orchestrator-specific feature.
|
||||
- Added `TicketFeatureAccess::{ReadOnly, Lifecycle}`.
|
||||
- Added explicit Ticket tool partitions:
|
||||
- read-only/status tools: `TicketList`, `TicketShow`, `TicketDoctor`;
|
||||
- mutating/lifecycle tools: create/comment/review/intake-ready/workflow-state/status/close.
|
||||
- Feature descriptors and installation now expose only the tools allowed by the configured access level.
|
||||
- Preserved the existing full lifecycle/default behavior for existing call sites.
|
||||
- Updated `TicketWorkflowState` tool description with the queued acceptance sequencing contract.
|
||||
- Preserved typed backend/tool transition enforcement.
|
||||
|
||||
Implementation:
|
||||
- Child commit: `3d662bc pod: split ticket feature access levels`
|
||||
- Merge commit: `merge: ticket lifecycle feature`
|
||||
|
||||
Review:
|
||||
- External reviewer `ticket-lifecycle-feature-reviewer-20260607` approved with no blockers.
|
||||
|
||||
Validation after merge:
|
||||
- `cargo test -p pod ticket --lib`
|
||||
- `cargo test -p ticket tool --lib`
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check`
|
||||
- `cargo check --workspace --all-targets`
|
||||
|
||||
---
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
---
|
||||
id: 20260607-035143-orchestrator-queued-ticket-routing
|
||||
slug: orchestrator-queued-ticket-routing
|
||||
title: Orchestrator queued Ticket routing
|
||||
status: closed
|
||||
kind: task
|
||||
priority: P1
|
||||
labels: [panel, orchestrator, ticket, routing, workflow]
|
||||
workflow_state: done
|
||||
created_at: 2026-06-07T03:51:43Z
|
||||
updated_at: 2026-06-07T05:13:36Z
|
||||
assignee: null
|
||||
legacy_ticket: null
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
`workspace-panel-orchestrator-queue-automation` defines the overall goal: Panel Queue should notify the workspace Orchestrator, and the Orchestrator should route queued Tickets and start work when unblocked. The first implementation slice should focus on routing acceptance, not full worktree/coder/reviewer/merge execution.
|
||||
|
||||
This ticket uses the current workflow contract as the basis: Queue is a human gate, `queued` means Orchestrator may route, and `inprogress` is the durable Orchestrator acceptance marker before implementation side effects.
|
||||
|
||||
## Goal
|
||||
|
||||
Implement the queued Ticket routing entrypoint for the workspace Orchestrator.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Fire a durable Orchestrator notification when a Panel action transitions a Ticket `ready -> queued`.
|
||||
- Notification content must instruct the Orchestrator to:
|
||||
- read the queued Ticket;
|
||||
- inspect current workspace state;
|
||||
- start if unblocked;
|
||||
- record a blocker/defer reason if not startable.
|
||||
- Update wording away from passive "implementation was not started" semantics. Queue authorizes routing; Orchestrator decides whether to start.
|
||||
- Orchestrator routing workflow/prompt must define the acceptance order:
|
||||
- inspect Ticket/workspace state;
|
||||
- if unblocked, transition `queued -> inprogress`;
|
||||
- do not create worktrees or spawn implementation/review Pods before `inprogress` acceptance;
|
||||
- if `queued -> inprogress` fails, do not create side effects.
|
||||
- First-pass blocker detection should be intentionally small and explicit:
|
||||
- Ticket is no longer `queued`;
|
||||
- `attention_required` is set;
|
||||
- explicit dependency/blocker/preflight gap is recorded in the Ticket;
|
||||
- same Ticket already has an active local role/session/worktree;
|
||||
- branch/worktree name collision;
|
||||
- main workspace is clearly unsafe for orchestration;
|
||||
- a current in-progress Ticket clearly conflicts based on explicit Ticket text or known scope.
|
||||
- If blocked, Orchestrator records a concise decision/comment and leaves the Ticket queued or explicitly defers through existing Ticket status/state mechanisms.
|
||||
- Progress/acceptance should be recorded in the Ticket thread using existing typed events/comments where available.
|
||||
- Use Ticket lifecycle domain tools once available; do not model this as an Orchestrator-specific Ticket tool feature.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Implementing coder/reviewer worktree routing; that belongs to `orchestrator-worktree-agent-routing`.
|
||||
- Implementing merge/close completion; that belongs to `orchestrator-merge-completion`.
|
||||
- Implementing dynamic workflow state-machine tool gating.
|
||||
- Implementing composite `StartTicketWork` unless it is trivial and does not replace the prompt/workflow contract.
|
||||
- Polling newly-created `intake` Tickets or auto-starting Intake.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- Panel Queue produces a durable Orchestrator routing notification with correct semantics.
|
||||
- Orchestrator prompt/workflow states that `queued -> inprogress` acceptance must precede worktree/SpawnPod side effects.
|
||||
- A queued Ticket can be accepted and marked `inprogress` by Orchestrator routing when unblocked.
|
||||
- Blocked queued Tickets get a recorded reason rather than silent no-op.
|
||||
- Tests or prompt/resource tests cover Queue notification wording and the acceptance-order instruction.
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
Implemented, reviewed, merged, and validated.
|
||||
|
||||
Summary:
|
||||
- Updated Panel Queue notice so `ready -> queued` reports that Orchestrator routing is authorized and implementation side effects still require `queued -> inprogress` acceptance.
|
||||
- Updated Orchestrator notification content to include Ticket slug/id/title and instruct the Orchestrator to read the Ticket, inspect workspace state, accept with `queued -> inprogress` before worktree/SpawnPod side effects when unblocked, and record a concise blocked/defer reason when blocked.
|
||||
- Updated `.yoi/workflow/ticket-orchestrator-routing.md` so queued notifications are routing authorization, not passive no-op and not blind spawn permission.
|
||||
- Kept worktree/coder/reviewer routing, merge completion, and orchestration plan tooling out of this ticket.
|
||||
- Added focused tests for Queue notice and Orchestrator notification contract.
|
||||
|
||||
Implementation:
|
||||
- Child commit: `ccf43f8 tui: update queued ticket routing notification`
|
||||
- Parent workflow/report commit: `df6d7ee ticket: record queued routing implementation`
|
||||
- Merge commit: `merge: queued ticket routing`
|
||||
|
||||
Review:
|
||||
- External reviewer `orchestrator-routing-reviewer-20260607` approved with no blockers.
|
||||
|
||||
Validation after merge:
|
||||
- `cargo test -p tui multi_pod --lib`
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check`
|
||||
- `target/debug/yoi ticket doctor`
|
||||
|
|
@ -0,0 +1,201 @@
|
|||
<!-- event: create author: LocalTicketBackend at: 2026-06-07T03:51:43Z -->
|
||||
|
||||
## Created
|
||||
|
||||
Created by LocalTicketBackend create.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: plan author: hare at: 2026-06-07T04:57:33Z -->
|
||||
|
||||
## Plan
|
||||
|
||||
## Preflight / implementation intent
|
||||
|
||||
Classification: implementation-ready first slice of Orchestrator automation.
|
||||
|
||||
Intent:
|
||||
- Implement the queued Ticket routing entrypoint without starting coder/reviewer/worktree routing in this ticket.
|
||||
- Panel Queue should notify the workspace Orchestrator with active routing semantics: read Ticket/workspace state and start if unblocked.
|
||||
- Orchestrator workflow/prompt should define `queued -> inprogress` as the acceptance marker that must happen before implementation side effects.
|
||||
|
||||
Requirements:
|
||||
- Update Panel Queue notice and Orchestrator notification text away from passive/implementation-not-started semantics.
|
||||
- Notification must include Ticket id/slug/title where practical and instruct:
|
||||
- read the Ticket;
|
||||
- inspect current workspace state;
|
||||
- if unblocked, transition `queued -> inprogress` before worktree/SpawnPod side effects;
|
||||
- if blocked, record a concise reason and leave queued or explicitly defer.
|
||||
- Update `ticket-orchestrator-routing` workflow guidance to reflect queued-trigger routing and the acceptance-order invariant.
|
||||
- Do not implement worktree/coder/reviewer routing here; that belongs to `orchestrator-worktree-agent-routing`.
|
||||
- Do not implement merge/close completion here; that belongs to `orchestrator-merge-completion`.
|
||||
- Do not implement the orchestration plan tool here; `ticket-orchestration-plan-tool` owns durable ordering/dependency/conflict notes.
|
||||
|
||||
Current code map:
|
||||
- `crates/tui/src/multi_pod.rs`
|
||||
- Queue action around `dispatch_ticket_action` currently says `No implementation was started.`
|
||||
- `notify_workspace_orchestrator` currently says `Do not start implementation directly from this notification; follow routing/preflight gates.`
|
||||
- Tests around Queue outcome and notify-only path are nearby.
|
||||
- `.yoi/workflow/ticket-orchestrator-routing.md`
|
||||
- Currently says this workflow is not an unattended scheduler and requires human/upper Orchestrator permission before coder/reviewer spawn.
|
||||
- It should be updated so queued notifications are treated as human authorization to route; implementation side effects still require `queued -> inprogress` acceptance first.
|
||||
- `crates/ticket/src/tool.rs`
|
||||
- `TicketWorkflowState` tool/backend already enforce allowed transitions; do not duplicate transition enforcement in panel text.
|
||||
|
||||
Validation:
|
||||
- Update focused tests for Queue notice / notification wording.
|
||||
- Run `cargo test -p tui multi_pod --lib`.
|
||||
- Run prompt/resource relevant checks if available, otherwise `target/debug/yoi ticket doctor`, `cargo fmt --check`, and `git diff --check`.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: intake_summary author: INSOMNIA at: 2026-06-07T04:57:43Z -->
|
||||
|
||||
## Intake summary
|
||||
|
||||
Implementation-ready first slice: update Panel Queue notification/notice and Orchestrator routing workflow so queued Tickets actively trigger Orchestrator routing, with `queued -> inprogress` acceptance required before worktree/SpawnPod side effects. Worktree/coder/reviewer and merge completion remain separate follow-up tickets.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: INSOMNIA at: 2026-06-07T04:57:43Z from: intake to: ready reason: intake_ready field: workflow_state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Ticket intake complete; workflow_state intake -> ready.
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: hare at: 2026-06-07T04:58:26Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
## Delegation
|
||||
|
||||
Implementation delegated to child Pod `orchestrator-routing-coder-20260607` in worktree `.worktree/orchestrator-queued-ticket-routing` on branch `work/orchestrator-queued-ticket-routing`.
|
||||
|
||||
Scope:
|
||||
- Update Panel Queue notice and Orchestrator notification wording to actively trigger routing.
|
||||
- Preserve the contract that implementation side effects occur only after `queued -> inprogress` acceptance.
|
||||
- Update focused TUI tests.
|
||||
- Do not implement worktree/coder/reviewer routing, merge completion, or the orchestration plan tool in this ticket.
|
||||
|
||||
Workflow note:
|
||||
- Child worktree excludes `.yoi`, so workflow file updates under `.yoi/workflow` must be reported back for parent-side application rather than edited in the child worktree.
|
||||
|
||||
The child should commit implementation work in the child worktree and report diff/tests. Merge, review, Ticket closure, workflow file updates, and cleanup remain with the parent/human workflow.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: hare at: 2026-06-07T05:07:50Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
## Implementation report
|
||||
|
||||
Coder Pod `orchestrator-routing-coder-20260607` completed the child-worktree code slice in `.worktree/orchestrator-queued-ticket-routing`.
|
||||
|
||||
Commit:
|
||||
- `ccf43f8 tui: update queued ticket routing notification`
|
||||
|
||||
Reported summary:
|
||||
- Updated Panel Queue notice so it says Orchestrator routing is authorized and implementation side effects still require `queued -> inprogress` acceptance.
|
||||
- Updated Orchestrator notification text to include Ticket slug/id/title and instruct Orchestrator to read the Ticket, inspect workspace state, transition `queued -> inprogress` before worktree/SpawnPod side effects when unblocked, and record a concise blocked reason when blocked.
|
||||
- Added focused tests for the routing notification contract.
|
||||
- Did not implement worktree/coder/reviewer routing, merge completion, or plan-tool behavior.
|
||||
|
||||
Reported validation:
|
||||
- `cargo test -p tui multi_pod --lib`
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check`
|
||||
- `nix build .#yoi`
|
||||
|
||||
Parent-side workflow update:
|
||||
- Applied the reported `.yoi/workflow/ticket-orchestrator-routing.md` update in the main workspace because child worktrees exclude `.yoi`.
|
||||
- The workflow now treats Panel Queue / queued notification as routing authorization, requires `queued -> inprogress` before implementation side effects, and documents blocked queued handling.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: hare at: 2026-06-07T05:08:38Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
## Review delegation
|
||||
|
||||
External review delegated to `orchestrator-routing-reviewer-20260607`.
|
||||
|
||||
Review scope:
|
||||
- Child implementation commit `ccf43f8 tui: update queued ticket routing notification` in `.worktree/orchestrator-queued-ticket-routing`.
|
||||
- Parent-side workflow update in `.yoi/workflow/ticket-orchestrator-routing.md` committed on `develop` in `df6d7ee`.
|
||||
|
||||
Review focus:
|
||||
- Queue notice/notification should actively authorize Orchestrator routing without implying passive no-op behavior.
|
||||
- Notification must not instruct blind implementation Pod spawning.
|
||||
- Workflow must require `queued -> inprogress` acceptance before worktree/SpawnPod side effects.
|
||||
- Worktree/coder/reviewer routing, merge completion, and plan-tool behavior must remain out of scope.
|
||||
- Focused tests should cover the changed wording/contract.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: review author: orchestrator-routing-reviewer-20260607 at: 2026-06-07T05:12:59Z status: approve -->
|
||||
|
||||
## Review: approve
|
||||
|
||||
Approved.
|
||||
|
||||
Evidence:
|
||||
- Panel Queue no longer presents passive/no-op semantics. It records `ready -> queued`, notifies the workspace Orchestrator, and reports that Orchestrator routing is authorized while implementation side effects still require `queued -> inprogress` acceptance.
|
||||
- Orchestrator notification includes Ticket slug/id/title and instructs the Orchestrator to read the Ticket, inspect workspace state, transition `queued -> inprogress` before worktree/SpawnPod side effects when unblocked, and record a concise blocked/defer reason when blocked.
|
||||
- Wording does not encourage blind implementation Pod spawning from notification alone.
|
||||
- Parent-side `.yoi/workflow/ticket-orchestrator-routing.md` update matches the queued acceptance contract.
|
||||
- No worktree/coder/reviewer/merge implementation leaked into this ticket; child diff is limited to `crates/tui/src/multi_pod.rs` wording/tests.
|
||||
- Focused tests cover Queue notice and Orchestrator notification contract.
|
||||
|
||||
Reviewer validation:
|
||||
- `cargo test -p tui multi_pod --lib`
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check develop...HEAD`
|
||||
- `nix build .#yoi`
|
||||
- `git merge-tree --write-tree develop HEAD`
|
||||
|
||||
Merge readiness: approved; merge-tree against current `develop` succeeded. The workflow update is already present on `develop`.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: hare at: 2026-06-07T05:13:36Z from: ready to: done reason: closed field: workflow_state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Ticket closed; workflow_state set to done.
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: close author: hare at: 2026-06-07T05:13:36Z status: closed -->
|
||||
|
||||
## Closed
|
||||
|
||||
Implemented, reviewed, merged, and validated.
|
||||
|
||||
Summary:
|
||||
- Updated Panel Queue notice so `ready -> queued` reports that Orchestrator routing is authorized and implementation side effects still require `queued -> inprogress` acceptance.
|
||||
- Updated Orchestrator notification content to include Ticket slug/id/title and instruct the Orchestrator to read the Ticket, inspect workspace state, accept with `queued -> inprogress` before worktree/SpawnPod side effects when unblocked, and record a concise blocked/defer reason when blocked.
|
||||
- Updated `.yoi/workflow/ticket-orchestrator-routing.md` so queued notifications are routing authorization, not passive no-op and not blind spawn permission.
|
||||
- Kept worktree/coder/reviewer routing, merge completion, and orchestration plan tooling out of this ticket.
|
||||
- Added focused tests for Queue notice and Orchestrator notification contract.
|
||||
|
||||
Implementation:
|
||||
- Child commit: `ccf43f8 tui: update queued ticket routing notification`
|
||||
- Parent workflow/report commit: `df6d7ee ticket: record queued routing implementation`
|
||||
- Merge commit: `merge: queued ticket routing`
|
||||
|
||||
Review:
|
||||
- External reviewer `orchestrator-routing-reviewer-20260607` approved with no blockers.
|
||||
|
||||
Validation after merge:
|
||||
- `cargo test -p tui multi_pod --lib`
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check`
|
||||
- `target/debug/yoi ticket doctor`
|
||||
|
||||
---
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
---
|
||||
id: 20260607-035201-orchestrator-worktree-agent-routing
|
||||
slug: orchestrator-worktree-agent-routing
|
||||
title: Orchestrator worktree agent routing
|
||||
status: closed
|
||||
kind: task
|
||||
priority: P1
|
||||
labels: [orchestrator, worktree, pod, review, workflow]
|
||||
workflow_state: done
|
||||
created_at: 2026-06-07T03:52:01Z
|
||||
updated_at: 2026-06-07T05:55:57Z
|
||||
assignee: null
|
||||
legacy_ticket: null
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
After a queued Ticket is accepted as `inprogress`, the Orchestrator should be able to route implementation work through the existing `worktree-workflow` and `multi-agent-workflow` operational model. The current workflows already describe the desired behavior: create a child worktree, spawn a coder Pod with scoped write access, spawn an independent reviewer sibling, and run review/fix loops.
|
||||
|
||||
This ticket is the worktree/coder/reviewer execution slice under `workspace-panel-orchestrator-queue-automation`.
|
||||
|
||||
## Goal
|
||||
|
||||
Make the workspace Orchestrator execute accepted in-progress Tickets through worktree + coder/reviewer Pod routing using the existing workflow contracts as builtin/role guidance.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Use `worktree-workflow` as the mechanical worktree creation/management contract.
|
||||
- Use `multi-agent-workflow` as the coder/reviewer sibling orchestration contract.
|
||||
- Orchestrator must only start this path after the Ticket is accepted as `inprogress`.
|
||||
- Create one implementation worktree per Ticket/bounded task under `.worktree/<task-name>`.
|
||||
- Exclude `.yoi` from child worktrees and keep Ticket/thread/workflow updates in the main workspace.
|
||||
- Spawn coder Pods with read access to the main workspace and write access only to the child worktree or narrower implementation scope.
|
||||
- Spawn reviewer Pods as Orchestrator siblings, not coder children, with read-only scope by default.
|
||||
- Coder task prompts must include an intent packet: intent, requirements, invariants, non-goals, escalation conditions, validation expectations, worktree path, and branch.
|
||||
- Reviewer task prompts must focus on Ticket intent, diff, invariants, validation adequacy, and blocker/non-blocker classification.
|
||||
- Orchestrator must record durable progress without misrepresenting branch-local review as main-branch Ticket approval:
|
||||
- main Ticket thread may record accepted worktree/branch plan, coder delegated, coder completed/blocked, reviewer delegated, and fix-loop progress;
|
||||
- reviewer verdicts for an unmerged implementation branch should be captured in the merge-ready dossier or branch-scoped review report, not as a final main-branch Ticket approval event before merge;
|
||||
- if a reviewer requests changes, the Orchestrator may record a concise progress/blocker summary in the Ticket thread, but the branch remains unapproved until the fix loop and final merge-completion phase.
|
||||
- Orchestrator must not merge in this ticket's scope. It should produce a merge-ready dossier for `orchestrator-merge-completion` that includes the independent reviewer verdict and validation evidence.
|
||||
- If compaction occurs, workflow obligations should remain understandable; relate to `preserve-active-workflows-across-compaction` but do not block first implementation on it.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Queue notification / queued acceptance; that belongs to `orchestrator-queued-ticket-routing`.
|
||||
- Merge/cleanup/close; that belongs to `orchestrator-merge-completion`.
|
||||
- Broad conflict/dependency inference beyond the first-pass routing decision.
|
||||
- Dynamic workflow-state tool gating.
|
||||
- Letting coder Pods edit main workspace Ticket records or `.yoi`.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- Orchestrator has builtin/role guidance or implementation wiring that follows `worktree-workflow` and `multi-agent-workflow` for accepted Tickets.
|
||||
- Worktree creation excludes `.yoi`.
|
||||
- Coder and reviewer Pods are spawned as siblings under Orchestrator with the expected scopes.
|
||||
- Progress is recorded durably, while branch-local review verdicts are kept in the merge-ready dossier/review report rather than committed as final main-branch Ticket approval before merge.
|
||||
- A merge-ready dossier format is produced after branch review and validation.
|
||||
- Tests or prompt/resource tests cover the worktree/coder/reviewer routing instructions and scope boundaries.
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
Implemented, reviewed, merged, and validated.
|
||||
|
||||
Summary:
|
||||
- Added role-specific Orchestrator/Coder/Reviewer guidance for accepted in-progress Tickets.
|
||||
- Orchestrator guidance now routes only after `workflow_state = inprogress`, references `worktree-workflow` and `multi-agent-workflow`, records bounded progress, and stops at a merge-ready dossier.
|
||||
- Coder guidance keeps implementation in the child worktree/branch, forbids main `.yoi` / Ticket / workflow / docs / memory edits, requires validation/reporting, and forbids merge/close/delete/push.
|
||||
- Reviewer guidance keeps review sibling/read-only by default, focused on diff/intent/validation/blocker classification, with branch-local verdicts held for the merge-ready dossier rather than final main Ticket approval.
|
||||
- Panel Queue notification guidance now describes the post-acceptance worktree/coder/reviewer sibling route and merge-ready dossier stop without implementing merge/close.
|
||||
- No broad scheduler or worktree manager was added.
|
||||
|
||||
Implementation:
|
||||
- Child commit: `c7d6bb8 orchestrator: add agent routing guidance`
|
||||
- Merge commit: `merge: orchestrator agent routing`
|
||||
|
||||
Review:
|
||||
- External reviewer `orchestrator-agent-routing-reviewer-20260607` approved with no blockers.
|
||||
|
||||
Validation after merge:
|
||||
- `cargo test -p client ticket_role --lib`
|
||||
- `cargo test -p tui ticket_queue_notification_message_carries_routing_contract --lib`
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check`
|
||||
- `cargo build -p yoi`
|
||||
- `target/debug/yoi ticket doctor`
|
||||
|
|
@ -0,0 +1,202 @@
|
|||
<!-- event: create author: LocalTicketBackend at: 2026-06-07T03:52:01Z -->
|
||||
|
||||
## Created
|
||||
|
||||
Created by LocalTicketBackend create.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: plan author: hare at: 2026-06-07T05:39:34Z -->
|
||||
|
||||
## Plan
|
||||
|
||||
## Preflight / implementation intent
|
||||
|
||||
Classification: implementation-ready as the second Orchestrator automation slice, after `orchestrator-queued-ticket-routing` landed.
|
||||
|
||||
Intent:
|
||||
- Enable the workspace Orchestrator to route an accepted `inprogress` Ticket through the existing worktree + coder/reviewer sibling workflow.
|
||||
- Treat `worktree-workflow` and `multi-agent-workflow` as the operational contract to be builtin/role guidance for Orchestrator behavior.
|
||||
- Stop at a merge-ready dossier; do not merge/close in this ticket.
|
||||
|
||||
Prerequisite state:
|
||||
- `orchestrator-queued-ticket-routing` now makes Panel Queue notify Orchestrator and requires `queued -> inprogress` before implementation side effects.
|
||||
- Ticket role launch config strict validation and `yoi ticket init` scaffold are in place.
|
||||
- Ticket lifecycle feature access levels are in place.
|
||||
- The local role session registry exists for local Pod/session association.
|
||||
|
||||
Requirements for this slice:
|
||||
- Orchestrator must only create worktrees/spawn coder/reviewer after the Ticket is already `inprogress`.
|
||||
- Use `worktree-workflow` for mechanical worktree rules:
|
||||
- `.worktree/<task-name>`;
|
||||
- child worktree excludes `.yoi`;
|
||||
- main workspace remains authority for Ticket/workflow/docs records.
|
||||
- Use `multi-agent-workflow` for sibling coder/reviewer loop:
|
||||
- coder and reviewer are siblings under Orchestrator;
|
||||
- coder gets narrow write scope to child worktree;
|
||||
- reviewer is read-only by default;
|
||||
- coder gets intent packet, worktree/branch, validation, report expectations;
|
||||
- reviewer gets Ticket intent/diff/validation and blocker/non-blocker criteria.
|
||||
- Record durable progress in Ticket thread without prematurely recording main-branch approval:
|
||||
- allowed: worktree plan, coder delegated/completed/blocked, reviewer delegated, blocker/fix-loop summaries;
|
||||
- branch-local reviewer verdict stays in merge-ready dossier/review report until merge-completion phase.
|
||||
- Produce a merge-ready dossier for `orchestrator-merge-completion` after review approval and validation.
|
||||
|
||||
Likely implementation surfaces:
|
||||
- `.yoi/workflow/ticket-orchestrator-routing.md`, `.yoi/workflow/worktree-workflow.md`, `.yoi/workflow/multi-agent-workflow.md` for role/workflow guidance. Child worktree excludes `.yoi`; any workflow edits must be reported for parent-side application.
|
||||
- `crates/client/src/ticket_role.rs` already supports coder/reviewer launch context with worktree path, branch, intent packet, validation, and report expectations.
|
||||
- `crates/tui/src/multi_pod.rs` / panel runtime may already handle Orchestrator notifications; avoid adding full scheduler behavior here unless a small clear routing hook exists.
|
||||
- Worktree creation may remain prompt/workflow-driven for first pass; do not build a broad worktree manager unless clearly necessary.
|
||||
|
||||
Non-goals:
|
||||
- Do not implement Queue notification/acceptance; already done.
|
||||
- Do not implement merge/cleanup/close; `orchestrator-merge-completion` owns it.
|
||||
- Do not add dynamic state-machine tool gating.
|
||||
- Do not record reviewer approve as a final main Ticket approval before merge.
|
||||
- Do not let coder Pods edit main workspace `.yoi` or Ticket records.
|
||||
|
||||
Validation:
|
||||
- Add/update prompt/resource tests if available; otherwise add focused unit tests around any helper/prompt text changed.
|
||||
- Run focused tests for changed crates, plus `cargo fmt --check`, `git diff --check`, and `target/debug/yoi ticket doctor` if Ticket/workflow files are touched.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: intake_summary author: INSOMNIA at: 2026-06-07T05:39:48Z -->
|
||||
|
||||
## Intake summary
|
||||
|
||||
Implementation-ready as the second Orchestrator automation slice: after `queued -> inprogress` acceptance, route accepted Tickets through `worktree-workflow` and `multi-agent-workflow` as builtin/role guidance, spawn coder/reviewer sibling Pods with correct scopes, record progress durably, and stop at a merge-ready dossier without merge/close.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: INSOMNIA at: 2026-06-07T05:39:48Z from: intake to: ready reason: intake_ready field: workflow_state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Ticket intake complete; workflow_state intake -> ready.
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: hare at: 2026-06-07T05:40:37Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
## Delegation
|
||||
|
||||
Implementation delegated to child Pod `orchestrator-agent-routing-coder-20260607` in worktree `.worktree/orchestrator-worktree-agent-routing` on branch `work/orchestrator-worktree-agent-routing`.
|
||||
|
||||
Scope:
|
||||
- Add builtin/role guidance or focused implementation wiring for accepted `inprogress` Tickets to proceed through worktree + coder/reviewer sibling routing.
|
||||
- Follow `worktree-workflow` and `multi-agent-workflow` contracts.
|
||||
- Preserve `.yoi` exclusion in child worktrees and main-workspace authority for Ticket/workflow records.
|
||||
- Stop at merge-ready dossier; do not implement merge/close.
|
||||
- Keep branch-local reviewer verdicts out of final main Ticket approval until merge-completion.
|
||||
|
||||
Workflow note:
|
||||
- Child worktree excludes `.yoi`; if workflow file updates are needed, the child should report an exact parent-side patch rather than editing `.yoi`.
|
||||
|
||||
The child should commit implementation work in the child worktree and report diff/tests. Merge, review, Ticket closure, workflow file updates, and cleanup remain with the parent/human workflow.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: hare at: 2026-06-07T05:49:40Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
## Implementation report
|
||||
|
||||
Coder Pod `orchestrator-agent-routing-coder-20260607` completed implementation in `.worktree/orchestrator-worktree-agent-routing`.
|
||||
|
||||
Commit:
|
||||
- `c7d6bb8 orchestrator: add agent routing guidance`
|
||||
|
||||
Reported summary:
|
||||
- Added generated role guidance in `crates/client/src/ticket_role.rs`:
|
||||
- Orchestrator: route only after `workflow_state = inprogress`, use `worktree-workflow` and `multi-agent-workflow`, record bounded Ticket-thread progress, and stop at merge-ready dossier.
|
||||
- Coder: operate only in child worktree/branch, avoid main `.yoi`/Ticket/workflow/docs records, report validation/status, and do not merge/close/delete.
|
||||
- Reviewer: read-only by default, review branch diff/validation against intent, keep branch-local verdict in dossier/report, and do not record final main approval.
|
||||
- Expanded `crates/tui/src/multi_pod.rs` Queue notification guidance to mention the post-acceptance worktree/coder/reviewer sibling route and stopping at merge-ready dossier.
|
||||
- Added prompt-generation and queue-notification tests for these contracts.
|
||||
- Did not edit/copy `.yoi` workflow files in the child worktree.
|
||||
|
||||
Reported validation:
|
||||
- `cargo test -p client ticket_role -- --nocapture`
|
||||
- `cargo test -p tui ticket_queue_notification_message_carries_routing_contract -- --nocapture`
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check`
|
||||
- `cargo build -p yoi`
|
||||
- child-built `yoi ticket doctor` against the main workspace
|
||||
- `nix build .#yoi`
|
||||
|
||||
Parent-side workflow patch needed: none reported.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: review author: orchestrator-agent-routing-reviewer-20260607 at: 2026-06-07T05:55:11Z status: approve -->
|
||||
|
||||
## Review: approve
|
||||
|
||||
Approved.
|
||||
|
||||
Evidence:
|
||||
- Role-specific guidance in `crates/client/src/ticket_role.rs` gates worktree/Pod side effects on `workflow_state = inprogress`, names `worktree-workflow` and `multi-agent-workflow`, excludes `.yoi`, keeps Ticket/workflow/docs/memory authority in the main workspace, and stops at a merge-ready dossier.
|
||||
- Coder guidance limits work to the child worktree/branch, forbids main-workspace `.yoi` / Ticket / workflow / docs / memory edits, requires validation/status reporting, and forbids merge/push/close/delete.
|
||||
- Reviewer guidance states sibling/read-only-by-default review, diff/validation-based judgement, blocker classification, branch-local verdict capture, and no final main-branch approval/merge/close/push.
|
||||
- Panel Queue notification guidance preserves the queue/inprogress boundary and only mentions `.worktree/<task-name>`, `.yoi` exclusion, sibling coder/reviewer Pods, and merge-ready dossier after inprogress acceptance.
|
||||
- No broad scheduler or worktree manager was added; the diff is prompt/notification guidance plus focused tests.
|
||||
- Tests cover the prompt/notification contract for this slice.
|
||||
|
||||
Reviewer validation:
|
||||
- `cargo test -p client ticket_role -- --nocapture`
|
||||
- `cargo test -p tui ticket_queue_notification_message_carries_routing_contract -- --nocapture`
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check`
|
||||
- `cargo build -p yoi`
|
||||
- `nix build .#yoi --no-link`
|
||||
- child-built `yoi ticket doctor` against main workspace
|
||||
- `git merge-tree --write-tree develop HEAD`
|
||||
|
||||
Merge readiness: approved; implementation worktree clean and merge-tree against current `develop` succeeded.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: hare at: 2026-06-07T05:55:57Z from: ready to: done reason: closed field: workflow_state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Ticket closed; workflow_state set to done.
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: close author: hare at: 2026-06-07T05:55:57Z status: closed -->
|
||||
|
||||
## Closed
|
||||
|
||||
Implemented, reviewed, merged, and validated.
|
||||
|
||||
Summary:
|
||||
- Added role-specific Orchestrator/Coder/Reviewer guidance for accepted in-progress Tickets.
|
||||
- Orchestrator guidance now routes only after `workflow_state = inprogress`, references `worktree-workflow` and `multi-agent-workflow`, records bounded progress, and stops at a merge-ready dossier.
|
||||
- Coder guidance keeps implementation in the child worktree/branch, forbids main `.yoi` / Ticket / workflow / docs / memory edits, requires validation/reporting, and forbids merge/close/delete/push.
|
||||
- Reviewer guidance keeps review sibling/read-only by default, focused on diff/intent/validation/blocker classification, with branch-local verdicts held for the merge-ready dossier rather than final main Ticket approval.
|
||||
- Panel Queue notification guidance now describes the post-acceptance worktree/coder/reviewer sibling route and merge-ready dossier stop without implementing merge/close.
|
||||
- No broad scheduler or worktree manager was added.
|
||||
|
||||
Implementation:
|
||||
- Child commit: `c7d6bb8 orchestrator: add agent routing guidance`
|
||||
- Merge commit: `merge: orchestrator agent routing`
|
||||
|
||||
Review:
|
||||
- External reviewer `orchestrator-agent-routing-reviewer-20260607` approved with no blockers.
|
||||
|
||||
Validation after merge:
|
||||
- `cargo test -p client ticket_role --lib`
|
||||
- `cargo test -p tui ticket_queue_notification_message_carries_routing_contract --lib`
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check`
|
||||
- `cargo build -p yoi`
|
||||
- `target/debug/yoi ticket doctor`
|
||||
|
||||
---
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
<!-- event: create author: yoi ticket at: 2026-06-06T06:05:48Z -->
|
||||
|
||||
## Created
|
||||
|
||||
Created by LocalTicketBackend create.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: plan author: hare at: 2026-06-06T06:06:30Z -->
|
||||
|
||||
## Plan
|
||||
|
||||
Created after closing the first-pass workspace orchestration panel implementation.
|
||||
|
||||
The first pass deliberately prioritized end-to-end behavior over visual/layout polish. This ticket owns the next tuning pass: row labels, key hints, detail pane content, composer target visibility, concise diagnostics, and optional phase/dependency/timeline display, while preserving existing TUI conventions and the thin ViewModel/action-dispatch boundaries.
|
||||
|
||||
|
||||
---
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
---
|
||||
id: 20260606-233520-workspace-panel-nonblocking-transitions
|
||||
slug: workspace-panel-nonblocking-transitions
|
||||
title: Make workspace panel transitions non-blocking
|
||||
status: open
|
||||
kind: task
|
||||
priority: P2
|
||||
labels: [tui, panel, ux, performance]
|
||||
created_at: 2026-06-06T23:35:20Z
|
||||
updated_at: 2026-06-06T23:36:10Z
|
||||
assignee: null
|
||||
legacy_ticket: null
|
||||
workflow_state: intake
|
||||
attention_required: null
|
||||
queued_by: null
|
||||
queued_at: null
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
`yoi panel` currently feels delayed when opening the panel, attaching from the panel to a Pod, and returning from an attached Pod back to the panel.
|
||||
|
||||
The delay is not waiting for an LLM response. It is mostly waiting for synchronous local/runtime work before the next screen is drawn:
|
||||
|
||||
- initial panel load waits for a full panel snapshot;
|
||||
- Ticket-enabled panel load can ensure/observe the workspace Orchestrator before first draw;
|
||||
- opening a Pod waits for socket connect or restore/spawn before visible transition;
|
||||
- returning from a nested Pod waits for a synchronous panel reload before showing the panel again.
|
||||
|
||||
The panel already has a background `PendingReload` mechanism while it is visible, but the open/attach/return transition paths still block on reload/connect work before changing the visible screen.
|
||||
|
||||
## Goal
|
||||
|
||||
Make workspace panel transitions feel immediate by showing the next relevant screen/state first and moving snapshot reload / Pod connection / Orchestrator observation work into background or explicit progress states where practical.
|
||||
|
||||
## Problem points
|
||||
|
||||
Current important paths:
|
||||
|
||||
```text
|
||||
yoi panel
|
||||
→ MultiPodApp::load(...)
|
||||
→ load_multi_pod_snapshot(..., OrchestratorLifecycleMode::Ensure)
|
||||
→ first draw only after snapshot/ensure completes
|
||||
```
|
||||
|
||||
```text
|
||||
panel Enter on Pod
|
||||
→ prepare_open()
|
||||
→ run_pod_name_nested(...)
|
||||
→ try_connect_live_pod / restore/spawn
|
||||
→ single-Pod screen only after connect/restore is ready
|
||||
```
|
||||
|
||||
```text
|
||||
return from attached Pod
|
||||
→ app.finish_open(...)
|
||||
→ app.reload_or_notice().await
|
||||
→ panel draw only after reload completes
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- Returning from an attached Pod should redraw the previous panel immediately, with a clear refreshing notice, then perform snapshot reload in the background.
|
||||
- Avoid awaiting `app.reload_or_notice().await` on the attach-return path before the panel is visible.
|
||||
- Opening/attaching a Pod should visibly transition to an attaching/restoring progress state before waiting on socket connect or restore/spawn.
|
||||
- Initial `yoi panel` open should avoid blocking first draw on non-essential refresh/ensure work where practical.
|
||||
- A minimal/loading panel or last/current partial snapshot is acceptable.
|
||||
- Orchestrator ensure can be surfaced as background progress/diagnostic if it cannot be completed before first draw cheaply.
|
||||
- Preserve correctness: background reload must still update Pod state, Ticket rows, Orchestrator state, and diagnostics when it completes.
|
||||
- Preserve no-Ticket Pod-centric behavior.
|
||||
- Do not lose key input during transition/progress states.
|
||||
- Do not introduce duplicate overlapping reload tasks; reuse/extend `PendingReload` or similar guard.
|
||||
- Do not change Ticket workflow semantics or Pod restore/spawn authority.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Changing Ticket workflow state.
|
||||
- Changing Orchestrator scheduling semantics.
|
||||
- Rewriting the whole TUI runtime loop.
|
||||
- Replacing the single-Pod TUI.
|
||||
- Cosmetic layout tuning unrelated to transition latency.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- Returning from a nested Pod displays the panel before panel snapshot reload completes.
|
||||
- Panel shows a concise `refreshing` / `attaching` / `restoring` style notice during background work.
|
||||
- Background reload completion updates the panel without dropping selection/composer text unnecessarily.
|
||||
- Attach/open path gives visible feedback before slow socket connect/restore work.
|
||||
- Tests cover the non-blocking return/reload behavior where practical.
|
||||
- Existing validation for panel, Pod-centric mode, and Ticket-enabled mode continues to pass.
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<!-- event: create author: "yoi ticket" at: 2026-06-06T23:35:20Z -->
|
||||
|
||||
## Created
|
||||
|
||||
Created by LocalTicketBackend create.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: plan author: hare at: 2026-06-06T23:36:10Z -->
|
||||
|
||||
## Plan
|
||||
|
||||
Created from investigation of perceived delays when opening `yoi panel`, attaching to a Pod, and returning from an attached Pod.
|
||||
|
||||
Root cause recorded:
|
||||
- the panel waits for full snapshot/Orchestrator work before first draw;
|
||||
- attach waits for socket connect or restore/spawn before a visible transition;
|
||||
- return from nested Pod awaits `app.reload_or_notice().await` before showing the panel again.
|
||||
|
||||
Desired direction:
|
||||
- show the panel/progress state first;
|
||||
- move reload/connect/ensure work into background or explicit progress states where practical;
|
||||
- especially make return-from-attach redraw the previous panel immediately and refresh in the background.
|
||||
|
||||
|
||||
---
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
---
|
||||
id: 20260607-001651-companion-status-context-tool-policy
|
||||
slug: companion-status-context-tool-policy
|
||||
title: Companion status context and tool policy
|
||||
status: open
|
||||
kind: task
|
||||
priority: P2
|
||||
labels: [companion, profile, prompt, tools, panel]
|
||||
workflow_state: intake
|
||||
created_at: 2026-06-07T00:16:51Z
|
||||
updated_at: 2026-06-07T02:45:32Z
|
||||
assignee: null
|
||||
legacy_ticket: null
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
The workspace Companion should help the human understand and steer the workspace, not act as a direct implementation worker. It should know what is complete, what remains, what is queued/in progress, which Pods exist, and where attention may be useful. It should not directly edit files, mutate Tickets, spawn implementation Pods, or perform destructive actions.
|
||||
|
||||
This requires a distinct prompt/profile/tool policy from ordinary coder/reviewer/orchestrator Pods.
|
||||
|
||||
## Goal
|
||||
|
||||
Define and implement the Companion's prompt/profile/tool policy so it can provide real-time situational assistance while being prevented from direct write/mutation authority.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Add or define a Companion role/profile/prompt for workspace panel usage.
|
||||
- System prompt should instruct the Companion to:
|
||||
- summarize current workspace status;
|
||||
- explain completed/remaining work;
|
||||
- help the human understand Ticket/Pod/Orchestrator state;
|
||||
- suggest next safe high-level actions;
|
||||
- avoid directly implementing, editing, or mutating project state.
|
||||
- Tool policy should prohibit direct file writes and direct Ticket mutation by default.
|
||||
- Tool policy should prohibit spawning implementation/review Pods directly unless a later explicit design grants that authority.
|
||||
- Companion may have read/status capabilities needed for situational awareness, such as bounded Ticket list/show, Pod list/state, and possibly read-only docs/context access.
|
||||
- If read-only status is provided through a specialized summary/context mechanism rather than raw tools, keep it auditable and derived from authoritative Ticket/Pod state.
|
||||
- Companion should not receive hidden, non-history context that affects behavior without being committed to an appropriate history/event path.
|
||||
- Keep secrets/private input out of diagnostics, status summaries, and prompts.
|
||||
- Preserve Orchestrator as the actor responsible for scheduling/routing work; Companion is human-facing support, not scheduler authority.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Giving Companion write access.
|
||||
- Replacing Orchestrator.
|
||||
- Replacing Ticket tools/workflows.
|
||||
- Building a full scheduler.
|
||||
- Final UI layout tuning.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- A Companion prompt/profile/tool policy exists and is used by `yoi panel` Companion lifecycle.
|
||||
- Companion can answer status questions from read-only/derived authoritative sources.
|
||||
- Companion cannot directly mutate repository files or Ticket records under default policy.
|
||||
- Companion cannot directly launch implementation/review Pods under default policy.
|
||||
- Prompt/tool docs clearly distinguish Companion from Orchestrator, Intake, coder, and reviewer roles.
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
<!-- event: create author: "yoi ticket" at: 2026-06-07T00:16:51Z -->
|
||||
|
||||
## Created
|
||||
|
||||
Created by LocalTicketBackend create.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: comment author: hare at: 2026-06-07T01:21:43Z -->
|
||||
|
||||
## Comment
|
||||
|
||||
## Status context boundary
|
||||
|
||||
When local role session / Ticket claim overlay support is added, it can become one source of read-only Companion status context. The Companion should treat it as local runtime status, distinct from authoritative git-tracked Ticket project records.
|
||||
|
||||
Default Companion policy should still prohibit direct mutation of Ticket records and direct role Pod spawning/claiming unless a later explicit design grants that authority.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: decision author: hare at: 2026-06-07T02:45:32Z -->
|
||||
|
||||
## Decision
|
||||
|
||||
## Companion Bash policy decision
|
||||
|
||||
Default Companion policy should not include Bash.
|
||||
|
||||
Rationale:
|
||||
- Companion and Orchestrator both operate around the workspace root, but only Orchestrator should hold workspace operation authority.
|
||||
- Companion is a human-facing status/understanding assistant, not an actor that creates orchestration side effects.
|
||||
- Bash is too broad to treat as safely read-only by prompt alone. Even seemingly read-only commands can touch git locks/index state, build caches, `target/`, package caches, or long-running CPU/IO resources.
|
||||
- Adding reliable read-only constraints to Bash would become a sandbox/policy redesign, not a small Companion-policy detail.
|
||||
|
||||
Policy:
|
||||
- Default Companion: no Bash, no direct file writes, no Ticket mutation, no SpawnPod/worktree/merge authority.
|
||||
- Prefer typed read/status tools and derived panel/registry/Ticket/Pod context for situational awareness.
|
||||
- If future dogfooding shows Companion needs shell diagnostics, create a separate explicit design/ticket for an opt-in diagnostic Bash/read-only shell capability rather than adding Bash to the default Companion profile.
|
||||
|
||||
Operational trigger for revisiting:
|
||||
- Users repeatedly want Companion to perform clear shell-based diagnostics; or
|
||||
- Prompt-level "read-only" instructions prove insufficient and Companion attempts or performs unsafe Bash actions.
|
||||
|
||||
---
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
---
|
||||
id: 20260607-001651-workspace-panel-companion-interface
|
||||
slug: workspace-panel-companion-interface
|
||||
title: Workspace panel Companion interface
|
||||
status: open
|
||||
kind: task
|
||||
priority: P2
|
||||
labels: [tui, panel, companion, orchestration]
|
||||
workflow_state: intake
|
||||
created_at: 2026-06-07T00:16:51Z
|
||||
updated_at: 2026-06-07T03:13:01Z
|
||||
assignee: null
|
||||
legacy_ticket: null
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
The current `yoi panel` `Companion` composer target is a misleading name: it is effectively the selected-Pod direct-send path. The desired UX is different.
|
||||
|
||||
The workspace panel should have a real Companion: a workspace-scoped Pod used for the foreground management conversation. It should help the human understand what is done, what remains, what is blocked/queued/in progress, and where intervention is useful. It should not be a direct write-capable worker.
|
||||
|
||||
Direct message sending from the panel to arbitrary selected Pods should be removed. The panel composer should talk to the Companion by default, with Ticket Intake as the other explicit creation path in Ticket-enabled workspaces.
|
||||
|
||||
## Goal
|
||||
|
||||
Redesign the workspace panel composer around a real workspace Companion Pod and remove the selected-Pod direct-send UX.
|
||||
|
||||
## Target model
|
||||
|
||||
- `yoi panel` restores/spawns a workspace Companion Pod.
|
||||
- The Companion Pod name is based on the workspace name, matching the default workspace Pod naming direction where practical.
|
||||
- The panel composer default target is the Companion conversation.
|
||||
- Ticket Intake remains a separate target for creating/materializing new Ticket requests.
|
||||
- Selected Pod direct send is removed from the panel.
|
||||
- Opening/attaching to a Pod remains available for inspecting details.
|
||||
- The Companion is status-aware but not write-capable.
|
||||
|
||||
## Child tickets
|
||||
|
||||
1. `workspace-panel-remove-direct-pod-send`
|
||||
- Remove selected-Pod direct send from `yoi panel`; keep attach/open and Ticket Intake.
|
||||
|
||||
2. `workspace-panel-companion-pod-lifecycle`
|
||||
- Restore/spawn the workspace-named Companion Pod and route Companion composer input to it.
|
||||
|
||||
3. `companion-status-context-tool-policy`
|
||||
- Define Companion prompt/profile/tool policy: status-awareness and human assistance, no direct repository writes or Ticket mutations.
|
||||
|
||||
4. `workspace-panel-local-role-session-registry`
|
||||
- Define the local user-data overlay for role sessions and Ticket claims; keep local Pod assignment out of git-tracked Ticket metadata.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Removing Pod attach/open.
|
||||
- Removing Ticket Intake target.
|
||||
- Removing Orchestrator lifecycle.
|
||||
- Giving Companion direct write access or implementation authority.
|
||||
- Reintroducing `--multi` or `:ticket`.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- The workspace panel no longer treats `Companion` as selected-Pod direct send.
|
||||
- The panel has a real workspace Companion Pod/session as the foreground management chat.
|
||||
- Direct selected-Pod send is removed or disabled with clear UI behavior.
|
||||
- Companion tool policy prevents direct file/Ticket mutation while still allowing status awareness.
|
||||
- No-Ticket workspaces still get a useful Companion/Pod-inspection panel.
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
<!-- event: create author: "yoi ticket" at: 2026-06-07T00:16:51Z -->
|
||||
|
||||
## Created
|
||||
|
||||
Created by LocalTicketBackend create.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: decision author: hare at: 2026-06-07T00:18:58Z -->
|
||||
|
||||
## Decision
|
||||
|
||||
Decision from user discussion:
|
||||
|
||||
The panel should not provide direct messaging to arbitrary selected Pods. The existing `Companion` composer target is currently a misleading label for selected-Pod direct send and should be replaced by a real workspace Companion Pod.
|
||||
|
||||
Target model:
|
||||
- default panel composer talks to a workspace-named Companion Pod;
|
||||
- Companion is a foreground management chat for the human;
|
||||
- Ticket Intake remains a separate target for new requests;
|
||||
- selected Pod direct send is removed;
|
||||
- Pod attach/open remains available for inspection;
|
||||
- Companion should be status-aware but not directly write/mutate project state;
|
||||
- Companion prompt/tool policy should focus on situational awareness and human support, with direct writes/Ticket mutations/implementation spawning prohibited by default.
|
||||
|
||||
Split child tickets were created for direct-send removal, Companion lifecycle, and Companion prompt/tool policy.
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: decision author: hare at: 2026-06-07T01:21:43Z -->
|
||||
|
||||
## Decision
|
||||
|
||||
## Local role/session overlay split
|
||||
|
||||
The Companion/panel redesign should not store Ticket↔local Pod assignment in git-tracked Ticket metadata. Local Pod names, runtime/session identity, socket/restorable state, and current claims are per-machine runtime concerns.
|
||||
|
||||
A new child ticket `workspace-panel-local-role-session-registry` covers the local overlay model:
|
||||
|
||||
- Ticket project records keep workflow state and auditable summaries only.
|
||||
- A Ticket may have at most one active local Pod claim at a time.
|
||||
- A Pod/session may relate to zero or more Tickets.
|
||||
- Intake is not 1:1 with Ticket: pre-Ticket Intake may have no Ticket yet, and one Intake session may materialize/split into multiple Tickets.
|
||||
- Existing `ticket-*` Pod visibility in the panel is acceptable as an interim access path until the registry UI/model exists.
|
||||
- The panel should not poll and automatically start Intake for newly-created `workflow_state = intake` Tickets; starting/claiming remains an explicit action.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: decision author: hare at: 2026-06-07T02:02:21Z -->
|
||||
|
||||
## Decision
|
||||
|
||||
## Orchestrator automation follow-up
|
||||
|
||||
Created `workspace-panel-orchestrator-queue-automation` to cover the missing end-to-end route from Panel Intake/Queue into Orchestrator-managed work.
|
||||
|
||||
This follow-up fixes the intended `queued` semantics:
|
||||
- `queued` means the human authorized Orchestrator routing;
|
||||
- the Orchestrator should inspect Ticket/workspace state and start if unblocked;
|
||||
- conflicts, dependency blockers, preflight gaps, and capacity constraints are Orchestrator decisions, not reasons for the panel to stay passive forever.
|
||||
|
||||
This remains separate from Companion lifecycle and local role/session registry work, but may consume the registry once available to avoid duplicate ownership.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: decision author: hare at: 2026-06-07T03:13:01Z -->
|
||||
|
||||
## Decision
|
||||
|
||||
## Priority adjustment
|
||||
|
||||
Companion work is useful but not required for near-term panel operation. The panel can still be used by manually attaching/opening Pods, and the direct selected-Pod send path has already been removed.
|
||||
|
||||
Decision: downgrade Companion-related follow-up priority to P2 so near-term focus can stay on Ticket role config strictness/init, Orchestrator queue automation, and workflow/compaction reliability.
|
||||
|
||||
---
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
---
|
||||
id: 20260607-001651-workspace-panel-companion-pod-lifecycle
|
||||
slug: workspace-panel-companion-pod-lifecycle
|
||||
title: Workspace panel Companion Pod lifecycle
|
||||
status: open
|
||||
kind: task
|
||||
priority: P2
|
||||
labels: [tui, panel, companion, pod]
|
||||
workflow_state: intake
|
||||
created_at: 2026-06-07T00:16:51Z
|
||||
updated_at: 2026-06-07T01:21:43Z
|
||||
assignee: null
|
||||
legacy_ticket: null
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
The panel needs a real Companion Pod rather than treating `Companion` as selected-Pod direct send. The Companion is the foreground workspace management chat: the place where the human asks what is happening, what is done, what remains, and where attention is useful.
|
||||
|
||||
The desired identity is a workspace-named Pod, similar to the default Pod created from a workspace name when starting normally. This should make `yoi panel` feel like opening the workspace's management conversation rather than a raw Pod dashboard.
|
||||
|
||||
## Goal
|
||||
|
||||
Restore or spawn a workspace-named Companion Pod when `yoi panel` opens, and route the panel Companion composer target to that Pod.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Define the Companion Pod naming rule.
|
||||
- It should be based on the workspace name/path, consistent with existing default Pod naming where practical.
|
||||
- Avoid colliding with the workspace Orchestrator name.
|
||||
- On `yoi panel` open:
|
||||
- if Companion is live, use it;
|
||||
- if restorable, restore it;
|
||||
- if missing, spawn it with the Companion profile/prompt/policy from the companion tool-policy ticket;
|
||||
- surface bounded diagnostics if unavailable.
|
||||
- Panel close must not stop the Companion.
|
||||
- The panel composer `Companion` target sends user text to the Companion Pod history, not to arbitrary selected Pods.
|
||||
- The Companion target is available in both Ticket-enabled and no-Ticket workspaces.
|
||||
- Ticket Intake remains a separate target in Ticket-enabled workspaces and must not append Intake text to Companion history.
|
||||
- Preserve Pod attach/open for inspection.
|
||||
- Avoid blocking panel redraw longer than necessary; coordinate with `workspace-panel-nonblocking-transitions` if needed.
|
||||
- Do not give the Companion broad write scope by default.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Direct selected-Pod send.
|
||||
- Orchestrator scheduling semantics.
|
||||
- Ticket Intake handoff changes beyond preserving separation.
|
||||
- Tool/prompt policy details beyond consuming the agreed Companion profile/policy.
|
||||
- Reintroducing `--multi` or `:ticket`.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- `yoi panel` has a real Companion Pod identity and lifecycle.
|
||||
- Companion composer messages are committed to the Companion Pod's history.
|
||||
- No-Ticket workspaces still have a Companion management chat plus Pod attach/open.
|
||||
- Ticket Intake messages do not enter Companion history.
|
||||
- Companion remains running after panel exits.
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<!-- event: create author: "yoi ticket" at: 2026-06-07T00:16:51Z -->
|
||||
|
||||
## Created
|
||||
|
||||
Created by LocalTicketBackend create.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: comment author: hare at: 2026-06-07T01:21:43Z -->
|
||||
|
||||
## Comment
|
||||
|
||||
## Dependency note: local role/session registry
|
||||
|
||||
Companion lifecycle should remain separate from Ticket/role Pod claim authority. Local role session and Ticket claim data belongs in the user-data workspace overlay planned by `workspace-panel-local-role-session-registry`, not in git-tracked Ticket metadata.
|
||||
|
||||
The Companion may eventually read/display this derived status, but it should not own the registry or gain mutation authority by default.
|
||||
|
||||
---
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
---
|
||||
id: 20260607-020215-workspace-panel-orchestrator-queue-automation
|
||||
slug: workspace-panel-orchestrator-queue-automation
|
||||
title: Workspace panel Orchestrator queue automation
|
||||
status: open
|
||||
kind: task
|
||||
priority: P1
|
||||
labels: [panel, orchestrator, ticket, automation, workflow]
|
||||
workflow_state: intake
|
||||
created_at: 2026-06-07T02:02:15Z
|
||||
updated_at: 2026-06-07T03:57:24Z
|
||||
assignee: null
|
||||
legacy_ticket: null
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
The panel currently has pieces of the Ticket orchestration path:
|
||||
|
||||
- Panel Ticket Intake can start an Intake role Pod from a user instruction.
|
||||
- Intake can materialize or update a Ticket and mark it `workflow_state = ready`.
|
||||
- Panel can Queue a ready Ticket, transitioning `ready -> queued` and notifying the workspace Orchestrator.
|
||||
|
||||
However, the intended semantics of `queued` are stronger than a passive notification. Once a human queues a Ticket, the Orchestrator should treat it as eligible for routing and proceed if the workspace state allows it. The Orchestrator, not the panel, should decide whether work can start now or should wait because of conflicts, dependencies, capacity, or preflight gaps.
|
||||
|
||||
The current route is not yet a complete automated path from Panel/Intake through Orchestrator-managed implementation, review, merge, and Ticket completion.
|
||||
|
||||
## Goal
|
||||
|
||||
Implement a first-class Panel -> Intake/Queue -> Orchestrator automation path where queued Tickets are actively routed by the Orchestrator and, when unblocked, progressed through implementation/review/merge/close workflow.
|
||||
|
||||
## Target semantics
|
||||
|
||||
- `ready`: Ticket is materialized and can be queued by the human.
|
||||
- `queued`: Human has authorized Orchestrator routing. The Orchestrator should inspect current state and start work if unblocked.
|
||||
- `inprogress`: Orchestrator has accepted the Ticket as its durable responsibility marker and owns routing/coordination until completion, explicit defer, or explicit block. It does not merely mean that a coder Pod is already running.
|
||||
- `done`: Implementation/review/merge outcome is complete and the Ticket can be closed or has been closed.
|
||||
|
||||
## Orchestrator acceptance contract
|
||||
|
||||
- `inprogress` is the durable Orchestrator acceptance marker for a queued Ticket.
|
||||
- Orchestrator must inspect the Ticket and current workspace state before accepting queued work.
|
||||
- Orchestrator must transition `queued -> inprogress` before creating implementation worktrees or spawning implementation/review Pods, unless a typed operation performs the acceptance and side-effect setup atomically.
|
||||
- If `queued -> inprogress` fails, Orchestrator must not spawn implementation/review Pods for that Ticket.
|
||||
- After `inprogress` acceptance, worktree/spawn/first-run failures should be recorded as progress/block/failure on the in-progress Ticket rather than silently reverting to queued or starting a duplicate route.
|
||||
- The short-term implementation may enforce this through Orchestrator prompt/workflow text plus the existing typed Ticket workflow-state tool.
|
||||
- The long-term target is a typed Orchestrator operation such as `AcceptQueuedTicket` or `StartTicketWork` that validates the queued state, records acceptance, establishes any local claim/lease, and returns the bounded context needed for worktree/Pod launch.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Update the panel Queue notification/prompt text so it clearly tells the Orchestrator to route the queued Ticket and start if unblocked, rather than implying that implementation must not start.
|
||||
- Add Orchestrator-side handling for queued Tickets:
|
||||
- read the Ticket and current workflow state;
|
||||
- inspect active worktrees/branches/Pods and queued/inprogress Tickets where available;
|
||||
- decide whether there are conflicts, dependency blockers, preflight gaps, or capacity constraints;
|
||||
- if unblocked, transition `queued -> inprogress` using the typed Ticket workflow tool/path before implementation side effects;
|
||||
- if blocked, record a concise reason and leave the Ticket queued or defer through the existing Ticket status/state mechanism as appropriate.
|
||||
- Ensure Orchestrator cannot create implementation worktrees or spawn implementation/review Pods for a queued Ticket until the Ticket is already accepted as `inprogress` or the same typed operation atomically accepts it.
|
||||
- Add or design toward a typed accept/start operation for Orchestrator routing so the `queued -> inprogress` acceptance contract is not only prompt-enforced.
|
||||
- Connect Orchestrator routing to the existing Ticket workflows:
|
||||
- `ticket-preflight-workflow` when requirements/design readiness are uncertain;
|
||||
- `worktree-workflow` for worktree creation/management;
|
||||
- `multi-agent-workflow` or equivalent sibling coder/reviewer routing for implementation and review.
|
||||
- Ensure Orchestrator can spawn or coordinate implementation/review Pods only after the queued Ticket has been accepted as in progress and scope/worktree boundaries are clear.
|
||||
- Carry enough run/assignment state for the panel to show that a queued Ticket has been accepted or is blocked/waiting.
|
||||
- Define the merge/completion boundary explicitly:
|
||||
- for local dogfooding, Orchestrator may proceed through merge/cleanup when the queued workflow and current policy authorize it;
|
||||
- otherwise it must stop at a merge-ready dossier with a clear human action required.
|
||||
- After successful merge/validation, record implementation/report/review events and close or mark the Ticket done according to the typed Ticket workflow state rules.
|
||||
- Preserve the human Queue gate: newly ready Tickets must not be automatically started without being queued.
|
||||
- Preserve no-polling semantics for Intake creation; the automation begins from explicit Queue or explicit Orchestrator action, not background file polling.
|
||||
- Coordinate with `workspace-panel-local-role-session-registry` if local role/session assignment is needed to prevent duplicate Orchestrator/coder/reviewer ownership.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Making the panel itself a scheduler.
|
||||
- Starting implementation directly from Intake before the Ticket reaches `ready` and is queued.
|
||||
- Ignoring conflicts with existing active worktrees, branches, or in-progress Tickets.
|
||||
- Blindly spawning coder Pods from a notification without reading the Ticket and current workspace state.
|
||||
- Replacing the existing Ticket workflow documents; this ticket should wire them into the Orchestrator route.
|
||||
- Reintroducing direct selected-Pod send from the panel.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- Queue notification/prompt semantics say that Orchestrator should route and start if unblocked.
|
||||
- A queued Ticket can be accepted by the Orchestrator, moved to `inprogress`, and routed to implementation through existing worktree/coder/reviewer workflows.
|
||||
- Implementation/review Pod spawning is prevented for a queued Ticket unless the Ticket has already been accepted as `inprogress` or a typed accept/start operation performs that transition atomically.
|
||||
- If acceptance fails, no implementation side effects occur; if post-acceptance side effects fail, the failure/block is recorded on the in-progress Ticket.
|
||||
- Blocking conditions are recorded instead of silently doing nothing.
|
||||
- The panel can distinguish at least queued-waiting, in-progress, and blocked/deferred outcomes from Orchestrator routing.
|
||||
- The path from Panel Intake -> ready Ticket -> Queue -> Orchestrator routing is documented and covered by focused tests or integration-style unit tests around the routing logic.
|
||||
- The merge/completion boundary is explicit and validated against current policy before automatic merge/close is allowed.
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
<!-- event: create author: LocalTicketBackend at: 2026-06-07T02:02:15Z -->
|
||||
|
||||
## Created
|
||||
|
||||
Created by LocalTicketBackend create.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: decision author: hare at: 2026-06-07T02:35:20Z -->
|
||||
|
||||
## Decision
|
||||
|
||||
## In-progress acceptance contract
|
||||
|
||||
`workflow_state = inprogress` should be the durable Orchestrator acceptance marker, not merely proof that a coder Pod is already running.
|
||||
|
||||
Contract:
|
||||
- `ready`: materialized Ticket waiting for the human Queue gate.
|
||||
- `queued`: human has authorized Orchestrator routing; Orchestrator should inspect Ticket/workspace state and start if unblocked.
|
||||
- `inprogress`: Orchestrator has accepted the queued Ticket and owns coordination/progress/block/failure reporting until completion or explicit defer/block.
|
||||
|
||||
Ordering invariant:
|
||||
- Orchestrator must not create implementation worktrees or spawn implementation/review Pods for a queued Ticket unless the Ticket has already been accepted as `inprogress`, or the same typed operation atomically accepts it.
|
||||
- If `queued -> inprogress` fails, do not spawn implementation Pods.
|
||||
- If side effects fail after `inprogress` acceptance, record the failure/block under the in-progress Ticket rather than silently returning to queued or starting a duplicate path.
|
||||
|
||||
Short-term implementation may enforce this through Orchestrator workflow/prompt text plus the existing typed workflow-state tool. Longer-term, prefer a typed Orchestrator operation such as `AcceptQueuedTicket` / `StartTicketWork` that validates `queued`, records the acceptance transition, establishes any local claim/lease, and only then allows worktree/Pod launch.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: decision author: hare at: 2026-06-07T03:35:44Z -->
|
||||
|
||||
## Decision
|
||||
|
||||
## Split: Ticket lifecycle domain feature
|
||||
|
||||
Created `ticket-lifecycle-pod-feature` to pull the domain tool/feature part out of Orchestrator automation.
|
||||
|
||||
Decision:
|
||||
- Ticket lifecycle should be implemented as a Ticket-domain `pod::feature`, not an Orchestrator feature.
|
||||
- Features represent domain capabilities; roles/profiles decide which subset/authority they receive.
|
||||
- Orchestrator automation should consume the Ticket lifecycle feature for queued Ticket inspection and lifecycle transitions.
|
||||
- Companion/Intake/coder/reviewer may receive different Ticket feature subsets according to their policies.
|
||||
|
||||
This keeps `workspace-panel-orchestrator-queue-automation` focused on routing behavior, queued notification handling, acceptance sequencing, and worktree/Pod orchestration rather than basic Ticket tool registration.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: decision author: hare at: 2026-06-07T03:56:37Z -->
|
||||
|
||||
## Decision
|
||||
|
||||
## Split into routing / agent execution / merge completion
|
||||
|
||||
Split the Orchestrator automation work into three child tickets so the implementation can proceed in bounded slices while preserving the existing `worktree-workflow` and `multi-agent-workflow` contracts.
|
||||
|
||||
Child tickets:
|
||||
|
||||
1. `orchestrator-queued-ticket-routing`
|
||||
- Panel Queue notification and Orchestrator routing entrypoint.
|
||||
- `queued -> inprogress` acceptance before implementation side effects.
|
||||
- First-pass blocker/dependency/conflict recording.
|
||||
|
||||
2. `orchestrator-worktree-agent-routing`
|
||||
- Use `worktree-workflow` and `multi-agent-workflow` as builtin/role guidance for accepted in-progress Tickets.
|
||||
- Create child worktree, spawn coder/reviewer sibling Pods with correct scopes, run review/fix loop, and produce merge-ready dossier.
|
||||
|
||||
3. `orchestrator-merge-completion`
|
||||
- Merge authority boundary, reviewer approval, validation, merge, Ticket done/close, and worktree/branch/Pod cleanup.
|
||||
- Dogfooding workspace may authorize merge/cleanup/close; conservative/default mode stops at merge-ready dossier.
|
||||
|
||||
This keeps the umbrella focused on the end-to-end Panel Queue -> Orchestrator automation while allowing each operational slice to land independently.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: decision author: hare at: 2026-06-07T03:57:24Z -->
|
||||
|
||||
## Decision
|
||||
|
||||
## Related planning-memory split
|
||||
|
||||
Created `ticket-orchestration-plan-tool` for the lightweight Orchestrator memory/tool surface around Ticket ordering, dependency, conflict, capacity, and accepted-plan decisions.
|
||||
|
||||
Decision:
|
||||
- Use Ticket/orchestration domain records for project-relevant routing decisions rather than session-lifetime Task tools.
|
||||
- Keep local Pod/session claims in the local role session registry.
|
||||
- Keep this separate from the core queued routing / worktree-agent / merge-completion slices so the first automation path can still rely on prompt/workflow sequencing while gaining a durable place for ordering/dependency decisions.
|
||||
|
||||
---
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
---
|
||||
id: 20260607-022328-preserve-active-workflows-across-compaction
|
||||
slug: preserve-active-workflows-across-compaction
|
||||
title: Preserve active workflows across compaction
|
||||
status: open
|
||||
kind: task
|
||||
priority: P1
|
||||
labels: [workflow, compaction, history, orchestration]
|
||||
workflow_state: intake
|
||||
created_at: 2026-06-07T02:23:28Z
|
||||
updated_at: 2026-06-07T02:23:28Z
|
||||
assignee: null
|
||||
legacy_ticket: null
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
Long-running orchestration work often spans compaction. Workflow text can be strongly available in the turn where the workflow is invoked, but after compaction the durable state may only retain a loose summary. This can cause the agent to forget that it is still operating under workflow constraints such as worktree/reviewer/merge/commit handling from `multi-agent-workflow` and `worktree-workflow`.
|
||||
|
||||
The problem is not merely summarization quality: active workflow invocation state and workflow obligations should be represented durably enough to survive compaction.
|
||||
|
||||
## Goal
|
||||
|
||||
Preserve active Workflow invocations and their operational obligations across compaction so long-running workflow-governed tasks continue to follow the invoked workflow after context pruning/compaction.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Represent user-invoked or otherwise active workflows as durable, typed history/state rather than only transient context text.
|
||||
- Preserve at least:
|
||||
- workflow slug;
|
||||
- invocation source/time;
|
||||
- intended task/scope when available;
|
||||
- whether the workflow remains active or has completed;
|
||||
- concise current obligations/checkpoints relevant to the active workflow.
|
||||
- Compaction must carry active workflow state forward explicitly.
|
||||
- After compaction, context construction must be able to rehydrate active workflow guidance from durable state.
|
||||
- The implementation must not inject workflow instructions into model context based only on turn-local/transient information that is absent from history/state.
|
||||
- Workflow guidance after compaction should distinguish:
|
||||
- workflow availability/advertisement;
|
||||
- workflow currently active for this task.
|
||||
- Decide and document whether rehydration uses the latest workflow body by slug or an invocation-time snapshot; make the choice explicit.
|
||||
- Active workflow obligations should be cleared or marked completed when the workflow-governed task finishes, so stale workflow constraints do not leak into unrelated work.
|
||||
- Include coverage for at least a long-running worktree/multi-agent style flow where compaction occurs between review delegation and merge/close handling.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Rewriting workflow content itself.
|
||||
- Solving all memory summarization quality issues.
|
||||
- Automatically invoking workflows without user or orchestration intent.
|
||||
- Making workflows immutable unless the snapshot/latest-body decision chooses that explicitly.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- A workflow-governed task can cross compaction without losing the fact that `/worktree-workflow` or `/multi-agent-workflow` is active.
|
||||
- The post-compaction context clearly distinguishes active workflow obligations from merely available resident workflows.
|
||||
- Workflow instructions that affect model behavior are traceable to durable history/state.
|
||||
- Stale active workflow state is cleared at task completion or explicit cancellation.
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<!-- event: create author: LocalTicketBackend at: 2026-06-07T02:23:28Z -->
|
||||
|
||||
## Created
|
||||
|
||||
Created by LocalTicketBackend create.
|
||||
|
||||
---
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
---
|
||||
id: 20260607-035231-orchestrator-merge-completion
|
||||
slug: orchestrator-merge-completion
|
||||
title: Orchestrator merge completion
|
||||
status: open
|
||||
kind: task
|
||||
priority: P1
|
||||
labels: [orchestrator, merge, ticket, workflow, validation]
|
||||
workflow_state: intake
|
||||
created_at: 2026-06-07T03:52:31Z
|
||||
updated_at: 2026-06-07T05:00:22Z
|
||||
assignee: null
|
||||
legacy_ticket: null
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
Once Orchestrator-managed implementation and independent review have produced a merge-ready dossier, the final automation slice is merging, validating, cleanup, and Ticket completion. The current `multi-agent-workflow` already describes the desired manual/orchestrated behavior. This ticket makes that behavior explicit for workspace Orchestrator automation.
|
||||
|
||||
Merge authority must be explicit. In this dogfooding workspace, Orchestrator may proceed through merge/cleanup/close when workflow/policy conditions are met. More conservative/public defaults may stop at merge-ready dossier instead.
|
||||
|
||||
## Goal
|
||||
|
||||
Implement Orchestrator merge/completion behavior for reviewed in-progress Tickets, based on the existing multi-agent workflow.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Define the merge authority boundary clearly:
|
||||
- in local dogfooding mode/workspace policy, Orchestrator may merge/cleanup/close after review approval and validation;
|
||||
- otherwise Orchestrator must stop at a merge-ready dossier with human action required.
|
||||
- Require an independent reviewer approval in the merge-ready dossier before merge, unless a human explicitly overrides and records a decision.
|
||||
- Treat the reviewer verdict before merge as branch-scoped evidence, not a final main-branch Ticket approval event.
|
||||
- Commit main-branch Ticket review/approval and completion records only in the merge-completion phase, after the branch is merged or as part of the same controlled completion sequence, so the main Ticket history does not claim approval for code that is not in main.
|
||||
- Require a merge-ready dossier including:
|
||||
- Ticket id/slug;
|
||||
- branch/worktree;
|
||||
- commits;
|
||||
- intent/invariant check;
|
||||
- implementation summary;
|
||||
- coder/reviewer Pods;
|
||||
- blockers fixed or rejected findings with reasons;
|
||||
- validation performed;
|
||||
- residual risks;
|
||||
- dirty state.
|
||||
- Before merge, verify main workspace state is safe and unrelated dirty changes are understood.
|
||||
- Merge with `git merge --no-ff <branch>` or the agreed project merge method.
|
||||
- Run post-merge validation appropriate to the change. Minimum baseline should include:
|
||||
- focused tests from the ticket/dossier;
|
||||
- `cargo fmt --check`;
|
||||
- `git diff --check`;
|
||||
- `target/debug/yoi ticket doctor` where applicable.
|
||||
- Use broader validation (`cargo check --workspace --all-targets`, `nix build .#yoi`, etc.) when risk or touched files warrant it.
|
||||
- Record review/merge/validation outcome in the Ticket thread during the merge-completion phase, after confirming that the reviewed branch is the branch being merged.
|
||||
- Transition `inprogress -> done` or close the Ticket according to typed Ticket workflow rules.
|
||||
- Remove merged child worktree and delete merged branch unless the user/workflow explicitly asks to keep them.
|
||||
- Stop coder/reviewer Pods and reclaim scopes after completion.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Queue notification / queued acceptance; that belongs to `orchestrator-queued-ticket-routing`.
|
||||
- Worktree/coder/reviewer routing; that belongs to `orchestrator-worktree-agent-routing`.
|
||||
- Removing the human override path for unusual reviews/merge decisions.
|
||||
- Making merge authority implicit for all public/default configurations.
|
||||
- Solving active workflow compaction persistence; relate to `preserve-active-workflows-across-compaction`.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- Orchestrator has explicit merge/completion guidance or implementation wiring based on `multi-agent-workflow`.
|
||||
- Reviewed in-progress Tickets can be merged, validated, closed/done, and cleaned up by Orchestrator in authorized dogfooding mode.
|
||||
- Conservative mode or missing authorization stops at merge-ready dossier rather than merging.
|
||||
- Ticket thread records branch-reviewed merge/completion decision and validation evidence in the merge-completion phase, not as premature main-branch approval before merge.
|
||||
- Worktrees/branches/Pods are cleaned up after successful merge/close.
|
||||
- Tests or prompt/resource tests cover merge authority boundary and required dossier/validation fields.
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<!-- event: create author: LocalTicketBackend at: 2026-06-07T03:52:31Z -->
|
||||
|
||||
## Created
|
||||
|
||||
Created by LocalTicketBackend create.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: decision author: hare at: 2026-06-07T05:00:22Z -->
|
||||
|
||||
## Decision
|
||||
|
||||
## Git/Ticket review record boundary
|
||||
|
||||
Decision:
|
||||
- Reviewer verdicts on an unmerged implementation branch are branch-scoped evidence, not final main-branch Ticket approval.
|
||||
- The Orchestrator should keep branch-local review results in the merge-ready dossier/review report during the worktree-agent phase.
|
||||
- Main Ticket thread may still record durable progress such as worktree plan, coder/reviewer delegation, blockers, and fix-loop status.
|
||||
- The final Ticket review/approval/completion record should be written during the merge-completion phase, after confirming the reviewed branch is the branch being merged, or as part of the same controlled completion sequence.
|
||||
|
||||
Rationale:
|
||||
- Avoid main branch Ticket history claiming approval for code that is not yet in main.
|
||||
- Keep git history aligned: implementation branch review evidence leads to merge; main Ticket lifecycle records the completed merge/validation outcome.
|
||||
- Builtin Orchestrator workflow should make this explicit so automation does not reproduce the ad-hoc parent-side `TicketReview` timing used during manual dogfooding.
|
||||
|
||||
---
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
---
|
||||
id: 20260607-035710-ticket-orchestration-plan-tool
|
||||
slug: ticket-orchestration-plan-tool
|
||||
title: Ticket orchestration plan tool
|
||||
status: open
|
||||
kind: task
|
||||
priority: P1
|
||||
labels: [ticket, orchestrator, planning, workflow, tools]
|
||||
workflow_state: intake
|
||||
created_at: 2026-06-07T03:57:10Z
|
||||
updated_at: 2026-06-07T03:57:10Z
|
||||
assignee: null
|
||||
legacy_ticket: null
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
Orchestrator queue automation needs a lightweight way to remember routing decisions across turns while coordinating multiple Tickets. The existing Task tool is session-lifetime and human/user-visible, but Ticket ordering, dependency, conflict, and routing decisions belong to the Ticket orchestration domain.
|
||||
|
||||
The immediate need is not a full scheduler or dependency graph. The Orchestrator needs a small typed surface to record decisions such as:
|
||||
|
||||
- Ticket A should run before Ticket B;
|
||||
- Ticket B is blocked by Ticket A;
|
||||
- Ticket C conflicts with Ticket D and should not run in parallel;
|
||||
- Ticket E is ready but waiting for capacity;
|
||||
- Ticket F has been accepted by Orchestrator and has a planned worktree/branch.
|
||||
|
||||
These records should help the Orchestrator avoid relying only on prompt memory, especially across compaction and multi-turn routing.
|
||||
|
||||
## Goal
|
||||
|
||||
Design and implement a lightweight Ticket orchestration note/plan tool surface for Orchestrator use, similar in convenience to Task tools but scoped to Ticket ordering and routing decisions.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Provide a typed, lightweight tool/API for recording and querying Orchestrator routing decisions about Tickets.
|
||||
- Support at least:
|
||||
- ordering relation: before/after;
|
||||
- dependency/blocker relation: blocked_by / blocks;
|
||||
- conflict relation: do_not_parallelize / conflicts_with;
|
||||
- capacity/waiting note;
|
||||
- accepted work plan summary such as branch/worktree/pod plan when applicable.
|
||||
- Distinguish durable project-relevant relations from local ephemeral execution notes:
|
||||
- dependency/order/conflict decisions that should affect future orchestration should be recorded in git-tracked Ticket records or typed Ticket thread events/artifacts;
|
||||
- local runtime claims/session details should remain in the local role session registry, not Ticket metadata.
|
||||
- Keep this a Ticket/orchestration domain capability, not a generic TaskStore replacement.
|
||||
- Ensure records are queryable by Ticket id/slug and by relation kind.
|
||||
- Keep the first version simple; do not implement a full scheduler, graph solver, or automatic topological planner.
|
||||
- Integrate with Orchestrator prompts/workflows so the Orchestrator consults these records before accepting queued Tickets.
|
||||
- Avoid using session-lifetime Task tools for durable Ticket dependency/order decisions.
|
||||
- Ensure compaction does not erase the authoritative record; the tool should write to a durable record path.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Replacing Ticket workflow_state.
|
||||
- Replacing the local role session registry for Pod/session claims.
|
||||
- Implementing full dependency graph scheduling.
|
||||
- Automatically reordering all queued Tickets without Orchestrator judgment.
|
||||
- Making Companion a mutating orchestration actor.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- Orchestrator has a simple typed way to record Ticket ordering/dependency/conflict/capacity decisions.
|
||||
- The records survive compaction and are queryable in later turns.
|
||||
- Project-relevant decisions are stored in the Ticket system rather than only session-lifetime Task state.
|
||||
- Local runtime execution details remain out of git-tracked Ticket metadata.
|
||||
- Orchestrator queue routing can consult the recorded decisions before accepting a queued Ticket.
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<!-- event: create author: LocalTicketBackend at: 2026-06-07T03:57:10Z -->
|
||||
|
||||
## Created
|
||||
|
||||
Created by LocalTicketBackend create.
|
||||
|
||||
---
|
||||
|
|
@ -10,6 +10,8 @@ Yoi の multi-agent 運用で、Intake や人間が作成した Ticket を Orche
|
|||
|
||||
これは scheduler ではない。目的は、Ticket の fields / body / thread / artifacts / 現在の repository/Pod 状態を明示的に確認し、隠れた会話状態ではなく Ticket に基づいて routing 判断を残すことである。
|
||||
|
||||
Panel Queue / queued notification は、人間が Orchestrator に routing を開始してよいと許可した signal であり、unattended scheduler ではない。implementation side effect に進む場合は、Orchestrator が Ticket と workspace state を再確認し、unblocked と判断してから `queued -> inprogress` を記録する必要がある。
|
||||
|
||||
## 位置づけ
|
||||
|
||||
```text
|
||||
|
|
@ -22,6 +24,7 @@ TicketCreate / TicketComment
|
|||
- Intake は Ticket の materialization を担当する。
|
||||
- Orchestrator は Ticket の next action を分類する。
|
||||
- `ticket-preflight-workflow` は実装前の設計・要件 gate。
|
||||
- `ready -> queued` は人間が Orchestrator routing を許可した状態であり、worktree 作成や Pod 起動の許可そのものではない。
|
||||
- `multi-agent-workflow` は coder / reviewer Pod と worktree を使う実装・レビュー loop。
|
||||
- この Workflow は自動 scheduler / lease / unattended maintainer ではない。
|
||||
|
||||
|
|
@ -33,15 +36,19 @@ Orchestrator は以下を行う。
|
|||
- 必要に応じて関連 Ticket を `TicketList` / `TicketShow` で確認する。
|
||||
- Ticket body / thread / artifacts / resolution / review / implementation report を読む。
|
||||
- repository 状態、関連 docs/code、既存 worktree、visible Pods を必要に応じて明示的に確認する。
|
||||
- queued notification を受けた場合も、Ticket と workspace state を再確認してから routing する。
|
||||
- next action を routing classification として決める。
|
||||
- routing decision を `TicketComment` で Ticket thread に記録する。
|
||||
- implementation-ready の場合は `multi-agent-workflow` に渡す `IntentPacket` を作る。
|
||||
- implementation-ready かつ Ticket が `queued` の場合は、worktree 作成 / implementation Pod `SpawnPod` / coder routing などの side effect の前に、既存の typed Ticket backend/tool path で `queued -> inprogress` を記録する。
|
||||
- preflight-needed の場合は coder Pod に直投げせず、`ticket-preflight-workflow` に接続する。
|
||||
|
||||
## Orchestrator がしないこと
|
||||
|
||||
- 自動 scheduler として unattended に実行しない。
|
||||
- 人間/上位 Orchestrator の許可なしに coder / reviewer / investigator Pod を起動しない。
|
||||
- Panel Queue / queued notification だけを unattended scheduler trigger として扱わない。
|
||||
- `queued -> inprogress` acceptance なしに worktree 作成、implementation Pod `SpawnPod`、coder/reviewer routing を行わない。
|
||||
- 人間/上位 Orchestrator の許可または明示的な routing acceptance なしに coder / reviewer / investigator Pod を起動しない。
|
||||
- 設計境界の未決定を勝手に implementation-ready として固定しない。
|
||||
- merge / close / cleanup 権限を持たない場面で勝手に完了処理しない。
|
||||
- Ticket tools があるからといって arbitrary filesystem write を行わない。
|
||||
|
|
@ -55,11 +62,26 @@ Orchestrator は以下を行う。
|
|||
- `TicketShow`: 対象 Ticket の body / thread / artifacts / resolution 確認。
|
||||
- `TicketComment`: routing decision / intent packet / blocked reason / next question の記録。
|
||||
- `TicketStatus`: pending/open などの状態整理が明示的に許可された場合だけ使う。
|
||||
- `TicketWorkflowState`: `queued -> inprogress` acceptance など、workflow_state 遷移が明示的に許可・必要な場合だけ使う。
|
||||
- `TicketClose`: 完了権限と resolution が揃っている場合だけ使う。
|
||||
- `TicketDoctor`: routing 前後の整合性確認。
|
||||
|
||||
`TicketCreate` は通常 Intake の責務だが、routing 中に follow-up Ticket が必要だと判断した場合は、ユーザー/上位 Orchestrator の合意後にだけ使う。
|
||||
|
||||
## Queued acceptance contract
|
||||
|
||||
`workflow_state = queued` は、Ticket が routing 対象として人間により Orchestrator へ渡された状態である。Orchestrator は queued notification を受けたら、Ticket と workspace state を読んで、次のどちらかを行う。
|
||||
|
||||
- unblocked と判断する場合: `queued -> inprogress` を記録してから worktree 作成、implementation/review Pod spawn、その他の implementation side effect に進む。
|
||||
- blocked / not-ready と判断する場合: concise な理由を Ticket thread に記録し、queued のまま待つか、既存の Ticket status/state mechanism で明示的に defer/block する。
|
||||
|
||||
Invariant:
|
||||
|
||||
- `queued -> inprogress` は Orchestrator acceptance marker であり、coder Pod が既に動いていることの後追い記録ではない。
|
||||
- `queued -> inprogress` に失敗した場合、implementation/review Pod を spawn しない。
|
||||
- `inprogress` acceptance 後に worktree/spawn/first-run が失敗した場合は、queued に黙って戻さず、in-progress Ticket に failure/block/recovery note を記録する。
|
||||
- Queue notification だけを根拠に blind spawn しない。必ず Ticket と workspace state を再確認する。
|
||||
|
||||
## Routing classification
|
||||
|
||||
Orchestrator は対象 Ticket を以下のいずれかに分類する。
|
||||
|
|
|
|||
|
|
@ -8,10 +8,11 @@ use std::io;
|
|||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
use manifest::{ProfileDiscovery, ProfileResolveOptions, ProfileResolver, ProfileSelector};
|
||||
use protocol::{ErrorCode, Event, InvokeKind, Method, Segment};
|
||||
use thiserror::Error;
|
||||
pub use ticket::config::TicketRole;
|
||||
use ticket::config::{TicketConfig, TicketConfigError};
|
||||
use ticket::config::{TicketConfig, TicketConfigError, TicketRoleLaunchConfigError};
|
||||
|
||||
use crate::{PodClient, PodRuntimeCommand, SpawnConfig, SpawnError, SpawnReady, spawn_pod};
|
||||
|
||||
|
|
@ -91,9 +92,9 @@ impl TicketIntakeHandoff {
|
|||
out.push_str("\nPanel handoff:\n");
|
||||
push_bounded_bullet(out, "workspace", &self.workspace_label);
|
||||
push_bounded_bullet(out, "workspace_orchestrator_pod", &self.orchestrator_pod);
|
||||
out.push_str("- When Intake has clarified the request and created/updated the Ticket, notify/report readiness to this Orchestrator.\n");
|
||||
out.push_str("- Handoff report fields: created_or_updated_ticket_id_or_slug, readiness, needs_preflight, risk_flags, user_go_required, intake_summary.\n");
|
||||
out.push_str("- Do not start implementation automatically; wait for Orchestrator routing/preflight and human Go gates.\n");
|
||||
out.push_str("- When Intake has clarified the request and created/updated the Ticket, use the typed Ticket tool surface to append `intake_summary` and set `workflow_state = ready` when the Ticket is ready to queue.\n");
|
||||
out.push_str("- Handoff report fields: created_or_updated_ticket_id_or_slug, workflow_state, needs_preflight, risk_flags, intake_summary.\n");
|
||||
out.push_str("- Do not start implementation automatically; the user queues a ready Ticket via panel (`ready -> queued`), and Orchestrator treats `queued` as schedulable before moving it to `inprogress` when starting.\n");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -199,6 +200,16 @@ impl TicketRoleLaunchOptions {
|
|||
pub enum TicketRoleLaunchError {
|
||||
#[error(transparent)]
|
||||
Config(#[from] TicketConfigError),
|
||||
#[error(transparent)]
|
||||
LaunchConfig(#[from] TicketRoleLaunchConfigError),
|
||||
#[error(
|
||||
"Ticket role `{role}` profile selector `{selector}` is not resolvable before launch: {message}. Configure `[roles.{role}].profile` with an executable concrete profile selector such as `builtin:default` or a project/user profile"
|
||||
)]
|
||||
ProfileResolution {
|
||||
role: TicketRole,
|
||||
selector: String,
|
||||
message: String,
|
||||
},
|
||||
#[error("Ticket role Pod name must not be empty")]
|
||||
EmptyPodName,
|
||||
#[error(
|
||||
|
|
@ -239,7 +250,7 @@ pub fn plan_ticket_role_launch_with_config(
|
|||
context: TicketRoleLaunchContext,
|
||||
config: &TicketConfig,
|
||||
) -> Result<TicketRoleLaunchPlan, TicketRoleLaunchError> {
|
||||
let role_config = config.role(context.role);
|
||||
let role_config = config.role_launch_config(context.role)?;
|
||||
let profile = role_config.profile.as_str().to_string();
|
||||
let workflow = role_config.workflow.as_str().to_string();
|
||||
let launch_prompt_ref = role_config
|
||||
|
|
@ -251,6 +262,7 @@ pub fn plan_ticket_role_launch_with_config(
|
|||
Some(name) => name.to_string(),
|
||||
None => default_pod_name(context.role, context.ticket.as_ref()),
|
||||
};
|
||||
validate_ticket_role_profile(context.role, &profile, &context.workspace_root, &pod_name)?;
|
||||
let prompt = build_launch_prompt(&context, &profile, &workflow, launch_prompt_ref.as_deref());
|
||||
|
||||
Ok(TicketRoleLaunchPlan {
|
||||
|
|
@ -269,6 +281,35 @@ pub fn plan_ticket_role_launch_with_config(
|
|||
})
|
||||
}
|
||||
|
||||
fn validate_ticket_role_profile(
|
||||
role: TicketRole,
|
||||
profile: &str,
|
||||
workspace_root: &std::path::Path,
|
||||
pod_name: &str,
|
||||
) -> Result<(), TicketRoleLaunchError> {
|
||||
let selector = ProfileSelector::parse_cli(profile);
|
||||
let registry = ProfileDiscovery::for_cwd(workspace_root)
|
||||
.discover()
|
||||
.map_err(|source| TicketRoleLaunchError::ProfileResolution {
|
||||
role,
|
||||
selector: profile.to_string(),
|
||||
message: source.to_string(),
|
||||
})?;
|
||||
ProfileResolver::new()
|
||||
.with_workspace_base(workspace_root)
|
||||
.resolve_from_registry(
|
||||
&selector,
|
||||
®istry,
|
||||
ProfileResolveOptions::with_pod_name(pod_name),
|
||||
)
|
||||
.map(|_| ())
|
||||
.map_err(|source| TicketRoleLaunchError::ProfileResolution {
|
||||
role,
|
||||
selector: profile.to_string(),
|
||||
message: source.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Spawn the Pod, connect to its socket, send the first `Method::Run` input,
|
||||
/// and wait for bounded acceptance evidence from the Pod event stream.
|
||||
pub async fn launch_ticket_role_pod<F>(
|
||||
|
|
@ -489,9 +530,46 @@ fn build_launch_prompt(
|
|||
);
|
||||
}
|
||||
|
||||
append_role_execution_guidance(&mut out, context.role);
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
fn append_role_execution_guidance(out: &mut String, role: TicketRole) {
|
||||
match role {
|
||||
TicketRole::Orchestrator => append_orchestrator_agent_routing_guidance(out),
|
||||
TicketRole::Coder => append_coder_agent_routing_guidance(out),
|
||||
TicketRole::Reviewer => append_reviewer_agent_routing_guidance(out),
|
||||
TicketRole::Intake | TicketRole::Investigator => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn append_orchestrator_agent_routing_guidance(out: &mut String) {
|
||||
out.push_str("\nOrchestrator worktree + agent routing guidance:\n");
|
||||
out.push_str("- Treat `ticket-orchestrator-routing` as the routing gate. Read the Ticket and workspace state first; `ready -> queued` authorizes routing, not implementation side effects.\n");
|
||||
out.push_str("- Create worktrees or spawn coder/reviewer Pods only after `workflow_state = inprogress` is already recorded and accepted. If the Ticket is still queued and unblocked, record `queued -> inprogress` before any worktree/SpawnPod side effect.\n");
|
||||
out.push_str("- Use `worktree-workflow` for the mechanical worktree plan: create `.worktree/<task-name>`, exclude `.yoi` from the child worktree, and keep the main workspace as the authority for Ticket, workflow, docs, and memory records.\n");
|
||||
out.push_str("- Use `multi-agent-workflow` for the sibling loop: coder and reviewer are siblings under this Orchestrator; coder gets narrow write scope to the child worktree; reviewer is read-only by default.\n");
|
||||
out.push_str("- Give the coder an intent packet, child worktree/branch, validation commands, and report expectations; require Bash commands to `cd` into the child worktree and prohibit editing main-workspace `.yoi`/Ticket/workflow/docs records.\n");
|
||||
out.push_str("- Give the reviewer the Ticket intent, diff/commits, validation evidence, and blocker/non-blocker criteria; keep branch-local reviewer verdicts in the review report or merge-ready dossier rather than recording them as final main-branch Ticket approval.\n");
|
||||
out.push_str("- Ticket thread progress may record worktree plan, coder delegated/completed/blocked, reviewer delegated, blocker/fix-loop summaries, and merge-ready dossier pointer; do not merge, close, or record final main approval in this phase.\n");
|
||||
out.push_str("- Stop at a merge-ready dossier for `orchestrator-merge-completion` containing branch, commits, conceptual implementation summary, validation evidence, coder/reviewer evidence, blocker loop outcome, residual risk, and parent decision needs.\n");
|
||||
}
|
||||
|
||||
fn append_coder_agent_routing_guidance(out: &mut String) {
|
||||
out.push_str("\nCoder worktree routing guidance:\n");
|
||||
out.push_str("- Implement only in the provided child worktree/branch. Use `cd <worktree>` before Bash commands and do not edit main-workspace `.yoi`, Ticket, workflow, docs, or memory records.\n");
|
||||
out.push_str("- Treat the intent packet, invariants, non-goals, validation expectations, and report expectations as the contract. Escalate to Orchestrator rather than expanding scope when design, permission, history, prompt-context, dependency, or Ticket-boundary questions appear.\n");
|
||||
out.push_str("- Report worktree path, branch, commits/status, changed files, implementation summary, validation run, unresolved notes, and whether the branch is ready for external review. Do not merge, push, close Tickets, or delete worktrees.\n");
|
||||
}
|
||||
|
||||
fn append_reviewer_agent_routing_guidance(out: &mut String) {
|
||||
out.push_str("\nReviewer worktree routing guidance:\n");
|
||||
out.push_str("- Review as a sibling of the coder under Orchestrator, read-only by default. Read the Ticket/intent packet, branch diff or commits, and validation evidence before judging.\n");
|
||||
out.push_str("- Classify findings as blockers, non-blocking follow-ups, or parent-decision items against the intent, requirements, invariants, and non-goals; include concrete file/line evidence where useful.\n");
|
||||
out.push_str("- Keep the branch-local reviewer verdict in the review report for the Orchestrator merge-ready dossier. Do not record final main-branch Ticket approval, merge, close, push, or instruct the coder directly.\n");
|
||||
}
|
||||
|
||||
fn default_pod_name(role: TicketRole, ticket: Option<&TicketRef>) -> String {
|
||||
let mut name = format!("ticket-{}", role.as_str());
|
||||
if let Some(seed) = ticket.and_then(TicketRef::pod_name_seed) {
|
||||
|
|
@ -597,6 +675,16 @@ mod tests {
|
|||
std::fs::write(dir.join("ticket.config.toml"), content).unwrap();
|
||||
}
|
||||
|
||||
fn write_builtin_role_config(workspace: &std::path::Path, roles: &[TicketRole]) {
|
||||
let mut config = String::new();
|
||||
for role in roles {
|
||||
config.push_str(&format!(
|
||||
"\n[roles.{role}]\nprofile = \"builtin:default\"\n"
|
||||
));
|
||||
}
|
||||
write_config(workspace, &config);
|
||||
}
|
||||
|
||||
fn text_segment(plan: &TicketRoleLaunchPlan) -> &str {
|
||||
match &plan.run_segments[1] {
|
||||
Segment::Text { content } => content,
|
||||
|
|
@ -751,26 +839,130 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn default_config_role_launch_plan_uses_defaults() {
|
||||
fn default_config_role_launch_plan_requires_explicit_role_config() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let mut context = TicketRoleLaunchContext::new(temp.path(), TicketRole::Coder);
|
||||
context.ticket = Some(TicketRef::slug("Ticket Role Pod Launcher"));
|
||||
|
||||
let err = plan_ticket_role_launch(context).unwrap_err();
|
||||
|
||||
assert!(
|
||||
err.to_string()
|
||||
.contains("Ticket role `coder` is not launch-configured")
|
||||
);
|
||||
assert!(err.to_string().contains("[roles.coder]"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backend_only_config_is_not_sufficient_for_role_launch_plan() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
write_config(
|
||||
temp.path(),
|
||||
r#"
|
||||
[backend]
|
||||
provider = "builtin:yoi_local"
|
||||
root = ".yoi/tickets"
|
||||
"#,
|
||||
);
|
||||
let context = TicketRoleLaunchContext::new(temp.path(), TicketRole::Intake);
|
||||
|
||||
let err = plan_ticket_role_launch(context).unwrap_err();
|
||||
|
||||
assert!(
|
||||
err.to_string()
|
||||
.contains("Ticket role `intake` is not launch-configured")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_inherit_profile_fails_before_launch_planning() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
write_config(
|
||||
temp.path(),
|
||||
r#"
|
||||
[roles.intake]
|
||||
profile = "inherit"
|
||||
"#,
|
||||
);
|
||||
let context = TicketRoleLaunchContext::new(temp.path(), TicketRole::Intake);
|
||||
|
||||
let err = plan_ticket_role_launch(context).unwrap_err();
|
||||
|
||||
assert!(err.to_string().contains("profile = \"inherit\""));
|
||||
assert!(err.to_string().contains("top-level Ticket role launch"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unresolvable_profile_selector_fails_before_spawn() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
write_config(
|
||||
temp.path(),
|
||||
r#"
|
||||
[roles.intake]
|
||||
profile = "project:no-such-ticket-role-profile"
|
||||
"#,
|
||||
);
|
||||
let context = TicketRoleLaunchContext::new(temp.path(), TicketRole::Intake);
|
||||
|
||||
let err = plan_ticket_role_launch(context).unwrap_err();
|
||||
|
||||
assert!(
|
||||
err.to_string().contains(
|
||||
"profile selector `project:no-such-ticket-role-profile` is not resolvable"
|
||||
)
|
||||
);
|
||||
assert!(err.to_string().contains("[roles.intake].profile"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_concrete_role_config_allows_launch_planning() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
write_builtin_role_config(temp.path(), &[TicketRole::Intake]);
|
||||
let context = TicketRoleLaunchContext::new(temp.path(), TicketRole::Intake);
|
||||
|
||||
let plan = plan_ticket_role_launch(context).unwrap();
|
||||
|
||||
assert_eq!(plan.role, TicketRole::Coder);
|
||||
assert_eq!(plan.pod_name, "ticket-coder-ticket-role-pod-launcher");
|
||||
assert_eq!(plan.profile, "inherit");
|
||||
assert_eq!(plan.workflow, "multi-agent-workflow");
|
||||
assert_eq!(plan.launch_prompt_ref, None);
|
||||
assert!(matches!(
|
||||
&plan.run_segments[0],
|
||||
Segment::WorkflowInvoke { slug } if slug == "multi-agent-workflow"
|
||||
));
|
||||
assert!(text_segment(&plan).contains("Profile selector: inherit"));
|
||||
assert_eq!(plan.role, TicketRole::Intake);
|
||||
assert_eq!(plan.profile, "builtin:default");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_config_allows_intake_and_orchestrator_launch_planning() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
write_config(temp.path(), &ticket::config::ticket_config_scaffold());
|
||||
|
||||
let intake = plan_ticket_role_launch(TicketRoleLaunchContext::new(
|
||||
temp.path(),
|
||||
TicketRole::Intake,
|
||||
))
|
||||
.unwrap();
|
||||
assert_eq!(intake.role, TicketRole::Intake);
|
||||
assert_eq!(intake.profile, "builtin:default");
|
||||
assert_eq!(intake.workflow, TicketRole::Intake.default_workflow());
|
||||
|
||||
let orchestrator = plan_ticket_role_launch(TicketRoleLaunchContext::new(
|
||||
temp.path(),
|
||||
TicketRole::Orchestrator,
|
||||
))
|
||||
.unwrap();
|
||||
assert_eq!(orchestrator.role, TicketRole::Orchestrator);
|
||||
assert_eq!(orchestrator.profile, "builtin:default");
|
||||
assert_eq!(
|
||||
orchestrator.workflow,
|
||||
TicketRole::Orchestrator.default_workflow()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spawn_config_still_rejects_inherit_profile_defensively() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let mut plan = test_launch_plan(temp.path());
|
||||
plan.profile = "inherit".to_string();
|
||||
|
||||
let err = plan
|
||||
.spawn_config(PodRuntimeCommand::for_executable("/bin/yoi"))
|
||||
.unwrap_err();
|
||||
|
||||
assert!(matches!(
|
||||
err,
|
||||
TicketRoleLaunchError::UnsupportedInheritProfile
|
||||
|
|
@ -785,7 +977,7 @@ mod tests {
|
|||
temp.path(),
|
||||
r#"
|
||||
[roles.reviewer]
|
||||
profile = "project:reviewer"
|
||||
profile = "builtin:default"
|
||||
launch_prompt = "$workspace/ticket/reviewer/launch"
|
||||
workflow = "ticket-review-workflow"
|
||||
"#,
|
||||
|
|
@ -802,7 +994,7 @@ workflow = "ticket-review-workflow"
|
|||
let text = text_segment(&plan);
|
||||
|
||||
assert_eq!(plan.pod_name, "reviewer-fixed");
|
||||
assert_eq!(plan.profile, "project:reviewer");
|
||||
assert_eq!(plan.profile, "builtin:default");
|
||||
assert_eq!(plan.workflow, "ticket-review-workflow");
|
||||
assert_eq!(
|
||||
plan.launch_prompt_ref.as_deref(),
|
||||
|
|
@ -816,19 +1008,28 @@ workflow = "ticket-review-workflow"
|
|||
"Configured launch_prompt ref (unresolved): $workspace/ticket/reviewer/launch"
|
||||
));
|
||||
assert!(text.contains("Workflow: ticket-review-workflow"));
|
||||
assert!(text.contains("Profile selector: project:reviewer"));
|
||||
assert!(text.contains("Profile selector: builtin:default"));
|
||||
assert!(!text.contains("system_instruction"));
|
||||
let spawn = plan
|
||||
.spawn_config(PodRuntimeCommand::for_executable("/bin/yoi"))
|
||||
.unwrap();
|
||||
assert_eq!(spawn.pod_name, "reviewer-fixed");
|
||||
assert_eq!(spawn.profile.as_deref(), Some("project:reviewer"));
|
||||
assert_eq!(spawn.profile.as_deref(), Some("builtin:default"));
|
||||
assert_eq!(spawn.cwd, temp.path());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generated_prompt_covers_intake_orchestrator_and_reviewer_context() {
|
||||
fn generated_prompt_covers_intake_orchestrator_coder_and_reviewer_context() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
write_builtin_role_config(
|
||||
temp.path(),
|
||||
&[
|
||||
TicketRole::Intake,
|
||||
TicketRole::Orchestrator,
|
||||
TicketRole::Coder,
|
||||
TicketRole::Reviewer,
|
||||
],
|
||||
);
|
||||
|
||||
let mut intake = TicketRoleLaunchContext::new(temp.path(), TicketRole::Intake);
|
||||
intake.user_instruction = Some("Clarify and materialize this request as a Ticket.".into());
|
||||
|
|
@ -849,8 +1050,12 @@ workflow = "ticket-review-workflow"
|
|||
assert!(handoff_text.contains("workspace_orchestrator_pod: panel-orchestrator-demo"));
|
||||
assert!(handoff_text.contains("workspace: Demo workspace"));
|
||||
assert!(handoff_text.contains("created_or_updated_ticket_id_or_slug"));
|
||||
assert!(handoff_text.contains("Do not start implementation automatically"));
|
||||
assert!(handoff_text.contains("human Go gates"));
|
||||
assert!(handoff_text.contains("workflow_state"));
|
||||
assert!(handoff_text.contains("Ticket tool surface"));
|
||||
assert!(handoff_text.contains("ready -> queued"));
|
||||
assert!(handoff_text.contains("queued` as schedulable"));
|
||||
assert!(!handoff_text.contains("user_go_required"));
|
||||
assert!(!handoff_text.contains("human Go gates"));
|
||||
|
||||
let mut orchestrator = TicketRoleLaunchContext::new(temp.path(), TicketRole::Orchestrator);
|
||||
orchestrator.ticket = Some(TicketRef::slug("launcher"));
|
||||
|
|
@ -861,6 +1066,29 @@ workflow = "ticket-review-workflow"
|
|||
assert!(orchestrator_text.contains("Role: orchestrator"));
|
||||
assert!(orchestrator_text.contains("Route to implementation after preflight."));
|
||||
assert!(orchestrator_text.contains("cargo check --workspace --all-targets"));
|
||||
assert!(orchestrator_text.contains("workflow_state = inprogress"));
|
||||
assert!(orchestrator_text.contains("worktree-workflow"));
|
||||
assert!(orchestrator_text.contains("multi-agent-workflow"));
|
||||
assert!(orchestrator_text.contains("coder and reviewer are siblings"));
|
||||
assert!(orchestrator_text.contains("branch-local reviewer verdicts"));
|
||||
assert!(orchestrator_text.contains("merge-ready dossier"));
|
||||
assert!(orchestrator_text.contains("do not merge, close, or record final main approval"));
|
||||
|
||||
let mut coder = TicketRoleLaunchContext::new(temp.path(), TicketRole::Coder);
|
||||
coder.ticket = Some(TicketRef::id("20260605-190330-ticket-role-pod-launcher"));
|
||||
coder.worktree_path = Some(PathBuf::from("/tmp/yoi-code"));
|
||||
coder.branch = Some("work/ticket-role-pod-launcher".into());
|
||||
coder.validation = vec!["cargo test -p client ticket_role".into()];
|
||||
coder.report_expectations = vec!["implementation report with validation".into()];
|
||||
let coder_plan = plan_ticket_role_launch(coder).unwrap();
|
||||
let coder_text = text_segment(&coder_plan);
|
||||
assert!(coder_text.contains("Role: coder"));
|
||||
assert!(coder_text.contains("path: /tmp/yoi-code"));
|
||||
assert!(coder_text.contains("branch: work/ticket-role-pod-launcher"));
|
||||
assert!(coder_text.contains("cargo test -p client ticket_role"));
|
||||
assert!(coder_text.contains("provided child worktree/branch"));
|
||||
assert!(coder_text.contains("do not edit main-workspace `.yoi`"));
|
||||
assert!(coder_text.contains("Do not merge, push, close Tickets, or delete worktrees"));
|
||||
|
||||
let mut reviewer = TicketRoleLaunchContext::new(temp.path(), TicketRole::Reviewer);
|
||||
reviewer.ticket = Some(TicketRef::id("20260605-190330-ticket-role-pod-launcher"));
|
||||
|
|
@ -873,11 +1101,15 @@ workflow = "ticket-review-workflow"
|
|||
assert!(reviewer_text.contains("path: /tmp/yoi-review"));
|
||||
assert!(reviewer_text.contains("branch: work/ticket-role-pod-launcher"));
|
||||
assert!(reviewer_text.contains("approve or request changes"));
|
||||
assert!(reviewer_text.contains("read-only by default"));
|
||||
assert!(reviewer_text.contains("branch-local reviewer verdict"));
|
||||
assert!(reviewer_text.contains("Do not record final main-branch Ticket approval"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn caller_provided_pod_name_is_used_exactly() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
write_builtin_role_config(temp.path(), &[TicketRole::Intake]);
|
||||
let mut context = TicketRoleLaunchContext::new(temp.path(), TicketRole::Intake);
|
||||
context.pod_name = Some("custom-intake-pod".into());
|
||||
|
||||
|
|
|
|||
|
|
@ -8,4 +8,6 @@ pub mod task;
|
|||
pub mod ticket;
|
||||
|
||||
pub use task::{TaskFeature, task_tools_feature};
|
||||
pub use ticket::{TicketFeature, ticket_tools_feature};
|
||||
pub use ticket::{
|
||||
TicketFeature, TicketFeatureAccess, ticket_tools_feature, ticket_tools_feature_with_access,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -9,8 +9,7 @@ use std::path::{Path, PathBuf};
|
|||
use ticket::{
|
||||
LocalTicketBackend,
|
||||
config::{DEFAULT_TICKET_BACKEND_RELATIVE_PATH, TicketConfig},
|
||||
tool::TICKET_TOOL_NAMES,
|
||||
tool::ticket_tools,
|
||||
tool::{TICKET_READ_ONLY_TOOL_NAMES, TICKET_TOOL_NAMES, ticket_tools},
|
||||
};
|
||||
|
||||
use crate::feature::{
|
||||
|
|
@ -24,27 +23,58 @@ const FEATURE_DESCRIPTION: &str = "Typed local Ticket work-item operations over
|
|||
The tools operate through the ticket crate backend and do not grant generic filesystem write scope.";
|
||||
const AUTHORITY_REASON: &str = "Use a configured local Ticket backend root for typed work-item operations without generic filesystem write authority.";
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum TicketFeatureAccess {
|
||||
/// Status/diagnostic access for views such as Companion that must not mutate Tickets.
|
||||
ReadOnly,
|
||||
/// Full Ticket lifecycle access, including the read-only tools and all mutating Ticket tools.
|
||||
Lifecycle,
|
||||
}
|
||||
|
||||
impl TicketFeatureAccess {
|
||||
pub fn tool_names(self) -> &'static [&'static str] {
|
||||
match self {
|
||||
Self::ReadOnly => &TICKET_READ_ONLY_TOOL_NAMES,
|
||||
Self::Lifecycle => &TICKET_TOOL_NAMES,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TicketFeature {
|
||||
backend_root: PathBuf,
|
||||
config_error: Option<String>,
|
||||
access: TicketFeatureAccess,
|
||||
}
|
||||
|
||||
impl TicketFeature {
|
||||
pub fn new(backend_root: impl Into<PathBuf>) -> Self {
|
||||
Self::new_with_access(backend_root, TicketFeatureAccess::Lifecycle)
|
||||
}
|
||||
|
||||
pub fn new_with_access(backend_root: impl Into<PathBuf>, access: TicketFeatureAccess) -> Self {
|
||||
Self {
|
||||
backend_root: backend_root.into(),
|
||||
config_error: None,
|
||||
access,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn for_workspace(workspace: impl AsRef<Path>) -> Self {
|
||||
Self::for_workspace_with_access(workspace, TicketFeatureAccess::Lifecycle)
|
||||
}
|
||||
|
||||
pub fn for_workspace_with_access(
|
||||
workspace: impl AsRef<Path>,
|
||||
access: TicketFeatureAccess,
|
||||
) -> Self {
|
||||
let workspace = workspace.as_ref();
|
||||
match TicketConfig::load_workspace(workspace) {
|
||||
Ok(config) => Self::new(config.backend_root().to_path_buf()),
|
||||
Ok(config) => Self::new_with_access(config.backend_root().to_path_buf(), access),
|
||||
Err(error) => Self {
|
||||
backend_root: workspace.join(DEFAULT_TICKET_BACKEND_RELATIVE_PATH),
|
||||
config_error: Some(error.to_string()),
|
||||
access,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -53,6 +83,10 @@ impl TicketFeature {
|
|||
&self.backend_root
|
||||
}
|
||||
|
||||
pub fn access(&self) -> TicketFeatureAccess {
|
||||
self.access
|
||||
}
|
||||
|
||||
fn authority(&self) -> HostAuthority {
|
||||
HostAuthority::TicketBackend {
|
||||
root: self.backend_root.display().to_string(),
|
||||
|
|
@ -87,8 +121,8 @@ impl FeatureModule for TicketFeature {
|
|||
self.authority(),
|
||||
AUTHORITY_REASON,
|
||||
));
|
||||
for name in TICKET_TOOL_NAMES {
|
||||
descriptor = descriptor.with_tool(ToolDeclaration::new(name, tool_description(name)));
|
||||
for name in self.access.tool_names() {
|
||||
descriptor = descriptor.with_tool(ToolDeclaration::new(*name, tool_description(name)));
|
||||
}
|
||||
descriptor
|
||||
}
|
||||
|
|
@ -116,10 +150,14 @@ impl FeatureModule for TicketFeature {
|
|||
};
|
||||
let authority = self.authority();
|
||||
let backend = LocalTicketBackend::new(usable_root);
|
||||
let allowed_tool_names = self.access.tool_names();
|
||||
let mut tools = context.tools();
|
||||
for definition in ticket_tools(backend) {
|
||||
let (meta, _) = definition();
|
||||
let name = meta.name.clone();
|
||||
if !allowed_tool_names.contains(&name.as_str()) {
|
||||
continue;
|
||||
}
|
||||
tools.register(
|
||||
ToolContribution::new(name, definition)
|
||||
.with_required_host_authorities(vec![authority.clone()]),
|
||||
|
|
@ -140,6 +178,12 @@ fn tool_description(name: &str) -> &'static str {
|
|||
"Append a comment/plan/decision/implementation_report event to a Ticket."
|
||||
}
|
||||
"TicketReview" => "Append an approve/request_changes review event to a Ticket.",
|
||||
"TicketIntakeReady" => {
|
||||
"Mark an intake Ticket ready and append the typed intake summary/state transition events."
|
||||
}
|
||||
"TicketWorkflowState" => {
|
||||
"Transition Ticket workflow_state; queued -> inprogress is the accepted implementation start, so implementation side effects should happen only after that transition is accepted and recorded."
|
||||
}
|
||||
"TicketStatus" => "Move a Ticket between open and pending; use TicketClose for closed.",
|
||||
"TicketClose" => "Close a Ticket with a resolution through the typed local Ticket backend.",
|
||||
"TicketDoctor" => "Run typed local Ticket backend consistency checks.",
|
||||
|
|
@ -151,6 +195,13 @@ pub fn ticket_tools_feature(workspace: impl AsRef<Path>) -> TicketFeature {
|
|||
TicketFeature::for_workspace(workspace)
|
||||
}
|
||||
|
||||
pub fn ticket_tools_feature_with_access(
|
||||
workspace: impl AsRef<Path>,
|
||||
access: TicketFeatureAccess,
|
||||
) -> TicketFeature {
|
||||
TicketFeature::for_workspace_with_access(workspace, access)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -193,6 +244,95 @@ mod tests {
|
|||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_only_descriptor_declares_only_status_tools() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let feature = ticket_tools_feature_with_access(temp.path(), TicketFeatureAccess::ReadOnly);
|
||||
let descriptor = feature.descriptor();
|
||||
assert_eq!(feature.access(), TicketFeatureAccess::ReadOnly);
|
||||
assert_eq!(descriptor.tools.len(), TICKET_READ_ONLY_TOOL_NAMES.len());
|
||||
assert_eq!(
|
||||
descriptor
|
||||
.tools
|
||||
.iter()
|
||||
.map(|tool| tool.name.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
TICKET_READ_ONLY_TOOL_NAMES
|
||||
);
|
||||
assert_eq!(descriptor.requested_host_authorities.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_only_installation_does_not_expose_mutating_tools() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
make_ticket_root(&temp.path().join(DEFAULT_TICKET_BACKEND_RELATIVE_PATH));
|
||||
let mut pending_tools = Vec::new();
|
||||
let mut hooks = HookRegistryBuilder::default();
|
||||
let report = FeatureRegistryBuilder::new()
|
||||
.with_module(ticket_tools_feature_with_access(
|
||||
temp.path(),
|
||||
TicketFeatureAccess::ReadOnly,
|
||||
))
|
||||
.install_into_pending(&mut pending_tools, &mut hooks);
|
||||
|
||||
assert_eq!(pending_tools.len(), TICKET_READ_ONLY_TOOL_NAMES.len());
|
||||
assert_eq!(
|
||||
report.reports[0].installed_tools,
|
||||
TICKET_READ_ONLY_TOOL_NAMES
|
||||
);
|
||||
let pending_names = pending_tools
|
||||
.iter()
|
||||
.map(|definition| definition().0.name)
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(pending_names, TICKET_READ_ONLY_TOOL_NAMES);
|
||||
for name in ticket::tool::TICKET_MUTATING_TOOL_NAMES {
|
||||
assert!(
|
||||
!report.reports[0]
|
||||
.installed_tools
|
||||
.iter()
|
||||
.any(|tool| tool == name)
|
||||
);
|
||||
assert!(!pending_names.iter().any(|tool| tool == name));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lifecycle_installation_exposes_lifecycle_tools() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
make_ticket_root(&temp.path().join(DEFAULT_TICKET_BACKEND_RELATIVE_PATH));
|
||||
let mut pending_tools = Vec::new();
|
||||
let mut hooks = HookRegistryBuilder::default();
|
||||
let report = FeatureRegistryBuilder::new()
|
||||
.with_module(ticket_tools_feature_with_access(
|
||||
temp.path(),
|
||||
TicketFeatureAccess::Lifecycle,
|
||||
))
|
||||
.install_into_pending(&mut pending_tools, &mut hooks);
|
||||
|
||||
assert_eq!(pending_tools.len(), TICKET_TOOL_NAMES.len());
|
||||
assert_eq!(report.reports[0].installed_tools, TICKET_TOOL_NAMES);
|
||||
for name in ticket::tool::TICKET_MUTATING_TOOL_NAMES {
|
||||
assert!(
|
||||
report.reports[0]
|
||||
.installed_tools
|
||||
.iter()
|
||||
.any(|tool| tool == name)
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
report.reports[0]
|
||||
.installed_tools
|
||||
.iter()
|
||||
.any(|tool| tool == "TicketIntakeReady")
|
||||
);
|
||||
assert!(
|
||||
report.reports[0]
|
||||
.installed_tools
|
||||
.iter()
|
||||
.any(|tool| tool == "TicketWorkflowState")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn installs_ticket_tools_when_default_root_is_usable() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
//! launch prompts, and workflows so this crate remains independent from `pod`
|
||||
//! and `manifest` runtime resolution.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::fmt;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
|
@ -16,6 +16,33 @@ use thiserror::Error;
|
|||
pub const TICKET_CONFIG_RELATIVE_PATH: &str = ".yoi/ticket.config.toml";
|
||||
/// Workspace-relative default root for the built-in local Ticket backend.
|
||||
pub const DEFAULT_TICKET_BACKEND_RELATIVE_PATH: &str = ".yoi/tickets";
|
||||
/// Concrete profile selector used by the initial Ticket role scaffold.
|
||||
pub const TICKET_CONFIG_SCAFFOLD_PROFILE: &str = "builtin:default";
|
||||
|
||||
/// Return the explicit workspace Ticket config scaffold written by `yoi ticket init`.
|
||||
///
|
||||
/// The scaffold intentionally configures every fixed Ticket role with a concrete
|
||||
/// profile so strict role launch planning can validate the config without runtime
|
||||
/// fallback.
|
||||
pub fn ticket_config_scaffold() -> String {
|
||||
let mut out = String::from("[backend]\n");
|
||||
out.push_str(&format!(
|
||||
"provider = \"{}\"\n",
|
||||
TicketBackendProvider::BuiltinYoiLocal.as_str()
|
||||
));
|
||||
out.push_str(&format!(
|
||||
"root = \"{}\"\n",
|
||||
DEFAULT_TICKET_BACKEND_RELATIVE_PATH
|
||||
));
|
||||
for role in TicketRole::ALL {
|
||||
out.push_str(&format!(
|
||||
"\n[roles.{role}]\nprofile = \"{}\"\nworkflow = \"{}\"\n",
|
||||
TICKET_CONFIG_SCAFFOLD_PROFILE,
|
||||
role.default_workflow()
|
||||
));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum TicketConfigError {
|
||||
|
|
@ -86,6 +113,13 @@ impl TicketConfig {
|
|||
self.roles.get(role)
|
||||
}
|
||||
|
||||
pub fn role_launch_config(
|
||||
&self,
|
||||
role: TicketRole,
|
||||
) -> Result<&TicketRoleConfig, TicketRoleLaunchConfigError> {
|
||||
self.roles.launch_config(role)
|
||||
}
|
||||
|
||||
pub fn profile_for(&self, role: TicketRole) -> &ProfileSelectorRef {
|
||||
&self.role(role).profile
|
||||
}
|
||||
|
|
@ -200,6 +234,8 @@ impl fmt::Display for TicketRole {
|
|||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct TicketRoleProfiles {
|
||||
inner: BTreeMap<TicketRole, TicketRoleConfig>,
|
||||
configured_roles: BTreeSet<TicketRole>,
|
||||
profile_configured_roles: BTreeSet<TicketRole>,
|
||||
}
|
||||
|
||||
impl TicketRoleProfiles {
|
||||
|
|
@ -209,6 +245,31 @@ impl TicketRoleProfiles {
|
|||
.expect("TicketRoleProfiles always contains all fixed roles")
|
||||
}
|
||||
|
||||
pub fn role_is_configured(&self, role: TicketRole) -> bool {
|
||||
self.configured_roles.contains(&role)
|
||||
}
|
||||
|
||||
pub fn profile_is_configured(&self, role: TicketRole) -> bool {
|
||||
self.profile_configured_roles.contains(&role)
|
||||
}
|
||||
|
||||
pub fn launch_config(
|
||||
&self,
|
||||
role: TicketRole,
|
||||
) -> Result<&TicketRoleConfig, TicketRoleLaunchConfigError> {
|
||||
if !self.role_is_configured(role) {
|
||||
return Err(TicketRoleLaunchConfigError::MissingRoleTable { role });
|
||||
}
|
||||
if !self.profile_is_configured(role) {
|
||||
return Err(TicketRoleLaunchConfigError::MissingProfile { role });
|
||||
}
|
||||
let config = self.get(role);
|
||||
if config.profile.as_str() == "inherit" {
|
||||
return Err(TicketRoleLaunchConfigError::InheritProfile { role });
|
||||
}
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = (TicketRole, &TicketRoleConfig)> {
|
||||
TicketRole::ALL
|
||||
.into_iter()
|
||||
|
|
@ -222,8 +283,28 @@ impl Default for TicketRoleProfiles {
|
|||
.into_iter()
|
||||
.map(|role| (role, TicketRoleConfig::default_for_role(role)))
|
||||
.collect();
|
||||
Self { inner }
|
||||
Self {
|
||||
inner,
|
||||
configured_roles: BTreeSet::new(),
|
||||
profile_configured_roles: BTreeSet::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Error)]
|
||||
pub enum TicketRoleLaunchConfigError {
|
||||
#[error(
|
||||
"Ticket role `{role}` is not launch-configured; add `[roles.{role}]` with `profile = \"builtin:default\"` or another executable concrete profile selector"
|
||||
)]
|
||||
MissingRoleTable { role: TicketRole },
|
||||
#[error(
|
||||
"Ticket role `{role}` has no launch profile; set `[roles.{role}].profile` to `builtin:default` or another executable concrete profile selector"
|
||||
)]
|
||||
MissingProfile { role: TicketRole },
|
||||
#[error(
|
||||
"Ticket role `{role}` uses `profile = \"inherit\"`; top-level Ticket role launch requires an explicit executable profile selector such as `builtin:default` or a project/user profile"
|
||||
)]
|
||||
InheritProfile { role: TicketRole },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
|
|
@ -407,7 +488,12 @@ impl RawTicketConfig {
|
|||
path: path.to_path_buf(),
|
||||
message: format!("unknown Ticket role `{name}`"),
|
||||
})?;
|
||||
let profile_configured = raw_role.profile.is_some();
|
||||
roles.inner.insert(role, raw_role.resolve(role));
|
||||
roles.configured_roles.insert(role);
|
||||
if profile_configured {
|
||||
roles.profile_configured_roles.insert(role);
|
||||
}
|
||||
}
|
||||
Ok(TicketConfig {
|
||||
backend: self.backend.resolve(workspace_root).map_err(|message| {
|
||||
|
|
@ -467,7 +553,8 @@ impl RawBackendConfig {
|
|||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct RawTicketRoleConfig {
|
||||
profile: ProfileSelectorRef,
|
||||
#[serde(default)]
|
||||
profile: Option<ProfileSelectorRef>,
|
||||
#[serde(default)]
|
||||
launch_prompt: Option<PromptRef>,
|
||||
#[serde(default)]
|
||||
|
|
@ -477,7 +564,7 @@ struct RawTicketRoleConfig {
|
|||
impl RawTicketRoleConfig {
|
||||
fn resolve(self, role: TicketRole) -> TicketRoleConfig {
|
||||
TicketRoleConfig {
|
||||
profile: self.profile,
|
||||
profile: self.profile.unwrap_or_else(ProfileSelectorRef::inherit),
|
||||
launch_prompt: self.launch_prompt,
|
||||
workflow: self
|
||||
.workflow
|
||||
|
|
@ -586,6 +673,36 @@ workflow = "ticket-orchestrator-routing"
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_config_includes_backend_and_all_fixed_roles() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let scaffold = ticket_config_scaffold();
|
||||
|
||||
assert!(scaffold.contains("[backend]\n"));
|
||||
assert!(scaffold.contains("provider = \"builtin:yoi_local\""));
|
||||
assert!(scaffold.contains("root = \".yoi/tickets\""));
|
||||
for role in TicketRole::ALL {
|
||||
assert!(scaffold.contains(&format!("[roles.{role}]")));
|
||||
assert!(scaffold.contains(&format!(
|
||||
"[roles.{role}]\nprofile = \"builtin:default\"\nworkflow = \"{}\"",
|
||||
role.default_workflow()
|
||||
)));
|
||||
}
|
||||
|
||||
let config = TicketConfig::from_toml(
|
||||
temp.path(),
|
||||
temp.path().join(TICKET_CONFIG_RELATIVE_PATH),
|
||||
&scaffold,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(config.backend_root(), temp.path().join(".yoi/tickets"));
|
||||
for role in TicketRole::ALL {
|
||||
let role_config = config.role_launch_config(role).unwrap();
|
||||
assert_eq!(role_config.profile.as_str(), "builtin:default");
|
||||
assert_eq!(role_config.workflow.as_str(), role.default_workflow());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_role_config_keeps_role_defaults() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
|
|
@ -605,6 +722,100 @@ profile = "project:coder"
|
|||
assert_eq!(config.profile_for(TicketRole::Reviewer).as_str(), "inherit");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backend_only_config_is_not_role_launch_ready() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
write_config(
|
||||
temp.path(),
|
||||
r#"
|
||||
[backend]
|
||||
provider = "builtin:yoi_local"
|
||||
root = ".yoi/tickets"
|
||||
"#,
|
||||
);
|
||||
|
||||
let config = TicketConfig::load_workspace(temp.path()).unwrap();
|
||||
assert_eq!(config.backend.root, temp.path().join(".yoi/tickets"));
|
||||
assert_eq!(
|
||||
config.role_launch_config(TicketRole::Intake).unwrap_err(),
|
||||
TicketRoleLaunchConfigError::MissingRoleTable {
|
||||
role: TicketRole::Intake
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_role_config_only_marks_configured_roles_launch_ready() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
write_config(
|
||||
temp.path(),
|
||||
r#"
|
||||
[roles.intake]
|
||||
profile = "builtin:default"
|
||||
"#,
|
||||
);
|
||||
|
||||
let config = TicketConfig::load_workspace(temp.path()).unwrap();
|
||||
assert_eq!(
|
||||
config
|
||||
.role_launch_config(TicketRole::Intake)
|
||||
.unwrap()
|
||||
.profile
|
||||
.as_str(),
|
||||
"builtin:default"
|
||||
);
|
||||
assert_eq!(
|
||||
config
|
||||
.role_launch_config(TicketRole::Orchestrator)
|
||||
.unwrap_err(),
|
||||
TicketRoleLaunchConfigError::MissingRoleTable {
|
||||
role: TicketRole::Orchestrator
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn role_table_without_profile_is_not_role_launch_ready() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
write_config(
|
||||
temp.path(),
|
||||
r#"
|
||||
[roles.orchestrator]
|
||||
workflow = "ticket-orchestrator-routing"
|
||||
"#,
|
||||
);
|
||||
|
||||
let config = TicketConfig::load_workspace(temp.path()).unwrap();
|
||||
assert_eq!(
|
||||
config
|
||||
.role_launch_config(TicketRole::Orchestrator)
|
||||
.unwrap_err(),
|
||||
TicketRoleLaunchConfigError::MissingProfile {
|
||||
role: TicketRole::Orchestrator
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inherit_profile_is_not_role_launch_ready() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
write_config(
|
||||
temp.path(),
|
||||
r#"
|
||||
[roles.intake]
|
||||
profile = "inherit"
|
||||
"#,
|
||||
);
|
||||
|
||||
let config = TicketConfig::load_workspace(temp.path()).unwrap();
|
||||
assert_eq!(
|
||||
config.role_launch_config(TicketRole::Intake).unwrap_err(),
|
||||
TicketRoleLaunchConfigError::InheritProfile {
|
||||
role: TicketRole::Intake
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_roles_are_rejected() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -14,8 +14,8 @@ use serde_json::{Value, json};
|
|||
use crate::{
|
||||
ExtensibleTicketStatus, LocalTicketBackend, MarkdownText, NewTicket, NewTicketEvent, Ticket,
|
||||
TicketBackend, TicketDoctorDiagnostic, TicketDoctorReport, TicketDoctorSeverity, TicketError,
|
||||
TicketEventKind, TicketIdOrSlug, TicketRef, TicketReview, TicketReviewResult, TicketStatus,
|
||||
TicketSummary,
|
||||
TicketEventKind, TicketIdOrSlug, TicketIntakeSummary, TicketRef, TicketReview,
|
||||
TicketReviewResult, TicketStateChange, TicketStatus, TicketSummary, TicketWorkflowState,
|
||||
};
|
||||
|
||||
const DEFAULT_LIST_LIMIT: usize = 100;
|
||||
|
|
@ -29,17 +29,31 @@ const MAX_BODY_MAX_BYTES: usize = 64 * 1024;
|
|||
const DEFAULT_DIAGNOSTIC_LIMIT: usize = 100;
|
||||
const MAX_DIAGNOSTIC_LIMIT: usize = 500;
|
||||
|
||||
pub const TICKET_TOOL_NAMES: [&str; 8] = [
|
||||
pub const TICKET_TOOL_NAMES: [&str; 10] = [
|
||||
"TicketCreate",
|
||||
"TicketList",
|
||||
"TicketShow",
|
||||
"TicketComment",
|
||||
"TicketReview",
|
||||
"TicketIntakeReady",
|
||||
"TicketWorkflowState",
|
||||
"TicketStatus",
|
||||
"TicketClose",
|
||||
"TicketDoctor",
|
||||
];
|
||||
|
||||
pub const TICKET_READ_ONLY_TOOL_NAMES: [&str; 3] = ["TicketList", "TicketShow", "TicketDoctor"];
|
||||
|
||||
pub const TICKET_MUTATING_TOOL_NAMES: [&str; 7] = [
|
||||
"TicketCreate",
|
||||
"TicketComment",
|
||||
"TicketReview",
|
||||
"TicketIntakeReady",
|
||||
"TicketWorkflowState",
|
||||
"TicketStatus",
|
||||
"TicketClose",
|
||||
];
|
||||
|
||||
const CREATE_DESCRIPTION: &str = "Create a Ticket through the configured typed Ticket backend. \
|
||||
Inputs mirror the Ticket `item.md` fields; `title` is required, `body` is Markdown, and the \
|
||||
backend assigns the id and writes the local Ticket file layout under the configured backend root.";
|
||||
|
|
@ -54,6 +68,14 @@ const COMMENT_DESCRIPTION: &str = "Append a typed Ticket thread event. `role` mu
|
|||
configured Ticket backend root.";
|
||||
const REVIEW_DESCRIPTION: &str = "Append a Ticket review event. `result` must be `approve` or \
|
||||
`request_changes`; `body` is Markdown. Writes stay inside the configured Ticket backend root.";
|
||||
const INTAKE_READY_DESCRIPTION: &str = "Mark an existing Ticket intake as ready through the typed \
|
||||
Ticket backend. The tool appends a bounded `intake_summary`, appends a typed `state_changed` event \
|
||||
for `workflow_state`, and transitions workflow_state to `ready`.";
|
||||
const WORKFLOW_STATE_DESCRIPTION: &str = "Transition Ticket `workflow_state` through the typed \
|
||||
Ticket backend with a bounded `state_changed` event. This does not move local open/pending/closed \
|
||||
status; use `TicketStatus` or `TicketClose` for local status changes. Treat `queued -> inprogress` \
|
||||
as the implementation acceptance step: implementation side effects should happen only after that \
|
||||
transition is accepted and recorded.";
|
||||
const STATUS_DESCRIPTION: &str = "Move a Ticket between non-closed local statuses through the typed \
|
||||
Ticket backend. Use `TicketClose` for closing because closed Tickets require a resolution accepted \
|
||||
by `yoi ticket doctor`.";
|
||||
|
|
@ -103,6 +125,40 @@ struct TicketCreateParams {
|
|||
/// Optional action-required frontmatter value.
|
||||
#[serde(default)]
|
||||
action_required: Option<String>,
|
||||
/// Optional workflow_state frontmatter value. Defaults to `intake`.
|
||||
#[serde(default)]
|
||||
workflow_state: Option<TicketWorkflowStateParam>,
|
||||
/// Optional attention_required overlay frontmatter value.
|
||||
#[serde(default)]
|
||||
attention_required: Option<String>,
|
||||
/// Optional queued_by frontmatter value.
|
||||
#[serde(default)]
|
||||
queued_by: Option<String>,
|
||||
/// Optional queued_at frontmatter value.
|
||||
#[serde(default)]
|
||||
queued_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Deserialize, schemars::JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
enum TicketWorkflowStateParam {
|
||||
Intake,
|
||||
Ready,
|
||||
Queued,
|
||||
Inprogress,
|
||||
Done,
|
||||
}
|
||||
|
||||
impl TicketWorkflowStateParam {
|
||||
fn into_state(self) -> TicketWorkflowState {
|
||||
match self {
|
||||
Self::Intake => TicketWorkflowState::Intake,
|
||||
Self::Ready => TicketWorkflowState::Ready,
|
||||
Self::Queued => TicketWorkflowState::Queued,
|
||||
Self::Inprogress => TicketWorkflowState::InProgress,
|
||||
Self::Done => TicketWorkflowState::Done,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, schemars::JsonSchema)]
|
||||
|
|
@ -212,6 +268,40 @@ struct TicketStatusParams {
|
|||
status: TicketStatusParam,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, schemars::JsonSchema)]
|
||||
struct TicketIntakeReadyParams {
|
||||
/// Ticket id or slug.
|
||||
ticket: String,
|
||||
/// Concise bounded intake summary to append as a typed intake_summary event.
|
||||
intake_summary: String,
|
||||
/// Optional author for both intake_summary and state_changed events.
|
||||
#[serde(default)]
|
||||
author: Option<String>,
|
||||
/// Reason attached to the state_changed event. Defaults to `intake_ready`.
|
||||
#[serde(default)]
|
||||
reason: Option<String>,
|
||||
/// Optional state_changed body. If omitted, a concise default is used.
|
||||
#[serde(default)]
|
||||
state_change_body: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, schemars::JsonSchema)]
|
||||
struct TicketWorkflowStateParams {
|
||||
/// Ticket id or slug.
|
||||
ticket: String,
|
||||
/// Expected current workflow_state. The backend rejects stale transitions.
|
||||
from: TicketWorkflowStateParam,
|
||||
/// Target workflow_state.
|
||||
to: TicketWorkflowStateParam,
|
||||
/// Reason attached to the typed state_changed event.
|
||||
reason: String,
|
||||
/// Markdown body for the typed state_changed event.
|
||||
body: String,
|
||||
/// Optional thread author.
|
||||
#[serde(default)]
|
||||
author: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, schemars::JsonSchema)]
|
||||
struct TicketCloseParams {
|
||||
/// Ticket id or slug.
|
||||
|
|
@ -278,6 +368,16 @@ struct TicketReviewTool {
|
|||
backend: LocalTicketBackend,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TicketIntakeReadyTool {
|
||||
backend: LocalTicketBackend,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TicketWorkflowStateTool {
|
||||
backend: LocalTicketBackend,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TicketStatusTool {
|
||||
backend: LocalTicketBackend,
|
||||
|
|
@ -316,6 +416,12 @@ impl Tool for TicketCreateTool {
|
|||
input.needs_preflight = params.needs_preflight;
|
||||
input.risk_flags = params.risk_flags;
|
||||
input.action_required = params.action_required;
|
||||
input.workflow_state = params
|
||||
.workflow_state
|
||||
.map(TicketWorkflowStateParam::into_state);
|
||||
input.attention_required = params.attention_required;
|
||||
input.queued_by = params.queued_by;
|
||||
input.queued_at = params.queued_at;
|
||||
|
||||
let created = self
|
||||
.backend
|
||||
|
|
@ -470,6 +576,76 @@ impl Tool for TicketReviewTool {
|
|||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Tool for TicketIntakeReadyTool {
|
||||
async fn execute(&self, input_json: &str) -> Result<ToolOutput, ToolError> {
|
||||
let params: TicketIntakeReadyParams = parse_input("TicketIntakeReady", input_json)?;
|
||||
let from = TicketWorkflowState::Intake;
|
||||
let reason = params.reason.unwrap_or_else(|| "intake_ready".to_string());
|
||||
let body = params.state_change_body.unwrap_or_else(|| {
|
||||
format!(
|
||||
"Ticket intake complete; workflow_state {} -> ready.\n",
|
||||
from.as_str()
|
||||
)
|
||||
});
|
||||
let mut summary = TicketIntakeSummary::new(params.intake_summary);
|
||||
summary.author = params.author.clone();
|
||||
let mut change = TicketStateChange::new(
|
||||
from.as_str(),
|
||||
TicketWorkflowState::Ready.as_str(),
|
||||
reason,
|
||||
body,
|
||||
);
|
||||
change.author = params.author;
|
||||
self.backend
|
||||
.mark_intake_ready(
|
||||
TicketIdOrSlug::Query(params.ticket.clone()),
|
||||
summary,
|
||||
change,
|
||||
)
|
||||
.map_err(|error| backend_error("TicketIntakeReady", error))?;
|
||||
Ok(json_output(
|
||||
format!("Marked ticket {} workflow_state ready", params.ticket),
|
||||
json!({ "ticket": params.ticket, "workflow_state": "ready", "ok": true }),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Tool for TicketWorkflowStateTool {
|
||||
async fn execute(&self, input_json: &str) -> Result<ToolOutput, ToolError> {
|
||||
let params: TicketWorkflowStateParams = parse_input("TicketWorkflowState", input_json)?;
|
||||
let from = params.from.into_state();
|
||||
let to = params.to.into_state();
|
||||
if from == to {
|
||||
return Err(ToolError::InvalidArgument(
|
||||
"workflow_state transition must change state".to_string(),
|
||||
));
|
||||
}
|
||||
let mut change =
|
||||
TicketStateChange::new(from.as_str(), to.as_str(), params.reason, params.body);
|
||||
change.author = params.author;
|
||||
self.backend
|
||||
.set_workflow_state(TicketIdOrSlug::Query(params.ticket.clone()), change)
|
||||
.map_err(|error| backend_error("TicketWorkflowState", error))?;
|
||||
Ok(json_output(
|
||||
format!(
|
||||
"Transitioned ticket {} workflow_state {} -> {}",
|
||||
params.ticket,
|
||||
from.as_str(),
|
||||
to.as_str()
|
||||
),
|
||||
json!({
|
||||
"ticket": params.ticket,
|
||||
"from": from.as_str(),
|
||||
"to": to.as_str(),
|
||||
"workflow_state": to.as_str(),
|
||||
"ok": true
|
||||
}),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Tool for TicketStatusTool {
|
||||
async fn execute(&self, input_json: &str) -> Result<ToolOutput, ToolError> {
|
||||
|
|
@ -586,6 +762,11 @@ fn ticket_summary_json(ticket: TicketSummary) -> Value {
|
|||
"readiness": ticket.readiness,
|
||||
"needs_preflight": ticket.needs_preflight,
|
||||
"action_required": ticket.action_required,
|
||||
"workflow_state": ticket.workflow_state.as_str(),
|
||||
"workflow_state_explicit": ticket.workflow_state_explicit,
|
||||
"attention_required": ticket.attention_required,
|
||||
"queued_by": ticket.queued_by,
|
||||
"queued_at": ticket.queued_at,
|
||||
"updated_at": ticket.updated_at,
|
||||
})
|
||||
}
|
||||
|
|
@ -607,6 +788,11 @@ fn ticket_json(
|
|||
"author": event.author,
|
||||
"at": event.at,
|
||||
"status": event.status,
|
||||
"from": event.from,
|
||||
"to": event.to,
|
||||
"reason": event.reason,
|
||||
"state_field": event.state_field,
|
||||
"attributes": event.attributes,
|
||||
"heading": event.heading,
|
||||
"body": truncate_text(event.body.as_str(), body_max_bytes),
|
||||
})
|
||||
|
|
@ -636,6 +822,11 @@ fn ticket_json(
|
|||
"needs_preflight": ticket.meta.needs_preflight,
|
||||
"risk_flags": ticket.meta.risk_flags,
|
||||
"action_required": ticket.meta.action_required,
|
||||
"workflow_state": ticket.meta.workflow_state.as_str(),
|
||||
"workflow_state_explicit": ticket.meta.workflow_state_explicit,
|
||||
"attention_required": ticket.meta.attention_required,
|
||||
"queued_by": ticket.meta.queued_by,
|
||||
"queued_at": ticket.meta.queued_at,
|
||||
},
|
||||
"body": truncate_text(ticket.document.body.as_str(), body_max_bytes),
|
||||
"events": {
|
||||
|
|
@ -731,6 +922,10 @@ fn input_schema(name: &str) -> Value {
|
|||
"TicketShow" => serde_json::to_value(schemars::schema_for!(TicketShowParams)),
|
||||
"TicketComment" => serde_json::to_value(schemars::schema_for!(TicketCommentParams)),
|
||||
"TicketReview" => serde_json::to_value(schemars::schema_for!(TicketReviewParams)),
|
||||
"TicketIntakeReady" => serde_json::to_value(schemars::schema_for!(TicketIntakeReadyParams)),
|
||||
"TicketWorkflowState" => {
|
||||
serde_json::to_value(schemars::schema_for!(TicketWorkflowStateParams))
|
||||
}
|
||||
"TicketStatus" => serde_json::to_value(schemars::schema_for!(TicketStatusParams)),
|
||||
"TicketClose" => serde_json::to_value(schemars::schema_for!(TicketCloseParams)),
|
||||
"TicketDoctor" => serde_json::to_value(schemars::schema_for!(TicketDoctorParams)),
|
||||
|
|
@ -754,6 +949,8 @@ impl_from_backend!(TicketListTool);
|
|||
impl_from_backend!(TicketShowTool);
|
||||
impl_from_backend!(TicketCommentTool);
|
||||
impl_from_backend!(TicketReviewTool);
|
||||
impl_from_backend!(TicketIntakeReadyTool);
|
||||
impl_from_backend!(TicketWorkflowStateTool);
|
||||
impl_from_backend!(TicketStatusTool);
|
||||
impl_from_backend!(TicketCloseTool);
|
||||
impl_from_backend!(TicketDoctorTool);
|
||||
|
|
@ -766,6 +963,16 @@ pub fn ticket_tools(backend: LocalTicketBackend) -> Vec<ToolDefinition> {
|
|||
tool_definition::<TicketShowTool>("TicketShow", SHOW_DESCRIPTION, backend.clone()),
|
||||
tool_definition::<TicketCommentTool>("TicketComment", COMMENT_DESCRIPTION, backend.clone()),
|
||||
tool_definition::<TicketReviewTool>("TicketReview", REVIEW_DESCRIPTION, backend.clone()),
|
||||
tool_definition::<TicketIntakeReadyTool>(
|
||||
"TicketIntakeReady",
|
||||
INTAKE_READY_DESCRIPTION,
|
||||
backend.clone(),
|
||||
),
|
||||
tool_definition::<TicketWorkflowStateTool>(
|
||||
"TicketWorkflowState",
|
||||
WORKFLOW_STATE_DESCRIPTION,
|
||||
backend.clone(),
|
||||
),
|
||||
tool_definition::<TicketStatusTool>("TicketStatus", STATUS_DESCRIPTION, backend.clone()),
|
||||
tool_definition::<TicketCloseTool>("TicketClose", CLOSE_DESCRIPTION, backend.clone()),
|
||||
tool_definition::<TicketDoctorTool>("TicketDoctor", DOCTOR_DESCRIPTION, backend),
|
||||
|
|
@ -796,6 +1003,50 @@ mod tests {
|
|||
.expect("tool exists")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ticket_tool_name_partitions_are_explicit() {
|
||||
assert_eq!(
|
||||
TICKET_READ_ONLY_TOOL_NAMES,
|
||||
["TicketList", "TicketShow", "TicketDoctor"]
|
||||
);
|
||||
assert_eq!(
|
||||
TICKET_MUTATING_TOOL_NAMES,
|
||||
[
|
||||
"TicketCreate",
|
||||
"TicketComment",
|
||||
"TicketReview",
|
||||
"TicketIntakeReady",
|
||||
"TicketWorkflowState",
|
||||
"TicketStatus",
|
||||
"TicketClose"
|
||||
]
|
||||
);
|
||||
for name in TICKET_READ_ONLY_TOOL_NAMES {
|
||||
assert!(TICKET_TOOL_NAMES.contains(&name));
|
||||
assert!(!TICKET_MUTATING_TOOL_NAMES.contains(&name));
|
||||
}
|
||||
for name in TICKET_MUTATING_TOOL_NAMES {
|
||||
assert!(TICKET_TOOL_NAMES.contains(&name));
|
||||
assert!(!TICKET_READ_ONLY_TOOL_NAMES.contains(&name));
|
||||
}
|
||||
assert_eq!(
|
||||
TICKET_READ_ONLY_TOOL_NAMES.len() + TICKET_MUTATING_TOOL_NAMES.len(),
|
||||
TICKET_TOOL_NAMES.len()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workflow_state_tool_description_explains_queued_acceptance() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let definition = ticket_tools(backend(&temp))
|
||||
.into_iter()
|
||||
.find(|definition| definition().0.name == "TicketWorkflowState")
|
||||
.expect("workflow state tool exists");
|
||||
let (meta, _) = definition();
|
||||
assert!(meta.description.contains("queued -> inprogress"));
|
||||
assert!(meta.description.contains("implementation side effects"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ticket_tools_create_list_show_and_doctor() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
|
|
@ -908,6 +1159,217 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ticket_workflow_tools_mark_ready_and_transition_state() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let backend = backend(&temp);
|
||||
let created = backend.create(NewTicket::new("Workflow Tool")).unwrap();
|
||||
let intake_ready = tool_by_name(backend.clone(), "TicketIntakeReady");
|
||||
let workflow = tool_by_name(backend.clone(), "TicketWorkflowState");
|
||||
|
||||
intake_ready
|
||||
.execute(
|
||||
&json!({
|
||||
"ticket": created.slug,
|
||||
"intake_summary": "Requirements accepted; implementation can be queued.",
|
||||
"author": "intake-pod"
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
backend
|
||||
.queue_ready(TicketIdOrSlug::Id(created.id.clone()), "panel")
|
||||
.unwrap();
|
||||
workflow
|
||||
.execute(
|
||||
&json!({
|
||||
"ticket": created.slug,
|
||||
"from": "queued",
|
||||
"to": "inprogress",
|
||||
"reason": "orchestrator_started",
|
||||
"body": "Orchestrator started implementation.\n",
|
||||
"author": "orchestrator"
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
workflow
|
||||
.execute(
|
||||
&json!({
|
||||
"ticket": created.slug,
|
||||
"from": "inprogress",
|
||||
"to": "done",
|
||||
"reason": "implementation_complete",
|
||||
"body": "Implementation finished and is ready for close.\n",
|
||||
"author": "orchestrator"
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let record = backend.show(TicketIdOrSlug::Query(created.slug)).unwrap();
|
||||
assert_eq!(record.meta.workflow_state, TicketWorkflowState::Done);
|
||||
assert_eq!(record.meta.status.as_local(), Some(TicketStatus::Open));
|
||||
assert!(
|
||||
record
|
||||
.events
|
||||
.iter()
|
||||
.any(|event| event.kind == TicketEventKind::IntakeSummary)
|
||||
);
|
||||
let transitions = record
|
||||
.events
|
||||
.iter()
|
||||
.filter(|event| {
|
||||
event.kind == TicketEventKind::StateChanged
|
||||
&& event.state_field.as_deref() == Some("workflow_state")
|
||||
})
|
||||
.map(|event| (event.from.as_deref(), event.to.as_deref()))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
transitions,
|
||||
vec![
|
||||
(Some("intake"), Some("ready")),
|
||||
(Some("ready"), Some("queued")),
|
||||
(Some("queued"), Some("inprogress")),
|
||||
(Some("inprogress"), Some("done"))
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ticket_workflow_tool_rejects_stale_transition_without_status_move() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let backend = backend(&temp);
|
||||
let created = backend
|
||||
.create(NewTicket::new("Stale Workflow Tool"))
|
||||
.unwrap();
|
||||
let workflow = tool_by_name(backend.clone(), "TicketWorkflowState");
|
||||
|
||||
let error = workflow
|
||||
.execute(
|
||||
&json!({
|
||||
"ticket": created.id,
|
||||
"from": "queued",
|
||||
"to": "inprogress",
|
||||
"reason": "orchestrator_started",
|
||||
"body": "Should not apply.\n"
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert!(
|
||||
error
|
||||
.to_string()
|
||||
.contains("workflow_state changed concurrently")
|
||||
);
|
||||
let record = backend.show(TicketIdOrSlug::Query(created.slug)).unwrap();
|
||||
assert_eq!(record.meta.workflow_state, TicketWorkflowState::Intake);
|
||||
assert_eq!(record.meta.status.as_local(), Some(TicketStatus::Open));
|
||||
assert!(!record.events.iter().any(|event| {
|
||||
event.kind == TicketEventKind::StateChanged
|
||||
&& event.state_field.as_deref() == Some("workflow_state")
|
||||
}));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ticket_workflow_tool_rejects_disallowed_transition_graph_edges() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let backend = backend(&temp);
|
||||
let workflow = tool_by_name(backend.clone(), "TicketWorkflowState");
|
||||
|
||||
let mut ready_input = NewTicket::new("Ready Bypass");
|
||||
ready_input.workflow_state = Some(TicketWorkflowState::Ready);
|
||||
let ready = backend.create(ready_input).unwrap();
|
||||
let ready_error = workflow
|
||||
.execute(
|
||||
&json!({
|
||||
"ticket": ready.id,
|
||||
"from": "ready",
|
||||
"to": "inprogress",
|
||||
"reason": "bypass_queue",
|
||||
"body": "Should not bypass Queue.\n"
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(ready_error.to_string().contains("not allowed"));
|
||||
|
||||
let mut done_input = NewTicket::new("Backward Bypass");
|
||||
done_input.workflow_state = Some(TicketWorkflowState::Done);
|
||||
let done = backend.create(done_input).unwrap();
|
||||
let backward_error = workflow
|
||||
.execute(
|
||||
&json!({
|
||||
"ticket": done.id,
|
||||
"from": "done",
|
||||
"to": "intake",
|
||||
"reason": "backwards",
|
||||
"body": "Should not move backwards.\n"
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(backward_error.to_string().contains("not allowed"));
|
||||
|
||||
let mut queued_input = NewTicket::new("Skip Bypass");
|
||||
queued_input.workflow_state = Some(TicketWorkflowState::Queued);
|
||||
let queued = backend.create(queued_input).unwrap();
|
||||
let skip_error = workflow
|
||||
.execute(
|
||||
&json!({
|
||||
"ticket": queued.id,
|
||||
"from": "queued",
|
||||
"to": "done",
|
||||
"reason": "skip_inprogress",
|
||||
"body": "Should not skip inprogress.\n"
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(skip_error.to_string().contains("not allowed"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ticket_intake_ready_tool_rejects_non_intake_ticket() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let backend = backend(&temp);
|
||||
let mut input = NewTicket::new("Already Ready");
|
||||
input.workflow_state = Some(TicketWorkflowState::Ready);
|
||||
let created = backend.create(input).unwrap();
|
||||
let intake_ready = tool_by_name(backend.clone(), "TicketIntakeReady");
|
||||
|
||||
let error = intake_ready
|
||||
.execute(
|
||||
&json!({
|
||||
"ticket": created.id,
|
||||
"intake_summary": "Should not rewrite ready ticket."
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert!(
|
||||
error
|
||||
.to_string()
|
||||
.contains("workflow_state changed concurrently")
|
||||
);
|
||||
let record = backend.show(TicketIdOrSlug::Query(created.slug)).unwrap();
|
||||
assert_eq!(record.meta.workflow_state, TicketWorkflowState::Ready);
|
||||
assert!(!record.events.iter().any(|event| {
|
||||
event.kind == TicketEventKind::StateChanged
|
||||
&& event.state_field.as_deref() == Some("workflow_state")
|
||||
}));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ticket_show_requires_exactly_one_identifier() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
|
|
|
|||
|
|
@ -11,8 +11,7 @@ use crate::block::{
|
|||
};
|
||||
use crate::cache::FileCache;
|
||||
use crate::command::{
|
||||
CommandAction, CommandCandidate, CommandEnvironment, CommandExecution, CommandInputMode,
|
||||
CommandRegistry,
|
||||
CommandCandidate, CommandEnvironment, CommandExecution, CommandInputMode, CommandRegistry,
|
||||
};
|
||||
use crate::input::InputBuffer;
|
||||
use crate::scroll::Scroll;
|
||||
|
|
@ -247,7 +246,6 @@ pub struct App {
|
|||
pub input_mode: CommandInputMode,
|
||||
pub command_registry: CommandRegistry,
|
||||
command_completion_selected: Option<usize>,
|
||||
pending_command_action: Option<CommandAction>,
|
||||
pub quit: bool,
|
||||
/// 2-tap guard for `Ctrl-C` when the Pod is not running. First press
|
||||
/// records the instant; a second press within the timeout exits the
|
||||
|
|
@ -316,7 +314,6 @@ impl App {
|
|||
input_mode: CommandInputMode::Composer,
|
||||
command_registry: CommandRegistry::default(),
|
||||
command_completion_selected: None,
|
||||
pending_command_action: None,
|
||||
quit: false,
|
||||
quit_confirm: None,
|
||||
blocks: Vec::new(),
|
||||
|
|
@ -1629,14 +1626,9 @@ impl App {
|
|||
self.rewind_picker = None;
|
||||
self.rewind_request_pending = true;
|
||||
}
|
||||
self.pending_command_action = result.action;
|
||||
result.method
|
||||
}
|
||||
|
||||
pub fn take_pending_command_action(&mut self) -> Option<CommandAction> {
|
||||
self.pending_command_action.take()
|
||||
}
|
||||
|
||||
fn push_command_diagnostic(&mut self, message: impl Into<String>) {
|
||||
self.blocks.push(Block::Alert {
|
||||
level: AlertLevel::Warn,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
use client::ticket_role::TicketRole;
|
||||
use protocol::Method;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
|
|
@ -50,22 +49,9 @@ pub struct CommandEnvironment {
|
|||
pub paused: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum CommandAction {
|
||||
TicketRole(TicketRoleCommand),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct TicketRoleCommand {
|
||||
pub role: TicketRole,
|
||||
pub ticket: Option<String>,
|
||||
pub instruction: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CommandExecution {
|
||||
pub method: Option<Method>,
|
||||
pub action: Option<CommandAction>,
|
||||
pub diagnostics: Vec<CommandDiagnostic>,
|
||||
pub exit_command_mode: bool,
|
||||
pub clear_input: bool,
|
||||
|
|
@ -75,7 +61,6 @@ impl CommandExecution {
|
|||
pub fn diagnostic(message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
method: None,
|
||||
action: None,
|
||||
diagnostics: vec![CommandDiagnostic::new(message)],
|
||||
exit_command_mode: false,
|
||||
clear_input: false,
|
||||
|
|
@ -85,17 +70,6 @@ impl CommandExecution {
|
|||
pub fn notice(message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
method: None,
|
||||
action: None,
|
||||
diagnostics: vec![CommandDiagnostic::new(message)],
|
||||
exit_command_mode: true,
|
||||
clear_input: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn local_action(message: impl Into<String>, action: CommandAction) -> Self {
|
||||
Self {
|
||||
method: None,
|
||||
action: Some(action),
|
||||
diagnostics: vec![CommandDiagnostic::new(message)],
|
||||
exit_command_mode: true,
|
||||
clear_input: true,
|
||||
|
|
@ -191,15 +165,6 @@ impl CommandRegistry {
|
|||
can_execute: peer_available,
|
||||
executor: peer_command,
|
||||
});
|
||||
registry.register(CommandSpec {
|
||||
name: "ticket",
|
||||
aliases: &[],
|
||||
usage: "ticket <intake|route|investigate|implement|review> ...",
|
||||
description: "Launch a fixed Ticket role Pod using .yoi/ticket.config.toml.",
|
||||
argument_parser: ticket_args,
|
||||
can_execute: always_available,
|
||||
executor: ticket_command,
|
||||
});
|
||||
registry
|
||||
}
|
||||
|
||||
|
|
@ -357,37 +322,6 @@ fn peer_args(raw: &str) -> Result<CommandArgs, CommandDiagnostic> {
|
|||
}
|
||||
}
|
||||
|
||||
fn ticket_args(raw: &str) -> Result<CommandArgs, CommandDiagnostic> {
|
||||
let args = CommandArgs::parse_whitespace(raw);
|
||||
let Some(action) = args.argv().first().map(String::as_str) else {
|
||||
return Err(CommandDiagnostic::new(
|
||||
"Invalid arguments. Usage: ticket <intake|route|investigate|implement|review> ...",
|
||||
));
|
||||
};
|
||||
match action {
|
||||
"intake" if args.argv().len() >= 2 => Ok(args),
|
||||
"intake" => Err(CommandDiagnostic::new(
|
||||
"Invalid arguments. Usage: ticket intake <context...>",
|
||||
)),
|
||||
"route" | "investigate" | "implement" | "review" if args.argv().len() >= 2 => Ok(args),
|
||||
"route" => Err(CommandDiagnostic::new(
|
||||
"Invalid arguments. Usage: ticket route <ticket-id-or-slug> [instruction...]",
|
||||
)),
|
||||
"investigate" => Err(CommandDiagnostic::new(
|
||||
"Invalid arguments. Usage: ticket investigate <ticket-id-or-slug> [instruction...]",
|
||||
)),
|
||||
"implement" => Err(CommandDiagnostic::new(
|
||||
"Invalid arguments. Usage: ticket implement <ticket-id-or-slug> [instruction...]",
|
||||
)),
|
||||
"review" => Err(CommandDiagnostic::new(
|
||||
"Invalid arguments. Usage: ticket review <ticket-id-or-slug> [instruction...]",
|
||||
)),
|
||||
_ => Err(CommandDiagnostic::new(format!(
|
||||
"Unknown ticket action: {action}. Usage: ticket <intake|route|investigate|implement|review> ..."
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
fn compact_available(environment: &CommandEnvironment) -> Result<(), CommandDiagnostic> {
|
||||
if !environment.connected {
|
||||
return Err(CommandDiagnostic::new(
|
||||
|
|
@ -476,7 +410,6 @@ fn compact_command(invocation: CommandInvocation<'_>) -> CommandExecution {
|
|||
let _ = invocation.args.raw();
|
||||
CommandExecution {
|
||||
method: Some(Method::Compact),
|
||||
action: None,
|
||||
diagnostics: vec![CommandDiagnostic::new("compact requested")],
|
||||
exit_command_mode: true,
|
||||
clear_input: true,
|
||||
|
|
@ -489,7 +422,6 @@ fn rewind_command(invocation: CommandInvocation<'_>) -> CommandExecution {
|
|||
let _ = invocation.args.raw();
|
||||
CommandExecution {
|
||||
method: Some(Method::ListRewindTargets),
|
||||
action: None,
|
||||
diagnostics: vec![CommandDiagnostic::new("rewind picker requested")],
|
||||
exit_command_mode: true,
|
||||
clear_input: true,
|
||||
|
|
@ -502,7 +434,6 @@ fn peer_command(invocation: CommandInvocation<'_>) -> CommandExecution {
|
|||
let name = invocation.args.argv()[0].clone();
|
||||
CommandExecution {
|
||||
method: Some(Method::RegisterPeer { name: name.clone() }),
|
||||
action: None,
|
||||
diagnostics: vec![CommandDiagnostic::new(format!(
|
||||
"peer metadata registration requested with `{name}`"
|
||||
))],
|
||||
|
|
@ -511,81 +442,6 @@ fn peer_command(invocation: CommandInvocation<'_>) -> CommandExecution {
|
|||
}
|
||||
}
|
||||
|
||||
fn ticket_command(invocation: CommandInvocation<'_>) -> CommandExecution {
|
||||
let _ = invocation.command;
|
||||
let _ = invocation.environment;
|
||||
let Some((action, rest)) = split_first_word(invocation.args.raw()) else {
|
||||
return CommandExecution::diagnostic(
|
||||
"Invalid arguments. Usage: ticket <intake|route|investigate|implement|review> ...",
|
||||
);
|
||||
};
|
||||
|
||||
let Some(role) = ticket_role_for_action(action) else {
|
||||
return CommandExecution::diagnostic(format!(
|
||||
"Unknown ticket action: {action}. Usage: ticket <intake|route|investigate|implement|review> ..."
|
||||
));
|
||||
};
|
||||
|
||||
let (ticket, instruction) = if action == "intake" {
|
||||
let Some(instruction) = non_empty_string(rest) else {
|
||||
return CommandExecution::diagnostic(
|
||||
"Invalid arguments. Usage: ticket intake <context...>",
|
||||
);
|
||||
};
|
||||
(None, Some(instruction))
|
||||
} else {
|
||||
let Some((ticket, rest)) = split_first_word(rest) else {
|
||||
return CommandExecution::diagnostic(format!(
|
||||
"Invalid arguments. Usage: ticket {action} <ticket-id-or-slug> [instruction...]"
|
||||
));
|
||||
};
|
||||
(Some(ticket.to_owned()), non_empty_string(rest))
|
||||
};
|
||||
|
||||
CommandExecution::local_action(
|
||||
format!("ticket {action} launch requested"),
|
||||
CommandAction::TicketRole(TicketRoleCommand {
|
||||
role,
|
||||
ticket,
|
||||
instruction,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn ticket_role_for_action(action: &str) -> Option<TicketRole> {
|
||||
match action {
|
||||
"intake" => Some(TicketRole::Intake),
|
||||
"route" => Some(TicketRole::Orchestrator),
|
||||
"investigate" => Some(TicketRole::Investigator),
|
||||
"implement" => Some(TicketRole::Coder),
|
||||
"review" => Some(TicketRole::Reviewer),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn split_first_word(raw: &str) -> Option<(&str, &str)> {
|
||||
let trimmed = raw.trim_start();
|
||||
if trimmed.is_empty() {
|
||||
return None;
|
||||
}
|
||||
match trimmed.find(char::is_whitespace) {
|
||||
Some(idx) => {
|
||||
let (word, rest) = trimmed.split_at(idx);
|
||||
Some((word, rest.trim_start()))
|
||||
}
|
||||
None => Some((trimmed, "")),
|
||||
}
|
||||
}
|
||||
|
||||
fn non_empty_string(raw: &str) -> Option<String> {
|
||||
let trimmed = raw.trim();
|
||||
if trimmed.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(trimmed.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -723,86 +579,15 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn ticket_intake_command_returns_local_ticket_action() {
|
||||
fn ticket_command_is_unknown() {
|
||||
let registry = CommandRegistry::builtins();
|
||||
let result = registry.dispatch("ticket intake add role shortcuts", &env());
|
||||
assert!(result.method.is_none());
|
||||
assert!(result.exit_command_mode);
|
||||
assert!(result.clear_input);
|
||||
assert!(result.diagnostics[0].message.contains("ticket intake"));
|
||||
assert!(matches!(
|
||||
result.action,
|
||||
Some(CommandAction::TicketRole(TicketRoleCommand {
|
||||
role: TicketRole::Intake,
|
||||
ticket: None,
|
||||
instruction: Some(ref instruction),
|
||||
})) if instruction == "add role shortcuts"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ticket_intake_requires_context() {
|
||||
let registry = CommandRegistry::builtins();
|
||||
for command in ["ticket intake", "ticket intake "] {
|
||||
let result = registry.dispatch(command, &env());
|
||||
assert!(result.method.is_none());
|
||||
assert!(result.action.is_none());
|
||||
assert!(!result.exit_command_mode);
|
||||
assert_eq!(
|
||||
result.diagnostics[0].message,
|
||||
"Invalid arguments. Usage: ticket intake <context...>"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ticket_role_commands_map_to_fixed_roles() {
|
||||
let registry = CommandRegistry::builtins();
|
||||
for (command, role) in [
|
||||
("route", TicketRole::Orchestrator),
|
||||
("investigate", TicketRole::Investigator),
|
||||
("implement", TicketRole::Coder),
|
||||
("review", TicketRole::Reviewer),
|
||||
] {
|
||||
let result =
|
||||
registry.dispatch(&format!("ticket {command} abc-123 extra context"), &env());
|
||||
assert!(result.method.is_none());
|
||||
assert!(matches!(
|
||||
result.action,
|
||||
Some(CommandAction::TicketRole(TicketRoleCommand {
|
||||
role: actual_role,
|
||||
ticket: Some(ref ticket),
|
||||
instruction: Some(ref instruction),
|
||||
})) if actual_role == role && ticket == "abc-123" && instruction == "extra context"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ticket_non_intake_requires_ticket_reference() {
|
||||
let registry = CommandRegistry::builtins();
|
||||
let result = registry.dispatch("ticket implement", &env());
|
||||
assert!(result.method.is_none());
|
||||
assert!(result.action.is_none());
|
||||
assert!(!result.exit_command_mode);
|
||||
assert!(
|
||||
result.diagnostics[0]
|
||||
.message
|
||||
.contains("ticket implement <ticket-id-or-slug>")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ticket_unknown_action_is_local_diagnostic() {
|
||||
let registry = CommandRegistry::builtins();
|
||||
let result = registry.dispatch("ticket close abc-123", &env());
|
||||
assert!(result.method.is_none());
|
||||
assert!(result.action.is_none());
|
||||
assert!(!result.exit_command_mode);
|
||||
assert!(
|
||||
result.diagnostics[0]
|
||||
.message
|
||||
.contains("Unknown ticket action")
|
||||
.contains("Unknown command: ticket")
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ mod markdown;
|
|||
mod multi_pod;
|
||||
mod picker;
|
||||
mod pod_list;
|
||||
mod role_session_registry;
|
||||
mod scroll;
|
||||
mod single_pod;
|
||||
mod spawn;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
556
crates/tui/src/role_session_registry.rs
Normal file
556
crates/tui/src/role_session_registry.rs
Normal file
|
|
@ -0,0 +1,556 @@
|
|||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::fs::{self, OpenOptions};
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::thread;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const REGISTRY_VERSION: u32 = 1;
|
||||
const REGISTRY_FILE: &str = "role-sessions.json";
|
||||
const REGISTRY_LOCK_FILE: &str = "role-sessions.lock";
|
||||
const CLAIMS_DIR: &str = "ticket-claims";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct PanelRegistryStore {
|
||||
root: PathBuf,
|
||||
workspace_root: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub(crate) struct RoleSessionRegistry {
|
||||
pub version: u32,
|
||||
pub workspace_root: String,
|
||||
pub sessions: BTreeMap<String, RoleSessionRecord>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub(crate) struct RoleSessionRecord {
|
||||
pub role: String,
|
||||
pub pod_name: String,
|
||||
pub origin: RoleSessionOrigin,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub session_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub related_tickets: Vec<RelatedTicketRef>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub(crate) enum RoleSessionOrigin {
|
||||
PreTicketIntake,
|
||||
TicketClaim,
|
||||
RoleLaunch,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub(crate) struct RelatedTicketRef {
|
||||
pub id: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub slug: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub(crate) struct TicketClaim {
|
||||
pub ticket_id: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub ticket_slug: Option<String>,
|
||||
pub pod_name: String,
|
||||
pub role: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct PanelRegistrySnapshot {
|
||||
pub sessions: Vec<RoleSessionRecord>,
|
||||
pub claims: Vec<TicketClaim>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) enum TicketClaimResult {
|
||||
Claimed,
|
||||
AlreadyOwned(TicketClaim),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum PanelRegistryError {
|
||||
Io(io::Error),
|
||||
Json(serde_json::Error),
|
||||
TicketAlreadyClaimed(TicketClaim),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PanelRegistryError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Io(error) => write!(f, "local role session registry I/O error: {error}"),
|
||||
Self::Json(error) => write!(f, "local role session registry JSON error: {error}"),
|
||||
Self::TicketAlreadyClaimed(claim) => write!(
|
||||
f,
|
||||
"Ticket {} is already claimed locally by {} ({})",
|
||||
claim.ticket_id, claim.pod_name, claim.role
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for PanelRegistryError {}
|
||||
|
||||
impl From<io::Error> for PanelRegistryError {
|
||||
fn from(error: io::Error) -> Self {
|
||||
Self::Io(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for PanelRegistryError {
|
||||
fn from(error: serde_json::Error) -> Self {
|
||||
Self::Json(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl PanelRegistryStore {
|
||||
pub(crate) fn default_for_workspace(workspace_root: &Path) -> Result<Self, PanelRegistryError> {
|
||||
let data_dir = manifest::paths::data_dir().ok_or_else(|| {
|
||||
PanelRegistryError::Io(io::Error::other("failed to resolve yoi data directory"))
|
||||
})?;
|
||||
Ok(Self::for_data_dir(data_dir, workspace_root))
|
||||
}
|
||||
|
||||
pub(crate) fn for_data_dir(data_dir: impl AsRef<Path>, workspace_root: &Path) -> Self {
|
||||
let workspace_root = normalized_workspace_key(workspace_root);
|
||||
let leaf = workspace_leaf(&workspace_root);
|
||||
let digest = fnv1a64_hex(workspace_root.as_bytes());
|
||||
Self {
|
||||
root: data_dir
|
||||
.as_ref()
|
||||
.join("panel")
|
||||
.join("workspaces")
|
||||
.join(format!("{leaf}-{digest}")),
|
||||
workspace_root: Some(workspace_root),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_root(root: impl Into<PathBuf>) -> Self {
|
||||
Self {
|
||||
root: root.into(),
|
||||
workspace_root: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn root(&self) -> &Path {
|
||||
&self.root
|
||||
}
|
||||
|
||||
pub(crate) fn snapshot(&self) -> Result<PanelRegistrySnapshot, PanelRegistryError> {
|
||||
let registry = self.load_registry()?;
|
||||
let claims = self.load_claims()?;
|
||||
Ok(PanelRegistrySnapshot {
|
||||
sessions: registry.sessions.into_values().collect(),
|
||||
claims,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn load_registry(&self) -> Result<RoleSessionRegistry, PanelRegistryError> {
|
||||
match fs::read(self.registry_path()) {
|
||||
Ok(bytes) => Ok(serde_json::from_slice(&bytes)?),
|
||||
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(RoleSessionRegistry {
|
||||
version: REGISTRY_VERSION,
|
||||
workspace_root: self.workspace_root.clone().unwrap_or_default(),
|
||||
sessions: BTreeMap::new(),
|
||||
}),
|
||||
Err(error) => Err(error.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn record_session(
|
||||
&self,
|
||||
pod_name: impl Into<String>,
|
||||
role: impl Into<String>,
|
||||
origin: RoleSessionOrigin,
|
||||
session_id: Option<String>,
|
||||
related_tickets: impl IntoIterator<Item = RelatedTicketRef>,
|
||||
) -> Result<(), PanelRegistryError> {
|
||||
let pod_name = pod_name.into();
|
||||
let role = role.into();
|
||||
let related_tickets: Vec<RelatedTicketRef> = related_tickets.into_iter().collect();
|
||||
self.update_registry(|registry| {
|
||||
let now = now_timestamp_string();
|
||||
let mut tickets: BTreeSet<RelatedTicketRef> = registry
|
||||
.sessions
|
||||
.get(&pod_name)
|
||||
.map(|record| record.related_tickets.iter().cloned().collect())
|
||||
.unwrap_or_default();
|
||||
tickets.extend(related_tickets);
|
||||
let created_at = registry
|
||||
.sessions
|
||||
.get(&pod_name)
|
||||
.map(|record| record.created_at.clone())
|
||||
.unwrap_or_else(|| now.clone());
|
||||
registry.sessions.insert(
|
||||
pod_name.clone(),
|
||||
RoleSessionRecord {
|
||||
role,
|
||||
pod_name,
|
||||
origin,
|
||||
created_at,
|
||||
updated_at: now,
|
||||
session_id,
|
||||
related_tickets: tickets.into_iter().collect(),
|
||||
},
|
||||
);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn claim_ticket(
|
||||
&self,
|
||||
ticket_id: &str,
|
||||
ticket_slug: Option<&str>,
|
||||
pod_name: &str,
|
||||
role: &str,
|
||||
) -> Result<TicketClaimResult, PanelRegistryError> {
|
||||
fs::create_dir_all(self.claims_dir())?;
|
||||
let claim_path = self.claim_path(ticket_id);
|
||||
let claim = TicketClaim {
|
||||
ticket_id: ticket_id.to_string(),
|
||||
ticket_slug: ticket_slug.map(ToOwned::to_owned),
|
||||
pod_name: pod_name.to_string(),
|
||||
role: role.to_string(),
|
||||
};
|
||||
match self.create_claim_file(&claim_path, &claim) {
|
||||
Ok(()) => {
|
||||
if let Err(error) = self.record_session(
|
||||
pod_name.to_string(),
|
||||
role.to_string(),
|
||||
RoleSessionOrigin::TicketClaim,
|
||||
None,
|
||||
[RelatedTicketRef {
|
||||
id: ticket_id.to_string(),
|
||||
slug: ticket_slug.map(ToOwned::to_owned),
|
||||
}],
|
||||
) {
|
||||
let _ = fs::remove_file(&claim_path);
|
||||
return Err(error);
|
||||
}
|
||||
Ok(TicketClaimResult::Claimed)
|
||||
}
|
||||
Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {
|
||||
let existing = self.load_claim(ticket_id)?;
|
||||
if existing.pod_name == pod_name && existing.role == role {
|
||||
Ok(TicketClaimResult::AlreadyOwned(existing))
|
||||
} else {
|
||||
Err(PanelRegistryError::TicketAlreadyClaimed(existing))
|
||||
}
|
||||
}
|
||||
Err(error) => Err(error.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn load_claim(&self, ticket_id: &str) -> Result<TicketClaim, PanelRegistryError> {
|
||||
let bytes = fs::read(self.claim_path(ticket_id))?;
|
||||
Ok(serde_json::from_slice(&bytes)?)
|
||||
}
|
||||
|
||||
pub(crate) fn claim_for_ticket(
|
||||
&self,
|
||||
ticket_id: &str,
|
||||
) -> Result<Option<TicketClaim>, PanelRegistryError> {
|
||||
match self.load_claim(ticket_id) {
|
||||
Ok(claim) => Ok(Some(claim)),
|
||||
Err(PanelRegistryError::Io(error)) if error.kind() == io::ErrorKind::NotFound => {
|
||||
Ok(None)
|
||||
}
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
fn update_registry(
|
||||
&self,
|
||||
update: impl FnOnce(&mut RoleSessionRegistry) -> Result<(), PanelRegistryError>,
|
||||
) -> Result<(), PanelRegistryError> {
|
||||
fs::create_dir_all(&self.root)?;
|
||||
let _lock = self.acquire_registry_lock()?;
|
||||
let mut registry = self.load_registry()?;
|
||||
registry.version = REGISTRY_VERSION;
|
||||
if let Some(workspace_root) = self.workspace_root.as_ref() {
|
||||
registry.workspace_root = workspace_root.clone();
|
||||
}
|
||||
update(&mut registry)?;
|
||||
self.save_registry(®istry)
|
||||
}
|
||||
|
||||
fn acquire_registry_lock(&self) -> Result<RegistryLockGuard, PanelRegistryError> {
|
||||
let lock_path = self.root.join(REGISTRY_LOCK_FILE);
|
||||
for _ in 0..50 {
|
||||
match OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(&lock_path)
|
||||
{
|
||||
Ok(_) => return Ok(RegistryLockGuard { path: lock_path }),
|
||||
Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
Err(error) => return Err(error.into()),
|
||||
}
|
||||
}
|
||||
Err(PanelRegistryError::Io(io::Error::new(
|
||||
io::ErrorKind::WouldBlock,
|
||||
"timed out acquiring panel role session registry lock",
|
||||
)))
|
||||
}
|
||||
|
||||
fn save_registry(&self, registry: &RoleSessionRegistry) -> Result<(), PanelRegistryError> {
|
||||
let path = self.registry_path();
|
||||
let temp_path = path.with_extension(format!("json.{}.tmp", now_timestamp_string()));
|
||||
let bytes = serde_json::to_vec_pretty(registry)?;
|
||||
fs::write(&temp_path, [&bytes[..], b"\n"].concat())?;
|
||||
fs::rename(temp_path, path)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_claim_file(&self, claim_path: &Path, claim: &TicketClaim) -> io::Result<()> {
|
||||
let temp_path = self
|
||||
.claims_dir()
|
||||
.join(format!(".{}.tmp", now_timestamp_string()));
|
||||
let bytes = serde_json::to_vec_pretty(claim).map_err(io::Error::other)?;
|
||||
fs::write(&temp_path, [&bytes[..], b"\n"].concat())?;
|
||||
let link_result = fs::hard_link(&temp_path, claim_path);
|
||||
let remove_result = fs::remove_file(&temp_path);
|
||||
match (link_result, remove_result) {
|
||||
(Ok(()), Ok(())) | (Ok(()), Err(_)) => Ok(()),
|
||||
(Err(error), _) => Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
fn load_claims(&self) -> Result<Vec<TicketClaim>, PanelRegistryError> {
|
||||
let mut claims: Vec<TicketClaim> = Vec::new();
|
||||
match fs::read_dir(self.claims_dir()) {
|
||||
Ok(entries) => {
|
||||
for entry in entries {
|
||||
let entry = entry?;
|
||||
if entry.file_type()?.is_file()
|
||||
&& entry
|
||||
.path()
|
||||
.extension()
|
||||
.is_some_and(|extension| extension == "json")
|
||||
{
|
||||
let bytes = fs::read(entry.path())?;
|
||||
claims.push(serde_json::from_slice(&bytes)?);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) if error.kind() == io::ErrorKind::NotFound => {}
|
||||
Err(error) => return Err(error.into()),
|
||||
}
|
||||
claims.sort_by(|left, right| left.ticket_id.cmp(&right.ticket_id));
|
||||
Ok(claims)
|
||||
}
|
||||
|
||||
fn registry_path(&self) -> PathBuf {
|
||||
self.root.join(REGISTRY_FILE)
|
||||
}
|
||||
|
||||
fn claims_dir(&self) -> PathBuf {
|
||||
self.root.join(CLAIMS_DIR)
|
||||
}
|
||||
|
||||
fn claim_path(&self, ticket_id: &str) -> PathBuf {
|
||||
self.claims_dir()
|
||||
.join(format!("{}.json", encode_path_component(ticket_id)))
|
||||
}
|
||||
}
|
||||
|
||||
struct RegistryLockGuard {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl Drop for RegistryLockGuard {
|
||||
fn drop(&mut self) {
|
||||
let _ = fs::remove_file(&self.path);
|
||||
}
|
||||
}
|
||||
|
||||
impl PanelRegistrySnapshot {
|
||||
pub(crate) fn empty() -> Self {
|
||||
Self {
|
||||
sessions: Vec::new(),
|
||||
claims: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn claim_for_ticket(&self, ticket_id: &str) -> Option<&TicketClaim> {
|
||||
self.claims
|
||||
.iter()
|
||||
.find(|claim| claim.ticket_id == ticket_id)
|
||||
}
|
||||
}
|
||||
|
||||
fn normalized_workspace_key(path: &Path) -> String {
|
||||
path.to_string_lossy().replace('\\', "/")
|
||||
}
|
||||
|
||||
fn workspace_leaf(workspace_root: &str) -> String {
|
||||
let leaf = workspace_root
|
||||
.rsplit('/')
|
||||
.find(|part| !part.is_empty())
|
||||
.unwrap_or("workspace");
|
||||
let sanitized = leaf
|
||||
.chars()
|
||||
.map(|ch| {
|
||||
if ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_') {
|
||||
ch
|
||||
} else {
|
||||
'-'
|
||||
}
|
||||
})
|
||||
.collect::<String>()
|
||||
.trim_matches('-')
|
||||
.to_string();
|
||||
if sanitized.is_empty() {
|
||||
"workspace".to_string()
|
||||
} else {
|
||||
sanitized
|
||||
}
|
||||
}
|
||||
|
||||
fn fnv1a64_hex(bytes: &[u8]) -> String {
|
||||
let mut hash = 0xcbf29ce484222325u64;
|
||||
for byte in bytes {
|
||||
hash ^= u64::from(*byte);
|
||||
hash = hash.wrapping_mul(0x100000001b3);
|
||||
}
|
||||
format!("{hash:016x}")
|
||||
}
|
||||
|
||||
fn encode_path_component(value: &str) -> String {
|
||||
let mut encoded = String::with_capacity(value.len());
|
||||
for byte in value.bytes() {
|
||||
match byte {
|
||||
b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'-' | b'_' => encoded.push(byte as char),
|
||||
_ => encoded.push_str(&format!("%{byte:02X}")),
|
||||
}
|
||||
}
|
||||
encoded
|
||||
}
|
||||
|
||||
fn now_timestamp_string() -> String {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|duration| duration.as_nanos().to_string())
|
||||
.unwrap_or_else(|_| "0".to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn registry_path_is_workspace_scoped_under_data_dir() {
|
||||
let data_dir = TempDir::new().unwrap();
|
||||
let store = PanelRegistryStore::for_data_dir(data_dir.path(), Path::new("/repo/yoi"));
|
||||
let other = PanelRegistryStore::for_data_dir(data_dir.path(), Path::new("/repo/other"));
|
||||
|
||||
assert!(store.root().starts_with(data_dir.path()));
|
||||
let root = store.root().to_string_lossy();
|
||||
assert!(root.contains("panel/workspaces/yoi-"));
|
||||
assert_ne!(store.root(), other.root());
|
||||
|
||||
store
|
||||
.record_session(
|
||||
"ticket-intake-preticket",
|
||||
"intake",
|
||||
RoleSessionOrigin::PreTicketIntake,
|
||||
None,
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(store.load_registry().unwrap().workspace_root, "/repo/yoi");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claim_ticket_rejects_second_active_local_pod() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let store = PanelRegistryStore::from_root(temp.path().join("registry"));
|
||||
|
||||
assert!(matches!(
|
||||
store.claim_ticket("T-1", Some("ticket-one"), "ticket-one-intake", "intake"),
|
||||
Ok(TicketClaimResult::Claimed)
|
||||
));
|
||||
|
||||
let error = store
|
||||
.claim_ticket("T-1", Some("ticket-one"), "ticket-two-intake", "intake")
|
||||
.unwrap_err();
|
||||
assert!(matches!(error, PanelRegistryError::TicketAlreadyClaimed(_)));
|
||||
let claim = store.claim_for_ticket("T-1").unwrap().unwrap();
|
||||
assert_eq!(claim.pod_name, "ticket-one-intake");
|
||||
assert_eq!(claim.ticket_slug.as_deref(), Some("ticket-one"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intake_session_relation_is_not_one_to_one_with_tickets() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let store = PanelRegistryStore::from_root(temp.path().join("registry"));
|
||||
|
||||
store
|
||||
.record_session(
|
||||
"ticket-intake-preticket",
|
||||
"intake",
|
||||
RoleSessionOrigin::PreTicketIntake,
|
||||
None,
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
store
|
||||
.record_session(
|
||||
"ticket-intake-shared",
|
||||
"intake",
|
||||
RoleSessionOrigin::RoleLaunch,
|
||||
None,
|
||||
[
|
||||
RelatedTicketRef {
|
||||
id: "T-1".to_string(),
|
||||
slug: Some("one".to_string()),
|
||||
},
|
||||
RelatedTicketRef {
|
||||
id: "T-2".to_string(),
|
||||
slug: Some("two".to_string()),
|
||||
},
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let snapshot = store.snapshot().unwrap();
|
||||
let preticket = snapshot
|
||||
.sessions
|
||||
.iter()
|
||||
.find(|session| session.pod_name == "ticket-intake-preticket")
|
||||
.unwrap();
|
||||
let shared = snapshot
|
||||
.sessions
|
||||
.iter()
|
||||
.find(|session| session.pod_name == "ticket-intake-shared")
|
||||
.unwrap();
|
||||
|
||||
assert!(preticket.related_tickets.is_empty());
|
||||
assert_eq!(shared.role, "intake");
|
||||
assert_eq!(shared.origin, RoleSessionOrigin::RoleLaunch);
|
||||
assert!(!shared.created_at.is_empty());
|
||||
assert!(!shared.updated_at.is_empty());
|
||||
assert_eq!(
|
||||
shared.related_tickets,
|
||||
vec![
|
||||
RelatedTicketRef {
|
||||
id: "T-1".to_string(),
|
||||
slug: Some("one".to_string()),
|
||||
},
|
||||
RelatedTicketRef {
|
||||
id: "T-2".to_string(),
|
||||
slug: Some("two".to_string()),
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -20,14 +20,9 @@ use ratatui::backend::CrosstermBackend;
|
|||
use session_store::SegmentId;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use client::ticket_role::TicketRef;
|
||||
use client::{
|
||||
PodClient, PodRuntimeCommand, TicketRoleLaunchContext, TicketRoleLaunchError,
|
||||
launch_ticket_role_pod,
|
||||
};
|
||||
use client::{PodClient, PodRuntimeCommand};
|
||||
|
||||
use crate::app::{ActionbarNoticeLevel, ActionbarNoticeSource, App};
|
||||
use crate::command::{CommandAction, TicketRoleCommand};
|
||||
use crate::picker::PickerOutcome;
|
||||
use crate::spawn::{SpawnOutcome, SpawnReady};
|
||||
use crate::{multi_pod, picker, spawn, ui};
|
||||
|
|
@ -304,7 +299,6 @@ type TerminalEventResult = io::Result<TermEvent>;
|
|||
const TERMINAL_POLL_INTERVAL: Duration = Duration::from_millis(50);
|
||||
const TERMINAL_EVENT_DRAIN_LIMIT: usize = 64;
|
||||
const POD_EVENT_DRAIN_LIMIT: usize = 32;
|
||||
const TICKET_ROLE_NOTICE_DURATION: Duration = Duration::from_secs(5);
|
||||
|
||||
struct TerminalEventReader {
|
||||
stop: Arc<AtomicBool>,
|
||||
|
|
@ -487,14 +481,12 @@ async fn handle_terminal_event(
|
|||
app: &mut App,
|
||||
client: &mut PodClient,
|
||||
event: TermEvent,
|
||||
runtime_command: &PodRuntimeCommand,
|
||||
_runtime_command: &PodRuntimeCommand,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
match event {
|
||||
TermEvent::Key(key) => {
|
||||
if let Some(method) = handle_key(app, key) {
|
||||
client.send(&method).await?;
|
||||
} else if let Some(action) = app.take_pending_command_action() {
|
||||
handle_command_action(app, action, runtime_command).await;
|
||||
}
|
||||
}
|
||||
TermEvent::Mouse(mouse) => {
|
||||
|
|
@ -543,96 +535,6 @@ fn handle_mouse(app: &mut App, mouse: MouseEvent) {
|
|||
}
|
||||
}
|
||||
|
||||
async fn handle_command_action(
|
||||
app: &mut App,
|
||||
action: CommandAction,
|
||||
runtime_command: &PodRuntimeCommand,
|
||||
) {
|
||||
match action {
|
||||
CommandAction::TicketRole(command) => {
|
||||
handle_ticket_role_command(app, command, runtime_command).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_ticket_role_command(
|
||||
app: &mut App,
|
||||
command: TicketRoleCommand,
|
||||
runtime_command: &PodRuntimeCommand,
|
||||
) {
|
||||
let role_label = command.role.as_str();
|
||||
app.flash_actionbar_notice(
|
||||
format!("Launching ticket {role_label} Pod..."),
|
||||
ActionbarNoticeLevel::Info,
|
||||
ActionbarNoticeSource::Tui,
|
||||
TICKET_ROLE_NOTICE_DURATION,
|
||||
);
|
||||
|
||||
let workspace_root = match std::env::current_dir() {
|
||||
Ok(path) => path,
|
||||
Err(err) => {
|
||||
app.flash_actionbar_notice(
|
||||
format!("Ticket role launch failed: could not resolve current directory: {err}"),
|
||||
ActionbarNoticeLevel::Error,
|
||||
ActionbarNoticeSource::Tui,
|
||||
TICKET_ROLE_NOTICE_DURATION,
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let context = ticket_role_launch_context(workspace_root, command);
|
||||
let mut progress = Vec::new();
|
||||
match launch_ticket_role_pod(context, runtime_command.clone(), |message| {
|
||||
progress.push(message.to_owned());
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(result) => {
|
||||
let profile = result.plan.profile;
|
||||
app.flash_actionbar_notice(
|
||||
format!(
|
||||
"Launched ticket {role_label} Pod `{}` with profile `{profile}`",
|
||||
result.ready.pod_name
|
||||
),
|
||||
ActionbarNoticeLevel::Info,
|
||||
ActionbarNoticeSource::Tui,
|
||||
TICKET_ROLE_NOTICE_DURATION,
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
app.flash_actionbar_notice(
|
||||
format_ticket_role_launch_error(&err),
|
||||
ActionbarNoticeLevel::Error,
|
||||
ActionbarNoticeSource::Tui,
|
||||
TICKET_ROLE_NOTICE_DURATION,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ticket_role_launch_context(
|
||||
workspace_root: std::path::PathBuf,
|
||||
command: TicketRoleCommand,
|
||||
) -> TicketRoleLaunchContext {
|
||||
let mut context = TicketRoleLaunchContext::new(workspace_root, command.role);
|
||||
context.ticket = command.ticket.map(TicketRef::slug);
|
||||
context.user_instruction = command.instruction;
|
||||
context
|
||||
}
|
||||
|
||||
fn format_ticket_role_launch_error(error: &TicketRoleLaunchError) -> String {
|
||||
match error {
|
||||
TicketRoleLaunchError::UnsupportedInheritProfile => concat!(
|
||||
"Ticket role launch failed: role profile is `inherit`. ",
|
||||
"Top-level TUI ticket launches require concrete role profiles in ",
|
||||
".yoi/ticket.config.toml until an inheritance-aware launch path exists."
|
||||
)
|
||||
.to_owned(),
|
||||
_ => format!("Ticket role launch failed: {error}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_key(app: &mut App, key: KeyEvent) -> Option<Method> {
|
||||
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
|
||||
let shift = key.modifiers.contains(KeyModifiers::SHIFT);
|
||||
|
|
@ -1926,40 +1828,6 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ticket_role_launch_context_uses_slug_reference_and_instruction() {
|
||||
let context = ticket_role_launch_context(
|
||||
PathBuf::from("/tmp/workspace"),
|
||||
TicketRoleCommand {
|
||||
role: client::ticket_role::TicketRole::Coder,
|
||||
ticket: Some("abc-123".to_owned()),
|
||||
instruction: Some("focus parser tests".to_owned()),
|
||||
},
|
||||
);
|
||||
assert_eq!(context.role, client::ticket_role::TicketRole::Coder);
|
||||
assert_eq!(context.workspace_root, PathBuf::from("/tmp/workspace"));
|
||||
assert_eq!(
|
||||
context
|
||||
.ticket
|
||||
.as_ref()
|
||||
.and_then(|ticket| ticket.slug.as_deref()),
|
||||
Some("abc-123")
|
||||
);
|
||||
assert_eq!(
|
||||
context.user_instruction.as_deref(),
|
||||
Some("focus parser tests")
|
||||
);
|
||||
assert!(context.pod_name.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_inherit_profile_message_explains_tui_boundary() {
|
||||
let message =
|
||||
format_ticket_role_launch_error(&TicketRoleLaunchError::UnsupportedInheritProfile);
|
||||
assert!(message.contains("Top-level TUI ticket launches require concrete role profiles"));
|
||||
assert!(message.contains(".yoi/ticket.config.toml"));
|
||||
}
|
||||
|
||||
fn key(code: KeyCode) -> KeyEvent {
|
||||
KeyEvent::new(code, KeyModifiers::NONE)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ use protocol::PodStatus;
|
|||
use ticket::config::{TICKET_CONFIG_RELATIVE_PATH, TicketConfig};
|
||||
use ticket::{
|
||||
ExtensibleTicketStatus, LocalTicketBackend, TicketBackend, TicketError, TicketEvent,
|
||||
TicketEventKind, TicketFilter, TicketIdOrSlug, TicketMeta, TicketReviewResult, TicketStatus,
|
||||
TicketSummary,
|
||||
TicketFilter, TicketIdOrSlug, TicketMeta, TicketStatus, TicketSummary, TicketWorkflowState,
|
||||
};
|
||||
|
||||
use crate::pod_list::{PodList, PodListEntry, StoredMetadataState};
|
||||
use crate::role_session_registry::{PanelRegistrySnapshot, PanelRegistryStore};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct WorkspacePanelViewModel {
|
||||
|
|
@ -152,84 +152,33 @@ pub(crate) enum PanelRowKind {
|
|||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub(crate) enum ActionPriority {
|
||||
UserReply,
|
||||
ReadyForGo,
|
||||
Decision,
|
||||
ReadyForQueue,
|
||||
Blocked,
|
||||
ActiveWork,
|
||||
Background,
|
||||
}
|
||||
|
||||
impl ActionPriority {
|
||||
pub(crate) fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::UserReply => "user action",
|
||||
Self::ReadyForGo => "ready",
|
||||
Self::Decision => "decision",
|
||||
Self::Blocked => "blocked",
|
||||
Self::ActiveWork => "active",
|
||||
Self::Background => "background",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum NextUserAction {
|
||||
Clarify,
|
||||
ApproveIntake,
|
||||
Go,
|
||||
Review,
|
||||
Queue,
|
||||
Close,
|
||||
Defer,
|
||||
Edit,
|
||||
Wait,
|
||||
OpenPod,
|
||||
SendToPod,
|
||||
}
|
||||
|
||||
impl NextUserAction {
|
||||
pub(crate) fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Clarify => "Clarify",
|
||||
Self::ApproveIntake => "Approve",
|
||||
Self::Go => "Go",
|
||||
Self::Review => "Review",
|
||||
Self::Queue => "Queue",
|
||||
Self::Close => "Close",
|
||||
Self::Defer => "Defer",
|
||||
Self::Edit => "Edit",
|
||||
Self::Wait => "Wait",
|
||||
Self::OpenPod => "Open",
|
||||
Self::SendToPod => "Send",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum TicketPanelPhase {
|
||||
Intake,
|
||||
RequirementsSync,
|
||||
Preflight,
|
||||
Spike,
|
||||
Implementing,
|
||||
Reviewing,
|
||||
CloseReady,
|
||||
Blocked,
|
||||
Open,
|
||||
Pending,
|
||||
}
|
||||
|
||||
impl TicketPanelPhase {
|
||||
pub(crate) fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Intake => "intake",
|
||||
Self::RequirementsSync => "requirements",
|
||||
Self::Preflight => "preflight",
|
||||
Self::Spike => "spike",
|
||||
Self::Implementing => "implementing",
|
||||
Self::Reviewing => "review",
|
||||
Self::CloseReady => "close-ready",
|
||||
Self::Blocked => "blocked",
|
||||
Self::Open => "open",
|
||||
Self::Pending => "pending",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -243,13 +192,40 @@ pub(crate) struct TicketPanelEntry {
|
|||
pub(crate) kind: String,
|
||||
pub(crate) priority: String,
|
||||
pub(crate) labels: Vec<String>,
|
||||
pub(crate) phase: TicketPanelPhase,
|
||||
pub(crate) workflow_state: TicketWorkflowState,
|
||||
pub(crate) workflow_state_explicit: bool,
|
||||
pub(crate) attention_required: Option<String>,
|
||||
pub(crate) next_action: Option<NextUserAction>,
|
||||
pub(crate) updated_at: Option<String>,
|
||||
pub(crate) latest_event_kind: Option<String>,
|
||||
pub(crate) latest_event_excerpt: Option<String>,
|
||||
pub(crate) blocked_reason: Option<String>,
|
||||
pub(crate) related_pods: Vec<String>,
|
||||
pub(crate) local_claim: Option<TicketLocalClaimEntry>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct TicketLocalClaimEntry {
|
||||
pub(crate) pod_name: String,
|
||||
pub(crate) role: String,
|
||||
pub(crate) status: TicketLocalClaimStatus,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum TicketLocalClaimStatus {
|
||||
Live,
|
||||
Restorable,
|
||||
Stale,
|
||||
}
|
||||
|
||||
impl TicketLocalClaimStatus {
|
||||
pub(crate) fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Live => "live",
|
||||
Self::Restorable => "restorable",
|
||||
Self::Stale => "stale",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
|
|
@ -270,7 +246,6 @@ pub(crate) struct PanelRow {
|
|||
impl PanelRow {
|
||||
pub(crate) fn is_ticket_action(&self) -> bool {
|
||||
!matches!(self.kind, PanelRowKind::Pod)
|
||||
&& (self.priority != ActionPriority::Background || self.next_action.is_some())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -422,7 +397,44 @@ pub(crate) fn build_workspace_panel(
|
|||
workspace_root: &Path,
|
||||
pods: &PodList,
|
||||
) -> WorkspacePanelViewModel {
|
||||
let registry = match PanelRegistryStore::default_for_workspace(workspace_root)
|
||||
.and_then(|store| store.snapshot())
|
||||
{
|
||||
Ok(snapshot) => snapshot,
|
||||
Err(error) => {
|
||||
let mut model = WorkspacePanelViewModel::empty(workspace_root);
|
||||
model
|
||||
.header
|
||||
.diagnostics
|
||||
.push(bounded_panel_diagnostic(format!(
|
||||
"Panel local role registry unavailable: {error}"
|
||||
)));
|
||||
return build_workspace_panel_with_registry_model(
|
||||
model,
|
||||
workspace_root,
|
||||
pods,
|
||||
&PanelRegistrySnapshot::empty(),
|
||||
);
|
||||
}
|
||||
};
|
||||
build_workspace_panel_with_registry(workspace_root, pods, ®istry)
|
||||
}
|
||||
|
||||
fn build_workspace_panel_with_registry(
|
||||
workspace_root: &Path,
|
||||
pods: &PodList,
|
||||
registry: &PanelRegistrySnapshot,
|
||||
) -> WorkspacePanelViewModel {
|
||||
let model = WorkspacePanelViewModel::empty(workspace_root);
|
||||
build_workspace_panel_with_registry_model(model, workspace_root, pods, registry)
|
||||
}
|
||||
|
||||
fn build_workspace_panel_with_registry_model(
|
||||
mut model: WorkspacePanelViewModel,
|
||||
workspace_root: &Path,
|
||||
pods: &PodList,
|
||||
registry: &PanelRegistrySnapshot,
|
||||
) -> WorkspacePanelViewModel {
|
||||
match ticket_config_availability(workspace_root) {
|
||||
TicketConfigAvailability::Absent => {}
|
||||
TicketConfigAvailability::Usable => {
|
||||
|
|
@ -432,7 +444,7 @@ pub(crate) fn build_workspace_panel(
|
|||
Ok(config) => {
|
||||
model.header.ticket_root = config.backend_root().to_path_buf();
|
||||
let backend = LocalTicketBackend::new(config.backend_root().to_path_buf());
|
||||
match build_ticket_rows(&backend, pods) {
|
||||
match build_ticket_rows(&backend, pods, registry) {
|
||||
Ok(rows) => model.rows.extend(rows),
|
||||
Err(error) => {
|
||||
model
|
||||
|
|
@ -485,7 +497,8 @@ pub(crate) fn build_current_ticket_row(
|
|||
)));
|
||||
}
|
||||
let summary = ticket_summary_from_meta(&ticket.meta);
|
||||
Ok(ticket_row(summary, &ticket.events, pods))
|
||||
let registry = PanelRegistrySnapshot::empty();
|
||||
Ok(ticket_row(summary, &ticket.events, pods, ®istry))
|
||||
}
|
||||
|
||||
fn ticket_summary_from_meta(meta: &TicketMeta) -> TicketSummary {
|
||||
|
|
@ -500,6 +513,11 @@ fn ticket_summary_from_meta(meta: &TicketMeta) -> TicketSummary {
|
|||
readiness: meta.readiness.clone(),
|
||||
needs_preflight: meta.needs_preflight,
|
||||
action_required: meta.action_required.clone(),
|
||||
workflow_state: meta.workflow_state,
|
||||
workflow_state_explicit: meta.workflow_state_explicit,
|
||||
attention_required: meta.attention_required.clone(),
|
||||
queued_by: meta.queued_by.clone(),
|
||||
queued_at: meta.queued_at.clone(),
|
||||
updated_at: meta.updated_at.clone(),
|
||||
}
|
||||
}
|
||||
|
|
@ -507,6 +525,7 @@ fn ticket_summary_from_meta(meta: &TicketMeta) -> TicketSummary {
|
|||
fn build_ticket_rows(
|
||||
backend: &LocalTicketBackend,
|
||||
pods: &PodList,
|
||||
registry: &PanelRegistrySnapshot,
|
||||
) -> ticket::Result<Vec<PanelRow>> {
|
||||
let mut rows = Vec::new();
|
||||
for summary in backend.list(TicketFilter::all())? {
|
||||
|
|
@ -514,14 +533,20 @@ fn build_ticket_rows(
|
|||
continue;
|
||||
}
|
||||
let ticket = backend.show(TicketIdOrSlug::Query(summary.slug.clone()))?;
|
||||
rows.push(ticket_row(summary, &ticket.events, pods));
|
||||
rows.push(ticket_row(summary, &ticket.events, pods, registry));
|
||||
}
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
fn ticket_row(summary: TicketSummary, events: &[TicketEvent], pods: &PodList) -> PanelRow {
|
||||
let related_pods = related_pods_for_ticket(&summary, pods);
|
||||
let derived = derive_ticket_state(&summary, events);
|
||||
fn ticket_row(
|
||||
summary: TicketSummary,
|
||||
events: &[TicketEvent],
|
||||
pods: &PodList,
|
||||
registry: &PanelRegistrySnapshot,
|
||||
) -> PanelRow {
|
||||
let local_claim = local_claim_for_ticket(&summary, pods, registry);
|
||||
let related_pods = related_pods_for_ticket(&summary, pods, registry);
|
||||
let derived = derive_ticket_state(&summary);
|
||||
let latest_event = events.last();
|
||||
let entry = TicketPanelEntry {
|
||||
id: summary.id.clone(),
|
||||
|
|
@ -531,13 +556,16 @@ fn ticket_row(summary: TicketSummary, events: &[TicketEvent], pods: &PodList) ->
|
|||
kind: summary.kind.clone(),
|
||||
priority: summary.priority.clone(),
|
||||
labels: summary.labels.clone(),
|
||||
phase: derived.phase,
|
||||
workflow_state: summary.workflow_state,
|
||||
workflow_state_explicit: summary.workflow_state_explicit,
|
||||
attention_required: summary.attention_required.clone(),
|
||||
next_action: derived.action,
|
||||
updated_at: summary.updated_at.clone(),
|
||||
latest_event_kind: latest_event.map(|event| event.kind.as_str().to_string()),
|
||||
latest_event_excerpt: latest_event.and_then(|event| excerpt(event.body.as_str(), 72)),
|
||||
blocked_reason: derived.blocked_reason.clone(),
|
||||
related_pods: related_pods.clone(),
|
||||
local_claim,
|
||||
};
|
||||
let subtitle = ticket_subtitle(&entry);
|
||||
PanelRow {
|
||||
|
|
@ -545,7 +573,7 @@ fn ticket_row(summary: TicketSummary, events: &[TicketEvent], pods: &PodList) ->
|
|||
kind: derived.kind,
|
||||
title: summary.title,
|
||||
subtitle,
|
||||
status: derived.status,
|
||||
status: summary.workflow_state.as_str().to_string(),
|
||||
priority: derived.priority,
|
||||
next_action: derived.action,
|
||||
ticket: Some(entry),
|
||||
|
|
@ -558,8 +586,6 @@ fn ticket_row(summary: TicketSummary, events: &[TicketEvent], pods: &PodList) ->
|
|||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct DerivedTicketState {
|
||||
kind: PanelRowKind,
|
||||
phase: TicketPanelPhase,
|
||||
status: String,
|
||||
priority: ActionPriority,
|
||||
action: Option<NextUserAction>,
|
||||
disabled_reason: Option<String>,
|
||||
|
|
@ -567,266 +593,165 @@ struct DerivedTicketState {
|
|||
blocked_reason: Option<String>,
|
||||
}
|
||||
|
||||
fn derive_ticket_state(summary: &TicketSummary, events: &[TicketEvent]) -> DerivedTicketState {
|
||||
let action_required = summary.action_required.as_deref().map(str::trim);
|
||||
let action_required_lc = action_required.map(lowercase);
|
||||
let intake = is_intake_ticket(summary);
|
||||
let spike = is_spike_ticket(summary);
|
||||
|
||||
if let Some(reason) = action_required_lc.as_deref() {
|
||||
if reason.contains("block") || reason.contains("blocked") {
|
||||
return DerivedTicketState {
|
||||
kind: PanelRowKind::Blocked,
|
||||
phase: TicketPanelPhase::Blocked,
|
||||
status: "blocked".to_string(),
|
||||
priority: ActionPriority::Blocked,
|
||||
action: Some(NextUserAction::Edit),
|
||||
disabled_reason: Some(
|
||||
"Requires an explicit human/project decision before work continues."
|
||||
.to_string(),
|
||||
),
|
||||
key_hint: Some("Edit/decide in Ticket; no automatic unblock".to_string()),
|
||||
blocked_reason: action_required.map(ToOwned::to_owned),
|
||||
};
|
||||
}
|
||||
return DerivedTicketState {
|
||||
kind: if intake {
|
||||
PanelRowKind::Intake
|
||||
} else {
|
||||
PanelRowKind::Ticket
|
||||
},
|
||||
phase: if intake {
|
||||
TicketPanelPhase::Intake
|
||||
} else {
|
||||
TicketPanelPhase::RequirementsSync
|
||||
},
|
||||
status: action_required.unwrap_or("action required").to_string(),
|
||||
priority: ActionPriority::UserReply,
|
||||
action: Some(if intake {
|
||||
NextUserAction::ApproveIntake
|
||||
} else {
|
||||
NextUserAction::Clarify
|
||||
}),
|
||||
disabled_reason: None,
|
||||
key_hint: Some(
|
||||
"Human response is required; dispatch must re-check Ticket state".to_string(),
|
||||
),
|
||||
blocked_reason: None,
|
||||
};
|
||||
}
|
||||
|
||||
let latest_impl = latest_event_index(events, TicketEventKind::ImplementationReport);
|
||||
let latest_review = latest_event_index(events, TicketEventKind::Review);
|
||||
let latest_plan = latest_event_index(events, TicketEventKind::Plan);
|
||||
let latest_review_result = latest_review.and_then(|index| events[index].status.as_deref());
|
||||
|
||||
if latest_review_result == Some(TicketReviewResult::Approve.as_str())
|
||||
&& latest_review > latest_impl
|
||||
{
|
||||
return DerivedTicketState {
|
||||
kind: PanelRowKind::Review,
|
||||
phase: TicketPanelPhase::CloseReady,
|
||||
status: "review approved".to_string(),
|
||||
priority: ActionPriority::Decision,
|
||||
action: Some(NextUserAction::Close),
|
||||
disabled_reason: None,
|
||||
key_hint: Some("Close affordance only; closing must write a resolution".to_string()),
|
||||
blocked_reason: None,
|
||||
};
|
||||
}
|
||||
|
||||
if latest_impl.is_some() && latest_impl > latest_review {
|
||||
return DerivedTicketState {
|
||||
kind: PanelRowKind::Review,
|
||||
phase: TicketPanelPhase::Reviewing,
|
||||
status: "implementation reported".to_string(),
|
||||
priority: ActionPriority::Decision,
|
||||
action: Some(NextUserAction::Review),
|
||||
disabled_reason: None,
|
||||
key_hint: Some("Review affordance only; inspect evidence before approving".to_string()),
|
||||
blocked_reason: None,
|
||||
};
|
||||
}
|
||||
|
||||
if latest_review_result == Some(TicketReviewResult::RequestChanges.as_str()) {
|
||||
return DerivedTicketState {
|
||||
kind: PanelRowKind::ActiveWork,
|
||||
phase: TicketPanelPhase::Implementing,
|
||||
status: "changes requested".to_string(),
|
||||
priority: ActionPriority::ActiveWork,
|
||||
action: Some(NextUserAction::Wait),
|
||||
disabled_reason: Some("Waiting for implementation changes after review.".to_string()),
|
||||
key_hint: None,
|
||||
blocked_reason: None,
|
||||
};
|
||||
}
|
||||
|
||||
fn derive_ticket_state(summary: &TicketSummary) -> DerivedTicketState {
|
||||
if summary.status.as_local() == Some(TicketStatus::Pending) {
|
||||
return DerivedTicketState {
|
||||
kind: PanelRowKind::Blocked,
|
||||
phase: TicketPanelPhase::Pending,
|
||||
status: "pending/deferred".to_string(),
|
||||
priority: ActionPriority::Blocked,
|
||||
action: Some(NextUserAction::Defer),
|
||||
disabled_reason: Some(
|
||||
"Pending Ticket is shown for visibility; no automation is implied.".to_string(),
|
||||
"Pending Ticket is deferred; queueing is disabled until it is reopened and readied."
|
||||
.to_string(),
|
||||
),
|
||||
key_hint: None,
|
||||
key_hint: Some("Open/defer operation lives in Ticket controls".to_string()),
|
||||
blocked_reason: None,
|
||||
};
|
||||
}
|
||||
|
||||
if intake {
|
||||
if let Some(reason) = summary
|
||||
.attention_required
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
return DerivedTicketState {
|
||||
kind: PanelRowKind::Intake,
|
||||
phase: TicketPanelPhase::Intake,
|
||||
status: "intake draft".to_string(),
|
||||
kind: PanelRowKind::Blocked,
|
||||
priority: ActionPriority::UserReply,
|
||||
action: Some(NextUserAction::ApproveIntake),
|
||||
disabled_reason: None,
|
||||
key_hint: Some("Approve/edit intake before routing".to_string()),
|
||||
blocked_reason: None,
|
||||
action: Some(NextUserAction::Edit),
|
||||
disabled_reason: Some(
|
||||
"attention_required is set; resolve it before queueing or routing.".to_string(),
|
||||
),
|
||||
key_hint: Some(
|
||||
"Resolve attention_required in the Ticket frontmatter/thread".to_string(),
|
||||
),
|
||||
blocked_reason: Some(reason.to_string()),
|
||||
};
|
||||
}
|
||||
|
||||
if looks_ready_for_go(summary) {
|
||||
return DerivedTicketState {
|
||||
match summary.workflow_state {
|
||||
TicketWorkflowState::Ready => DerivedTicketState {
|
||||
kind: PanelRowKind::Ticket,
|
||||
phase: if summary.needs_preflight.unwrap_or(false) {
|
||||
TicketPanelPhase::Preflight
|
||||
} else {
|
||||
TicketPanelPhase::Open
|
||||
},
|
||||
status: "ready for Go".to_string(),
|
||||
priority: ActionPriority::ReadyForGo,
|
||||
action: Some(NextUserAction::Go),
|
||||
priority: ActionPriority::ReadyForQueue,
|
||||
action: Some(NextUserAction::Queue),
|
||||
disabled_reason: None,
|
||||
key_hint: Some(
|
||||
"Go is an authorization affordance; routing/preflight gates still apply"
|
||||
.to_string(),
|
||||
"Queue transitions ready -> queued and may notify Orchestrator".to_string(),
|
||||
),
|
||||
blocked_reason: None,
|
||||
};
|
||||
}
|
||||
|
||||
if spike && latest_plan.is_some() {
|
||||
return DerivedTicketState {
|
||||
},
|
||||
TicketWorkflowState::Queued => DerivedTicketState {
|
||||
kind: PanelRowKind::ActiveWork,
|
||||
phase: TicketPanelPhase::Spike,
|
||||
status: "spike running".to_string(),
|
||||
priority: ActionPriority::ActiveWork,
|
||||
action: Some(NextUserAction::Wait),
|
||||
disabled_reason: Some("Spike has a plan but no implementation report yet.".to_string()),
|
||||
disabled_reason: Some("Ticket is queued for Orchestrator routing.".to_string()),
|
||||
key_hint: None,
|
||||
blocked_reason: None,
|
||||
};
|
||||
}
|
||||
|
||||
if spike {
|
||||
return DerivedTicketState {
|
||||
kind: PanelRowKind::Ticket,
|
||||
phase: TicketPanelPhase::Spike,
|
||||
status: "spike needed".to_string(),
|
||||
priority: ActionPriority::Background,
|
||||
action: None,
|
||||
disabled_reason: Some(
|
||||
"Spike candidate is shown as background until explicitly readied or planned."
|
||||
.to_string(),
|
||||
),
|
||||
key_hint: None,
|
||||
blocked_reason: None,
|
||||
};
|
||||
}
|
||||
|
||||
if latest_plan.is_some() {
|
||||
return DerivedTicketState {
|
||||
},
|
||||
TicketWorkflowState::InProgress => DerivedTicketState {
|
||||
kind: PanelRowKind::ActiveWork,
|
||||
phase: TicketPanelPhase::Implementing,
|
||||
status: "planned/active".to_string(),
|
||||
priority: ActionPriority::ActiveWork,
|
||||
action: Some(NextUserAction::Wait),
|
||||
disabled_reason: Some(
|
||||
"Ticket has a plan but no implementation report yet.".to_string(),
|
||||
),
|
||||
disabled_reason: Some("Ticket is already in progress.".to_string()),
|
||||
key_hint: None,
|
||||
blocked_reason: None,
|
||||
};
|
||||
}
|
||||
|
||||
DerivedTicketState {
|
||||
kind: PanelRowKind::Ticket,
|
||||
phase: TicketPanelPhase::Open,
|
||||
status: "open backlog".to_string(),
|
||||
},
|
||||
TicketWorkflowState::Done => DerivedTicketState {
|
||||
kind: PanelRowKind::Review,
|
||||
priority: ActionPriority::Background,
|
||||
action: None,
|
||||
action: Some(NextUserAction::Close),
|
||||
disabled_reason: Some(
|
||||
"Open Ticket is not marked ready; keep it out of the action section for now."
|
||||
.to_string(),
|
||||
"workflow_state is done; close if a resolution is still missing.".to_string(),
|
||||
),
|
||||
key_hint: None,
|
||||
blocked_reason: None,
|
||||
},
|
||||
TicketWorkflowState::Intake => DerivedTicketState {
|
||||
kind: PanelRowKind::Intake,
|
||||
priority: ActionPriority::Background,
|
||||
action: Some(NextUserAction::Clarify),
|
||||
disabled_reason: Some(
|
||||
"Ticket is still in intake; mark it ready before queueing.".to_string(),
|
||||
),
|
||||
key_hint: Some(
|
||||
"Intake/Orchestrator helpers can set workflow_state = ready".to_string(),
|
||||
),
|
||||
blocked_reason: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn looks_ready_for_go(summary: &TicketSummary) -> bool {
|
||||
summary
|
||||
.readiness
|
||||
.as_deref()
|
||||
.map(lowercase)
|
||||
.is_some_and(|value| value.contains("ready"))
|
||||
|| summary.needs_preflight.unwrap_or(false)
|
||||
|| summary
|
||||
.labels
|
||||
.iter()
|
||||
.any(|label| lowercase(label).contains("ready"))
|
||||
}
|
||||
|
||||
fn is_intake_ticket(summary: &TicketSummary) -> bool {
|
||||
summary.kind == "intake"
|
||||
|| summary.labels.iter().any(|label| label == "intake")
|
||||
|| lowercase(&summary.slug).contains("intake")
|
||||
|| lowercase(&summary.title).contains("intake")
|
||||
}
|
||||
|
||||
fn is_spike_ticket(summary: &TicketSummary) -> bool {
|
||||
lowercase(&summary.kind).contains("spike")
|
||||
|| summary
|
||||
.labels
|
||||
.iter()
|
||||
.any(|label| lowercase(label).contains("spike"))
|
||||
|| lowercase(&summary.slug).contains("spike")
|
||||
|| lowercase(&summary.title).contains("spike")
|
||||
}
|
||||
|
||||
fn latest_event_index(events: &[TicketEvent], kind: TicketEventKind) -> Option<usize> {
|
||||
events.iter().rposition(|event| event.kind == kind)
|
||||
}
|
||||
|
||||
fn related_pods_for_ticket(summary: &TicketSummary, pods: &PodList) -> Vec<String> {
|
||||
fn related_pods_for_ticket(
|
||||
summary: &TicketSummary,
|
||||
pods: &PodList,
|
||||
registry: &PanelRegistrySnapshot,
|
||||
) -> Vec<String> {
|
||||
let slug = lowercase(&summary.slug);
|
||||
let id = lowercase(&summary.id);
|
||||
pods.entries
|
||||
.iter()
|
||||
.filter_map(|pod| {
|
||||
let mut names = Vec::new();
|
||||
if let Some(claim) = registry.claim_for_ticket(&summary.id) {
|
||||
names.push(claim.pod_name.clone());
|
||||
}
|
||||
for pod in pods.entries.iter().filter_map(|pod| {
|
||||
let name = lowercase(&pod.name);
|
||||
if (!slug.is_empty() && name.contains(&slug)) || (!id.is_empty() && name.contains(&id))
|
||||
{
|
||||
if (!slug.is_empty() && name.contains(&slug)) || (!id.is_empty() && name.contains(&id)) {
|
||||
Some(pod.name.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
if !names.iter().any(|existing| existing == &pod) {
|
||||
names.push(pod);
|
||||
}
|
||||
if names.len() >= 5 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
names
|
||||
}
|
||||
|
||||
fn local_claim_for_ticket(
|
||||
summary: &TicketSummary,
|
||||
pods: &PodList,
|
||||
registry: &PanelRegistrySnapshot,
|
||||
) -> Option<TicketLocalClaimEntry> {
|
||||
let claim = registry.claim_for_ticket(&summary.id)?;
|
||||
let status = local_claim_status_for_pod(&claim.pod_name, pods);
|
||||
Some(TicketLocalClaimEntry {
|
||||
pod_name: claim.pod_name.clone(),
|
||||
role: claim.role.clone(),
|
||||
status,
|
||||
})
|
||||
.take(5)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn local_claim_status_for_pod(pod_name: &str, pods: &PodList) -> TicketLocalClaimStatus {
|
||||
let Some(entry) = pods.entries.iter().find(|entry| entry.name == pod_name) else {
|
||||
return TicketLocalClaimStatus::Stale;
|
||||
};
|
||||
if entry.live.as_ref().is_some_and(|live| live.reachable) {
|
||||
return TicketLocalClaimStatus::Live;
|
||||
}
|
||||
if entry.actions.can_restore {
|
||||
return TicketLocalClaimStatus::Restorable;
|
||||
}
|
||||
TicketLocalClaimStatus::Stale
|
||||
}
|
||||
|
||||
fn ticket_subtitle(entry: &TicketPanelEntry) -> Option<String> {
|
||||
let mut parts = vec![format!(
|
||||
"{} · {} · {}",
|
||||
"{} · {}",
|
||||
entry.slug,
|
||||
entry.phase.label(),
|
||||
entry.priority
|
||||
entry.workflow_state.as_str()
|
||||
)];
|
||||
if let Some(reason) = entry.attention_required.as_deref() {
|
||||
parts.push(format!("attention: {reason}"));
|
||||
}
|
||||
if let Some(claim) = entry.local_claim.as_ref() {
|
||||
parts.push(format!(
|
||||
"claim: {} ({})",
|
||||
claim.pod_name,
|
||||
claim.status.label()
|
||||
));
|
||||
}
|
||||
if !entry.related_pods.is_empty() {
|
||||
parts.push(format!("pods: {}", entry.related_pods.join(", ")));
|
||||
}
|
||||
|
|
@ -842,9 +767,7 @@ fn pod_rows(pods: &PodList) -> Vec<PanelRow> {
|
|||
|
||||
fn pod_row(entry: &PodListEntry) -> PanelRow {
|
||||
let status = pod_status_label(entry).to_string();
|
||||
let next_action = if entry.actions.can_send_now {
|
||||
Some(NextUserAction::SendToPod)
|
||||
} else if entry.actions.can_open {
|
||||
let next_action = if entry.actions.can_open {
|
||||
Some(NextUserAction::OpenPod)
|
||||
} else {
|
||||
None
|
||||
|
|
@ -870,7 +793,7 @@ fn pod_row(entry: &PodListEntry) -> PanelRow {
|
|||
ticket: None,
|
||||
related_pods: Vec::new(),
|
||||
disabled_reason: entry.actions.disabled_reason.clone(),
|
||||
key_hint: Some("Pod rows preserve existing open/direct-send behavior".to_string()),
|
||||
key_hint: Some("Press o or empty Enter to open/attach this Pod".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -941,7 +864,7 @@ mod tests {
|
|||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tempfile::TempDir;
|
||||
use ticket::{MarkdownText, NewTicket, NewTicketEvent, TicketReview};
|
||||
use ticket::{NewTicket, TicketWorkflowState};
|
||||
|
||||
fn empty_pods() -> PodList {
|
||||
PodList::from_sources(
|
||||
|
|
@ -1021,16 +944,16 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_panel_prioritizes_human_actions_before_background_pods() {
|
||||
fn workspace_panel_uses_explicit_workflow_state_for_queue_priority() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
write_ticket_config(temp.path());
|
||||
let backend = LocalTicketBackend::new(temp.path().join(".yoi/tickets"));
|
||||
create_ticket(&backend, "Ready Ticket", "ready-ticket", |input| {
|
||||
input.readiness = Some("implementation-ready".to_string());
|
||||
input.workflow_state = Some(TicketWorkflowState::Ready);
|
||||
});
|
||||
create_ticket(&backend, "Needs User", "needs-user", |input| {
|
||||
input.action_required = Some("answer clarification".to_string());
|
||||
input.labels = vec!["intake".to_string()];
|
||||
input.workflow_state = Some(TicketWorkflowState::Ready);
|
||||
input.attention_required = Some("answer clarification".to_string());
|
||||
});
|
||||
|
||||
let model = build_workspace_panel(temp.path(), &empty_pods());
|
||||
|
|
@ -1041,128 +964,135 @@ mod tests {
|
|||
let rows = model
|
||||
.rows
|
||||
.iter()
|
||||
.map(|row| (row.title.as_str(), row.priority, row.next_action))
|
||||
.map(|row| {
|
||||
(
|
||||
row.title.as_str(),
|
||||
row.status.as_str(),
|
||||
row.priority,
|
||||
row.next_action,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(rows[0].0, "Needs User");
|
||||
assert_eq!(rows[0].1, ActionPriority::UserReply);
|
||||
assert_eq!(rows[0].2, Some(NextUserAction::ApproveIntake));
|
||||
assert_eq!(rows[0].1, "ready");
|
||||
assert_eq!(rows[0].2, ActionPriority::UserReply);
|
||||
assert_eq!(rows[0].3, Some(NextUserAction::Edit));
|
||||
assert_eq!(rows[1].0, "Ready Ticket");
|
||||
assert_eq!(rows[1].1, ActionPriority::ReadyForGo);
|
||||
assert_eq!(rows[1].2, Some(NextUserAction::Go));
|
||||
assert_eq!(rows[1].1, "ready");
|
||||
assert_eq!(rows[1].2, ActionPriority::ReadyForQueue);
|
||||
assert_eq!(rows[1].3, Some(NextUserAction::Queue));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_panel_derives_spike_phase_without_marking_unready_spikes_ready_for_go() {
|
||||
fn workspace_panel_does_not_infer_workflow_state_from_labels_readiness_or_thread() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
write_ticket_config(temp.path());
|
||||
let backend = LocalTicketBackend::new(temp.path().join(".yoi/tickets"));
|
||||
create_ticket(
|
||||
&backend,
|
||||
"Investigate Spike",
|
||||
"investigate-spike",
|
||||
"Readiness Heuristic",
|
||||
"readiness-heuristic",
|
||||
|input| {
|
||||
input.labels = vec!["spike".to_string()];
|
||||
input.readiness = Some("implementation-ready".to_string());
|
||||
input.needs_preflight = Some(false);
|
||||
},
|
||||
);
|
||||
create_ticket(&backend, "Running Spike", "running-spike", |input| {
|
||||
input.kind = "spike".to_string();
|
||||
create_ticket(&backend, "Label Heuristic", "label-heuristic", |input| {
|
||||
input.labels = vec!["spike".to_string(), "intake".to_string()];
|
||||
});
|
||||
create_ticket(&backend, "Queued Explicit", "queued-explicit", |input| {
|
||||
input.workflow_state = Some(TicketWorkflowState::Queued);
|
||||
});
|
||||
backend
|
||||
.add_event(
|
||||
TicketIdOrSlug::Query("running-spike".to_string()),
|
||||
NewTicketEvent::new(TicketEventKind::Plan, "Run the spike."),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let model = build_workspace_panel(temp.path(), &empty_pods());
|
||||
let needed = model
|
||||
let readiness = model
|
||||
.rows
|
||||
.iter()
|
||||
.find(|row| row.title == "Investigate Spike")
|
||||
.find(|row| row.title == "Readiness Heuristic")
|
||||
.unwrap();
|
||||
let running = model
|
||||
let label = model
|
||||
.rows
|
||||
.iter()
|
||||
.find(|row| row.title == "Running Spike")
|
||||
.find(|row| row.title == "Label Heuristic")
|
||||
.unwrap();
|
||||
let queued = model
|
||||
.rows
|
||||
.iter()
|
||||
.find(|row| row.title == "Queued Explicit")
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
needed.ticket.as_ref().unwrap().phase,
|
||||
TicketPanelPhase::Spike
|
||||
);
|
||||
assert_eq!(needed.priority, ActionPriority::Background);
|
||||
assert_eq!(needed.next_action, None);
|
||||
assert!(!needed.is_ticket_action());
|
||||
assert_eq!(
|
||||
running.ticket.as_ref().unwrap().phase,
|
||||
TicketPanelPhase::Spike
|
||||
);
|
||||
assert_eq!(running.priority, ActionPriority::ActiveWork);
|
||||
assert_eq!(running.next_action, Some(NextUserAction::Wait));
|
||||
assert_eq!(readiness.status, "intake");
|
||||
assert_eq!(readiness.next_action, Some(NextUserAction::Clarify));
|
||||
assert_eq!(label.status, "intake");
|
||||
assert_eq!(label.next_action, Some(NextUserAction::Clarify));
|
||||
assert_eq!(queued.status, "queued");
|
||||
assert_eq!(queued.next_action, Some(NextUserAction::Wait));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_panel_keeps_ordinary_open_backlog_out_of_action_section() {
|
||||
fn workspace_panel_defaults_missing_open_state_to_intake_and_displays_done_state() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
write_ticket_config(temp.path());
|
||||
let backend = LocalTicketBackend::new(temp.path().join(".yoi/tickets"));
|
||||
create_ticket(&backend, "Plain Backlog", "plain-backlog", |_| {});
|
||||
create_ticket(&backend, "Done Explicit", "done-explicit", |input| {
|
||||
input.workflow_state = Some(TicketWorkflowState::Done);
|
||||
});
|
||||
|
||||
let model = build_workspace_panel(temp.path(), &empty_pods());
|
||||
let row = model
|
||||
let backlog = model
|
||||
.rows
|
||||
.iter()
|
||||
.find(|row| row.title == "Plain Backlog")
|
||||
.unwrap();
|
||||
let done = model
|
||||
.rows
|
||||
.iter()
|
||||
.find(|row| row.title == "Done Explicit")
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(row.priority, ActionPriority::Background);
|
||||
assert_eq!(row.next_action, None);
|
||||
assert!(!row.is_ticket_action());
|
||||
assert_eq!(backlog.status, "intake");
|
||||
assert_eq!(backlog.next_action, Some(NextUserAction::Clarify));
|
||||
assert!(backlog.is_ticket_action());
|
||||
assert_eq!(done.status, "done");
|
||||
assert_eq!(done.next_action, Some(NextUserAction::Close));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_panel_derives_review_and_close_actions_from_thread_roles() {
|
||||
fn workspace_panel_displays_local_ticket_claim_status() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
write_ticket_config(temp.path());
|
||||
let backend = LocalTicketBackend::new(temp.path().join(".yoi/tickets"));
|
||||
create_ticket(&backend, "Needs Review", "needs-review", |_| {});
|
||||
create_ticket(&backend, "Close Ready", "close-ready", |_| {});
|
||||
backend
|
||||
.add_event(
|
||||
TicketIdOrSlug::Query("needs-review".to_string()),
|
||||
NewTicketEvent::new(TicketEventKind::ImplementationReport, "Implemented."),
|
||||
)
|
||||
.unwrap();
|
||||
backend
|
||||
.add_event(
|
||||
TicketIdOrSlug::Query("close-ready".to_string()),
|
||||
NewTicketEvent::new(TicketEventKind::ImplementationReport, "Implemented."),
|
||||
)
|
||||
.unwrap();
|
||||
backend
|
||||
.review(
|
||||
TicketIdOrSlug::Query("close-ready".to_string()),
|
||||
TicketReview::approve(MarkdownText::new("Approved.")),
|
||||
)
|
||||
create_ticket(&backend, "Claimed Intake", "claimed-intake", |_| {});
|
||||
let summary = backend.list(TicketFilter::all()).unwrap().remove(0);
|
||||
let store = PanelRegistryStore::from_root(temp.path().join("local-registry"));
|
||||
store
|
||||
.claim_ticket(&summary.id, None, "ticket-claimed-intake", "intake")
|
||||
.unwrap();
|
||||
let registry = store.snapshot().unwrap();
|
||||
|
||||
let model = build_workspace_panel(temp.path(), &empty_pods());
|
||||
let review = model
|
||||
let model = build_workspace_panel_with_registry(
|
||||
temp.path(),
|
||||
&live_pods(&["ticket-claimed-intake"]),
|
||||
®istry,
|
||||
);
|
||||
let row = model
|
||||
.rows
|
||||
.iter()
|
||||
.find(|row| row.title == "Needs Review")
|
||||
.unwrap();
|
||||
let close = model
|
||||
.rows
|
||||
.iter()
|
||||
.find(|row| row.title == "Close Ready")
|
||||
.find(|row| row.title == "Claimed Intake")
|
||||
.unwrap();
|
||||
let claim = row.ticket.as_ref().unwrap().local_claim.as_ref().unwrap();
|
||||
|
||||
assert_eq!(review.priority, ActionPriority::Decision);
|
||||
assert_eq!(review.next_action, Some(NextUserAction::Review));
|
||||
assert_eq!(close.priority, ActionPriority::Decision);
|
||||
assert_eq!(close.next_action, Some(NextUserAction::Close));
|
||||
assert_eq!(claim.pod_name, "ticket-claimed-intake");
|
||||
assert_eq!(claim.status, TicketLocalClaimStatus::Live);
|
||||
assert_eq!(row.related_pods, vec!["ticket-claimed-intake"]);
|
||||
assert!(
|
||||
row.subtitle
|
||||
.as_deref()
|
||||
.unwrap()
|
||||
.contains("claim: ticket-claimed-intake (live)")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
use std::fmt;
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use ticket::config::TicketConfig;
|
||||
use ticket::config::{
|
||||
DEFAULT_TICKET_BACKEND_RELATIVE_PATH, TICKET_CONFIG_RELATIVE_PATH, TicketConfig,
|
||||
ticket_config_scaffold,
|
||||
};
|
||||
use ticket::{
|
||||
LocalTicketBackend, MarkdownText, NewTicket, NewTicketEvent, TicketBackend,
|
||||
TicketDoctorSeverity, TicketEventKind, TicketFilter, TicketIdOrSlug, TicketReview,
|
||||
|
|
@ -17,6 +21,7 @@ pub enum TicketCli {
|
|||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum TicketCommand {
|
||||
Init,
|
||||
Create(CreateOptions),
|
||||
List(ListOptions),
|
||||
Show { query: String },
|
||||
|
|
@ -129,12 +134,24 @@ impl From<ticket::config::TicketConfigError> for TicketCliError {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for TicketCliError {
|
||||
fn from(error: std::io::Error) -> Self {
|
||||
Self::new(error.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_ticket_args(args: &[String]) -> Result<TicketCli, TicketCliError> {
|
||||
if args.is_empty() || args.iter().any(|arg| arg == "--help" || arg == "-h") {
|
||||
return Ok(TicketCli::Help);
|
||||
}
|
||||
|
||||
let command = match args[0].as_str() {
|
||||
"init" => {
|
||||
if args.len() != 1 {
|
||||
return Err(TicketCliError::new("ticket init takes no arguments"));
|
||||
}
|
||||
TicketCommand::Init
|
||||
}
|
||||
"create" => TicketCommand::Create(parse_create(&args[1..])?),
|
||||
"list" => TicketCommand::List(parse_list(&args[1..])?),
|
||||
"show" => TicketCommand::Show {
|
||||
|
|
@ -185,6 +202,9 @@ fn run_command(
|
|||
command: TicketCommand,
|
||||
workspace: &Path,
|
||||
) -> Result<TicketCliOutput, TicketCliError> {
|
||||
match command {
|
||||
TicketCommand::Init => init(workspace),
|
||||
command => {
|
||||
let backend = backend_for_workspace(workspace)?;
|
||||
match command {
|
||||
TicketCommand::Create(options) => create(&backend, options),
|
||||
|
|
@ -195,7 +215,46 @@ fn run_command(
|
|||
TicketCommand::Status(options) => status(&backend, options),
|
||||
TicketCommand::Close(options) => close(&backend, options),
|
||||
TicketCommand::Doctor => doctor(&backend),
|
||||
TicketCommand::Init => unreachable!("init handled before backend setup"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn init(workspace: &Path) -> Result<TicketCliOutput, TicketCliError> {
|
||||
let config_path = workspace.join(TICKET_CONFIG_RELATIVE_PATH);
|
||||
if config_path.exists() {
|
||||
return Err(TicketCliError::new(format!(
|
||||
"ticket config already exists at {}; refusing to overwrite. Edit it manually or remove it before running `yoi ticket init`.",
|
||||
config_path.display()
|
||||
)));
|
||||
}
|
||||
|
||||
let yoi_dir = workspace.join(".yoi");
|
||||
fs::create_dir_all(&yoi_dir)?;
|
||||
let tickets_dir = workspace.join(DEFAULT_TICKET_BACKEND_RELATIVE_PATH);
|
||||
fs::create_dir_all(&tickets_dir)?;
|
||||
|
||||
let mut file = fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(&config_path)
|
||||
.map_err(|error| {
|
||||
if error.kind() == std::io::ErrorKind::AlreadyExists {
|
||||
TicketCliError::new(format!(
|
||||
"ticket config already exists at {}; refusing to overwrite. Edit it manually or remove it before running `yoi ticket init`.",
|
||||
config_path.display()
|
||||
))
|
||||
} else {
|
||||
TicketCliError::from(error)
|
||||
}
|
||||
})?;
|
||||
file.write_all(ticket_config_scaffold().as_bytes())?;
|
||||
|
||||
Ok(success(format!(
|
||||
"created\t{}\nensured\t{}\n",
|
||||
TICKET_CONFIG_RELATIVE_PATH, DEFAULT_TICKET_BACKEND_RELATIVE_PATH
|
||||
)))
|
||||
}
|
||||
|
||||
fn backend_for_workspace(workspace: &Path) -> Result<LocalTicketBackend, TicketCliError> {
|
||||
|
|
@ -745,7 +804,7 @@ fn default_author() -> String {
|
|||
}
|
||||
|
||||
fn help_text() -> &'static str {
|
||||
"yoi ticket\n\nUsage:\n yoi ticket create --title <title> [--slug <slug>] [--kind <kind>] [--priority P2] [--label a,b]\n yoi ticket list [--status open|pending|closed|all]\n yoi ticket show <id-or-slug>\n yoi ticket comment <id-or-slug> [--role comment|plan|decision|implementation_report] (--file <path>|--message <text>)\n yoi ticket review <id-or-slug> (--approve|--request-changes) (--file <path>|--message <text>)\n yoi ticket status <id-or-slug> <open|pending|closed>\n yoi ticket close <id-or-slug> (--resolution <text>|--file <path>)\n yoi ticket doctor\n\nOptions:\n -h, --help Print help\n\nBackend:\n Uses the workspace Ticket config at .yoi/ticket.config.toml when present.\n Supported provider: builtin:yoi_local.\n Without config, the local backend root is <cwd>/.yoi/tickets.\n"
|
||||
"yoi ticket\n\nUsage:\n yoi ticket init\n yoi ticket create --title <title> [--slug <slug>] [--kind <kind>] [--priority P2] [--label a,b]\n yoi ticket list [--status open|pending|closed|all]\n yoi ticket show <id-or-slug>\n yoi ticket comment <id-or-slug> [--role comment|plan|decision|implementation_report] (--file <path>|--message <text>)\n yoi ticket review <id-or-slug> (--approve|--request-changes) (--file <path>|--message <text>)\n yoi ticket status <id-or-slug> <open|pending|closed>\n yoi ticket close <id-or-slug> (--resolution <text>|--file <path>)\n yoi ticket doctor\n\nOptions:\n -h, --help Print help\n\nBackend:\n `yoi ticket init` writes .yoi/ticket.config.toml with explicit fixed role profiles.\n Uses the workspace Ticket config at .yoi/ticket.config.toml when present.\n Supported provider: builtin:yoi_local.\n Without config, the local backend root is <cwd>/.yoi/tickets.\n"
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -753,6 +812,7 @@ mod tests {
|
|||
use super::*;
|
||||
use tempfile::TempDir;
|
||||
use ticket::TicketEventKind;
|
||||
use ticket::config::TicketRole;
|
||||
|
||||
fn args(items: &[&str]) -> Vec<String> {
|
||||
items.iter().map(|item| item.to_string()).collect()
|
||||
|
|
@ -763,6 +823,54 @@ mod tests {
|
|||
run_in_workspace(cli, temp.path()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ticket_cli_init_writes_explicit_ticket_config_scaffold() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
|
||||
let initialized = run(&temp, &["init"]);
|
||||
assert_eq!(initialized.status, TicketCliStatus::Success);
|
||||
assert!(
|
||||
initialized
|
||||
.stdout
|
||||
.contains("created\t.yoi/ticket.config.toml")
|
||||
);
|
||||
assert!(initialized.stdout.contains("ensured\t.yoi/tickets"));
|
||||
assert!(temp.path().join(".yoi/tickets").exists());
|
||||
|
||||
let config = fs::read_to_string(temp.path().join(".yoi/ticket.config.toml")).unwrap();
|
||||
assert!(config.contains("[backend]\n"));
|
||||
assert!(config.contains("provider = \"builtin:yoi_local\""));
|
||||
assert!(config.contains("root = \".yoi/tickets\""));
|
||||
for role in TicketRole::ALL {
|
||||
assert!(config.contains(&format!(
|
||||
"[roles.{role}]\nprofile = \"builtin:default\"\nworkflow = \"{}\"",
|
||||
role.default_workflow()
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ticket_cli_init_does_not_overwrite_existing_config() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
fs::create_dir_all(temp.path().join(".yoi")).unwrap();
|
||||
let config_path = temp.path().join(".yoi/ticket.config.toml");
|
||||
fs::write(
|
||||
&config_path,
|
||||
"[backend]\nprovider = \"builtin:yoi_local\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let cli = parse_ticket_args(&args(&["init"])).unwrap();
|
||||
let err = run_in_workspace(cli, temp.path()).unwrap_err();
|
||||
assert!(err.to_string().contains("already exists"));
|
||||
assert!(err.to_string().contains("refusing to overwrite"));
|
||||
assert!(err.to_string().contains("yoi ticket init"));
|
||||
assert_eq!(
|
||||
fs::read_to_string(config_path).unwrap(),
|
||||
"[backend]\nprovider = \"builtin:yoi_local\"\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ticket_cli_create_list_show_comment_review_status_close_and_doctor() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
|
|
@ -942,6 +1050,7 @@ mod tests {
|
|||
fn ticket_cli_help_lists_required_commands() {
|
||||
let help = parse_ticket_args(&args(&["--help"])).unwrap();
|
||||
let output = run_in_workspace(help, Path::new(".")).unwrap();
|
||||
assert!(output.stdout.contains("yoi ticket init"));
|
||||
assert!(output.stdout.contains("yoi ticket create"));
|
||||
assert!(output.stdout.contains("yoi ticket doctor"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Tickets and development workflow
|
||||
|
||||
Yoi project work is tracked through Tickets. For normal use, interact with Tickets through the TUI role commands, Ticket tools, and Ticket workflows. Git history plus Ticket files remain the authoritative state-transition record behind those interfaces.
|
||||
Yoi project work is tracked through Tickets. For normal use, interact with Tickets through `yoi panel`, Ticket tools, the `yoi ticket ...` CLI, and Ticket workflows. Git history plus Ticket files remain the authoritative state-transition record behind those interfaces.
|
||||
|
||||
The current local backend stores Ticket files under `.yoi/tickets/`. That storage detail matters for maintainers and backend compatibility, but it is not the primary user-facing workflow.
|
||||
|
||||
|
|
@ -20,11 +20,11 @@ A Ticket may represent a feature, bug, cleanup, design decision, investigation,
|
|||
|
||||
Use the highest-level interface that matches the work:
|
||||
|
||||
- In the TUI, use `:ticket ...` commands to launch fixed Ticket-role Pods.
|
||||
- Use `yoi panel` for the Ticket/Intake/Orchestrator workspace UI and role-launch actions.
|
||||
- Inside Pods, use typed Ticket tools to create, inspect, comment, review, and close Tickets.
|
||||
- For multi-step work, follow the Ticket Intake, Orchestrator Routing, Preflight, and Multi-agent workflows.
|
||||
|
||||
Maintainers can inspect the local `.yoi/tickets/` files directly when debugging storage, but normal user instructions should go through TUI role actions, Ticket tools, or `yoi ticket ...`.
|
||||
Maintainers can inspect the local `.yoi/tickets/` files directly when debugging storage, but normal user instructions should go through `yoi panel`, Ticket tools, or `yoi ticket ...`.
|
||||
|
||||
## Ticket tools inside Pods
|
||||
|
||||
|
|
@ -118,7 +118,7 @@ If `.yoi/ticket.config.toml` is missing, defaults are:
|
|||
- reviewer: `multi-agent-workflow`
|
||||
- investigator: `ticket-orchestrator-routing`
|
||||
|
||||
Important: top-level TUI Ticket role launches cannot execute `profile = "inherit"` because top-level launch has no parent Profile to inherit from. Configure concrete role profiles in `.yoi/ticket.config.toml` before using TUI role-launch commands.
|
||||
Important: top-level Ticket role launches cannot execute `profile = "inherit"` because top-level launch has no parent Profile to inherit from. Configure concrete role profiles in `.yoi/ticket.config.toml` before using `yoi panel` role-launch actions.
|
||||
|
||||
## Workflow lifecycle
|
||||
|
||||
|
|
@ -219,61 +219,50 @@ Before closing, verify concrete evidence:
|
|||
|
||||
Close with a resolution that summarizes what changed, key commits, validation, review status, and remaining follow-ups.
|
||||
|
||||
## TUI Ticket role actions
|
||||
## Workspace panel Ticket role actions
|
||||
|
||||
TUI exposes explicit commands for fixed Ticket roles:
|
||||
`yoi panel` is the active Ticket/Intake/Orchestrator UI. It owns fixed Ticket role-launch actions and uses the shared client Ticket role launcher. The single-Pod TUI no longer supports `:ticket ...` commands; typing them in command mode is treated like any other unknown command.
|
||||
|
||||
```text
|
||||
:ticket intake <context...>
|
||||
:ticket route <ticket-id-or-slug> [instruction...]
|
||||
:ticket investigate <ticket-id-or-slug> [instruction...]
|
||||
:ticket implement <ticket-id-or-slug> [instruction...]
|
||||
:ticket review <ticket-id-or-slug> [instruction...]
|
||||
```
|
||||
Role actions map to the same fixed roles configured in `.yoi/ticket.config.toml`:
|
||||
|
||||
These commands call the shared client Ticket role launcher. TUI does not construct `SpawnConfig`, Profile semantics, workflow segments, or prompt content directly.
|
||||
|
||||
Command mapping:
|
||||
|
||||
- `intake` launches the intake role without an existing Ticket and requires freeform context.
|
||||
- `route` launches the orchestrator role for an existing Ticket.
|
||||
- `investigate` launches the investigator role for a read-only spike/investigation.
|
||||
- `implement` launches the coder role for an implementation assignment.
|
||||
- `review` launches the reviewer role for review.
|
||||
- intake launches the intake role without an existing Ticket and requires freeform context.
|
||||
- route launches the orchestrator role for an existing Ticket.
|
||||
- investigate launches the investigator role for a read-only spike/investigation.
|
||||
- implement launches the coder role for an implementation assignment.
|
||||
- review launches the reviewer role for review.
|
||||
|
||||
All actions are explicit and user-triggered. They are not a scheduler, queue, spawned-Pod panel, or automatic maintainer loop.
|
||||
|
||||
### TUI execution path
|
||||
### Panel execution path
|
||||
|
||||
The TUI path is:
|
||||
The role-launch path is:
|
||||
|
||||
```text
|
||||
User types :ticket ... in the TUI
|
||||
-> TUI parses the command into a fixed Ticket role action
|
||||
-> TUI builds a TicketRoleLaunchContext
|
||||
User triggers a Ticket action in yoi panel
|
||||
-> panel builds a TicketRoleLaunchContext
|
||||
-> client Ticket role launcher reads .yoi/ticket.config.toml
|
||||
-> launcher selects the role Profile and workflow
|
||||
-> launcher spawns the role Pod
|
||||
-> launcher sends Method::Run with WorkflowInvoke + Text segments
|
||||
-> launcher waits for run-acceptance evidence
|
||||
-> TUI shows success/failure in the actionbar
|
||||
-> panel reports success/failure
|
||||
```
|
||||
|
||||
The launched Pod receives dynamic Ticket/action context as its first committed run input. The TUI does not inject hidden context, does not write Ticket files directly, and does not construct prompt/workflow segments by hand.
|
||||
The launched Pod receives dynamic Ticket/action context as its first committed run input. The panel does not inject hidden context, does not write Ticket files directly, and does not construct prompt/workflow segments by hand.
|
||||
|
||||
The first run input contains:
|
||||
|
||||
- the selected fixed role;
|
||||
- the workflow slug from `.yoi/ticket.config.toml`;
|
||||
- Ticket id/slug when the command targets an existing Ticket;
|
||||
- freeform user instruction/context from the command;
|
||||
- Ticket id/slug when the action targets an existing Ticket;
|
||||
- freeform user instruction/context from the action;
|
||||
- configured `launch_prompt` reference if present, as an unresolved reference for future prompt resolution.
|
||||
|
||||
The selected Profile supplies durable system/role behavior. `ticket.config.toml` does not override system instruction.
|
||||
|
||||
### TUI setup
|
||||
### Panel setup
|
||||
|
||||
Because top-level TUI role launches cannot inherit a parent Profile, configure concrete role profiles before using these commands:
|
||||
Because top-level role launches cannot inherit a parent Profile, configure concrete role profiles before using panel role actions:
|
||||
|
||||
```toml
|
||||
# .yoi/ticket.config.toml
|
||||
|
|
@ -303,48 +292,13 @@ profile = "project:investigator"
|
|||
workflow = "ticket-orchestrator-routing"
|
||||
```
|
||||
|
||||
If a role still uses `profile = "inherit"`, TUI will fail closed with a diagnostic explaining that a concrete profile is required.
|
||||
If a role still uses `profile = "inherit"`, the panel fails closed with a diagnostic explaining that a concrete profile is required.
|
||||
|
||||
### TUI usage examples
|
||||
|
||||
Create or refine a Ticket from a broad request:
|
||||
|
||||
```text
|
||||
:ticket intake Add a safer retry policy for stream-open provider failures
|
||||
```
|
||||
|
||||
Route an existing Ticket:
|
||||
|
||||
```text
|
||||
:ticket route ticket-local-files-backend classify next action and record routing decision
|
||||
```
|
||||
|
||||
Start a read-only investigation role:
|
||||
|
||||
```text
|
||||
:ticket investigate plugin-extension-surface map current feature API boundaries
|
||||
```
|
||||
|
||||
Launch a coder role for an implementation-ready Ticket:
|
||||
|
||||
```text
|
||||
:ticket implement ticket-config-role-profile-mapping implement the accepted MVP only
|
||||
```
|
||||
|
||||
Launch a reviewer role:
|
||||
|
||||
```text
|
||||
:ticket review tui-ticket-role-actions review diff against Ticket requirements
|
||||
```
|
||||
|
||||
After launch, inspect the created Pod through normal Pod/TUI surfaces. The command confirms launch/run acceptance; it does not mean the role Pod completed the assignment.
|
||||
|
||||
### TUI troubleshooting
|
||||
### Panel troubleshooting
|
||||
|
||||
- `profile = "inherit"`: configure a concrete role Profile in `.yoi/ticket.config.toml`.
|
||||
- malformed `.yoi/ticket.config.toml`: fix the config and retry.
|
||||
- missing Ticket id/slug for `route`, `investigate`, `implement`, or `review`: provide the target Ticket.
|
||||
- empty `:ticket intake`: provide the request/context to clarify.
|
||||
- missing Ticket id/slug for route, investigate, implement, or review actions: provide the target Ticket.
|
||||
- launch success but no visible completion: attach to or inspect the launched Pod; completion notifications are hints, not authority.
|
||||
|
||||
## Granularity
|
||||
|
|
@ -399,7 +353,7 @@ The current LocalTicketBackend stores records under:
|
|||
resolution.md # closed Tickets only
|
||||
```
|
||||
|
||||
Backend integrations must preserve this format until an explicit migration changes it. The repository-root `work-items/` path is no longer a live mutable backend; do not recreate it for Ticket records. Human users should prefer TUI role actions or Ticket tools; maintainers may use `yoi ticket ...` when working directly with repository records.
|
||||
Backend integrations must preserve this format until an explicit migration changes it. `thread.md` is an append-only typed event log: existing events such as `create`, `comment`, `plan`, `decision`, `implementation_report`, `review`, `status_changed`, and `close` remain valid, while `state_changed` records durable transition metadata (`from`, `to`, `reason`, optional `field`, plus `author` and `at`) and `intake_summary` records the bounded Intake outcome body. Thread events are audit history, not current-state authority; current state belongs in `item.md` frontmatter or the owning backend record. The repository-root `work-items/` path is no longer a live mutable backend; do not recreate it for Ticket records. Human users should prefer `yoi panel`, Ticket tools, or `yoi ticket ...` when working directly with repository records.
|
||||
|
||||
## Validation
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user