Compare commits

..

No commits in common. "a40627ab9fed8d16f348f9d58ddde6c027911ab6" and "bee9455935efb8d4d78de65e8f665c2935a744c5" have entirely different histories.

544 changed files with 824 additions and 4216 deletions

View File

@ -1,3 +0,0 @@
[backend]
provider = "builtin:yoi_local"
root = ".yoi/tickets"

View File

@ -1,100 +0,0 @@
# Delegation intent: builtin yoi_local Ticket backend config
## Classification
`implementation-ready` with a transitional storage-root constraint.
`yoi-ticket-cli-parity` is complete, so the binary now owns direct Ticket CLI operations. The next step is to make the Ticket backend provider explicit as Yoi's built-in local backend without moving storage yet.
## Intent
Extend `.yoi/ticket.config.toml` backend config from implicit/generic local storage toward a canonical built-in provider:
```toml
[backend]
provider = "builtin:yoi_local"
root = "work-items" # transitional until migrate-ticket-storage-to-yoi-tickets
```
This ticket should introduce the provider field and canonical spelling. It must not migrate records to `.yoi/tickets`; that is the next ticket.
## Worktree / branch
- worktree: `/home/hare/Projects/yoi/.worktree/builtin-yoi-local-ticket-backend-config`
- branch: `work/builtin-yoi-local-ticket-backend-config`
## Requirements
- Update Ticket config parsing in `crates/ticket/src/config.rs`.
- Add a backend provider concept with canonical value `builtin:yoi_local`.
- Keep the backend implementation local and bounded to the configured root.
- Preserve current active storage for this ticket:
- missing config should continue to resolve to `<workspace>/work-items` until the storage migration ticket lands;
- docs/tests should call this transitional explicitly.
- Support `.yoi/ticket.config.toml` with:
```toml
[backend]
provider = "builtin:yoi_local"
root = "work-items"
```
- Decide and implement legacy handling for existing `kind = "local"`:
- Accept as a short transitional alias if needed for compatibility/tests, but mark it non-canonical in docs/comments; or
- Reject with a clear diagnostic if that is cleaner and does not break current tests.
- Prefer new public names such as `TicketBackendProvider::BuiltinYoiLocal` over overloading `TicketBackendKind::Local`.
- Keep unsupported providers fail-closed with bounded diagnostics.
- Update Pod Ticket feature adapter only as needed to use provider/root validation.
- Update examples/docs that currently show `kind = "local"` to use `provider = "builtin:yoi_local"` where active docs are touched.
## Non-goals
- Moving storage to `.yoi/tickets/`.
- Removing `tickets.sh`.
- External provider implementations.
- GitHub/Linear/Jira/MCP backend support.
- TUI UI changes.
- Scheduler/lease/queue automation.
- Changing role profile/launch prompt/workflow semantics.
## Current code map
- `crates/ticket/src/config.rs`
- Current backend config is `TicketBackendKind::Local` and root defaults to `<workspace>/work-items`.
- Add provider support here.
- `crates/pod/src/feature/builtin/ticket.rs`
- Uses `TicketConfig::load_workspace(...)` and `config.backend.root`.
- Should preserve fail-closed/no-register behavior for malformed config/unusable roots.
- `crates/yoi/src/ticket_cli.rs`
- Uses Ticket config/backend root for `yoi ticket ...`; make sure provider changes do not break CLI.
- `docs/development/work-items.md`
- Shows `.yoi/ticket.config.toml` examples; update from `kind = "local"` to `provider = "builtin:yoi_local"` if not already migrated by another change.
## Validation
Run at least:
- `cargo test -p ticket config`
- `cargo test -p ticket`
- `cargo test -p pod ticket --lib`
- `cargo test -p yoi ticket`
- `cargo check --workspace --all-targets`
- `cargo fmt --check`
- `git diff --check`
- `target/debug/yoi ticket doctor` or built binary equivalent
- `./tickets.sh doctor` during transition
Run `nix build .#yoi --no-link` if feasible.
## Completion report
Report:
- worktree path / branch;
- commit hash;
- final backend config schema;
- legacy `kind = "local"` handling;
- default root behavior and why storage was not moved;
- docs/tests updated;
- validation results;
- whether `migrate-ticket-storage-to-yoi-tickets` can proceed.

View File

@ -1,60 +0,0 @@
---
id: 20260605-203006-builtin-yoi-local-ticket-backend-config
slug: builtin-yoi-local-ticket-backend-config
title: Builtin yoi_local Ticket backend config
status: closed
kind: task
priority: P1
labels: [ticket, backend, config]
created_at: 2026-06-05T20:30:06Z
updated_at: 2026-06-05T21:26:56Z
assignee: null
legacy_ticket: null
---
## Background
The Ticket backend should be configured as an explicit built-in Yoi local backend rather than an implicit generic local root.
The desired config is:
```toml
[backend]
provider = "builtin:yoi_local"
root = ".yoi/tickets"
```
This makes the backend an explicit Yoi product capability and prepares for removing `tickets.sh` and moving storage under `.yoi/`.
## Requirements
- Extend `.yoi/ticket.config.toml` backend schema from `kind = "local"` toward `provider = "builtin:yoi_local"`.
- Support `provider = "builtin:yoi_local"` as the canonical spelling.
- Decide whether old `kind = "local"` is rejected immediately or accepted only as a short transitional alias. Prefer avoiding long-term compatibility aliases.
- Default backend provider should become `builtin:yoi_local`.
- Default backend root should become `.yoi/tickets` once migration is ready, or support a transition mode if this ticket lands before storage migration.
- Update Ticket tools / Pod Ticket feature adapter to use the configured provider/root.
- Update role launcher/TUI paths if they inspect backend diagnostics.
- Keep backend root path containment and fail-closed behavior.
- Do not auto-create active storage unless the CLI command explicitly creates/migrates records.
## Non-goals
- Moving existing records; handled by `migrate-ticket-storage-to-yoi-tickets`.
- Removing `tickets.sh`; handled by `remove-tickets-sh`.
- External provider implementation.
- GitHub/Linear/Jira/MCP backend support.
- TUI UI changes.
## Acceptance criteria
- `.yoi/ticket.config.toml` with `provider = "builtin:yoi_local"` parses and resolves.
- Missing config defaults to the selected built-in backend semantics.
- Tests cover provider parsing, unsupported provider diagnostics, relative root resolution, missing/unusable root behavior, and Pod Ticket feature adapter integration.
- Docs/examples use `provider = "builtin:yoi_local"`.
- `cargo test -p ticket` and focused Pod Ticket tests pass.
- `cargo check --workspace --all-targets`, `cargo fmt --check`, `git diff --check`, and Ticket doctor validations pass.
## Dependency
Prefer after `yoi-ticket-cli-parity`, so validation and migration can use `yoi ticket doctor`.

View File

@ -1,31 +0,0 @@
Implemented canonical Yoi local Ticket backend provider config.
Final backend config schema:
```toml
[backend]
provider = "builtin:yoi_local"
root = "work-items"
```
`provider = "builtin:yoi_local"` is the canonical spelling. Existing `kind = "local"` config is accepted as a short transitional alias so existing local workspaces/tests do not break during the migration, but docs now show the provider spelling.
Default root behavior intentionally remains `<workspace>/work-items` for missing config and for this transitional provider config. This ticket did not move storage to `.yoi/tickets`; that is owned by the follow-up `migrate-ticket-storage-to-yoi-tickets` ticket.
Implementation commit was merged from branch `work/builtin-yoi-local-ticket-backend-config`.
Validation after merge:
- `cargo test -p ticket config`
- `cargo test -p ticket`
- `cargo test -p pod ticket --lib`
- `cargo test -p yoi ticket`
- `cargo check --workspace --all-targets`
- `cargo fmt --check`
- `git diff --check HEAD~1..HEAD`
- `target/debug/yoi ticket doctor`
- `./tickets.sh doctor`
- `nix build .#yoi --no-link`
External review approved with no requested changes.
`migrate-ticket-storage-to-yoi-tickets` can proceed next.

View File

@ -1,78 +0,0 @@
<!-- event: create author: tickets.sh at: 2026-06-05T20:30:06Z -->
## Created
Created by tickets.sh create.
---
<!-- event: plan author: hare at: 2026-06-05T21:10:22Z -->
## Plan
Preflight result: `implementation-ready` with transitional root behavior.
`provider = "builtin:yoi_local"` should become the canonical backend spelling now, but this ticket must not move records yet. Until `migrate-ticket-storage-to-yoi-tickets` lands, missing config should continue to resolve the active root to `<workspace>/work-items` so existing Ticket tools/CLI remain usable.
Implementation should add provider parsing/diagnostics and update active docs/examples, while leaving storage migration and `tickets.sh` removal to follow-up tickets.
Detailed delegation intent is recorded in `artifacts/delegation-intent.md`.
---
<!-- event: review author: hare at: 2026-06-05T21:26:56Z status: approve -->
## Review: approve
External reviewer approved the implementation.
Review summary:
- No blocking issues found.
- `provider = "builtin:yoi_local"` is supported as the canonical backend spelling.
- Transitional `work-items` storage is preserved.
- Unsupported providers fail closed.
- Legacy `kind = "local"` is retained only as a documented transitional alias.
- Tests/docs were updated sufficiently for this ticket.
---
<!-- event: close author: hare at: 2026-06-05T21:26:56Z status: closed -->
## Closed
Implemented canonical Yoi local Ticket backend provider config.
Final backend config schema:
```toml
[backend]
provider = "builtin:yoi_local"
root = "work-items"
```
`provider = "builtin:yoi_local"` is the canonical spelling. Existing `kind = "local"` config is accepted as a short transitional alias so existing local workspaces/tests do not break during the migration, but docs now show the provider spelling.
Default root behavior intentionally remains `<workspace>/work-items` for missing config and for this transitional provider config. This ticket did not move storage to `.yoi/tickets`; that is owned by the follow-up `migrate-ticket-storage-to-yoi-tickets` ticket.
Implementation commit was merged from branch `work/builtin-yoi-local-ticket-backend-config`.
Validation after merge:
- `cargo test -p ticket config`
- `cargo test -p ticket`
- `cargo test -p pod ticket --lib`
- `cargo test -p yoi ticket`
- `cargo check --workspace --all-targets`
- `cargo fmt --check`
- `git diff --check HEAD~1..HEAD`
- `target/debug/yoi ticket doctor`
- `./tickets.sh doctor`
- `nix build .#yoi --no-link`
External review approved with no requested changes.
`migrate-ticket-storage-to-yoi-tickets` can proceed next.
---

View File

@ -1,126 +0,0 @@
# Delegation intent: migrate Ticket storage to `.yoi/tickets`
## Classification
`implementation-ready` with repository-record migration risk.
The prerequisite `builtin-yoi-local-ticket-backend-config` is complete: Ticket backend config now has canonical provider `builtin:yoi_local` while preserving transitional `work-items` storage. This ticket performs the storage move.
## Intent
Move the active built-in Yoi local Ticket backend storage from repository-root `work-items/` to `.yoi/tickets/` and make that the default/configured root for the Rust Ticket backend.
Target active layout:
```text
.yoi/tickets/{open,pending,closed}/<id>/item.md
.yoi/tickets/{open,pending,closed}/<id>/thread.md
.yoi/tickets/{open,pending,closed}/<id>/artifacts/
```
Target config example:
```toml
[backend]
provider = "builtin:yoi_local"
root = ".yoi/tickets"
```
## Worktree / branch
- worktree: `/home/hare/Projects/yoi/.worktree/migrate-ticket-storage-to-yoi-tickets`
- branch: `work/migrate-ticket-storage-to-yoi-tickets`
This ticket is an explicit exception to the normal child-worktree `.yoi` exclusion pattern because the implementation must add/modify tracked files under `.yoi/`. Do not read or edit `.yoi/memory/`; it is ignored generated memory state and is not part of this migration.
## Requirements
- Move tracked Ticket records from `work-items/` to `.yoi/tickets/` using git-tracked file moves.
- Update `crates/ticket/src/config.rs` defaults so missing Ticket config resolves to `<workspace>/.yoi/tickets`.
- Preserve canonical provider behavior from the previous ticket:
- `provider = "builtin:yoi_local"` remains the canonical provider;
- unsupported providers still fail closed;
- transitional `kind = "local"` handling should not become the documented path.
- Add or update the project `.yoi/ticket.config.toml` if needed so this repository explicitly configures:
```toml
[backend]
provider = "builtin:yoi_local"
root = ".yoi/tickets"
```
- Update code/docs/tests that refer to `work-items/` as the active backend root.
- Update `tickets.sh` only as a temporary compatibility/maintainer shim if it remains present after this ticket:
- it should operate on `.yoi/tickets` by default after the migration;
- its help text must stop claiming `work-items/` is canonical;
- keep `WORK_ITEMS_DIR` override if useful for recovery/back-compat.
- Keep Ticket tools, `yoi ticket ...`, TUI role launcher, and Pod Ticket feature registration working against the new root.
- Ensure `target/debug/yoi ticket doctor` / built binary equivalent sees the migrated `.yoi/tickets` records.
- Ensure `./tickets.sh doctor` works during the transition if `tickets.sh` still exists in this ticket.
## Non-goals
- Removing `tickets.sh`; that is the next ticket.
- Rewriting historical thread text/artifacts merely because they mention `work-items/`.
- Migrating generated memory, workflows, Pod sessions, or non-Ticket state.
- Changing Ticket role profile mappings, workflows, role launcher semantics, or TUI UI.
- External Ticket provider support.
## Current code map
- `crates/ticket/src/config.rs`
- Default root currently remains `work-items`; change to `.yoi/tickets`.
- Tests should be updated for the new default and explicit config root.
- `crates/pod/src/feature/builtin/ticket.rs`
- Uses `TicketConfig::load_workspace(...)` and `config.backend.root`; verify diagnostics/root handling remain correct.
- `crates/yoi/src/ticket_cli.rs`
- Uses Ticket config/backend root for `yoi ticket ...`; verify CLI commands against migrated storage.
- `tickets.sh`
- Still hardcodes `WORK_ITEMS_DIR=${WORK_ITEMS_DIR:-work-items}` and help text; update for transition or document if intentionally unsupported.
- `docs/development/work-items.md`
- Update active user documentation to `.yoi/tickets` and `yoi ticket`; `tickets.sh` should remain maintainer/transition-only until removal.
- `AGENTS.md` / project instructions references may still say `work-items/` is authoritative. Update active instructions if in scope so future agents do not mutate the wrong path.
## Migration cautions
- Do not create two active mutable roots. After migration, `.yoi/tickets` is active; `work-items/` should not remain as a second live backend.
- Do not leave open tickets split between old and new roots.
- Do not mass-rewrite old ticket thread prose solely for path hygiene.
- Verify lock-file behavior does not leave `.ticket-backend.lock` in the old root.
- Since the migration moves the ticket records themselves, expect the current ticket directory to move from `work-items/open/...` to `.yoi/tickets/open/...` in the implementation commit.
## Validation
Run at least:
- `cargo test -p ticket config`
- `cargo test -p ticket`
- `cargo test -p pod ticket --lib`
- `cargo test -p yoi ticket`
- `cargo check --workspace --all-targets`
- `cargo fmt --check`
- `git diff --check`
- `target/debug/yoi ticket doctor` or built binary equivalent
- `./tickets.sh doctor` during transition if still present
Run `nix build .#yoi --no-link` if feasible.
Also manually check:
- no active Ticket records remain under `work-items/` unless intentionally retained as a compatibility notice/stub;
- `.yoi/tickets/open`, `.yoi/tickets/pending`, and `.yoi/tickets/closed` contain the migrated records;
- new `yoi ticket create` creates under `.yoi/tickets` in a scratch temp workspace or controlled test fixture, not `work-items`.
## Completion report
Report:
- worktree path / branch;
- commit hash;
- final storage root and config behavior;
- whether `.yoi/ticket.config.toml` was added/updated;
- what happened to `work-items/`;
- whether `tickets.sh` was updated and how;
- docs/tests updated;
- validation results;
- whether `remove-tickets-sh` can proceed.

View File

@ -1,66 +0,0 @@
---
id: 20260605-203006-migrate-ticket-storage-to-yoi-tickets
slug: migrate-ticket-storage-to-yoi-tickets
title: Migrate Ticket storage to .yoi/tickets
status: closed
kind: task
priority: P1
labels: [ticket, migration, storage]
created_at: 2026-06-05T20:30:06Z
updated_at: 2026-06-05T21:51:58Z
assignee: null
legacy_ticket: null
---
## Background
The active Ticket storage should move from top-level `work-items/` to `.yoi/tickets/` so Yoi project orchestration state lives under `.yoi/` with workflows and Ticket config.
This is a project-record migration. Preserve all existing Ticket ids, thread history, artifacts, and resolutions.
## Requirements
- Move active storage:
```text
work-items/open/ -> .yoi/tickets/open/
work-items/pending/ -> .yoi/tickets/pending/
work-items/closed/ -> .yoi/tickets/closed/
```
- Use `git mv` or equivalent tracked moves so history remains inspectable.
- Update code defaults/tests/docs/workflows to refer to `.yoi/tickets` as active storage.
- Update `.yoi/ticket.config.toml` examples/defaults to `root = ".yoi/tickets"`.
- Update worktree workflow guidance if necessary: child worktrees still exclude `.yoi`; Ticket mutation remains main-workspace/orchestrator authority.
- Ensure `.yoi/tickets` is tracked project state, not ignored generated memory.
- Adjust `.gitignore` if needed so `.yoi/tickets`, `.yoi/workflow`, `.yoi/knowledge`, and `.yoi/ticket.config.toml` can be tracked while generated `.yoi/memory` remains ignored.
- Do not rewrite historical thread/artifact body references unless they are current docs/config paths. Historical mentions of `work-items/` may remain as history.
## Non-goals
- Removing `tickets.sh`; handled later.
- Changing Ticket ids/slugs/status semantics.
- External tracker migration.
- Scheduler/lease/queue automation.
## Acceptance criteria
- All active Ticket records are under `.yoi/tickets/`.
- Top-level `work-items/` no longer exists as active storage.
- `yoi ticket list/show/doctor` works against `.yoi/tickets/`.
- Ticket tools and TUI role actions use `.yoi/tickets/` through configured backend root.
- Documentation no longer tells users to use `work-items/` as active storage.
- `git status` shows intentional tracked moves, not delete/recreate loss of records.
- Validation passes:
- `yoi ticket doctor`
- transitional `./tickets.sh doctor` only if still present and intentionally updated;
- `cargo check --workspace --all-targets`;
- `cargo fmt --check`;
- `git diff --check`.
## Dependency
Prefer after:
- `yoi-ticket-cli-parity`
- `builtin-yoi-local-ticket-backend-config`

View File

@ -1,39 +0,0 @@
Migrated active Yoi local Ticket storage from repository-root `work-items/` to `.yoi/tickets/`.
Final storage/config behavior:
```toml
[backend]
provider = "builtin:yoi_local"
root = ".yoi/tickets"
```
- Missing Ticket config now defaults to `<workspace>/.yoi/tickets`.
- `provider = "builtin:yoi_local"` remains the canonical provider spelling.
- Legacy `kind = "local"` remains only a transitional alias and is not documented as the active path.
- Tracked Ticket records were moved to `.yoi/tickets/{open,pending,closed}/...`.
- `work-items/` now contains only a legacy README notice and is not a live mutable backend.
- `.yoi/ticket.config.toml` was added for this repository.
- `.yoi/memory/` was not migrated or touched.
- `tickets.sh` remains only as a transitional maintainer shim and now defaults to `.yoi/tickets`; `WORK_ITEMS_DIR` remains available for one-off legacy/recovery checks.
- The later workspace panel UI design update from `develop` was merged into the migration branch before final merge, so the design ticket content is preserved under `.yoi/tickets`.
Validation after merge:
- `cargo test -p ticket config`
- `cargo test -p ticket`
- `cargo test -p pod ticket --lib`
- `cargo test -p yoi ticket`
- `cargo check --workspace --all-targets`
- `cargo fmt --check`
- `git diff --check HEAD~1..HEAD`
- `cargo build -p yoi`
- `target/debug/yoi ticket doctor`
- `./tickets.sh doctor`
- scratch workspace `target/debug/yoi ticket create` creates under `.yoi/tickets` and does not create `work-items`
- `nix build .#yoi --no-link --print-out-paths`
A first post-merge attempt to run `target/debug/yoi ticket doctor` failed because the binary was stale and still expected the old config schema; rebuilding with `cargo build -p yoi` fixed it. This was a local validation-order issue, not a source failure.
External review approved with no requested changes.
`remove-tickets-sh` can proceed next.

View File

@ -1,109 +0,0 @@
<!-- event: create author: tickets.sh at: 2026-06-05T20:30:06Z -->
## Created
Created by tickets.sh create.
---
<!-- event: plan author: hare at: 2026-06-05T21:28:37Z -->
## Plan
Preflight result: `implementation-ready` with repository-record migration risk.
The provider-config prerequisite is complete. This ticket now owns the active storage move from `work-items/` to `.yoi/tickets/` and should make `.yoi/tickets` the default/configured built-in Yoi local backend root.
Important boundaries:
- no generated memory migration;
- do not read or edit `.yoi/memory/`;
- do not remove `tickets.sh` in this ticket, but update it as a transitional maintainer shim if it remains present;
- do not mass-rewrite historical thread prose solely because it mentions `work-items/`.
Detailed delegation intent is recorded in `artifacts/delegation-intent.md`.
---
<!-- event: implementation_report author: hare at: 2026-06-05T21:43:54Z -->
## Implementation report
Implemented the local Ticket storage migration to `.yoi/tickets/`.
- Moved tracked `work-items/{open,pending,closed}` records to `.yoi/tickets/{open,pending,closed}`.
- Added `.yoi/ticket.config.toml` with `provider = "builtin:yoi_local"` and `root = ".yoi/tickets"`.
- Updated default config resolution, Pod feature fallback, CLI tests/help, docs, and the transitional `tickets.sh` shim.
- Left `work-items/README.md` as a non-active compatibility notice only.
- Validated with the requested cargo tests/checks, both doctors, scratch default-create check, and `nix build .#yoi --no-link`.
---
<!-- event: review author: hare at: 2026-06-05T21:51:58Z status: approve -->
## Review: approve
External reviewer approved the implementation at branch head `9d142ec`.
Review summary:
- Active Ticket records moved under `.yoi/tickets/`.
- `work-items/` is reduced to a non-active legacy README and is not a second mutable root.
- Missing config/default root resolves to `<workspace>/.yoi/tickets`.
- `.yoi/ticket.config.toml` uses `provider = "builtin:yoi_local"` and `root = ".yoi/tickets"`.
- `tickets.sh` defaults to `.yoi/tickets` as a transitional shim.
- Rust Ticket CLI/tools and Pod feature preserve config-root behavior and fail-closed diagnostics.
- `.yoi/memory/` was not migrated or touched.
- The latest workspace panel design ticket update from `develop` was preserved after migration.
No changes requested.
---
<!-- event: close author: hare at: 2026-06-05T21:51:58Z status: closed -->
## Closed
Migrated active Yoi local Ticket storage from repository-root `work-items/` to `.yoi/tickets/`.
Final storage/config behavior:
```toml
[backend]
provider = "builtin:yoi_local"
root = ".yoi/tickets"
```
- Missing Ticket config now defaults to `<workspace>/.yoi/tickets`.
- `provider = "builtin:yoi_local"` remains the canonical provider spelling.
- Legacy `kind = "local"` remains only a transitional alias and is not documented as the active path.
- Tracked Ticket records were moved to `.yoi/tickets/{open,pending,closed}/...`.
- `work-items/` now contains only a legacy README notice and is not a live mutable backend.
- `.yoi/ticket.config.toml` was added for this repository.
- `.yoi/memory/` was not migrated or touched.
- `tickets.sh` remains only as a transitional maintainer shim and now defaults to `.yoi/tickets`; `WORK_ITEMS_DIR` remains available for one-off legacy/recovery checks.
- The later workspace panel UI design update from `develop` was merged into the migration branch before final merge, so the design ticket content is preserved under `.yoi/tickets`.
Validation after merge:
- `cargo test -p ticket config`
- `cargo test -p ticket`
- `cargo test -p pod ticket --lib`
- `cargo test -p yoi ticket`
- `cargo check --workspace --all-targets`
- `cargo fmt --check`
- `git diff --check HEAD~1..HEAD`
- `cargo build -p yoi`
- `target/debug/yoi ticket doctor`
- `./tickets.sh doctor`
- scratch workspace `target/debug/yoi ticket create` creates under `.yoi/tickets` and does not create `work-items`
- `nix build .#yoi --no-link --print-out-paths`
A first post-merge attempt to run `target/debug/yoi ticket doctor` failed because the binary was stale and still expected the old config schema; rebuilding with `cargo build -p yoi` fixed it. This was a local validation-order issue, not a source failure.
External review approved with no requested changes.
`remove-tickets-sh` can proceed next.
---

View File

@ -1,92 +0,0 @@
# Delegation intent: remove `tickets.sh`
## Classification
`implementation-ready` after storage migration.
The prerequisite migration is complete: active Ticket records live under `.yoi/tickets/`, the Rust `yoi ticket` CLI has parity, and `tickets.sh` is only a transitional shim. Keeping it now leaves an unnecessary second mutation path.
## Intent
Delete `tickets.sh` and update active documentation, project instructions, tests, and validation references so the repository uses the typed Rust Ticket backend through `yoi ticket ...` and Ticket tools.
## Worktree / branch
- worktree: `/home/hare/Projects/yoi/.worktree/remove-tickets-sh`
- branch: `work/remove-tickets-sh`
This ticket will edit tracked `.yoi/tickets` records and may edit `.yoi/ticket.config.toml` only if necessary. Do not read or edit `.yoi/memory/`; it is ignored generated memory state and is not part of this cleanup.
## Requirements
- Delete repository-root `tickets.sh`.
- Remove or update every active doc/workflow/test/project-instruction reference that tells users, maintainers, or agents to run `./tickets.sh`.
- Replace validation examples with `yoi ticket doctor`.
- Replace lifecycle examples with `yoi ticket create/list/show/comment/review/status/close/doctor`.
- Remove shell compatibility tests that execute `tickets.sh`, or port them to Rust backend / `yoi ticket ...` coverage.
- Ensure no production code shells out to `tickets.sh`.
- Preserve historical Ticket thread/artifact references when they are closed historical context; do not mass-rewrite old records just for path hygiene.
- Keep `.yoi/tickets/` as the active storage root and avoid reintroducing `work-items/` as mutable storage.
- Update `work-items/README.md` so it no longer points to the transitional script.
## Active references to inspect/update
Current active references include at least:
- `AGENTS.md`
- currently says `.yoi/tickets/` and `tickets.sh` are authoritative and lists `./tickets.sh ...` commands.
- Update to `yoi ticket ...` as the command path; `.yoi/tickets/` remains storage/project-record authority.
- `README.md`
- update dogfooding/doctor examples from `./tickets.sh doctor` to `yoi ticket doctor`.
- `docs/development/work-items.md`
- remove the `tickets.sh` maintainer CLI section or turn it into a historical note only if needed.
- normal user path remains TUI role actions / Ticket tools / workflows.
- `docs/development/validation.md`
- update validation command examples.
- `crates/workflow/README.md`
- `crates/lint-common/README.md`
- tests under `crates/ticket` that mention or execute `tickets.sh`.
Historical/closed records and old report artifacts may still mention `tickets.sh` or `work-items/`; leave them alone unless an active test/doc depends on them.
## Non-goals
- Moving storage; already done.
- Adding Ticket backend features.
- TUI UI changes.
- External tracker/provider support.
- Rewriting closed historical Ticket threads/artifacts.
## Validation
Run at least:
- `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`
- `cargo build -p yoi`
- `target/debug/yoi ticket doctor` or built binary equivalent
Run `nix build .#yoi --no-link` if feasible.
Also manually check:
- `test ! -e tickets.sh`
- `rg "tickets\.sh|./tickets.sh"` only returns closed historical records/report artifacts, or no active references.
- No active docs/workflows/AGENTS instructions present `tickets.sh` as a command.
- No tests or production code execute `./tickets.sh`.
## Completion report
Report:
- worktree path / branch;
- commit hash;
- deleted script path;
- tests/docs/instructions updated;
- remaining `tickets.sh` references and why they are historical/acceptable, if any;
- validation results;
- whether the umbrella `yoi-local-ticket-backend-migration` can be closed.

View File

@ -1,53 +0,0 @@
---
id: 20260605-203006-remove-tickets-sh
slug: remove-tickets-sh
title: Remove tickets.sh compatibility CLI
status: closed
kind: task
priority: P1
labels: [ticket, cleanup, cli]
created_at: 2026-06-05T20:30:06Z
updated_at: 2026-06-05T22:13:36Z
assignee: null
legacy_ticket: null
---
## Background
After `yoi ticket ...` has CLI parity and active Ticket storage has moved to `.yoi/tickets/`, the old shell compatibility CLI should be removed.
Keeping `tickets.sh` would leave a second mutation path and force duplicate semantics for create/comment/review/status/close/doctor.
## Requirements
- Delete `tickets.sh`.
- Remove or update every active doc/workflow/test reference that tells users/agents to run `./tickets.sh`.
- Replace validation references with `yoi ticket doctor`.
- Remove shell-specific tests if any, or port them to `yoi ticket ...` / Rust backend tests.
- Ensure no production code shells out to `tickets.sh`.
- Ensure all Ticket mutation paths use `crates/ticket` backend APIs or the `yoi ticket` CLI.
- Preserve historical Ticket thread/artifact mentions if they are closed historical context; do not rewrite old records unnecessarily.
## Non-goals
- Moving storage; must already be complete.
- Adding new Ticket features.
- External tracker support.
- TUI changes beyond documentation/help if needed.
## Acceptance criteria
- `tickets.sh` no longer exists.
- Repository docs/workflows no longer present `tickets.sh` as an active command.
- Validation docs use `yoi ticket doctor`.
- `rg "tickets.sh"` returns only closed historical records or no active references.
- `yoi ticket doctor` passes.
- `cargo check --workspace --all-targets`, `cargo fmt --check`, `git diff --check`, and relevant tests pass.
## Dependencies
Requires:
- `yoi-ticket-cli-parity`
- `builtin-yoi-local-ticket-backend-config`
- `migrate-ticket-storage-to-yoi-tickets`

View File

@ -1,32 +0,0 @@
Removed the transitional `tickets.sh` mutation path.
Changes:
- Deleted repository-root `tickets.sh`.
- Updated active docs, workflows, project instructions, and tests from `./tickets.sh` to `yoi ticket ...` / `yoi ticket doctor`.
- Removed shell compatibility tests that executed the script and kept Rust backend / `yoi ticket` coverage.
- Removed the remaining top-level `work-items/README.md` legacy notice after merge so the repository-root `work-items/` path is no longer present.
- Preserved `.yoi/tickets/` as the active Ticket storage root.
Validation after merge:
- `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`
- `test ! -e tickets.sh`
- active docs/code grep for `tickets.sh` / `./tickets.sh`
- `nix build .#yoi --no-link --print-out-paths`
Additional post-merge cleanup validation:
- `target/debug/yoi ticket doctor`
- `test ! -e tickets.sh`
- `test ! -e work-items`
- active docs/code grep for `tickets.sh` / `./tickets.sh`
- `git diff --check`
External review approved; a non-blocking stale "compatibility CLI" phrase was fixed before merge.
The umbrella `yoi-local-ticket-backend-migration` can be closed.

View File

@ -1,78 +0,0 @@
<!-- event: create author: tickets.sh at: 2026-06-05T20:30:06Z -->
## Created
Created by tickets.sh create.
---
<!-- event: plan author: hare at: 2026-06-05T21:52:55Z -->
## Plan
Preflight result: `implementation-ready` after storage migration.
`yoi ticket` has parity and active storage has moved to `.yoi/tickets/`, so `tickets.sh` should now be removed to eliminate the temporary second mutation path. Active docs/project instructions/tests should use `yoi ticket ...`; historical closed records and old report artifacts do not need mass rewriting.
Detailed delegation intent is recorded in `artifacts/delegation-intent.md`.
---
<!-- event: review author: hare at: 2026-06-05T22:13:36Z status: approve -->
## Review: approve
External reviewer approved the implementation with one non-blocking stale phrase in `docs/development/work-items.md`.
The stale phrase was fixed before merge. After merge, the remaining top-level `work-items/README.md` legacy notice was also removed so the repository-root `work-items/` path is not present as an active or compatibility surface.
Review summary:
- `tickets.sh` is deleted.
- Active docs/project instructions/tests use `yoi ticket ...` and `yoi ticket doctor`.
- No production code or tests execute `./tickets.sh`.
- `.yoi/tickets/` remains the active Ticket storage root.
- Remaining references are historical records/spec context, not active command instructions.
---
<!-- event: close author: hare at: 2026-06-05T22:13:36Z status: closed -->
## Closed
Removed the transitional `tickets.sh` mutation path.
Changes:
- Deleted repository-root `tickets.sh`.
- Updated active docs, workflows, project instructions, and tests from `./tickets.sh` to `yoi ticket ...` / `yoi ticket doctor`.
- Removed shell compatibility tests that executed the script and kept Rust backend / `yoi ticket` coverage.
- Removed the remaining top-level `work-items/README.md` legacy notice after merge so the repository-root `work-items/` path is no longer present.
- Preserved `.yoi/tickets/` as the active Ticket storage root.
Validation after merge:
- `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`
- `test ! -e tickets.sh`
- active docs/code grep for `tickets.sh` / `./tickets.sh`
- `nix build .#yoi --no-link --print-out-paths`
Additional post-merge cleanup validation:
- `target/debug/yoi ticket doctor`
- `test ! -e tickets.sh`
- `test ! -e work-items`
- active docs/code grep for `tickets.sh` / `./tickets.sh`
- `git diff --check`
External review approved; a non-blocking stale "compatibility CLI" phrase was fixed before merge.
The umbrella `yoi-local-ticket-backend-migration` can be closed.
---

View File

@ -1,106 +0,0 @@
---
id: 20260605-203006-yoi-local-ticket-backend-migration
slug: yoi-local-ticket-backend-migration
title: Yoi-local Ticket backend migration
status: closed
kind: task
priority: P1
labels: [ticket, backend, migration, cli]
created_at: 2026-06-05T20:30:06Z
updated_at: 2026-06-05T22:13:53Z
assignee: null
legacy_ticket: null
---
## Background
Ticket development is now user-facing through TUI role actions, typed Ticket tools, Ticket workflows, and the Rust `ticket` crate. The old `tickets.sh` CLI and top-level `work-items/` storage are no longer the right long-term authority boundary.
The desired end state is:
- `yoi` binary owns Ticket operations.
- Ticket backend is configured as a built-in Yoi local backend.
- Ticket records live under `.yoi/tickets/`.
- `tickets.sh` is removed.
- `work-items/` is removed after migration.
This is an umbrella for the migration. Child tickets should land in order so the repository remains operable at each step.
## Target model
```toml
# .yoi/ticket.config.toml
[backend]
provider = "builtin:yoi_local"
root = ".yoi/tickets"
```
Storage:
```text
.yoi/tickets/{open,pending,closed}/<id>/
item.md
thread.md
artifacts/
resolution.md # closed Tickets only
```
User-facing operations:
```text
yoi ticket create
yoi ticket list
yoi ticket show
yoi ticket comment
yoi ticket review
yoi ticket status
yoi ticket close
yoi ticket doctor
```
## Child tickets
1. `yoi-ticket-cli-parity`
- Add `yoi ticket ...` CLI parity over the Rust Ticket backend.
- Keep existing storage initially.
2. `builtin-yoi-local-ticket-backend-config`
- Add `provider = "builtin:yoi_local"` backend config and default root `.yoi/tickets`.
- Preserve compatibility with existing storage during transition.
3. `migrate-ticket-storage-to-yoi-tickets`
- Move existing records from `work-items/` to `.yoi/tickets/`.
- Update docs/workflows/tests/defaults.
4. `remove-tickets-sh`
- Remove `tickets.sh` after `yoi ticket ...` and `.yoi/tickets` are authoritative.
## Requirements
- Do not leave two authoritative mutation paths.
- Do not shell out from product code to `tickets.sh`.
- Preserve existing Ticket history and artifacts.
- Preserve `git history + Ticket files` as the durable project record.
- Keep child worktrees excluding `.yoi`; orchestration state remains in the main workspace.
- Keep `.yoi/tickets`, `.yoi/workflow`, and `.yoi/ticket.config.toml` tracked project state.
- Do not mix this migration with scheduler/lease/TUI dashboard work.
## Acceptance criteria
- All child tickets are closed.
- `yoi ticket ...` is the documented direct CLI path.
- Ticket tools and TUI role actions use the configured built-in backend.
- Existing records are under `.yoi/tickets/`.
- `work-items/` no longer exists as active storage.
- `tickets.sh` no longer exists.
- Repository validation uses `yoi ticket doctor` instead of `./tickets.sh doctor`.
- Docs explain user-facing TUI/Ticket tool workflows first and local backend details only as implementation details.
## Non-goals
- External tracker integration.
- GitHub/Linear/Jira/MCP backend support.
- Scheduler/lease/queue automation.
- Stateful workflow engine.
- Changing Ticket content semantics beyond storage/config migration.

View File

@ -1,32 +0,0 @@
Completed the Yoi-local Ticket backend migration.
Child tickets completed:
- `yoi-ticket-cli-parity`: added top-level `yoi ticket` commands for create/list/show/comment/review/status/close/doctor against the Rust Ticket backend.
- `builtin-yoi-local-ticket-backend-config`: introduced canonical backend provider config `provider = "builtin:yoi_local"`.
- `migrate-ticket-storage-to-yoi-tickets`: moved active Ticket records from `work-items/` to `.yoi/tickets/` and made `.yoi/tickets` the default/configured root.
- `remove-tickets-sh`: deleted the transitional `tickets.sh` mutation path and updated active docs/instructions/tests to use `yoi ticket ...`.
Final authority model:
```toml
[backend]
provider = "builtin:yoi_local"
root = ".yoi/tickets"
```
- `yoi` binary + `crates/ticket` backend own active Ticket operations.
- Active storage is `.yoi/tickets/`.
- Repository-root `work-items/` is no longer present and is not an active mutable backend.
- `tickets.sh` is removed.
- Historical records may still mention old paths/commands as history, but active instructions and validation use `yoi ticket ...`.
Final validation points across the sequence included:
- `cargo test -p ticket`
- `cargo test -p yoi ticket`
- `cargo test -p pod ticket --lib`
- `cargo check --workspace --all-targets`
- `cargo fmt --check`
- `cargo build -p yoi`
- `target/debug/yoi ticket doctor`
- `nix build .#yoi --no-link`
- absence checks for `tickets.sh` and repository-root `work-items/` after final cleanup.

View File

@ -1,94 +0,0 @@
<!-- event: create author: tickets.sh at: 2026-06-05T20:30:06Z -->
## Created
Created by tickets.sh create.
---
<!-- event: decision author: hare at: 2026-06-05T20:32:09Z -->
## Decision
Decision: migrate Ticket authority to the `yoi` binary and Yoi's built-in local backend.
Target state:
- Direct CLI operations use `yoi ticket ...`.
- Backend config uses `provider = "builtin:yoi_local"`.
- Active Ticket storage lives under `.yoi/tickets/`.
- `tickets.sh` is removed.
- Top-level `work-items/` is removed as active storage.
Rationale:
- Normal users should use TUI role actions, Ticket tools, workflows, and `yoi ticket ...`, not a shell script.
- Keeping `tickets.sh` as a live mutation path duplicates Ticket semantics and undermines the Rust backend as authority.
- `.yoi/tickets/` aligns Ticket records with `.yoi/workflow` and `.yoi/ticket.config.toml` as tracked project orchestration state.
- `work-items/` is legacy storage naming after the project concept was renamed to Ticket.
Migration should land in child tickets so the repository remains operable at each step.
---
<!-- event: plan author: hare at: 2026-06-05T20:32:09Z -->
## Plan
Plan:
1. `yoi-ticket-cli-parity`
- Add `yoi ticket ...` operations over the Rust Ticket backend.
2. `builtin-yoi-local-ticket-backend-config`
- Add canonical `provider = "builtin:yoi_local"` backend config and defaults.
3. `migrate-ticket-storage-to-yoi-tickets`
- Move active Ticket records from `work-items/` to `.yoi/tickets/`.
4. `remove-tickets-sh`
- Delete the shell compatibility CLI and update active docs/workflows/validation to `yoi ticket doctor`.
---
<!-- event: close author: hare at: 2026-06-05T22:13:53Z status: closed -->
## Closed
Completed the Yoi-local Ticket backend migration.
Child tickets completed:
- `yoi-ticket-cli-parity`: added top-level `yoi ticket` commands for create/list/show/comment/review/status/close/doctor against the Rust Ticket backend.
- `builtin-yoi-local-ticket-backend-config`: introduced canonical backend provider config `provider = "builtin:yoi_local"`.
- `migrate-ticket-storage-to-yoi-tickets`: moved active Ticket records from `work-items/` to `.yoi/tickets/` and made `.yoi/tickets` the default/configured root.
- `remove-tickets-sh`: deleted the transitional `tickets.sh` mutation path and updated active docs/instructions/tests to use `yoi ticket ...`.
Final authority model:
```toml
[backend]
provider = "builtin:yoi_local"
root = ".yoi/tickets"
```
- `yoi` binary + `crates/ticket` backend own active Ticket operations.
- Active storage is `.yoi/tickets/`.
- Repository-root `work-items/` is no longer present and is not an active mutable backend.
- `tickets.sh` is removed.
- Historical records may still mention old paths/commands as history, but active instructions and validation use `yoi ticket ...`.
Final validation points across the sequence included:
- `cargo test -p ticket`
- `cargo test -p yoi ticket`
- `cargo test -p pod ticket --lib`
- `cargo check --workspace --all-targets`
- `cargo fmt --check`
- `cargo build -p yoi`
- `target/debug/yoi ticket doctor`
- `nix build .#yoi --no-link`
- absence checks for `tickets.sh` and repository-root `work-items/` after final cleanup.
---

View File

@ -1,94 +0,0 @@
# Delegation intent: yoi ticket CLI parity
## Intent
Add `yoi ticket ...` CLI operations over the Rust Ticket backend so direct Ticket mutation is owned by the `yoi` binary rather than `tickets.sh`.
This is the first step in the Yoi-local Ticket backend migration. `tickets.sh` remains in place for this ticket only as transitional compatibility and validation reference.
## Worktree / branch
- worktree: `/home/hare/Projects/yoi/.worktree/yoi-ticket-cli-parity`
- branch: `work/yoi-ticket-cli-parity`
## Requirements
Implement these subcommands in the top-level `yoi` binary crate:
- `yoi ticket create --title "..." [--slug slug] [--kind task] [--priority P2] [--label a,b]`
- `yoi ticket list [--status open|pending|closed|all]`
- `yoi ticket show <id-or-slug>`
- `yoi ticket comment <id-or-slug> [--role comment|plan|decision|implementation_report] [--file path|--message text]`
- `yoi ticket review <id-or-slug> --approve|--request-changes [--file path|--message text]`
- `yoi ticket status <id-or-slug> open|pending|closed`
- `yoi ticket close <id-or-slug> [--resolution text|--file path]`
- `yoi ticket doctor`
Use `crates/ticket` backend APIs directly. Do not shell out to `tickets.sh`.
## Current code map
- `crates/yoi/src/main.rs`
- Owns top-level CLI parsing and dispatch.
- Existing subcommands include `pod`, `keys`, and `memory lint`.
- Add `ticket` parsing/dispatch here or in a new module such as `crates/yoi/src/ticket_cli.rs`.
- `crates/yoi/Cargo.toml`
- Add dependency on `ticket` if needed.
- `crates/ticket/src/lib.rs`
- Ticket domain/backend APIs: `LocalTicketBackend`, `TicketBackend`, `NewTicket`, `NewTicketEvent`, `TicketReview`, status/review/event types, doctor reports.
- `crates/ticket/src/config.rs`
- Workspace ticket config and backend root defaults. Use if it helps locate backend root; otherwise current active storage is still `work-items/` for this ticket.
- `tickets.sh`
- Compatibility/reference only. Do not invoke it from Rust code.
## Scope notes
- Active storage stays where it is for this ticket (`work-items/` unless config already directs otherwise).
- Keep `tickets.sh` in place.
- CLI output does not need exact `tickets.sh` formatting, but should be understandable and stable enough for tests.
- Prefer concise plain text output for human CLI; JSON is not required unless already easy.
- `yoi ticket doctor` should be usable as the future validation command.
## Non-goals
- Moving storage to `.yoi/tickets/`.
- Removing `tickets.sh`.
- Adding external tracker backends.
- TUI changes.
- Scheduler/lease/queue.
- Broad CLI framework refactor.
## Edge cases / validation expectations
- `comment` / `review` should accept exactly one body source: `--file` or `--message`, or provide a clear error.
- `review` requires exactly one of `--approve` / `--request-changes`.
- `close` requires resolution text/file or a clear error if the backend cannot close without a resolution.
- `status closed` should preserve backend semantics. If direct status-to-closed cannot write a resolution, either reject it with guidance to use `close` or keep parity only if existing backend supports it safely.
- `doctor` should print diagnostics and return non-zero on failures.
- `--help` for `yoi ticket` should document subcommands.
## Validation
Run at least:
- focused `cargo test -p yoi ticket` tests;
- `cargo test -p ticket`;
- `cargo check --workspace --all-targets`;
- `cargo fmt --check`;
- `git diff --check`;
- `./tickets.sh doctor`;
- built binary `yoi ticket doctor` against the repository if possible.
Run `nix build .#yoi --no-link` if feasible.
## Completion report
Report:
- worktree path / branch;
- commit hash;
- implemented command surface;
- backend root resolution behavior;
- tests/validation results;
- known compatibility gaps from `tickets.sh`, if any;
- whether ready for external review.

View File

@ -1,81 +0,0 @@
# Implementation report: yoi-ticket-cli-parity
## Worktree / branch
- Worktree: `/home/hare/Projects/yoi/.worktree/yoi-ticket-cli-parity`
- Branch: `work/yoi-ticket-cli-parity`
## Commit
- `4d5068b feat: add yoi ticket CLI`
## Summary
Added `yoi ticket ...` subcommands to the product `yoi` binary crate, backed directly by the Rust Ticket backend APIs.
The implementation does not shell out to `tickets.sh`. Active storage remains unchanged for this ticket: when `.yoi/ticket.config.toml` is absent, the CLI uses the current default `<workspace>/work-items`.
## Implemented command surface
- `yoi ticket create --title <title> [--slug <slug>] [--kind <kind>] [--priority P2] [--label a,b]`
- `yoi ticket list [--status open|pending|closed|all]`
- `yoi ticket show <id-or-slug>`
- `yoi ticket comment <id-or-slug> [--role comment|plan|decision|implementation_report] (--file <path>|--message <text>)`
- `yoi ticket review <id-or-slug> (--approve|--request-changes) (--file <path>|--message <text>)`
- `yoi ticket status <id-or-slug> <open|pending|closed>`
- `yoi ticket close <id-or-slug> (--resolution <text>|--file <path>)`
- `yoi ticket doctor`
`yoi ticket status ... closed` is intentionally rejected with guidance to use `yoi ticket close`, because status-only close would not write `resolution.md`.
## Backend behavior
- Uses `ticket::config::TicketConfig::load_workspace(<cwd>)` to resolve backend root.
- Missing `.yoi/ticket.config.toml` defaults to `<cwd>/work-items`.
- Relative configured roots resolve against the workspace.
- Malformed config fails through the existing config loader.
- No storage migration to `.yoi/tickets` is included.
## Changed files
- `Cargo.lock`
- `crates/ticket/src/lib.rs`
- `crates/yoi/Cargo.toml`
- `crates/yoi/src/main.rs`
- `crates/yoi/src/ticket_cli.rs`
- `package.nix`
## Review status
External sibling reviewer approved with no blockers.
Non-blocker follow-ups:
- `yoi ticket doctor` currently hides warning-only diagnostics because it prints `doctor: ok` when `error_count() == 0`.
- `show`, `list`, and doctor diagnostic output are not explicitly bounded.
- Body-source error text is generic and can mention `--resolution` for comment/review commands.
## Validation
Coder-reported validation passed:
- `cargo test -p yoi ticket`
- `cargo test -p ticket`
- `cargo check --workspace --all-targets`
- `cargo fmt --check`
- `git diff --check`
- `git diff --cached --check`
- `./tickets.sh doctor`
- `cargo build -p yoi`
- `target/debug/yoi ticket doctor`
- `nix build .#yoi --no-link`
Reviewer-rerun validation passed:
- `git diff --check develop...HEAD`
- `./tickets.sh doctor`
- production CLI grep found no shell-out to `tickets.sh`.
## Ready for merge
Yes.

View File

@ -1,62 +0,0 @@
# External review: yoi-ticket-cli-parity
## 1. Result
approve
## 2. Summary of implementation
The implementation adds a new `yoi ticket ...` command surface in the product `yoi` binary crate, with parsing and dispatch in `crates/yoi/src/main.rs` and the command implementation in `crates/yoi/src/ticket_cli.rs`.
The CLI resolves the local Ticket backend through `ticket::config::TicketConfig::load_workspace`, defaults to `<cwd>/work-items` when no `.yoi/ticket.config.toml` is present, and then calls `LocalTicketBackend` / `TicketBackend` operations directly. I found no production-path shell-out to `tickets.sh`; the only `tickets.sh` process invocations found are in `crates/ticket` compatibility tests.
The changed files are scoped to the `yoi` CLI crate, the `ticket` backend doctor reporting shape, and necessary dependency/package hash updates. I did not find storage migration, `tickets.sh` removal, TUI changes, scheduler/lease work, or broad refactoring.
## 3. Requirement-by-requirement assessment
- `yoi ticket` subcommands cover create/list/show/comment/review/status/close/doctor: satisfied. `TicketCommand` contains all requested operations and `help_text()` documents them.
- Product CLI ownership in the `yoi` binary crate: satisfied. `main.rs` adds `Mode::Ticket` and dispatches `ticket` before normal TUI argument handling.
- Uses Rust Ticket backend APIs directly, no `tickets.sh` shell-out: satisfied for the product CLI path. `ticket_cli.rs` calls `LocalTicketBackend` and `TicketBackend` methods directly.
- Backend root resolution and active storage: satisfied for this ticket. The CLI uses `.yoi/ticket.config.toml` when present and otherwise defaults to `<cwd>/work-items`, preserving current local storage rather than moving to `.yoi/tickets`.
- `status closed` safety: satisfied. `status closed` is rejected with guidance to use `yoi ticket close <ticket> --resolution <text>`.
- `comment`, `review`, and `close` body source validation: satisfied. The parser requires exactly one body source and rejects conflicting/missing inputs; `review` also requires exactly one result flag.
- Doctor success/failure behavior: satisfied for errors. `TicketCliStatus::Failure` maps to a failing process exit code, and diagnostics are printed when errors are present.
- Human-useful output: broadly satisfied. Output is concise tabular/plain text for create/list/status and readable Markdown-like output for show.
- Bounded output: partially satisfied only by the natural size of the local backend; `show`, `list`, and doctor diagnostics do not impose explicit limits. I classify this as a follow-up because the requested CLI parity is still implemented and the current compatibility CLI is also not explicitly bounded.
- Tests in temp roots/fixtures: satisfied in implementation. `ticket_cli.rs` exercises core operations in `TempDir`, including configured backend root behavior and validation edge cases; `crates/ticket` keeps compatibility tests.
- `Cargo.lock` / `package.nix`: necessary and safe on inspection. Adding `ticket` to the `yoi` crate requires the lockfile package dependency update, and the Nix cargo hash update is expected from the Cargo metadata/source change.
- Non-goals: satisfied. I found no `.yoi/tickets` migration, `tickets.sh` removal, TUI change, scheduler/lease addition, or broad refactor.
## 4. Blockers
None.
## 5. Non-blockers / follow-ups
- `yoi ticket doctor` suppresses warning-only diagnostics because `doctor()` returns early with `doctor: ok` when `report.error_count() == 0`. If backend warnings are intended to be user-visible, the CLI should print warnings while still exiting successfully. This is not a merge blocker because the old `tickets.sh doctor` only had errors and the required failure behavior for errors is present.
- The CLI does not explicitly bound `show`, `list`, or diagnostic output. Consider adding limits later if this command is expected to be safe for very large Ticket stores or oversized thread bodies.
- The generic body-source error text says `--message/--resolution` for all commands, so comment/review errors mention `--resolution` even though that flag is only for close. The validation is correct; the wording can be improved in follow-up.
## 6. Validation assessed or rerun
Rerun/read-only validation:
- `git diff --check develop...HEAD` — passed with no output.
- `./tickets.sh doctor` — passed: `doctor: ok`.
- `git status --short && git branch --show-current && git rev-parse --short=12 HEAD` — confirmed branch `work/yoi-ticket-cli-parity` at `4d5068ba3baf`; no worktree dirt was reported before this review artifact was written.
- `git grep -n "tickets.sh\|std::process::Command\|Command::new" -- crates/yoi crates/ticket` — no production CLI shell-out found; only `crates/ticket` compatibility tests invoke `tickets.sh`.
Inspected:
- Ticket and delegation intent.
- `crates/yoi/src/ticket_cli.rs`.
- `crates/yoi/src/main.rs`.
- `crates/ticket/src/lib.rs` diff.
- `crates/ticket/src/config.rs` backend default/config behavior.
- `crates/yoi/Cargo.toml`, `Cargo.lock` diff, and `package.nix` hash change.
Not rerun: `cargo test`, `cargo check`, `cargo fmt --check`, built `yoi ticket doctor`, or `nix build .#yoi`; I stayed to read-only validation commands as requested for this external sibling review.
## 7. Residual risk
The main residual risk is not semantic parity but operational validation: I did not rerun the Rust or Nix builds, so the final merge owner should rely on the coder's validation evidence or rerun the full acceptance suite before merging. There is also minor UX risk around unbounded `show/list` output and warning-only doctor output being hidden.

View File

@ -1,66 +0,0 @@
---
id: 20260605-203006-yoi-ticket-cli-parity
slug: yoi-ticket-cli-parity
title: Yoi ticket CLI parity
status: closed
kind: task
priority: P1
labels: [ticket, cli, backend]
created_at: 2026-06-05T20:30:06Z
updated_at: 2026-06-05T20:58:21Z
assignee: null
legacy_ticket: null
---
## Background
Before `tickets.sh` can be removed, the `yoi` binary must provide direct Ticket CLI operations over the Rust Ticket backend.
This ticket moves the direct local CLI surface from shell script compatibility into the product binary. It should use `crates/ticket` APIs directly, not shell out to `tickets.sh`.
## Requirements
Add `yoi ticket ...` subcommands with parity for the operations currently used through the local compatibility CLI:
- `yoi ticket create --title "..." [--slug slug] [--kind task] [--priority P2] [--label a,b]`
- `yoi ticket list [--status open|pending|closed|all]`
- `yoi ticket show <id-or-slug>`
- `yoi ticket comment <id-or-slug> [--role comment|plan|decision|implementation_report] [--file path|--message text]`
- `yoi ticket review <id-or-slug> --approve|--request-changes [--file path|--message text]`
- `yoi ticket status <id-or-slug> open|pending|closed`
- `yoi ticket close <id-or-slug> [--resolution text|--file path]`
- `yoi ticket doctor`
Use `ticket::TicketBackend` / `LocalTicketBackend` or the configured backend resolver if available.
## Scope
- Product CLI belongs in the `insomnia`/top-level `yoi` binary crate, consistent with current CLI ownership.
- CLI output should be stable enough for human use and tests, but does not need to preserve exact `tickets.sh` output formatting.
- Preserve existing Ticket file compatibility.
- Keep `tickets.sh` in place for this ticket; removal happens later.
- Keep active storage where it is for now unless the backend config ticket has already landed.
## Non-goals
- Moving storage to `.yoi/tickets/`.
- Removing `tickets.sh`.
- External tracker backends.
- TUI changes.
- Scheduler/lease/queue.
## Acceptance criteria
- `yoi ticket --help` documents available subcommands.
- Each required operation works through Rust backend APIs without invoking `tickets.sh`.
- `yoi ticket doctor` can replace `./tickets.sh doctor` in validation after the migration.
- Tests cover create/list/show/comment/review/status/close/doctor in temp roots or fixtures.
- Files written by `yoi ticket ...` are readable by existing backend/tests.
- `cargo test` for affected crates passes.
- `cargo check --workspace --all-targets`, `cargo fmt --check`, `git diff --check`, and both `yoi ticket doctor` and `./tickets.sh doctor` pass during the transition.
## Follow-up
- `builtin-yoi-local-ticket-backend-config`
- `migrate-ticket-storage-to-yoi-tickets`
- `remove-tickets-sh`

View File

@ -1,45 +0,0 @@
Yoi ticket CLI parity is complete and merged.
Implementation:
- `4d5068b feat: add yoi ticket CLI`
- merge commit: `4ba1b2f merge: add yoi ticket cli`
Summary:
- Added `yoi ticket ...` subcommands to the product `yoi` binary.
- Implemented:
- `yoi ticket create`
- `yoi ticket list`
- `yoi ticket show`
- `yoi ticket comment`
- `yoi ticket review`
- `yoi ticket status`
- `yoi ticket close`
- `yoi ticket doctor`
- The CLI uses `crates/ticket` backend APIs directly.
- The CLI does not shell out to `tickets.sh`.
- Active storage remains unchanged for this ticket: absent `.yoi/ticket.config.toml`, the backend defaults to `<workspace>/work-items`.
- `yoi ticket status ... closed` is intentionally rejected with guidance to use `yoi ticket close`, because status-only close would not write `resolution.md`.
Review:
- External sibling reviewer approved with no blockers.
- Non-blocker follow-ups:
- `yoi ticket doctor` currently hides warning-only diagnostics when there are no errors.
- `show`, `list`, and doctor diagnostic output are not explicitly bounded.
- Body-source error text is generic and can mention `--resolution` for comment/review commands.
Post-merge validation passed:
- `cargo test -p yoi ticket`
- `cargo test -p ticket`
- `cargo check --workspace --all-targets`
- `cargo fmt --check`
- `git diff --check`
- `./tickets.sh doctor`
- `cargo build -p yoi`
- `target/debug/yoi ticket doctor`
- `nix build .#yoi --no-link`
This clears the CLI prerequisite for the next migration step: `builtin-yoi-local-ticket-backend-config`.

View File

@ -1,321 +0,0 @@
<!-- event: create author: tickets.sh at: 2026-06-05T20:30:06Z -->
## Created
Created by tickets.sh create.
---
<!-- event: plan author: hare at: 2026-06-05T20:34:35Z -->
## Plan
# Delegation intent: yoi ticket CLI parity
## Intent
Add `yoi ticket ...` CLI operations over the Rust Ticket backend so direct Ticket mutation is owned by the `yoi` binary rather than `tickets.sh`.
This is the first step in the Yoi-local Ticket backend migration. `tickets.sh` remains in place for this ticket only as transitional compatibility and validation reference.
## Worktree / branch
- worktree: `/home/hare/Projects/yoi/.worktree/yoi-ticket-cli-parity`
- branch: `work/yoi-ticket-cli-parity`
## Requirements
Implement these subcommands in the top-level `yoi` binary crate:
- `yoi ticket create --title "..." [--slug slug] [--kind task] [--priority P2] [--label a,b]`
- `yoi ticket list [--status open|pending|closed|all]`
- `yoi ticket show <id-or-slug>`
- `yoi ticket comment <id-or-slug> [--role comment|plan|decision|implementation_report] [--file path|--message text]`
- `yoi ticket review <id-or-slug> --approve|--request-changes [--file path|--message text]`
- `yoi ticket status <id-or-slug> open|pending|closed`
- `yoi ticket close <id-or-slug> [--resolution text|--file path]`
- `yoi ticket doctor`
Use `crates/ticket` backend APIs directly. Do not shell out to `tickets.sh`.
## Current code map
- `crates/yoi/src/main.rs`
- Owns top-level CLI parsing and dispatch.
- Existing subcommands include `pod`, `keys`, and `memory lint`.
- Add `ticket` parsing/dispatch here or in a new module such as `crates/yoi/src/ticket_cli.rs`.
- `crates/yoi/Cargo.toml`
- Add dependency on `ticket` if needed.
- `crates/ticket/src/lib.rs`
- Ticket domain/backend APIs: `LocalTicketBackend`, `TicketBackend`, `NewTicket`, `NewTicketEvent`, `TicketReview`, status/review/event types, doctor reports.
- `crates/ticket/src/config.rs`
- Workspace ticket config and backend root defaults. Use if it helps locate backend root; otherwise current active storage is still `work-items/` for this ticket.
- `tickets.sh`
- Compatibility/reference only. Do not invoke it from Rust code.
## Scope notes
- Active storage stays where it is for this ticket (`work-items/` unless config already directs otherwise).
- Keep `tickets.sh` in place.
- CLI output does not need exact `tickets.sh` formatting, but should be understandable and stable enough for tests.
- Prefer concise plain text output for human CLI; JSON is not required unless already easy.
- `yoi ticket doctor` should be usable as the future validation command.
## Non-goals
- Moving storage to `.yoi/tickets/`.
- Removing `tickets.sh`.
- Adding external tracker backends.
- TUI changes.
- Scheduler/lease/queue.
- Broad CLI framework refactor.
## Edge cases / validation expectations
- `comment` / `review` should accept exactly one body source: `--file` or `--message`, or provide a clear error.
- `review` requires exactly one of `--approve` / `--request-changes`.
- `close` requires resolution text/file or a clear error if the backend cannot close without a resolution.
- `status closed` should preserve backend semantics. If direct status-to-closed cannot write a resolution, either reject it with guidance to use `close` or keep parity only if existing backend supports it safely.
- `doctor` should print diagnostics and return non-zero on failures.
- `--help` for `yoi ticket` should document subcommands.
## Validation
Run at least:
- focused `cargo test -p yoi ticket` tests;
- `cargo test -p ticket`;
- `cargo check --workspace --all-targets`;
- `cargo fmt --check`;
- `git diff --check`;
- `./tickets.sh doctor`;
- built binary `yoi ticket doctor` against the repository if possible.
Run `nix build .#yoi --no-link` if feasible.
## Completion report
Report:
- worktree path / branch;
- commit hash;
- implemented command surface;
- backend root resolution behavior;
- tests/validation results;
- known compatibility gaps from `tickets.sh`, if any;
- whether ready for external review.
---
<!-- event: review author: hare at: 2026-06-05T20:56:41Z status: approve -->
## Review: approve
# External review: yoi-ticket-cli-parity
## 1. Result
approve
## 2. Summary of implementation
The implementation adds a new `yoi ticket ...` command surface in the product `yoi` binary crate, with parsing and dispatch in `crates/yoi/src/main.rs` and the command implementation in `crates/yoi/src/ticket_cli.rs`.
The CLI resolves the local Ticket backend through `ticket::config::TicketConfig::load_workspace`, defaults to `<cwd>/work-items` when no `.yoi/ticket.config.toml` is present, and then calls `LocalTicketBackend` / `TicketBackend` operations directly. I found no production-path shell-out to `tickets.sh`; the only `tickets.sh` process invocations found are in `crates/ticket` compatibility tests.
The changed files are scoped to the `yoi` CLI crate, the `ticket` backend doctor reporting shape, and necessary dependency/package hash updates. I did not find storage migration, `tickets.sh` removal, TUI changes, scheduler/lease work, or broad refactoring.
## 3. Requirement-by-requirement assessment
- `yoi ticket` subcommands cover create/list/show/comment/review/status/close/doctor: satisfied. `TicketCommand` contains all requested operations and `help_text()` documents them.
- Product CLI ownership in the `yoi` binary crate: satisfied. `main.rs` adds `Mode::Ticket` and dispatches `ticket` before normal TUI argument handling.
- Uses Rust Ticket backend APIs directly, no `tickets.sh` shell-out: satisfied for the product CLI path. `ticket_cli.rs` calls `LocalTicketBackend` and `TicketBackend` methods directly.
- Backend root resolution and active storage: satisfied for this ticket. The CLI uses `.yoi/ticket.config.toml` when present and otherwise defaults to `<cwd>/work-items`, preserving current local storage rather than moving to `.yoi/tickets`.
- `status closed` safety: satisfied. `status closed` is rejected with guidance to use `yoi ticket close <ticket> --resolution <text>`.
- `comment`, `review`, and `close` body source validation: satisfied. The parser requires exactly one body source and rejects conflicting/missing inputs; `review` also requires exactly one result flag.
- Doctor success/failure behavior: satisfied for errors. `TicketCliStatus::Failure` maps to a failing process exit code, and diagnostics are printed when errors are present.
- Human-useful output: broadly satisfied. Output is concise tabular/plain text for create/list/status and readable Markdown-like output for show.
- Bounded output: partially satisfied only by the natural size of the local backend; `show`, `list`, and doctor diagnostics do not impose explicit limits. I classify this as a follow-up because the requested CLI parity is still implemented and the current compatibility CLI is also not explicitly bounded.
- Tests in temp roots/fixtures: satisfied in implementation. `ticket_cli.rs` exercises core operations in `TempDir`, including configured backend root behavior and validation edge cases; `crates/ticket` keeps compatibility tests.
- `Cargo.lock` / `package.nix`: necessary and safe on inspection. Adding `ticket` to the `yoi` crate requires the lockfile package dependency update, and the Nix cargo hash update is expected from the Cargo metadata/source change.
- Non-goals: satisfied. I found no `.yoi/tickets` migration, `tickets.sh` removal, TUI change, scheduler/lease addition, or broad refactor.
## 4. Blockers
None.
## 5. Non-blockers / follow-ups
- `yoi ticket doctor` suppresses warning-only diagnostics because `doctor()` returns early with `doctor: ok` when `report.error_count() == 0`. If backend warnings are intended to be user-visible, the CLI should print warnings while still exiting successfully. This is not a merge blocker because the old `tickets.sh doctor` only had errors and the required failure behavior for errors is present.
- The CLI does not explicitly bound `show`, `list`, or diagnostic output. Consider adding limits later if this command is expected to be safe for very large Ticket stores or oversized thread bodies.
- The generic body-source error text says `--message/--resolution` for all commands, so comment/review errors mention `--resolution` even though that flag is only for close. The validation is correct; the wording can be improved in follow-up.
## 6. Validation assessed or rerun
Rerun/read-only validation:
- `git diff --check develop...HEAD` — passed with no output.
- `./tickets.sh doctor` — passed: `doctor: ok`.
- `git status --short && git branch --show-current && git rev-parse --short=12 HEAD` — confirmed branch `work/yoi-ticket-cli-parity` at `4d5068ba3baf`; no worktree dirt was reported before this review artifact was written.
- `git grep -n "tickets.sh\|std::process::Command\|Command::new" -- crates/yoi crates/ticket` — no production CLI shell-out found; only `crates/ticket` compatibility tests invoke `tickets.sh`.
Inspected:
- Ticket and delegation intent.
- `crates/yoi/src/ticket_cli.rs`.
- `crates/yoi/src/main.rs`.
- `crates/ticket/src/lib.rs` diff.
- `crates/ticket/src/config.rs` backend default/config behavior.
- `crates/yoi/Cargo.toml`, `Cargo.lock` diff, and `package.nix` hash change.
Not rerun: `cargo test`, `cargo check`, `cargo fmt --check`, built `yoi ticket doctor`, or `nix build .#yoi`; I stayed to read-only validation commands as requested for this external sibling review.
## 7. Residual risk
The main residual risk is not semantic parity but operational validation: I did not rerun the Rust or Nix builds, so the final merge owner should rely on the coder's validation evidence or rerun the full acceptance suite before merging. There is also minor UX risk around unbounded `show/list` output and warning-only doctor output being hidden.
---
<!-- event: implementation_report author: hare at: 2026-06-05T20:56:42Z -->
## Implementation report
# Implementation report: yoi-ticket-cli-parity
## Worktree / branch
- Worktree: `/home/hare/Projects/yoi/.worktree/yoi-ticket-cli-parity`
- Branch: `work/yoi-ticket-cli-parity`
## Commit
- `4d5068b feat: add yoi ticket CLI`
## Summary
Added `yoi ticket ...` subcommands to the product `yoi` binary crate, backed directly by the Rust Ticket backend APIs.
The implementation does not shell out to `tickets.sh`. Active storage remains unchanged for this ticket: when `.yoi/ticket.config.toml` is absent, the CLI uses the current default `<workspace>/work-items`.
## Implemented command surface
- `yoi ticket create --title <title> [--slug <slug>] [--kind <kind>] [--priority P2] [--label a,b]`
- `yoi ticket list [--status open|pending|closed|all]`
- `yoi ticket show <id-or-slug>`
- `yoi ticket comment <id-or-slug> [--role comment|plan|decision|implementation_report] (--file <path>|--message <text>)`
- `yoi ticket review <id-or-slug> (--approve|--request-changes) (--file <path>|--message <text>)`
- `yoi ticket status <id-or-slug> <open|pending|closed>`
- `yoi ticket close <id-or-slug> (--resolution <text>|--file <path>)`
- `yoi ticket doctor`
`yoi ticket status ... closed` is intentionally rejected with guidance to use `yoi ticket close`, because status-only close would not write `resolution.md`.
## Backend behavior
- Uses `ticket::config::TicketConfig::load_workspace(<cwd>)` to resolve backend root.
- Missing `.yoi/ticket.config.toml` defaults to `<cwd>/work-items`.
- Relative configured roots resolve against the workspace.
- Malformed config fails through the existing config loader.
- No storage migration to `.yoi/tickets` is included.
## Changed files
- `Cargo.lock`
- `crates/ticket/src/lib.rs`
- `crates/yoi/Cargo.toml`
- `crates/yoi/src/main.rs`
- `crates/yoi/src/ticket_cli.rs`
- `package.nix`
## Review status
External sibling reviewer approved with no blockers.
Non-blocker follow-ups:
- `yoi ticket doctor` currently hides warning-only diagnostics because it prints `doctor: ok` when `error_count() == 0`.
- `show`, `list`, and doctor diagnostic output are not explicitly bounded.
- Body-source error text is generic and can mention `--resolution` for comment/review commands.
## Validation
Coder-reported validation passed:
- `cargo test -p yoi ticket`
- `cargo test -p ticket`
- `cargo check --workspace --all-targets`
- `cargo fmt --check`
- `git diff --check`
- `git diff --cached --check`
- `./tickets.sh doctor`
- `cargo build -p yoi`
- `target/debug/yoi ticket doctor`
- `nix build .#yoi --no-link`
Reviewer-rerun validation passed:
- `git diff --check develop...HEAD`
- `./tickets.sh doctor`
- production CLI grep found no shell-out to `tickets.sh`.
## Ready for merge
Yes.
---
<!-- event: close author: hare at: 2026-06-05T20:58:21Z status: closed -->
## Closed
Yoi ticket CLI parity is complete and merged.
Implementation:
- `4d5068b feat: add yoi ticket CLI`
- merge commit: `4ba1b2f merge: add yoi ticket cli`
Summary:
- Added `yoi ticket ...` subcommands to the product `yoi` binary.
- Implemented:
- `yoi ticket create`
- `yoi ticket list`
- `yoi ticket show`
- `yoi ticket comment`
- `yoi ticket review`
- `yoi ticket status`
- `yoi ticket close`
- `yoi ticket doctor`
- The CLI uses `crates/ticket` backend APIs directly.
- The CLI does not shell out to `tickets.sh`.
- Active storage remains unchanged for this ticket: absent `.yoi/ticket.config.toml`, the backend defaults to `<workspace>/work-items`.
- `yoi ticket status ... closed` is intentionally rejected with guidance to use `yoi ticket close`, because status-only close would not write `resolution.md`.
Review:
- External sibling reviewer approved with no blockers.
- Non-blocker follow-ups:
- `yoi ticket doctor` currently hides warning-only diagnostics when there are no errors.
- `show`, `list`, and doctor diagnostic output are not explicitly bounded.
- Body-source error text is generic and can mention `--resolution` for comment/review commands.
Post-merge validation passed:
- `cargo test -p yoi ticket`
- `cargo test -p ticket`
- `cargo check --workspace --all-targets`
- `cargo fmt --check`
- `git diff --check`
- `./tickets.sh doctor`
- `cargo build -p yoi`
- `target/debug/yoi ticket doctor`
- `nix build .#yoi --no-link`
This clears the CLI prerequisite for the next migration step: `builtin-yoi-local-ticket-backend-config`.
---

View File

@ -1,182 +0,0 @@
# Workspace orchestration panel UI design draft
## Position
The workspace panel should be a successor to the current `--multi` surface, not a two-pane composition of `--multi` plus the normal single-Pod UI. The default unit should be Ticket/action state; Pods are execution/detail resources that the user can attach to on demand.
The panel is not only a Ticket status dashboard. It is a workspace cockpit for reading Ticket state, handling human decision points, launching Intake/Orchestrator actions, and drilling into related Pods when needed.
## UI shape
```text
Workspace Orchestration Panel
├─ header: workspace, orchestrator state, companion state
├─ primary list: user-action-required Tickets / Intakes / Reviews
├─ detail pane: selected Ticket plan, current phase, related Pods, latest reports
├─ optional timeline/plan view: umbrella/child dependency lanes and phase progress
└─ composer: explicit target selector
```
The main list must not be a raw Pod list. It should sort by user decision priority:
1. Intake needs clarification or approval.
2. Ticket is ready for human Go.
3. Review or close decision is required.
4. Ticket is blocked by an explicit human/project decision.
5. Active implementation/review work.
6. Informational Pod status and restorable historical sessions.
## Visual design
The concrete look-and-feel should follow the existing TUI design language rather than inventing a second product surface. Reuse current terminal UI conventions for borders, list selection, status/action bars, composer placement, key-hint style, transient notices, color/status semantics, and attach/return behavior where possible.
The panel should feel like a more capable `--multi` workspace view, not a new unrelated dashboard. Any new visual treatment for Ticket/action rows or timeline lanes should be a small extension of existing list/detail components, so the single-Pod UI, current multi-Pod view, and workspace panel remain visually coherent.
## State sources and socket boundary
Unlike the normal single-Pod TUI, the panel should not treat a Pod socket transcript as its primary state. The default display can be built from local project records plus lightweight Pod state:
- `.yoi/tickets/` is authoritative for Ticket records, status, thread events, plans, implementation reports, reviews, closures, and umbrella/child relationships.
- Pod metadata/session state is used to show related live/restorable role Pods.
- Visible Pod/client operations are used at action boundaries: restore/spawn Orchestrator, launch Intake, notify/send to a Pod, or attach to a selected Pod.
So the panel itself is local-file-first and does not need to own a long-lived socket as its main state source. It still uses client/socket APIs for operations that actually target live Pods or attach to a transcript.
## UI intermediate representation
Use a thin UI-oriented intermediate representation. It is a render/action-dispatch contract, not a new Ticket backend, scheduler, or state machine.
```text
WorkspacePanelViewModel
- header: workspace label, companion state, orchestrator state, diagnostics
- rows: ordered display rows for user actions, Tickets, and secondary Pod/session entries
- detail: selected item detail blocks
- timeline: optional dependency/phase lanes
- composer: current target, placeholder, send eligibility, hints
```
Rows should carry only display-ready fields plus stable IDs for follow-up actions:
```text
PanelRow
- key: stable selection/action key
- kind: intake | ticket | review | blocked | pod | diagnostic
- title / subtitle
- status marker and priority
- next user action, if any
- related Ticket id/slug, if any
- related Pod names, if any
- disabled reason / key hints
```
Ticket-specific rows can be backed by a compact entry:
```text
TicketPanelEntry
- id / slug / title / status
- current phase: intake | preflight | implementing | reviewing | close_ready | blocked | closed
- next user action: clarify | go | review | close | wait | none
- parent/child relationship
- related role Pods and their live/restorable state
- latest plan / implementation report / review summary excerpts
- diagnostics and blocked reason
```
Keep this intentionally thin:
- Do not duplicate the Ticket backend state machine in the UI.
- Do not load full Markdown thread bodies into the default view model.
- Do not let render functions perform Ticket/Pod I/O.
- Do not execute mutations from stale view-model data alone; action dispatch should re-check the relevant Ticket/Pod authority before mutating.
## Relationship to `--multi` and normal TUI
Reuse the useful `--multi` behaviors:
- discover visible live/restorable Pods;
- show live/stopped/working state;
- choose a send/attach target;
- attach to a Pod/session and return to the workspace panel;
- keep background Pods alive when the panel exits.
Do not embed the normal single-Pod UI as a permanent second pane. The panel should drill into a selected Pod/session only when the user explicitly attaches. This avoids maintaining two simultaneous chat surfaces and keeps normal Pod history ownership unchanged.
## Composer target model
The composer has an explicit target selector:
- `Companion`: management chat for this workspace panel session.
- `New Intake`: send the composer body as the initial user input of a new Ticket Intake Pod.
- `Selected Ticket`: record a comment/decision/Go/review-style action against the selected Ticket, usually via Ticket tools or an Orchestrator notification.
- `Selected Pod`: advanced/direct send or attach path, not the main workflow.
Dynamic user text must be committed to the destination authority:
- Companion text goes to Companion history.
- New Intake text goes to the Intake Pod initial user input/history.
- Ticket decisions/comments go to Ticket records and/or the Pod that receives the actual message.
- Nothing should be injected only into hidden context.
## Action dispatcher boundary
The view model tells the UI what can be displayed and which actions appear available. It should not be the mutation authority. When a user triggers an action, the dispatcher should use the selected stable key to re-read or re-check the relevant authority before acting:
```text
selection key + action intent
→ re-check Ticket / Pod authority
→ run Ticket tool, client method, restore/spawn, send, or attach
→ rebuild view model from local files and Pod state
```
This keeps the panel responsive while avoiding stale UI state becoming a hidden source of truth.
## Timeline / Gantt-like view
A useful first version is a phase/dependency timeline rather than a date-based scheduler:
```text
yoi-local-ticket-backend-migration
├─ ✓ yoi-ticket-cli-parity
├─ ✓ builtin-yoi-local-ticket-backend-config
├─ ✓ migrate-ticket-storage-to-yoi-tickets
└─ ✓ remove-tickets-sh
workspace-orchestration-panel
├─ ▶ workspace-orchestration-panel-design
├─ ○ workspace-panel-orchestrator-lifecycle
├─ ○ workspace-panel-composer-targets
├─ ○ ticket-intake-orchestrator-handoff
└─ ○ workspace-panel-action-model
```
Per-ticket phase lanes can show progress without pretending the system has reliable time estimates:
```text
Ticket Intake Preflight Implement Review Close
panel design ▶ ○ ○ ○ ○
composer ○ ○ ○ ○ ○
action model ○ ○ ○ ○ ○
```
This captures what currently matters most: dependency order, gate state, responsible Pods, and human decision points. Duration/ETA scheduling can be added later if the backend starts recording enough authoritative timing data.
## Orchestrator lifecycle
When the workspace panel opens, restore or spawn a workspace Orchestrator named from the workspace directory, e.g. `<dir-name>-orchestrator`. The panel should display its state but should not own its lifetime; closing the panel leaves the Orchestrator running.
Intake Pods receive the Orchestrator handoff/notification target at launch so they can report clarified Ticket readiness without automatically starting implementation. The Orchestrator may prepare routing/preflight, but human Go remains an explicit action.
## Initial implementation boundary
Implement in this order:
1. Define the thin UI intermediate representation (`WorkspacePanelViewModel`, `PanelRow`, `TicketPanelEntry`, composer target/view state, timeline lane rows).
2. Add a local-file-first adapter that builds the view model from `.yoi/tickets/`, role-launch state, visible Pod metadata, and diagnostics.
3. Render the workspace panel using existing TUI visual conventions/components as much as possible.
4. Make the existing multi-Pod panel path consume enough of that model to show action-prioritized Ticket rows.
5. Add composer target selection for Companion vs New Intake.
6. Add action dispatch with re-checks against Ticket/Pod authority before mutation.
7. Add attach/drill-down and return behavior for related Pods.
8. Add the phase/dependency timeline view.
The existing `:ticket ...` commands remain as low-level fallback and debugging affordances, not the primary UX.

View File

@ -1,59 +0,0 @@
---
id: 20260605-210704-workspace-orchestration-panel-design
slug: workspace-orchestration-panel-design
title: Workspace orchestration panel design
status: closed
kind: task
priority: P1
labels: [tui, design, orchestration, panel]
created_at: 2026-06-05T21:07:04Z
updated_at: 2026-06-05T22:35:18Z
assignee: null
legacy_ticket: null
---
## Background
The next TUI should not be another small `:` command extension. The desired feature is a workspace-scoped orchestration panel with explicit Companion/Intake composer targets, background Orchestrator lifecycle, Intake handoff, and a user-action-oriented model.
## Requirements
Produce a design artifact that fixes:
- panel entrypoint and relation to current `--multi`;
- Companion lifecycle and identity;
- workspace Orchestrator lifecycle and naming rule (`<dir-name>-orchestrator`);
- composer target model:
- Companion;
- Ticket Intake;
- how Intake launch avoids writing the user request into the Companion/current Pod history;
- how Intake receives Orchestrator notification/handoff target;
- action model priorities:
- user response required;
- Intake draft ready;
- Ticket ready for Go;
- blocked/action-required;
- review/close/preflight decisions;
- active background work;
- informational Pod status;
- Go semantics for a fixed Ticket;
- relationship to existing `:ticket ...` command fallback;
- failure/diagnostic behavior;
- implementation sequence.
## Non-goals
- Code implementation.
- Generic scheduler/lease/queue.
- Automatic implementation without Go/authorization.
- Replacing Ticket workflows.
## Acceptance criteria
- A design artifact exists under this Ticket's `artifacts/` directory.
- The artifact defines the state/action model and responsibility boundaries clearly enough to implement child tickets.
- The artifact defines a thin UI intermediate representation between runtime/domain state and rendering, without turning it into a second Ticket backend or scheduler.
- The artifact states that the panel is local-file-first for display, using `.yoi/tickets/` as the Ticket authority and Pod/client sockets only for live operations such as spawn/restore/send/attach.
- The artifact specifies that the concrete visual design follows existing TUI conventions/components rather than inventing a separate dashboard style.
- The artifact explicitly preserves history/context rules: dynamic messages are committed to the destination Pod history, not injected into hidden context.
- Reviewer or parent approves the design before implementation child tickets proceed.

View File

@ -1,16 +0,0 @@
Completed the workspace orchestration panel design.
Final direction:
- The panel is a `--multi` successor, not a permanent split with the normal single-Pod TUI.
- The concrete look follows existing TUI conventions/components.
- The main unit is Ticket/action state; Pods are secondary execution/detail resources opened via attach/drill-down.
- Display is local-file-first from `.yoi/tickets/` plus lightweight Pod metadata/session state.
- Client/socket APIs are used at live operation boundaries: Orchestrator restore/spawn, Intake launch, send/notify, attach.
- The UI intermediate representation is intentionally thin: `WorkspacePanelViewModel`, `PanelRow`, `TicketPanelEntry`, composer state, and timeline lanes as render/action-dispatch data, not a second Ticket backend, scheduler, or state machine.
- The UI is not display-only; it is a Ticket-centric workspace cockpit for status, human decision points, Intake/Orchestrator actions, and Pod drill-down.
- First implementation should cover the end-to-end structure before detailed layout/display tuning.
Design artifact:
- `artifacts/workspace-panel-ui-design.md`
The implementation child tickets can proceed.

View File

@ -1,94 +0,0 @@
<!-- event: create author: yoi ticket at: 2026-06-05T21:07:04Z -->
## Created
Created by LocalTicketBackend create.
---
<!-- event: plan author: hare at: 2026-06-05T21:22:49Z -->
## Plan
Recorded an initial UI design draft from the panel discussion in `artifacts/workspace-panel-ui-design.md`.
Key direction:
- make the workspace panel a successor to `--multi`, not a permanent split with the normal single-Pod UI;
- use Ticket/action state as the default unit and attach to Pod/session details only on demand;
- provide an explicit composer target selector for Companion / New Intake / Selected Ticket / advanced Selected Pod;
- add a Gantt-like phase/dependency timeline based on Ticket gates and umbrella/child ordering rather than date estimates.
This is a design draft for review/iteration, not implementation approval for the child panel tickets yet.
---
<!-- event: decision author: hare at: 2026-06-05T21:32:54Z -->
## Decision
Updated the workspace panel design draft per UI direction:
- concrete look-and-feel should follow the existing TUI design language/components rather than introducing a separate dashboard style;
- the panel should remain a `--multi` successor with attach/drill-down, not a permanent split with the normal single-Pod UI;
- rendering should go through a UI intermediate representation (`WorkspacePanelViewModel`, `TicketPanelEntry`, action rows, composer view state, timeline lanes) built from Ticket/Pod/runtime sources before widgets render.
The design ticket acceptance criteria now explicitly require both the UI intermediate representation and existing-TUI visual convention constraints.
---
<!-- event: decision author: hare at: 2026-06-05T22:29:53Z -->
## Decision
Updated the workspace panel design to keep the UI intermediate representation intentionally thin.
Direction recorded:
- the panel is local-file-first for display: `.yoi/tickets/` is the Ticket authority, with Pod metadata/session state only for related live/restorable status;
- sockets/client APIs are used at operation boundaries such as spawn/restore/send/attach, not as the panel's primary state source;
- the intermediate representation is a render/action-dispatch contract (`WorkspacePanelViewModel`, rows, compact Ticket entries), not a second Ticket backend, scheduler, or UI-owned state machine;
- the panel is not display-only: it is a Ticket-centric workspace cockpit for status, decision points, Intake/Orchestrator actions, and Pod drill-down.
---
<!-- event: review author: hare at: 2026-06-05T22:35:18Z status: approve -->
## Review: approve
User approved the current workspace panel design direction and asked to implement a first end-to-end version before detailed layout/display tuning.
Approved implementation direction:
- follow the existing TUI visual language/components;
- keep the UI intermediate representation thin and simple;
- make panel display local-file-first from `.yoi/tickets/`;
- use client/socket APIs only at live operation boundaries such as spawn/restore/send/attach;
- build the first pass broadly enough to exercise the whole flow, then refine layout and displayed details afterward.
---
<!-- event: close author: hare at: 2026-06-05T22:35:18Z status: closed -->
## Closed
Completed the workspace orchestration panel design.
Final direction:
- The panel is a `--multi` successor, not a permanent split with the normal single-Pod TUI.
- The concrete look follows existing TUI conventions/components.
- The main unit is Ticket/action state; Pods are secondary execution/detail resources opened via attach/drill-down.
- Display is local-file-first from `.yoi/tickets/` plus lightweight Pod metadata/session state.
- Client/socket APIs are used at live operation boundaries: Orchestrator restore/spawn, Intake launch, send/notify, attach.
- The UI intermediate representation is intentionally thin: `WorkspacePanelViewModel`, `PanelRow`, `TicketPanelEntry`, composer state, and timeline lanes as render/action-dispatch data, not a second Ticket backend, scheduler, or state machine.
- The UI is not display-only; it is a Ticket-centric workspace cockpit for status, human decision points, Intake/Orchestrator actions, and Pod drill-down.
- First implementation should cover the end-to-end structure before detailed layout/display tuning.
Design artifact:
- `artifacts/workspace-panel-ui-design.md`
The implementation child tickets can proceed.
---

View File

@ -1,71 +0,0 @@
---
id: 20260605-210703-workspace-orchestration-panel
slug: workspace-orchestration-panel
title: Workspace orchestration panel
status: open
kind: task
priority: P1
labels: [tui, ticket, orchestration, panel]
created_at: 2026-06-05T21:07:03Z
updated_at: 2026-06-05T21:09:19Z
assignee: null
legacy_ticket: null
---
## Background
The current TUI Ticket role commands are a command-driven MVP. They prove that TUI can launch fixed Ticket-role Pods through the client launcher, but they do not provide the desired workspace orchestration experience.
The desired panel is a workspace-scoped orchestration UI, closer to an improved `--multi` surface, but organized around Ticket/Intake/Orchestrator action state rather than raw Pod idle/working state.
## Goal
Build a workspace orchestration panel where the user can:
- chat with a Companion management interface;
- send new requests directly to Ticket Intake without putting them into the Companion/current Pod session;
- have a workspace Orchestrator restored/spawned in the background when the panel opens;
- see user-action-required Intake/Ticket states before generic Pod status;
- give a simple Go signal once an Intake-prepared Ticket is fixed and understood by the human.
## Target model
- Panel is workspace-scoped.
- Companion is the foreground management chat target.
- Orchestrator is a background coordinator Pod named from the workspace directory, e.g. `<dir-name>-orchestrator`.
- Intake is spawned per user request from the composer target.
- Intake receives Orchestrator handoff/notification target at launch.
- Panel close does not stop Orchestrator.
- The primary list is an action model, not a Pod status list.
## Child tickets
1. `workspace-orchestration-panel-design`
- Produce the detailed architecture/design artifact for the panel.
2. `workspace-panel-orchestrator-lifecycle`
- Restore/spawn workspace Orchestrator when the panel opens; leave it running on panel close.
3. `workspace-panel-composer-targets`
- Add composer targets for Companion vs Ticket Intake; Intake sends the composer body directly to a new Intake Pod.
4. `ticket-intake-orchestrator-handoff`
- Define and implement Intake -> Orchestrator handoff/notification contract.
5. `workspace-panel-action-model`
- Display user-action-required Intake/Ticket states ahead of raw Pod status and support Go/Defer/Edit-style decisions.
## Non-goals
- Generic scheduler/lease/queue automation.
- Arbitrary role registry.
- Replacing the single-Pod TUI.
- Stopping background Orchestrator on panel close.
- Bypassing Ticket Intake / Routing / Preflight / Review gates.
## Acceptance criteria
- Child tickets are created and sequenced.
- The design ticket fixes the panel responsibility boundary before implementation tickets proceed.
- Implementation child tickets use existing Ticket tools/config/launcher where possible.
- The command-driven `:ticket ...` path remains available as a low-level fallback, but the panel design does not depend on users typing commands for the main flow.

View File

@ -1,34 +0,0 @@
<!-- event: create author: yoi ticket at: 2026-06-05T21:07:03Z -->
## Created
Created by LocalTicketBackend create.
---
<!-- event: plan author: hare at: 2026-06-05T21:09:19Z -->
## Plan
Plan: design the workspace orchestration panel before implementation.
The panel is not a `:ticket` command extension. It is a workspace-scoped orchestration surface with:
- Companion foreground management chat;
- Ticket Intake composer target that sends user input directly to Intake, not Companion history;
- background workspace Orchestrator restored/spawned as `<dir-name>-orchestrator`;
- Intake -> Orchestrator handoff;
- an action model prioritizing human-required Ticket/Intake decisions over raw Pod idle state.
Implementation is split into child tickets:
1. `workspace-orchestration-panel-design`
2. `workspace-panel-orchestrator-lifecycle`
3. `workspace-panel-composer-targets`
4. `ticket-intake-orchestrator-handoff`
5. `workspace-panel-action-model`
Existing `:ticket ...` commands remain as low-level fallback, not the main UX.
---

View File

@ -1,46 +0,0 @@
---
id: 20260605-210704-ticket-intake-orchestrator-handoff
slug: ticket-intake-orchestrator-handoff
title: Ticket intake to orchestrator handoff
status: open
kind: task
priority: P1
labels: [ticket, intake, orchestrator, handoff]
created_at: 2026-06-05T21:07:04Z
updated_at: 2026-06-05T21:07:04Z
assignee: null
legacy_ticket: null
---
## Background
Intake can create or refine Tickets, but the next Orchestrator routing step is currently manual. The workspace panel needs a safe handoff from Intake to the workspace Orchestrator.
## Requirements
- Define a machine-readable handoff contract from Intake to Orchestrator.
- Handoff should include:
- created/updated Ticket id or slug;
- readiness;
- needs_preflight;
- risk flags when available;
- whether user Go/approval is required;
- summary of the Intake result.
- Intake launch should receive the Orchestrator notification target in its initial input or durable metadata, not hidden context.
- Orchestrator should be notified through an existing durable/observable channel where possible.
- Handoff should not automatically start implementation.
- The panel should be able to show a Ticket-ready-for-Go or routing-needed state after Intake.
## Non-goals
- Full scheduler/queue/lease.
- Automatic coder/reviewer spawn.
- Generic notification bus redesign.
- External tracker integration.
## Acceptance criteria
- Intake can hand off a newly created/refined Ticket to the workspace Orchestrator without the user manually retyping the Ticket id.
- Handoff is visible/auditable in history or Ticket records.
- Orchestrator routing is triggered or queued only within the agreed Go/authorization boundary.
- Tests or fixtures cover handoff payload parsing/formatting where practical.

View File

@ -1,7 +0,0 @@
<!-- event: create author: yoi ticket at: 2026-06-05T21:07:04Z -->
## Created
Created by LocalTicketBackend create.
---

View File

@ -1,100 +0,0 @@
# Delegation intent: workspace panel action model
## Classification
`implementation-ready` as the first implementation slice after design approval.
The design ticket is closed and approved. The first pass should implement a simple, testable UI/action model that the existing multi-Pod dashboard can consume before we tune layout details.
## Intent
Add a thin workspace panel ViewModel/action model for Ticket-centric display and prioritization. This should be a render/action-dispatch contract, not a new Ticket backend, scheduler, or UI-owned state machine.
The panel should be local-file-first for display:
- read Ticket records from `.yoi/tickets/` through the Rust Ticket backend/config APIs;
- combine with existing Pod list data for related/background Pod status where practical;
- keep socket/client operations out of the model layer.
## Worktree / branch
- worktree: `/home/hare/Projects/yoi/.worktree/workspace-panel-action-model`
- branch: `work/workspace-panel-action-model`
This ticket may edit tracked `.yoi/tickets` records for implementation reports if needed. Do not read or edit `.yoi/memory/`.
## Requirements
- Define plain UI/data model types for the workspace panel, for example:
- `WorkspacePanelViewModel`;
- `PanelRow` / row key;
- `TicketPanelEntry`;
- action priority / next user action;
- optional timeline/dependency lane structs if useful for the first slice.
- Keep the model independent from terminal rendering and from live socket I/O.
- Add an adapter that builds Ticket/action rows from local Ticket backend state.
- Reuse existing `PodList`/multi-Pod data as lower-priority background rows where practical, without replacing or duplicating Pod registry logic.
- Prioritize rows roughly as:
1. Intake/user reply/approval needed;
2. Ticket ready for Go;
3. review/close decision required;
4. blocked/action-required;
5. active implementation/review/background work;
6. passive Pod/session information.
- Use simple heuristics from current Ticket records/thread roles; do not invent a full scheduler or hidden state machine.
- Preserve human authorization boundaries: a displayed Go/Review/Close action is an affordance, not automatic implementation authorization.
- Integrate enough with the current `--multi` dashboard to display action-prioritized Ticket rows above passive Pod rows, while keeping current Pod attach/direct-send behavior intact as much as possible.
- Follow existing TUI visual conventions; do not spend this ticket on fine layout tuning.
## Non-goals
- Final layout polish or detailed display tuning.
- Orchestrator restore/spawn lifecycle implementation.
- Composer target switching / New Intake send path.
- Intake -> Orchestrator handoff contract.
- Scheduler/lease/queue automation.
- Replacing the normal single-Pod TUI.
## Current code map
- `crates/tui/src/multi_pod.rs`
- Current `--multi` dashboard state, reload, sections, rendering, direct send/open behavior.
- Integrate only enough to show Ticket/action rows and preserve existing behavior.
- `crates/tui/src/pod_list.rs`
- Existing plain Pod list model. Reuse rather than duplicating live/stored Pod discovery semantics.
- `crates/tui/src/lib.rs` / `crates/tui/src/single_pod.rs`
- Current `LaunchMode::Multi` path enters `single_pod::run_multi(...)`.
- `crates/ticket/src/lib.rs`, `crates/ticket/src/config.rs`
- Local Ticket backend/config API for reading `.yoi/tickets/`.
- `crates/yoi/src/ticket_cli.rs`
- Examples of backend listing/status formatting; do not shell out.
- Design artifact:
- `.yoi/tickets/closed/20260605-210704-workspace-orchestration-panel-design/artifacts/workspace-panel-ui-design.md`
## Validation
Run at least:
- `cargo test -p tui multi` or targeted TUI tests covering the new model/render integration;
- `cargo test -p ticket` if Ticket backend API usage changes;
- `cargo test -p yoi ticket` if CLI/config interactions 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;
- model/types added;
- how Ticket rows are derived and prioritized;
- how Pod rows are preserved as secondary/background information;
- tests updated/added;
- validation results;
- known follow-up layout/display tuning items.

View File

@ -1,49 +0,0 @@
---
id: 20260605-210704-workspace-panel-action-model
slug: workspace-panel-action-model
title: Workspace panel action model
status: open
kind: task
priority: P1
labels: [tui, ticket, orchestration, panel]
created_at: 2026-06-05T21:07:04Z
updated_at: 2026-06-05T22:35:56Z
assignee: null
legacy_ticket: null
---
## Background
The workspace panel should not be a plain Pod idle/working list. It should prioritize the items where the user needs to decide, respond, approve, or inspect evidence.
## Requirements
- Define and implement a workspace action model that can rank/display:
- Intake needs user reply;
- Intake draft ready;
- Ticket ready for Go;
- requirements sync needed;
- preflight needed;
- spike needed/running;
- implementation running;
- review needed;
- blocked/action-required;
- close-ready;
- background informational Pod status.
- Prefer Ticket/routing/intake state over raw Pod idle state.
- Provide Go/Defer/Edit-style actions where supported by preceding tickets.
- Preserve explicit human authorization boundaries.
- Do not treat Pod completion notifications as authority; verify Ticket/Pod/output state.
## Non-goals
- Scheduler/lease/queue automation.
- Full implementation of all role actions if earlier handoff/lifecycle tickets are not landed.
- Generic issue tracker UI.
## Acceptance criteria
- Panel displays user-action-required items above passive background Pods.
- Ticket-ready Go action is easy to trigger but does not bypass Orchestrator routing/preflight gates.
- Action model is testable as plain data independent of terminal rendering.
- Existing multi-Pod status data can still be shown as lower-priority background information.

View File

@ -1,20 +0,0 @@
<!-- event: create author: yoi ticket at: 2026-06-05T21:07:04Z -->
## Created
Created by LocalTicketBackend create.
---
<!-- event: plan author: hare at: 2026-06-05T22:35:56Z -->
## Plan
Preflight result: `implementation-ready` as the first implementation slice after design approval.
Implementation should add a thin, testable workspace panel ViewModel/action model and integrate it enough into the current `--multi` dashboard to show Ticket/action rows above passive Pod rows. The model should be local-file-first from `.yoi/tickets/`, reuse existing Pod list data for background Pod state, avoid live socket I/O in the model layer, and leave final layout/display tuning to follow-up adjustments after the first end-to-end pass.
Detailed delegation intent is recorded in `artifacts/delegation-intent.md`.
---

View File

@ -1,46 +0,0 @@
---
id: 20260605-210704-workspace-panel-composer-targets
slug: workspace-panel-composer-targets
title: Workspace panel composer targets
status: open
kind: task
priority: P1
labels: [tui, composer, intake, panel]
created_at: 2026-06-05T21:07:04Z
updated_at: 2026-06-05T21:07:04Z
assignee: null
legacy_ticket: null
---
## Background
The workspace panel composer must let users choose whether a message goes to the Companion management chat or directly to Ticket Intake.
The key UX requirement is that a request sent to Intake must not be appended to the Companion/current Pod session history.
## Requirements
- Add a composer target model for the workspace panel:
- Companion;
- Ticket Intake.
- Companion target sends normal messages to the Companion Pod/session.
- Intake target launches a new Intake role Pod and sends the composer body as its first `Method::Run` input.
- Intake target must not send the body to Companion/current Pod history.
- Empty Intake messages are rejected.
- The UI clearly shows the active composer target.
- User can switch/cancel target without losing typed text where practical.
- Use Ticket role launcher rather than constructing spawn/profile/workflow prompt content inside UI.
## Non-goals
- Generic arbitrary target routing.
- Scheduler/queue.
- Intake -> Orchestrator handoff implementation.
- Action queue display.
## Acceptance criteria
- User can choose Companion or Intake before pressing Enter.
- Intake path creates a new Intake role launch with the typed body as dynamic run input.
- Existing Companion history does not receive the Intake body.
- Tests cover target switching and Enter behavior for both targets where practical.

View File

@ -1,7 +0,0 @@
<!-- event: create author: yoi ticket at: 2026-06-05T21:07:04Z -->
## Created
Created by LocalTicketBackend create.
---

View File

@ -1,43 +0,0 @@
---
id: 20260605-210704-workspace-panel-orchestrator-lifecycle
slug: workspace-panel-orchestrator-lifecycle
title: Workspace panel orchestrator lifecycle
status: open
kind: task
priority: P1
labels: [tui, pod, orchestrator, panel]
created_at: 2026-06-05T21:07:04Z
updated_at: 2026-06-05T21:07:04Z
assignee: null
legacy_ticket: null
---
## Background
The workspace orchestration panel needs a background Orchestrator Pod that is restored or spawned when the panel opens and remains alive after the panel closes.
## Requirements
- Derive Orchestrator Pod name from workspace directory, e.g. `<dir-name>-orchestrator`.
- On panel open:
- restore if restorable;
- attach/observe if already live;
- spawn if missing and permitted.
- Use `.yoi/ticket.config.toml` role profile for `orchestrator`.
- Use the Ticket role launcher where practical.
- Panel close must not stop the Orchestrator.
- Surface lifecycle diagnostics in the panel.
- Do not make Orchestrator the foreground composer target by default; Companion remains foreground management chat.
## Non-goals
- Full panel UI layout.
- Intake handoff contract.
- Scheduler/lease/queue.
- Automatic coder/reviewer spawning.
## Acceptance criteria
- Workspace panel startup can ensure an Orchestrator Pod exists or report why it cannot.
- Orchestrator lifecycle uses existing Pod restore/spawn semantics and does not duplicate registry logic.
- Tests cover name derivation and restore/spawn decision logic where practical.

View File

@ -1,7 +0,0 @@
<!-- event: create author: yoi ticket at: 2026-06-05T21:07:04Z -->
## Created
Created by LocalTicketBackend create.
---

View File

@ -268,7 +268,7 @@ Validation:
- cargo fmt --check
- cargo check --workspace
- cargo test ...
- yoi ticket doctor
- ./tickets.sh doctor
Parent decision needed:
- none / specific question

View File

@ -62,7 +62,7 @@ Intake は以下を行う。
Intake は `TicketReview`, `TicketStatus`, `TicketClose` を通常使わない。review / status transition / close は Orchestrator または reviewer / maintainer workflow の責務である。
Ticket tools が利用できない環境では、勝手に file write で代替しない。ユーザーまたは Orchestrator に「Ticket tools がないため materialize できない」と報告し、必要なら `yoi ticket` を使える人間/親 workflow に戻す。
Ticket tools が利用できない環境では、勝手に file write で代替しない。ユーザーまたは Orchestrator に「Ticket tools がないため materialize できない」と報告し、必要なら `tickets.sh` を使う人間/親 workflow に戻す。
## 手順

View File

@ -1,5 +1,5 @@
---
description: ticket を実装委譲する前に、要件・前提・設計境界・反証観点を同期し、Ticket thread に記録する preflight フロー
description: ticket を実装委譲する前に、要件・前提・設計境界・反証観点を同期し、tickets.sh に記録する preflight フロー
model_invokation: true
user_invocable: true
requires: []
@ -23,12 +23,12 @@ yoi プロジェクトで ticket を実装に渡す前に、要件・前提・
小さなバグ修正や仕様が明確な局所変更では、この Workflow は省略してよい。ただし省略理由が曖昧な場合は preflight する。
## Ticket 記録方針
## tickets.sh 運用方針
作業管理の authority は `.yoi/tickets/` に保存される Ticket と git history である。preflight の結果は、口頭の会話だけで終わらせず、Ticket tool または `yoi ticket ...`ticket の `thread.md` または `item.md` に残す。
作業管理の authority は `work-items/` と `tickets.sh` である。preflight の結果は、口頭の会話だけで終わらせず、ticket の `thread.md` または `item.md` に残す。
- 新規の前提・要件・受け入れ条件は、必要に応じて `item.md` を更新する。
- 調査結果・実装前 plan は `TicketComment` または `yoi ticket comment <ticket> --role plan --file <file>` で残す。
- 調査結果・実装前 plan は `./tickets.sh comment <ticket> --role plan --file <file>` で残す。
- 採用/却下した設計判断、実装停止判断、仕様同期の結論は `--role decision` で残す。
- 実装に入ってよい状態になったら、その根拠を intent packet として ticket thread に残す。
- 仕様が未決定なら、実装 ticket にせず requirements-sync / spike / design ticket として切り分ける。

View File

@ -47,20 +47,20 @@ docs-only など Nix build の価値が低い変更で省略する場合は、
## Work item / Ticket の運用について
作業管理は `.yoi/tickets/` に保存される Ticket と `yoi ticket ...` コマンド / Ticket tools を正とする。時系列・状態遷移の最終的な根拠は git history なので、work item の作成・更新・レビュー・完了は Ticket 操作と commit で表現する。
作業管理は `work-items/` と `tickets.sh` を正とする。時系列・状態遷移の最終的な根拠は git history なので、work item の作成・更新・レビュー・完了はファイル操作と commit で表現する。
### 基本コマンド
- 新規作成: `yoi ticket create --title "..." [--slug slug] [--kind task] [--priority P2] [--label a,b]`
- 一覧: `yoi ticket list [--status open|pending|closed|all]`
- 詳細: `yoi ticket show <id-or-slug>`
- コメント / 計画 / 判断 / 実装報告: `yoi ticket comment <id-or-slug> [--role comment|plan|decision|implementation_report] [--file path]`
- レビュー記録: `yoi ticket review <id-or-slug> --approve|--request-changes [--file path]`
- 状態変更: `yoi ticket status <id-or-slug> open|pending`
- 完了: `yoi ticket close <id-or-slug> [--resolution text|--file path]`
- 整合性確認: `yoi ticket doctor`
- 新規作成: `./tickets.sh create --title "..." [--slug slug] [--kind task] [--priority P2] [--label a,b]`
- 一覧: `./tickets.sh list [--status open|pending|closed|all]`
- 詳細: `./tickets.sh show <id-or-slug>`
- コメント / 計画 / 判断 / 実装報告: `./tickets.sh comment <id-or-slug> [--role comment|plan|decision|implementation_report] [--file path]`
- レビュー記録: `./tickets.sh review <id-or-slug> --approve|--request-changes [--file path]`
- 状態変更: `./tickets.sh status <id-or-slug> open|pending|closed`
- 完了: `./tickets.sh close <id-or-slug> [--resolution text|--file path]`
- 整合性確認: `./tickets.sh doctor`
`yoi ticket` は typed Ticket backend 経由で `.yoi/tickets/{open,pending,closed}/<id>/` 配下の `item.md`、`thread.md`、`artifacts/` を扱う。完了時は `resolution.md` も作られる。手でファイルを作るより、原則として `yoi ticket` または Ticket tools を使うこと。
`tickets.sh` は `work-items/{open,pending,closed}/<id>/` 配下の `item.md`、`thread.md`、`artifacts/` を扱う。完了時は `resolution.md` も作られる。手でファイルを作るより、原則としてスクリプトを使うこと。
### Work item の粒度
@ -71,10 +71,10 @@ docs-only など Nix build の価値が低い変更で省略する場合は、
### ライフサイクル
- 作成: `yoi ticket create ...` で `.yoi/tickets/open/...` を作成し、必要な前提を書いて commit する。
- 詳細化・前提変更: `item.md` を更新し、必要に応じて `yoi ticket comment` で `thread.md` に経緯を残して commit する。
- レビュー: `yoi ticket review <id-or-slug> --approve|--request-changes` で `thread.md` にレビュー結果を追記して commit する。
- 完了: `yoi ticket close <id-or-slug>` で `.yoi/tickets/closed/...` に移動し、`resolution.md` と完了状態を commit する。
- 作成: `./tickets.sh create ...` で `work-items/open/...` を作成し、必要な前提を書いて commit する。
- 詳細化・前提変更: `item.md` を更新し、必要に応じて `./tickets.sh comment` で `thread.md` に経緯を残して commit する。
- レビュー: `./tickets.sh review <id-or-slug> --approve|--request-changes` で `thread.md` にレビュー結果を追記して commit する。
- 完了: `./tickets.sh close <id-or-slug>` で `work-items/closed/...` に移動し、`resolution.md` と完了状態を commit する。
worktree と併用して作業を進める場合、必ずブランチを切る前に対象 work item を作成・詳細化して commit してから切ること。

1
Cargo.lock generated
View File

@ -4778,7 +4778,6 @@ dependencies = [
"serde_json",
"session-store",
"tempfile",
"ticket",
"tokio",
"tui",
]

View File

@ -68,12 +68,12 @@ Key docs:
## 5. Development
This repository dogfoods Yoi to develop Yoi. Work is tracked through `.yoi/tickets/` and `yoi ticket ...`; git history plus Ticket files are the authoritative project record.
This repository dogfoods Yoi to develop Yoi. Work is tracked through `work-items/` and `./tickets.sh`; git history plus Ticket files are the authoritative project record.
Common checks:
```sh
yoi ticket doctor
./tickets.sh doctor
git diff --check
cargo fmt --check
cargo check --workspace --all-targets

View File

@ -15,8 +15,8 @@ Does not own:
- memory-specific policy (`memory`)
- workflow-specific policy (`workflow`)
- Ticket backend policy (`ticket`)
- product CLI command shape (`yoi`)
- work item script behavior (`tickets.sh`)
## Design notes

View File

@ -16,7 +16,7 @@ Owns:
Does not own:
- authoritative project records (`.yoi/tickets/`, git history)
- authoritative project records (`work-items/`, git history)
- normal Pod turn orchestration (`llm-worker`)
- product CLI command shape (`yoi`)
- curated workflow definitions (`workflow`)

View File

@ -7,10 +7,7 @@
use std::path::{Path, PathBuf};
use ticket::{
LocalTicketBackend,
config::{DEFAULT_TICKET_BACKEND_RELATIVE_PATH, TicketConfig},
tool::TICKET_TOOL_NAMES,
tool::ticket_tools,
LocalTicketBackend, config::TicketConfig, tool::TICKET_TOOL_NAMES, tool::ticket_tools,
};
use crate::feature::{
@ -41,9 +38,9 @@ impl TicketFeature {
pub fn for_workspace(workspace: impl AsRef<Path>) -> 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(config.backend.root),
Err(error) => Self {
backend_root: workspace.join(DEFAULT_TICKET_BACKEND_RELATIVE_PATH),
backend_root: workspace.join("work-items"),
config_error: Some(error.to_string()),
},
}
@ -158,7 +155,7 @@ mod tests {
use crate::hook::HookRegistryBuilder;
use tempfile::TempDir;
fn make_ticket_root(root: &Path) {
fn make_work_items(root: &Path) {
std::fs::create_dir_all(root.join("open")).unwrap();
std::fs::create_dir_all(root.join("pending")).unwrap();
std::fs::create_dir_all(root.join("closed")).unwrap();
@ -194,9 +191,9 @@ mod tests {
}
#[test]
fn installs_ticket_tools_when_default_root_is_usable() {
fn installs_ticket_tools_when_work_items_root_is_usable() {
let temp = TempDir::new().unwrap();
make_ticket_root(&temp.path().join(DEFAULT_TICKET_BACKEND_RELATIVE_PATH));
make_work_items(&temp.path().join("work-items"));
let mut pending_tools = Vec::new();
let mut hooks = HookRegistryBuilder::default();
let report = FeatureRegistryBuilder::new()
@ -217,14 +214,13 @@ mod tests {
temp.path(),
r#"
[backend]
provider = "builtin:yoi_local"
root = "tickets"
[roles.coder]
profile = "project:coder"
"#,
);
make_ticket_root(&temp.path().join("tickets"));
make_work_items(&temp.path().join("tickets"));
let feature = ticket_tools_feature(temp.path());
assert_eq!(feature.backend_root(), temp.path().join("tickets"));
@ -242,7 +238,7 @@ profile = "project:coder"
#[test]
fn malformed_ticket_config_fails_closed() {
let temp = TempDir::new().unwrap();
make_ticket_root(&temp.path().join(DEFAULT_TICKET_BACKEND_RELATIVE_PATH));
make_work_items(&temp.path().join("work-items"));
write_ticket_config(
temp.path(),
r#"
@ -264,31 +260,6 @@ profile = "inherit"
assert!(message.contains("unknown Ticket role `operator`"));
}
#[test]
fn unsupported_ticket_backend_provider_fails_closed() {
let temp = TempDir::new().unwrap();
make_ticket_root(&temp.path().join(DEFAULT_TICKET_BACKEND_RELATIVE_PATH));
write_ticket_config(
temp.path(),
r#"
[backend]
provider = "github"
"#,
);
let mut pending_tools = Vec::new();
let mut hooks = HookRegistryBuilder::default();
let report = FeatureRegistryBuilder::new()
.with_module(ticket_tools_feature(temp.path()))
.install_into_pending(&mut pending_tools, &mut hooks);
assert!(pending_tools.is_empty());
assert!(report.reports[0].installed_tools.is_empty());
assert_eq!(report.reports[0].diagnostics.len(), 1);
let message = &report.reports[0].diagnostics[0].message;
assert!(message.contains("Ticket tools not registered"));
assert!(message.contains("unsupported Ticket backend provider `github`"));
}
#[test]
fn does_not_register_ticket_tools_when_root_is_missing() {
let temp = TempDir::new().unwrap();
@ -313,7 +284,7 @@ provider = "github"
#[test]
fn does_not_register_ticket_tools_when_root_lacks_status_dirs() {
let temp = TempDir::new().unwrap();
std::fs::create_dir_all(temp.path().join(DEFAULT_TICKET_BACKEND_RELATIVE_PATH)).unwrap();
std::fs::create_dir_all(temp.path().join("work-items")).unwrap();
let mut pending_tools = Vec::new();
let mut hooks = HookRegistryBuilder::default();
let report = FeatureRegistryBuilder::new()

View File

@ -14,8 +14,6 @@ use serde::{Deserialize, Serialize};
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";
#[derive(Debug, Error)]
pub enum TicketConfigError {
@ -101,44 +99,23 @@ impl TicketConfig {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TicketBackendConfig {
pub provider: TicketBackendProvider,
pub kind: TicketBackendKind,
pub root: PathBuf,
}
impl TicketBackendConfig {
pub fn default_for_workspace(workspace_root: &Path) -> Self {
Self {
provider: TicketBackendProvider::BuiltinYoiLocal,
root: workspace_root.join(DEFAULT_TICKET_BACKEND_RELATIVE_PATH),
kind: TicketBackendKind::Local,
root: workspace_root.join("work-items"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TicketBackendProvider {
#[serde(rename = "builtin:yoi_local")]
BuiltinYoiLocal,
}
impl TicketBackendProvider {
pub fn as_str(self) -> &'static str {
match self {
Self::BuiltinYoiLocal => "builtin:yoi_local",
}
}
pub fn parse(value: &str) -> Option<Self> {
match value {
"builtin:yoi_local" => Some(Self::BuiltinYoiLocal),
_ => None,
}
}
}
impl fmt::Display for TicketBackendProvider {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
#[serde(rename_all = "snake_case")]
pub enum TicketBackendKind {
Local,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
@ -410,12 +387,7 @@ impl RawTicketConfig {
roles.inner.insert(role, raw_role.resolve(role));
}
Ok(TicketConfig {
backend: self.backend.resolve(workspace_root).map_err(|message| {
TicketConfigError::Invalid {
path: path.to_path_buf(),
message,
}
})?,
backend: self.backend.resolve(workspace_root),
roles,
})
}
@ -425,42 +397,18 @@ impl RawTicketConfig {
#[serde(deny_unknown_fields)]
struct RawBackendConfig {
#[serde(default)]
provider: Option<String>,
#[serde(default)]
kind: Option<String>,
kind: Option<TicketBackendKind>,
#[serde(default)]
root: Option<PathBuf>,
}
impl RawBackendConfig {
fn resolve(self, workspace_root: &Path) -> Result<TicketBackendConfig, String> {
let provider = match (self.provider, self.kind) {
(Some(provider), None) => TicketBackendProvider::parse(&provider).ok_or_else(|| {
format!(
"unsupported Ticket backend provider `{provider}`; supported provider: `builtin:yoi_local`"
)
})?,
(None, Some(kind)) if kind == "local" => TicketBackendProvider::BuiltinYoiLocal,
(None, Some(kind)) => {
return Err(format!(
"unsupported legacy Ticket backend kind `{kind}`; use provider = \"builtin:yoi_local\""
));
}
(None, None) => TicketBackendProvider::BuiltinYoiLocal,
(Some(_), Some(_)) => {
return Err(
"backend.provider and legacy backend.kind are mutually exclusive; use provider = \"builtin:yoi_local\""
.to_string(),
);
}
};
let root = self
.root
.unwrap_or_else(|| PathBuf::from(DEFAULT_TICKET_BACKEND_RELATIVE_PATH));
Ok(TicketBackendConfig {
provider,
fn resolve(self, workspace_root: &Path) -> TicketBackendConfig {
let root = self.root.unwrap_or_else(|| PathBuf::from("work-items"));
TicketBackendConfig {
kind: self.kind.unwrap_or(TicketBackendKind::Local),
root: join_if_relative(workspace_root, &root),
})
}
}
}
@ -510,14 +458,8 @@ mod tests {
let temp = TempDir::new().unwrap();
let config = TicketConfig::load_workspace(temp.path()).unwrap();
assert_eq!(
config.backend.provider,
TicketBackendProvider::BuiltinYoiLocal
);
assert_eq!(
config.backend.root,
temp.path().join(DEFAULT_TICKET_BACKEND_RELATIVE_PATH)
);
assert_eq!(config.backend.kind, TicketBackendKind::Local);
assert_eq!(config.backend.root, temp.path().join("work-items"));
for role in TicketRole::ALL {
let role_config = config.role(role);
assert_eq!(role_config.profile.as_str(), "inherit");
@ -533,8 +475,8 @@ mod tests {
temp.path(),
r#"
[backend]
provider = "builtin:yoi_local"
root = "custom-tickets"
kind = "local"
root = "custom-work-items"
[roles.intake]
profile = "project:intake"
@ -564,11 +506,7 @@ workflow = "ticket-orchestrator-routing"
);
let config = TicketConfig::load_workspace(temp.path()).unwrap();
assert_eq!(
config.backend.provider,
TicketBackendProvider::BuiltinYoiLocal
);
assert_eq!(config.backend.root, temp.path().join("custom-tickets"));
assert_eq!(config.backend.root, temp.path().join("custom-work-items"));
assert_eq!(
config.profile_for(TicketRole::Intake).as_str(),
"project:intake"
@ -638,47 +576,7 @@ system_instruction = "$workspace/not-supported"
}
#[test]
fn legacy_backend_kind_local_is_transitional_alias() {
let temp = TempDir::new().unwrap();
write_config(
temp.path(),
r#"
[backend]
kind = "local"
root = "legacy-tickets"
"#,
);
let config = TicketConfig::load_workspace(temp.path()).unwrap();
assert_eq!(
config.backend.provider,
TicketBackendProvider::BuiltinYoiLocal
);
assert_eq!(config.backend_root(), temp.path().join("legacy-tickets"));
}
#[test]
fn unsupported_backend_provider_is_rejected() {
let temp = TempDir::new().unwrap();
write_config(
temp.path(),
r#"
[backend]
provider = "github"
"#,
);
let error = TicketConfig::load_workspace(temp.path()).unwrap_err();
assert!(
error
.to_string()
.contains("unsupported Ticket backend provider `github`")
);
assert!(error.to_string().contains("builtin:yoi_local"));
}
#[test]
fn unsupported_legacy_backend_kind_is_rejected() {
fn unsupported_backend_kind_is_rejected() {
let temp = TempDir::new().unwrap();
write_config(
temp.path(),
@ -689,31 +587,8 @@ kind = "github"
);
let error = TicketConfig::load_workspace(temp.path()).unwrap_err();
assert!(
error
.to_string()
.contains("unsupported legacy Ticket backend kind `github`")
);
}
#[test]
fn backend_provider_and_legacy_kind_are_mutually_exclusive() {
let temp = TempDir::new().unwrap();
write_config(
temp.path(),
r#"
[backend]
provider = "builtin:yoi_local"
kind = "local"
"#,
);
let error = TicketConfig::load_workspace(temp.path()).unwrap_err();
assert!(
error
.to_string()
.contains("backend.provider and legacy backend.kind are mutually exclusive")
);
assert!(error.to_string().contains("unknown variant"));
assert!(error.to_string().contains("github"));
}
#[test]
@ -723,12 +598,12 @@ kind = "local"
temp.path(),
r#"
[backend]
root = "nested/tickets"
root = "nested/work-items"
"#,
);
let config = TicketConfig::load_workspace(temp.path()).unwrap();
assert_eq!(config.backend_root(), temp.path().join("nested/tickets"));
assert_eq!(config.backend_root(), temp.path().join("nested/work-items"));
}
#[test]

View File

@ -1,8 +1,8 @@
//! Ticket domain types and the local `.yoi/tickets/` file backend.
//! Ticket domain types and the local `work-items/` file backend.
//!
//! The public domain name is **Ticket**. `LocalTicketBackend` preserves the
//! repository's current `.yoi/tickets/{open,pending,closed}/<id>/` layout and the
//! event/thread format while exposing typed Rust operations.
//! repository's current `work-items/{open,pending,closed}/<id>/` layout and the
//! event format used by `tickets.sh` while exposing typed Rust operations.
use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::fmt;
@ -514,14 +514,6 @@ impl TicketDoctorReport {
path,
});
}
pub fn push_warning(&mut self, message: impl Into<String>, path: Option<PathBuf>) {
self.diagnostics.push(TicketDoctorDiagnostic {
severity: TicketDoctorSeverity::Warning,
message: message.into(),
path,
});
}
}
pub trait TicketBackend {
@ -1018,13 +1010,15 @@ impl TicketBackend for LocalTicketBackend {
);
}
if status == TicketStatus::Closed && !dir.join("resolution.md").is_file() {
report.push_warning(
format!("closed ticket missing resolution.md: {}", dir.display()),
report.push_error(
format!("missing resolution.md for closed ticket: {}", dir.display()),
Some(dir.join("resolution.md")),
);
}
if thread.exists() {
doctor_thread_events(&thread, &mut report)?;
for diagnostic in doctor_thread_events(&thread)? {
report.push_error(diagnostic, Some(thread.clone()));
}
}
if artifacts.exists() {
doctor_artifacts(&artifacts, &mut report)?;
@ -1330,9 +1324,9 @@ fn parse_thread(path: &Path) -> Result<Vec<TicketEvent>> {
}
fn parse_event_comment(comment: &str) -> BTreeMap<String, String> {
// Thread event comments use unquoted `key: value` pairs separated by spaces.
// Values currently do not contain spaces; this parser intentionally preserves
// the local file format instead of treating thread.md as strict YAML.
// tickets.sh emits unquoted `key: value` pairs separated by spaces. Values
// currently do not contain spaces; this parser intentionally preserves the
// compatibility shape instead of treating thread.md as strict YAML.
let mut attrs = BTreeMap::new();
let mut iter = comment.split_whitespace().peekable();
while let Some(token) = iter.next() {
@ -1345,19 +1339,17 @@ fn parse_event_comment(comment: &str) -> BTreeMap<String, String> {
attrs
}
fn doctor_thread_events(path: &Path, report: &mut TicketDoctorReport) -> Result<()> {
fn doctor_thread_events(path: &Path) -> Result<Vec<String>> {
let content = fs::read_to_string(path).map_err(|e| io_err(path, e))?;
let mut diagnostics = Vec::new();
for (line_no, line) in content.lines().enumerate() {
let trimmed = line.trim();
if trimmed.starts_with("<!-- event:") && !trimmed.ends_with("-->") {
report.push_error(
format!(
"malformed thread event comment at {}:{}",
path.display(),
line_no + 1
),
Some(path.to_path_buf()),
);
diagnostics.push(format!(
"malformed thread event comment at {}:{}",
path.display(),
line_no + 1
));
}
if let Some(comment) = trimmed
.strip_prefix("<!-- ")
@ -1365,31 +1357,25 @@ fn doctor_thread_events(path: &Path, report: &mut TicketDoctorReport) -> Result<
{
let attrs = parse_event_comment(comment);
if attrs.contains_key("event") && attrs.get("at").is_none() {
report.push_error(
format!(
"thread event missing at: {}:{}",
path.display(),
line_no + 1
),
Some(path.to_path_buf()),
);
diagnostics.push(format!(
"thread event missing at: {}:{}",
path.display(),
line_no + 1
));
}
if attrs.get("event").map(String::as_str) == Some("review") {
match attrs.get("status").map(String::as_str) {
Some("approve" | "request_changes") => {}
_ => report.push_warning(
format!(
"legacy review event missing valid status at {}:{}",
path.display(),
line_no + 1
),
Some(path.to_path_buf()),
),
_ => diagnostics.push(format!(
"review event missing valid status at {}:{}",
path.display(),
line_no + 1
)),
}
}
}
}
Ok(())
Ok(diagnostics)
}
fn collect_artifacts(dir: &Path) -> Result<Vec<TicketArtifactRef>> {
@ -1525,7 +1511,32 @@ mod tests {
use tempfile::TempDir;
fn backend(dir: &TempDir) -> LocalTicketBackend {
LocalTicketBackend::new(dir.path().join("tickets"))
LocalTicketBackend::new(dir.path().join("work-items"))
}
fn script_path() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../..//tickets.sh")
}
fn run_tickets_sh(work_items: &Path, args: &[&str]) -> std::process::Output {
std::process::Command::new(script_path())
.current_dir(work_items.parent().unwrap())
.env("WORK_ITEMS_DIR", work_items)
.args(args)
.output()
.unwrap()
}
fn assert_script_ok(work_items: &Path, args: &[&str]) -> String {
let output = run_tickets_sh(work_items, args);
assert!(
output.status.success(),
"tickets.sh {:?} failed\nstdout:\n{}\nstderr:\n{}",
args,
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8(output.stdout).unwrap()
}
#[test]
@ -1561,23 +1572,22 @@ action_required: none
}
#[test]
fn create_writes_local_ticket_layout() {
fn create_writes_tickets_sh_compatible_layout() {
let tmp = TempDir::new().unwrap();
let backend = backend(&tmp);
let mut input = NewTicket::new("Example Ticket");
input.labels = vec!["ticket".into(), "backend".into()];
let ticket = backend.create(input).unwrap();
let dir = tmp.path().join("tickets/open").join(&ticket.id);
let dir = tmp.path().join("work-items/open").join(&ticket.id);
assert!(dir.join("item.md").exists());
assert!(dir.join("thread.md").exists());
assert!(dir.join("artifacts/.gitkeep").exists());
assert_eq!(ticket.slug, "example-ticket");
let report = backend.doctor().unwrap();
assert!(report.is_ok(), "{:?}", report.diagnostics);
assert_script_ok(&tmp.path().join("work-items"), &["doctor"]);
}
#[test]
fn add_event_review_status_and_close_preserve_local_layout() {
fn add_event_review_status_and_close_are_script_compatible() {
let tmp = TempDir::new().unwrap();
let backend = backend(&tmp);
let ticket = backend.create(NewTicket::new("Flow Ticket")).unwrap();
@ -1598,7 +1608,7 @@ action_required: none
.unwrap();
let pending_item = tmp
.path()
.join("tickets/pending")
.join("work-items/pending")
.join(&ticket.id)
.join("item.md");
assert!(pending_item.exists());
@ -1608,20 +1618,51 @@ action_required: none
MarkdownText::new("Done.\n"),
)
.unwrap();
let closed_dir = tmp.path().join("tickets/closed").join(&ticket.id);
let closed_dir = tmp.path().join("work-items/closed").join(&ticket.id);
assert!(closed_dir.join("resolution.md").exists());
let thread = fs::read_to_string(closed_dir.join("thread.md")).unwrap();
assert!(thread.contains("<!-- event: review"));
assert!(thread.contains("status: approve"));
assert!(thread.contains("<!-- event: close"));
let report = backend.doctor().unwrap();
assert!(report.is_ok(), "{:?}", report.diagnostics);
assert_script_ok(&tmp.path().join("work-items"), &["doctor"]);
}
#[test]
fn reads_ticket_created_by_tickets_sh_and_script_mutates_rust_ticket() {
let tmp = TempDir::new().unwrap();
let work_items = tmp.path().join("work-items");
let id = assert_script_ok(
&work_items,
&[
"create",
"--title",
"Shell Created",
"--slug",
"shell-created",
],
);
let id = id.trim();
let backend = LocalTicketBackend::new(&work_items);
let ticket = backend.show(TicketIdOrSlug::Id(id.to_string())).unwrap();
assert_eq!(ticket.meta.slug, "shell-created");
let rust_ticket = backend.create(NewTicket::new("Rust Created")).unwrap();
assert_script_ok(
&work_items,
&["comment", &rust_ticket.slug, "--author", "test"],
);
let shown = backend.show(TicketIdOrSlug::Id(rust_ticket.id)).unwrap();
assert!(
shown
.events
.iter()
.any(|event| event.kind == TicketEventKind::Comment)
);
}
#[test]
fn doctor_reports_core_consistency_errors() {
let tmp = TempDir::new().unwrap();
let root = tmp.path().join("tickets");
let root = tmp.path().join("work-items");
fs::create_dir_all(root.join("open/bad/artifacts")).unwrap();
fs::write(
root.join("open/bad/item.md"),
@ -1678,7 +1719,7 @@ action_required: none
#[test]
fn rejects_unsafe_components_for_status_moves() {
let tmp = TempDir::new().unwrap();
let root = tmp.path().join("tickets");
let root = tmp.path().join("work-items");
fs::create_dir_all(root.join("open/bad/artifacts")).unwrap();
fs::write(
root.join("open/bad/item.md"),

View File

@ -41,8 +41,8 @@ pub const TICKET_TOOL_NAMES: [&str; 8] = [
];
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.";
Inputs mirror the work-items item.md fields; `title` is required, `body` is Markdown, and the \
backend assigns the id and writes tickets.sh-compatible files under the configured backend root.";
const LIST_DESCRIPTION: &str = "List Tickets from the configured typed Ticket backend. Filter by \
status (`open`, `pending`, `closed`, or `all`) and optionally kind/priority/label. Output is a \
bounded JSON summary list, not full ticket bodies.";
@ -56,12 +56,12 @@ const REVIEW_DESCRIPTION: &str = "Append a Ticket review event. `result` must be
`request_changes`; `body` is Markdown. Writes stay inside the configured Ticket backend root.";
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`.";
by `tickets.sh doctor`.";
const CLOSE_DESCRIPTION: &str = "Close a Ticket with a Markdown resolution through the typed Ticket \
backend. The backend moves the Ticket to closed/, writes resolution.md, updates item.md, and appends \
a close event.";
const DOCTOR_DESCRIPTION: &str = "Run typed Ticket backend consistency checks and return bounded \
diagnostics through the typed backend without shelling out to external commands.";
diagnostics. This does not shell out to tickets.sh.";
#[derive(Debug, Deserialize, schemars::JsonSchema)]
struct TicketCreateParams {
@ -778,7 +778,7 @@ mod tests {
use tempfile::TempDir;
fn backend(temp: &TempDir) -> LocalTicketBackend {
LocalTicketBackend::new(temp.path().join("tickets"))
LocalTicketBackend::new(temp.path().join("work-items"))
}
fn tool(definition: ToolDefinition) -> Arc<dyn Tool> {

View File

@ -15,7 +15,7 @@ Owns:
Does not own:
- generated memory records (`memory`)
- Ticket file lifecycle (`crates/ticket`, `.yoi/tickets/`)
- work item file lifecycle (`tickets.sh`, `work-items/`)
- Pod orchestration decisions (`pod`, workflows executed by agents)
- product CLI command shape (`yoi`)

View File

@ -10,7 +10,6 @@ memory = { workspace = true }
manifest = { workspace = true }
pod = { workspace = true }
session-store = { workspace = true }
ticket = { workspace = true }
tui = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }

View File

@ -1,5 +1,4 @@
mod memory_lint;
mod ticket_cli;
use std::fmt;
use std::path::PathBuf;
@ -15,7 +14,6 @@ enum Mode {
Help,
MemoryLintHelp,
MemoryLint(LintCliOptions),
Ticket(ticket_cli::TicketCli),
PodRuntime(Vec<String>),
Keys,
Tui(LaunchMode),
@ -60,19 +58,6 @@ async fn main() -> ExitCode {
ExitCode::FAILURE
}
},
Mode::Ticket(cli) => match ticket_cli::run(cli) {
Ok(output) => {
print!("{}", output.stdout);
match output.status {
ticket_cli::TicketCliStatus::Success => ExitCode::SUCCESS,
ticket_cli::TicketCliStatus::Failure => ExitCode::FAILURE,
}
}
Err(e) => {
eprintln!("yoi ticket: {e}");
ExitCode::FAILURE
}
},
Mode::PodRuntime(args) => pod::entrypoint::run_cli_from("yoi pod", args).await,
Mode::Keys => tui::keys::launch().await,
Mode::Tui(mode) => {
@ -113,11 +98,6 @@ fn parse_args_slice(args: &[String]) -> Result<Mode, ParseError> {
match args[0].as_str() {
"--help" | "-h" => return Ok(Mode::Help),
"pod" => return Ok(Mode::PodRuntime(args[1..].to_vec())),
"ticket" => {
let ticket_cli =
ticket_cli::parse_ticket_args(&args[1..]).map_err(|e| ParseError(e.to_string()))?;
return Ok(Mode::Ticket(ticket_cli));
}
"keys" => {
if args.len() != 1 {
return Err(ParseError("yoi keys does not accept arguments".into()));
@ -342,7 +322,7 @@ fn parse_session_id(value: &str) -> Result<SegmentId, ParseError> {
fn print_help() {
println!(
"yoi\n\nUsage:\n yoi [OPTIONS] [POD_NAME]\n yoi keys\n yoi pod [POD_OPTIONS]\n yoi ticket <COMMAND> [OPTIONS]\n yoi memory lint [OPTIONS]\n\nOptions:\n -r, --resume Open the Pod picker and resume/attach a Pod\n --multi Open the multi-Pod dashboard\n --pod <NAME> Attach/restore/create a Pod by name\n --socket <PATH> Attach to a specific Pod socket with --pod\n --session <UUID> Resume a specific session segment\n --profile <REF> Start a fresh Pod from a profile\n -h, --help Print help\n"
"yoi\n\nUsage:\n yoi [OPTIONS] [POD_NAME]\n yoi keys\n yoi pod [POD_OPTIONS]\n yoi memory lint [OPTIONS]\n\nOptions:\n -r, --resume Open the Pod picker and resume/attach a Pod\n --multi Open the multi-Pod dashboard\n --pod <NAME> Attach/restore/create a Pod by name\n --socket <PATH> Attach to a specific Pod socket with --pod\n --session <UUID> Resume a specific session segment\n --profile <REF> Start a fresh Pod from a profile\n -h, --help Print help\n"
);
}
@ -406,22 +386,6 @@ mod tests {
}
}
#[test]
fn parse_ticket_subcommand_uses_ticket_mode() {
match parse_args_from(["ticket", "doctor"]).unwrap() {
Mode::Ticket(ticket_cli::TicketCli::Command(ticket_cli::TicketCommand::Doctor)) => {}
_ => panic!("expected Ticket doctor mode"),
}
}
#[test]
fn parse_ticket_help_uses_ticket_mode() {
match parse_args_from(["ticket", "--help"]).unwrap() {
Mode::Ticket(ticket_cli::TicketCli::Help) => {}
_ => panic!("expected Ticket help mode"),
}
}
#[test]
fn parse_keys_subcommand() {
match parse_args_from(["keys"]).unwrap() {

View File

@ -1,948 +0,0 @@
use std::fmt;
use std::fs;
use std::path::{Path, PathBuf};
use ticket::config::TicketConfig;
use ticket::{
LocalTicketBackend, MarkdownText, NewTicket, NewTicketEvent, TicketBackend,
TicketDoctorSeverity, TicketEventKind, TicketFilter, TicketIdOrSlug, TicketReview,
TicketReviewResult, TicketStatus,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TicketCli {
Help,
Command(TicketCommand),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TicketCommand {
Create(CreateOptions),
List(ListOptions),
Show { query: String },
Comment(CommentOptions),
Review(ReviewOptions),
Status(StatusOptions),
Close(CloseOptions),
Doctor,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CreateOptions {
pub title: String,
pub slug: Option<String>,
pub kind: String,
pub priority: String,
pub labels: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ListStatus {
Open,
Pending,
Closed,
All,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ListOptions {
pub status: ListStatus,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommentOptions {
pub query: String,
pub role: TicketEventKind,
pub body: BodySource,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReviewOptions {
pub query: String,
pub result: TicketReviewResult,
pub body: BodySource,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StatusTarget {
Open,
Pending,
Closed,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StatusOptions {
pub query: String,
pub status: StatusTarget,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CloseOptions {
pub query: String,
pub resolution: BodySource,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BodySource {
Message(String),
File(PathBuf),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TicketCliStatus {
Success,
Failure,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TicketCliOutput {
pub status: TicketCliStatus,
pub stdout: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TicketCliError(String);
impl TicketCliError {
fn new(message: impl Into<String>) -> Self {
Self(message.into())
}
}
impl fmt::Display for TicketCliError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for TicketCliError {}
impl From<ticket::TicketError> for TicketCliError {
fn from(error: ticket::TicketError) -> Self {
Self::new(error.to_string())
}
}
impl From<ticket::config::TicketConfigError> for TicketCliError {
fn from(error: ticket::config::TicketConfigError) -> 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() {
"create" => TicketCommand::Create(parse_create(&args[1..])?),
"list" => TicketCommand::List(parse_list(&args[1..])?),
"show" => TicketCommand::Show {
query: parse_one_positional("show", &args[1..])?,
},
"comment" => TicketCommand::Comment(parse_comment(&args[1..])?),
"review" => TicketCommand::Review(parse_review(&args[1..])?),
"status" => TicketCommand::Status(parse_status(&args[1..])?),
"close" => TicketCommand::Close(parse_close(&args[1..])?),
"doctor" => {
if args.len() != 1 {
return Err(TicketCliError::new("ticket doctor takes no arguments"));
}
TicketCommand::Doctor
}
"help" => return Ok(TicketCli::Help),
other => {
return Err(TicketCliError::new(format!(
"unknown ticket command: {other}"
)));
}
};
Ok(TicketCli::Command(command))
}
pub fn run(cli: TicketCli) -> Result<TicketCliOutput, TicketCliError> {
let workspace = std::env::current_dir().map_err(|error| {
TicketCliError::new(format!("failed to resolve current directory: {error}"))
})?;
run_in_workspace(cli, &workspace)
}
pub fn run_in_workspace(
cli: TicketCli,
workspace: &Path,
) -> Result<TicketCliOutput, TicketCliError> {
match cli {
TicketCli::Help => Ok(TicketCliOutput {
status: TicketCliStatus::Success,
stdout: help_text().to_string(),
}),
TicketCli::Command(command) => run_command(command, workspace),
}
}
fn run_command(
command: TicketCommand,
workspace: &Path,
) -> Result<TicketCliOutput, TicketCliError> {
let backend = backend_for_workspace(workspace)?;
match command {
TicketCommand::Create(options) => create(&backend, options),
TicketCommand::List(options) => list(&backend, options),
TicketCommand::Show { query } => show(&backend, query),
TicketCommand::Comment(options) => comment(&backend, options),
TicketCommand::Review(options) => review(&backend, options),
TicketCommand::Status(options) => status(&backend, options),
TicketCommand::Close(options) => close(&backend, options),
TicketCommand::Doctor => doctor(&backend),
}
}
fn backend_for_workspace(workspace: &Path) -> Result<LocalTicketBackend, TicketCliError> {
let config = TicketConfig::load_workspace(workspace)?;
Ok(LocalTicketBackend::new(config.backend_root().to_path_buf()))
}
fn create(
backend: &LocalTicketBackend,
options: CreateOptions,
) -> Result<TicketCliOutput, TicketCliError> {
let mut input = NewTicket::new(options.title);
input.slug = options.slug;
input.kind = options.kind;
input.priority = options.priority;
input.labels = options.labels;
input.author = Some("yoi ticket".to_string());
let created = backend.create(input)?;
Ok(success(format!(
"created\t{}\t{}\t{}\n",
created.id,
created.slug,
created.status.as_str()
)))
}
fn list(
backend: &LocalTicketBackend,
options: ListOptions,
) -> Result<TicketCliOutput, TicketCliError> {
let filter = match options.status {
ListStatus::Open => TicketFilter::status(TicketStatus::Open),
ListStatus::Pending => TicketFilter::status(TicketStatus::Pending),
ListStatus::Closed => TicketFilter::status(TicketStatus::Closed),
ListStatus::All => TicketFilter::all(),
};
let tickets = backend.list(filter)?;
let mut stdout = String::from("status\tid\tslug\ttitle\tkind\tpriority\tupdated_at\n");
for ticket in tickets {
stdout.push_str(&format!(
"{}\t{}\t{}\t{}\t{}\t{}\t{}\n",
ticket.status.as_str(),
ticket.id,
ticket.slug,
ticket.title,
ticket.kind,
ticket.priority,
ticket.updated_at.unwrap_or_default()
));
}
Ok(success(stdout))
}
fn show(backend: &LocalTicketBackend, query: String) -> Result<TicketCliOutput, TicketCliError> {
let ticket = backend.show(TicketIdOrSlug::Query(query))?;
let mut stdout = String::new();
stdout.push_str(&format!("# {}\n\n", ticket.meta.title));
stdout.push_str(&format!("Status: {}\n", ticket.meta.status.as_str()));
stdout.push_str(&format!("ID: {}\n", ticket.meta.id));
stdout.push_str(&format!("Slug: {}\n", ticket.meta.slug));
stdout.push_str(&format!("Kind: {}\n", ticket.meta.kind));
stdout.push_str(&format!("Priority: {}\n", ticket.meta.priority));
stdout.push_str(&format!("Labels: {}\n", ticket.meta.labels.join(", ")));
if let Some(updated_at) = &ticket.meta.updated_at {
stdout.push_str(&format!("Updated: {updated_at}\n"));
}
stdout.push_str("\n## item.md\n\n---\n");
for (key, value) in &ticket.document.raw_frontmatter {
stdout.push_str(&format!("{key}: {value}\n"));
}
stdout.push_str("---\n\n");
stdout.push_str(ticket.document.body.as_str());
if !stdout.ends_with('\n') {
stdout.push('\n');
}
stdout.push_str("\n## thread.md\n\n");
if ticket.events.is_empty() {
stdout.push_str("(no events)\n");
} else {
for event in &ticket.events {
stdout.push_str(&format!(
"- {}{}{}{}\n",
event.kind.as_str(),
event
.status
.as_ref()
.map(|status| format!(" [{status}]"))
.unwrap_or_default(),
event
.author
.as_ref()
.map(|author| format!(" by {author}"))
.unwrap_or_default(),
event
.at
.as_ref()
.map(|at| format!(" at {at}"))
.unwrap_or_default()
));
if let Some(heading) = &event.heading {
stdout.push_str(&format!(" ## {heading}\n"));
}
if !event.body.as_str().is_empty() {
stdout.push_str(event.body.as_str());
if !stdout.ends_with('\n') {
stdout.push('\n');
}
}
}
}
if !ticket.artifacts.is_empty() {
stdout.push_str("\n## artifacts\n\n");
for artifact in &ticket.artifacts {
stdout.push_str(&format!("- {}\n", artifact.relative_path.display()));
}
}
if let Some(resolution) = &ticket.resolution {
stdout.push_str("\n## resolution.md\n\n");
stdout.push_str(resolution.as_str());
if !stdout.ends_with('\n') {
stdout.push('\n');
}
}
Ok(success(stdout))
}
fn comment(
backend: &LocalTicketBackend,
options: CommentOptions,
) -> Result<TicketCliOutput, TicketCliError> {
let role = options.role.as_str().to_string();
let mut event = NewTicketEvent::new(options.role, read_body_source(&options.body)?);
event.author = Some(default_author());
backend.add_event(TicketIdOrSlug::Query(options.query.clone()), event)?;
Ok(success(format!("appended\t{}\t{}\n", options.query, role)))
}
fn review(
backend: &LocalTicketBackend,
options: ReviewOptions,
) -> Result<TicketCliOutput, TicketCliError> {
let result = options.result.as_str().to_string();
let review = TicketReview {
result: options.result,
author: Some(default_author()),
body: MarkdownText::new(read_body_source(&options.body)?),
};
backend.review(TicketIdOrSlug::Query(options.query.clone()), review)?;
Ok(success(format!(
"reviewed\t{}\t{}\n",
options.query, result
)))
}
fn status(
backend: &LocalTicketBackend,
options: StatusOptions,
) -> Result<TicketCliOutput, TicketCliError> {
let status = match options.status {
StatusTarget::Open => TicketStatus::Open,
StatusTarget::Pending => TicketStatus::Pending,
StatusTarget::Closed => {
return Err(TicketCliError::new(
"yoi ticket status <ticket> closed cannot write resolution.md; use `yoi ticket close <ticket> --resolution <text>` instead",
));
}
};
backend.set_status(TicketIdOrSlug::Query(options.query.clone()), status)?;
Ok(success(format!(
"status\t{}\t{}\n",
options.query,
status.as_str()
)))
}
fn close(
backend: &LocalTicketBackend,
options: CloseOptions,
) -> Result<TicketCliOutput, TicketCliError> {
backend.close(
TicketIdOrSlug::Query(options.query.clone()),
MarkdownText::new(read_body_source(&options.resolution)?),
)?;
Ok(success(format!("closed\t{}\n", options.query)))
}
fn doctor(backend: &LocalTicketBackend) -> Result<TicketCliOutput, TicketCliError> {
let report = backend.doctor()?;
let mut stdout = String::new();
if report.is_ok() {
stdout.push_str("doctor: ok\n");
return Ok(success(stdout));
}
for diagnostic in &report.diagnostics {
let severity = match diagnostic.severity {
TicketDoctorSeverity::Error => "error",
TicketDoctorSeverity::Warning => "warning",
};
stdout.push_str(&format!("doctor: {severity}: {}", diagnostic.message));
if let Some(path) = &diagnostic.path {
stdout.push_str(&format!(" ({})", path.display()));
}
stdout.push('\n');
}
stdout.push_str(&format!("doctor: {} error(s)\n", report.error_count()));
Ok(TicketCliOutput {
status: TicketCliStatus::Failure,
stdout,
})
}
fn success(stdout: String) -> TicketCliOutput {
TicketCliOutput {
status: TicketCliStatus::Success,
stdout,
}
}
fn parse_create(args: &[String]) -> Result<CreateOptions, TicketCliError> {
let mut title = None;
let mut slug = None;
let mut kind = "task".to_string();
let mut priority = "P2".to_string();
let mut labels = Vec::new();
let mut i = 0;
while i < args.len() {
match option_with_value(args, &mut i)? {
Some(("--title", value)) => title = Some(value),
Some(("--slug", value)) => slug = Some(value),
Some(("--kind", value)) => kind = value,
Some(("--priority", value)) => priority = value,
Some(("--label", value)) => labels.extend(parse_labels(&value)),
Some((name, _)) => {
return Err(TicketCliError::new(format!(
"unknown create argument: {name}"
)));
}
None => {
return Err(TicketCliError::new(format!(
"unknown create argument: {}",
args[i]
)));
}
}
}
let title = title.ok_or_else(|| TicketCliError::new("create requires --title"))?;
if title.trim().is_empty() {
return Err(TicketCliError::new("create --title must not be empty"));
}
Ok(CreateOptions {
title,
slug,
kind,
priority,
labels,
})
}
fn parse_list(args: &[String]) -> Result<ListOptions, TicketCliError> {
let mut status = ListStatus::Open;
let mut i = 0;
while i < args.len() {
match option_with_value(args, &mut i)? {
Some(("--status", value)) => status = parse_list_status(&value)?,
Some((name, _)) => {
return Err(TicketCliError::new(format!(
"unknown list argument: {name}"
)));
}
None => {
return Err(TicketCliError::new(format!(
"unknown list argument: {}",
args[i]
)));
}
}
}
Ok(ListOptions { status })
}
fn parse_comment(args: &[String]) -> Result<CommentOptions, TicketCliError> {
if args.is_empty() || args[0].starts_with('-') {
return Err(TicketCliError::new("comment requires <id-or-slug>"));
}
let query = args[0].clone();
let mut role = TicketEventKind::Comment;
let mut file = None;
let mut message = None;
let mut i = 1;
while i < args.len() {
match option_with_value(args, &mut i)? {
Some(("--role", value)) => role = parse_comment_role(&value)?,
Some(("--file", value)) => file = Some(PathBuf::from(value)),
Some(("--message", value)) => message = Some(value),
Some((name, _)) => {
return Err(TicketCliError::new(format!(
"unknown comment argument: {name}"
)));
}
None => {
return Err(TicketCliError::new(format!(
"unknown comment argument: {}",
args[i]
)));
}
}
}
Ok(CommentOptions {
query,
role,
body: exactly_one_body("comment", file, message)?,
})
}
fn parse_review(args: &[String]) -> Result<ReviewOptions, TicketCliError> {
if args.is_empty() || args[0].starts_with('-') {
return Err(TicketCliError::new("review requires <id-or-slug>"));
}
let query = args[0].clone();
let mut approve = false;
let mut request_changes = false;
let mut file = None;
let mut message = None;
let mut i = 1;
while i < args.len() {
match args[i].as_str() {
"--approve" => {
approve = true;
i += 1;
}
"--request-changes" => {
request_changes = true;
i += 1;
}
_ => match option_with_value(args, &mut i)? {
Some(("--file", value)) => file = Some(PathBuf::from(value)),
Some(("--message", value)) => message = Some(value),
Some((name, _)) => {
return Err(TicketCliError::new(format!(
"unknown review argument: {name}"
)));
}
None => {
return Err(TicketCliError::new(format!(
"unknown review argument: {}",
args[i]
)));
}
},
}
}
let result = match (approve, request_changes) {
(true, false) => TicketReviewResult::Approve,
(false, true) => TicketReviewResult::RequestChanges,
(false, false) => {
return Err(TicketCliError::new(
"review requires exactly one of --approve or --request-changes",
));
}
(true, true) => {
return Err(TicketCliError::new(
"review accepts exactly one of --approve or --request-changes",
));
}
};
Ok(ReviewOptions {
query,
result,
body: exactly_one_body("review", file, message)?,
})
}
fn parse_status(args: &[String]) -> Result<StatusOptions, TicketCliError> {
if args.len() != 2 {
return Err(TicketCliError::new(
"status requires <id-or-slug> <open|pending|closed>",
));
}
Ok(StatusOptions {
query: args[0].clone(),
status: parse_status_target(&args[1])?,
})
}
fn parse_close(args: &[String]) -> Result<CloseOptions, TicketCliError> {
if args.is_empty() || args[0].starts_with('-') {
return Err(TicketCliError::new("close requires <id-or-slug>"));
}
let query = args[0].clone();
let mut file = None;
let mut resolution = None;
let mut i = 1;
while i < args.len() {
match option_with_value(args, &mut i)? {
Some(("--resolution", value)) => resolution = Some(value),
Some(("--file", value)) => file = Some(PathBuf::from(value)),
Some((name, _)) => {
return Err(TicketCliError::new(format!(
"unknown close argument: {name}"
)));
}
None => {
return Err(TicketCliError::new(format!(
"unknown close argument: {}",
args[i]
)));
}
}
}
Ok(CloseOptions {
query,
resolution: exactly_one_body("close", file, resolution)?,
})
}
fn parse_one_positional(command: &str, args: &[String]) -> Result<String, TicketCliError> {
if args.len() != 1 || args[0].starts_with('-') {
Err(TicketCliError::new(format!(
"{command} requires <id-or-slug>"
)))
} else {
Ok(args[0].clone())
}
}
fn option_with_value(
args: &[String],
i: &mut usize,
) -> Result<Option<(&'static str, String)>, TicketCliError> {
let arg = &args[*i];
for name in [
"--title",
"--slug",
"--kind",
"--priority",
"--label",
"--status",
"--role",
"--file",
"--message",
"--resolution",
] {
if arg == name {
let value = args
.get(*i + 1)
.ok_or_else(|| TicketCliError::new(format!("{name} requires a value")))?;
if value.starts_with('-') {
return Err(TicketCliError::new(format!("{name} requires a value")));
}
*i += 2;
return Ok(Some((name, value.clone())));
}
if let Some(value) = arg.strip_prefix(&format!("{name}=")) {
if value.is_empty() {
return Err(TicketCliError::new(format!("{name} requires a value")));
}
*i += 1;
return Ok(Some((name, value.to_string())));
}
}
Ok(None)
}
fn parse_labels(value: &str) -> impl Iterator<Item = String> + '_ {
value
.split(',')
.map(str::trim)
.filter(|label| !label.is_empty())
.map(ToOwned::to_owned)
}
fn parse_list_status(value: &str) -> Result<ListStatus, TicketCliError> {
match value {
"open" => Ok(ListStatus::Open),
"pending" => Ok(ListStatus::Pending),
"closed" => Ok(ListStatus::Closed),
"all" => Ok(ListStatus::All),
_ => Err(TicketCliError::new(format!("invalid status: {value}"))),
}
}
fn parse_status_target(value: &str) -> Result<StatusTarget, TicketCliError> {
match value {
"open" => Ok(StatusTarget::Open),
"pending" => Ok(StatusTarget::Pending),
"closed" => Ok(StatusTarget::Closed),
_ => Err(TicketCliError::new(format!("invalid status: {value}"))),
}
}
fn parse_comment_role(value: &str) -> Result<TicketEventKind, TicketCliError> {
match value {
"comment" => Ok(TicketEventKind::Comment),
"plan" => Ok(TicketEventKind::Plan),
"decision" => Ok(TicketEventKind::Decision),
"implementation_report" => Ok(TicketEventKind::ImplementationReport),
_ => Err(TicketCliError::new(format!(
"invalid comment role: {value}"
))),
}
}
fn exactly_one_body(
command: &str,
file: Option<PathBuf>,
message: Option<String>,
) -> Result<BodySource, TicketCliError> {
match (file, message) {
(Some(_), Some(_)) => Err(TicketCliError::new(format!(
"{command} accepts exactly one of --file or --message/--resolution"
))),
(Some(path), None) => Ok(BodySource::File(path)),
(None, Some(message)) => Ok(BodySource::Message(ensure_trailing_newline(message))),
(None, None) => Err(TicketCliError::new(format!(
"{command} requires --file or --message/--resolution"
))),
}
}
fn read_body_source(source: &BodySource) -> Result<String, TicketCliError> {
match source {
BodySource::Message(message) => Ok(message.clone()),
BodySource::File(path) => fs::read_to_string(path)
.map(ensure_trailing_newline)
.map_err(|error| {
TicketCliError::new(format!("failed to read {}: {error}", path.display()))
}),
}
}
fn ensure_trailing_newline(mut value: String) -> String {
if !value.ends_with('\n') {
value.push('\n');
}
value
}
fn default_author() -> String {
std::env::var("USER").unwrap_or_else(|_| "unknown".to_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"
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
use ticket::TicketEventKind;
fn args(items: &[&str]) -> Vec<String> {
items.iter().map(|item| item.to_string()).collect()
}
fn run(temp: &TempDir, items: &[&str]) -> TicketCliOutput {
let cli = parse_ticket_args(&args(items)).unwrap();
run_in_workspace(cli, temp.path()).unwrap()
}
#[test]
fn ticket_cli_create_list_show_comment_review_status_close_and_doctor() {
let temp = TempDir::new().unwrap();
let created = run(
&temp,
&[
"create",
"--title",
"CLI Created",
"--slug",
"cli-created",
"--kind",
"task",
"--priority",
"P1",
"--label",
"ticket,cli",
],
);
assert_eq!(created.status, TicketCliStatus::Success);
assert!(created.stdout.contains("created\t"));
assert!(created.stdout.contains("\tcli-created\topen"));
assert!(temp.path().join(".yoi/tickets/open").exists());
assert!(!temp.path().join("work-items").exists());
let listed = run(&temp, &["list", "--status", "open"]);
assert!(listed.stdout.contains("status\tid\tslug"));
assert!(listed.stdout.contains("CLI Created"));
let shown = run(&temp, &["show", "cli-created"]);
assert!(shown.stdout.contains("# CLI Created"));
assert!(shown.stdout.contains("Labels: ticket, cli"));
let commented = run(
&temp,
&[
"comment",
"cli-created",
"--role",
"implementation_report",
"--message",
"Implemented.",
],
);
assert!(
commented
.stdout
.contains("appended\tcli-created\timplementation_report")
);
let reviewed = run(
&temp,
&[
"review",
"cli-created",
"--approve",
"--message",
"Looks good.",
],
);
assert!(reviewed.stdout.contains("reviewed\tcli-created\tapprove"));
let pending = run(&temp, &["status", "cli-created", "pending"]);
assert!(pending.stdout.contains("status\tcli-created\tpending"));
let closed = run(
&temp,
&[
"close",
"cli-created",
"--resolution",
"Done via yoi ticket.",
],
);
assert!(closed.stdout.contains("closed\tcli-created"));
let doctor = run(&temp, &["doctor"]);
assert_eq!(doctor.status, TicketCliStatus::Success);
assert_eq!(doctor.stdout, "doctor: ok\n");
let backend = LocalTicketBackend::new(temp.path().join(".yoi/tickets"));
let ticket = backend
.show(TicketIdOrSlug::Query("cli-created".to_string()))
.unwrap();
assert!(ticket.resolution.is_some());
assert!(
ticket
.events
.iter()
.any(|event| event.kind == TicketEventKind::ImplementationReport)
);
assert!(
ticket
.events
.iter()
.any(|event| event.kind == TicketEventKind::Review)
);
}
#[test]
fn ticket_cli_uses_configured_backend_root() {
let temp = TempDir::new().unwrap();
fs::create_dir_all(temp.path().join(".yoi")).unwrap();
fs::write(
temp.path().join(".yoi/ticket.config.toml"),
"[backend]\nprovider = \"builtin:yoi_local\"\nroot = \"custom-tickets\"\n",
)
.unwrap();
run(
&temp,
&[
"create",
"--title",
"Configured Root",
"--slug",
"configured-root",
],
);
assert!(
temp.path()
.join("custom-tickets/open")
.read_dir()
.unwrap()
.any(|entry| entry
.unwrap()
.file_name()
.to_string_lossy()
.contains("configured-root"))
);
assert!(!temp.path().join("work-items").exists());
}
#[test]
fn ticket_cli_rejects_ambiguous_body_sources() {
let err = parse_ticket_args(&args(&[
"comment",
"ticket",
"--file",
"body.md",
"--message",
"body",
]))
.unwrap_err();
assert!(err.to_string().contains("exactly one"));
}
#[test]
fn ticket_cli_rejects_ambiguous_review_result() {
let err = parse_ticket_args(&args(&[
"review",
"ticket",
"--approve",
"--request-changes",
"--message",
"body",
]))
.unwrap_err();
assert!(err.to_string().contains("exactly one"));
}
#[test]
fn ticket_cli_status_closed_requires_close_command() {
let temp = TempDir::new().unwrap();
run(
&temp,
&["create", "--title", "Close Me", "--slug", "close-me"],
);
let cli = parse_ticket_args(&args(&["status", "close-me", "closed"])).unwrap();
let err = run_in_workspace(cli, temp.path()).unwrap_err();
assert!(err.to_string().contains("use `yoi ticket close"));
}
#[test]
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 create"));
assert!(output.stdout.contains("yoi ticket doctor"));
}
}

View File

@ -7,7 +7,7 @@ Validation should match the change. Do not run expensive broad checks just to lo
Minimum checks:
```sh
yoi ticket doctor
./tickets.sh doctor
git diff --check
```
@ -37,7 +37,7 @@ Avoid repository-wide formatting churn when a validation failure is caused by pr
Run:
```sh
yoi ticket doctor
./tickets.sh doctor
```
Use work item review to verify that the implementation satisfies the ticket, not only that the diff looks plausible.

View File

@ -1,422 +1,43 @@
# Tickets and development workflow
# Work items
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 `work-items/` and `./tickets.sh`. Git history plus work item files are the authoritative state-transition record.
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.
Do not treat ad-hoc chat summaries, memory records, or Pod notifications as the final source of project state.
Do not treat ad-hoc chat summaries, memory records, or Pod notifications as the final source of project state. Notifications are hints to inspect concrete state, not proof of completion.
## Basic commands
## Concepts
- `Ticket`: durable project/orchestration record. It contains requirements, decisions, plans, implementation reports, reviews, artifacts, and resolution history.
- `Task`: session-local progress tracking inside a Pod. It is not the project record.
- `Assignment`: a concrete delegation from an Orchestrator to a coder/reviewer/investigator Pod.
- `IntentPacket`: the short implementation/review contract derived from a Ticket and handed to an Assignment.
- `LocalTicketBackend`: the current `.yoi/tickets/` markdown/thread/artifacts storage backend.
A Ticket may represent a feature, bug, cleanup, design decision, investigation, workflow change, release task, or orchestration umbrella. The common requirement is that the closed Ticket explains a completed outcome.
## User-facing entry points
Use the highest-level interface that matches the work:
- In the TUI, use `:ticket ...` commands to launch fixed Ticket-role Pods.
- 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 ...`.
## Ticket tools inside Pods
Pods with the Ticket built-in feature can use typed Ticket tools:
- `TicketCreate`
- `TicketList`
- `TicketShow`
- `TicketComment`
- `TicketReview`
- `TicketStatus`
- `TicketClose`
- `TicketDoctor`
These tools operate through the typed Ticket backend. They are not arbitrary filesystem write permission to `.yoi/tickets/`.
Use them when a Pod needs to materialize or update project records:
- Intake creates a new Ticket after user agreement.
- Orchestrator records routing decisions and intent packets.
- Reviewer records approve/request-changes review results.
- Maintainer closes a Ticket with a resolution when merge/validation/cleanup evidence is complete.
Do not bypass workflow gates just because Ticket tools are available. Ticket mutation is a project-record operation and should remain auditable.
## Ticket configuration
Workspace Ticket orchestration is configured by `.yoi/ticket.config.toml` when present.
MVP shape:
```toml
[backend]
provider = "builtin:yoi_local"
root = ".yoi/tickets"
[roles.intake]
profile = "project:intake"
launch_prompt = "$workspace/ticket/intake/launch"
workflow = "ticket-intake-workflow"
[roles.orchestrator]
profile = "project:orchestrator"
launch_prompt = "$workspace/ticket/orchestrator/launch"
workflow = "ticket-orchestrator-routing"
[roles.coder]
profile = "project:coder"
launch_prompt = "$workspace/ticket/coder/launch"
workflow = "multi-agent-workflow"
[roles.reviewer]
profile = "project:reviewer"
launch_prompt = "$workspace/ticket/reviewer/launch"
workflow = "multi-agent-workflow"
[roles.investigator]
profile = "project:investigator"
launch_prompt = "$workspace/ticket/investigator/launch"
workflow = "ticket-orchestrator-routing"
```sh
./tickets.sh create --title "..." [--slug slug] [--kind task] [--priority P2] [--label a,b]
./tickets.sh list [--status open|pending|closed|all]
./tickets.sh show <id-or-slug>
./tickets.sh comment <id-or-slug> [--role comment|plan|decision|implementation_report] [--file path]
./tickets.sh review <id-or-slug> --approve|--request-changes [--file path]
./tickets.sh status <id-or-slug> open|pending|closed
./tickets.sh close <id-or-slug> [--resolution text|--file path]
./tickets.sh doctor
```
Fixed roles are:
- `intake`
- `orchestrator`
- `coder`
- `reviewer`
- `investigator`
This is not an arbitrary role registry. The fixed roles are the roles required by Ticket orchestration.
`profile` selects the Pod runtime Profile for that role. The selected Profile owns durable role/system behavior. `ticket.config.toml` does not have a role-level `system_instruction` field.
`launch_prompt` is a per-action first-run prompt reference for future prompt resolution. Current launcher behavior exposes the ref but does not treat it as system instruction.
`workflow` is the workflow the launched role should follow. Workflow state and phase-specific prompt injection are future work; any dynamic prompt content must be committed as history before it affects model context.
`provider = "builtin:yoi_local"` selects Yoi's built-in local Ticket backend. `root = ".yoi/tickets"` is the canonical local storage root for this repository. Legacy `kind = "local"` is accepted only as a short transitional alias; new configs should use `provider`.
If `.yoi/ticket.config.toml` is missing, defaults are:
- backend provider: `builtin:yoi_local`
- backend root: `<workspace>/.yoi/tickets`
- all role profiles: `inherit`
- no launch prompt refs
- workflows:
- intake: `ticket-intake-workflow`
- orchestrator: `ticket-orchestrator-routing`
- coder: `multi-agent-workflow`
- 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.
## Workflow lifecycle
Ticket-driven development normally moves through these gates:
1. Intake
2. Orchestrator routing
3. Preflight or spike when needed
4. Implementation assignment
5. Review
6. Merge / validation / cleanup
7. Close
Each gate records its decision or evidence in the Ticket thread or artifacts.
### 1. Intake
Use `ticket-intake-workflow` when a user request is broad, ambiguous, or not yet a Ticket.
Intake should:
- clarify user intent;
- check duplicate/related Tickets;
- draft background, requirements, acceptance criteria, non-goals, readiness, risk flags, and validation;
- create or update the Ticket only after user agreement.
Intake should not schedule implementation, spawn coder/reviewer Pods, create worktrees, merge, or close Tickets.
### 2. Orchestrator routing
Use `ticket-orchestrator-routing` to classify the next action for an existing Ticket.
Routing classifications include:
- `requirements_sync_needed`
- `preflight_needed`
- `spike_needed`
- `implementation_ready`
- `review_needed`
- `blocked_action_required`
- `close_ready`
- `defer_pending`
- `closed_or_noop`
Routing decisions should be recorded with `TicketComment` using `plan` or `decision` role. The decision should state the classification, evidence checked, reason, next action, and escalation conditions.
### 3. Preflight
Use `ticket-preflight-workflow` before implementation when the Ticket touches design/authority boundaries, has multiple natural implementation strategies, or cannot produce a short IntentPacket.
Preflight should resolve or record:
- requirements and acceptance criteria;
- current code map;
- invariants and non-goals;
- critical risks and failure modes;
- implementation-ready vs requirements-sync/spike/blocked classification.
Do not send preflight-needed Tickets directly to coder Pods.
### 4. Implementation assignment
Use `multi-agent-workflow` for implementation-ready Tickets.
The Orchestrator should prepare an `IntentPacket` with:
- intent;
- requirements;
- invariants;
- non-goals;
- escalation conditions;
- validation;
- current code map;
- critical risks.
Implementation normally happens in a child git worktree created by the Orchestrator, not by the coder Pod. The coder Pod receives narrow write scope to the worktree and must report changed files, implementation summary, validation, unresolved risks, and review readiness.
### 5. Review
Reviewer Pods should be sibling Pods, not children of coder Pods. They should read the Ticket, intent packet, diff, implementation report, and validation evidence.
Review results should be recorded with the `TicketReview` tool. Maintainers working directly with the local backend can use the `yoi ticket` CLI documented later.
Blockers must be fixed or explicitly escalated before merge-ready submission.
### 6. Merge and close
Unless explicitly authorized otherwise, final merge, cleanup, design-boundary decisions, and Ticket closure remain Orchestrator/human responsibilities.
Before closing, verify concrete evidence:
- child Pod output via `ReadPodOutput`;
- worktree status and diff;
- validation command output;
- review result;
- Ticket requirements and acceptance criteria;
- merge/cleanup state in the main workspace.
Close with a resolution that summarizes what changed, key commits, validation, review status, and remaining follow-ups.
## TUI Ticket role actions
TUI exposes explicit commands for fixed Ticket roles:
```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...]
```
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.
All actions are explicit and user-triggered. They are not a scheduler, queue, spawned-Pod panel, or automatic maintainer loop.
### TUI execution path
The TUI path is:
```text
User types :ticket ... in the TUI
-> TUI parses the command into a fixed Ticket role action
-> TUI 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
```
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 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;
- 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
Because top-level TUI role launches cannot inherit a parent Profile, configure concrete role profiles before using these commands:
```toml
# .yoi/ticket.config.toml
[backend]
provider = "builtin:yoi_local"
root = ".yoi/tickets"
[roles.intake]
profile = "project:intake"
workflow = "ticket-intake-workflow"
[roles.orchestrator]
profile = "project:orchestrator"
workflow = "ticket-orchestrator-routing"
[roles.coder]
profile = "project:coder"
workflow = "multi-agent-workflow"
[roles.reviewer]
profile = "project:reviewer"
workflow = "multi-agent-workflow"
[roles.investigator]
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.
### 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
- `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.
- launch success but no visible completion: attach to or inspect the launched Pod; completion notifications are hints, not authority.
## Granularity
One Ticket should describe a complete change that can be explained as a feature, behavior, design decision, investigation result, or maintenance outcome when closed.
One work item should describe a complete change that can be explained as a feature, behavior, design decision, or maintenance outcome when closed.
Avoid Tickets that only mirror an implementation step unless that step is independently reviewable and useful. Phase/step lists inside a Ticket are execution order, not a separate dependency system.
Avoid tickets that only mirror an implementation step unless that step is independently reviewable and useful. Phase/step lists inside a ticket are execution order, not a separate dependency system.
Use umbrella Tickets only to track a split. Child Tickets should be independently reviewable and closeable.
## Contents
## Ticket contents
A useful work item states:
A useful Ticket states:
- background and motivation
- requirements
- acceptance criteria
- relevant constraints
- review/implementation reports when work is submitted
- final resolution when closed
- background and motivation;
- requirements;
- acceptance criteria;
- relevant constraints and non-goals;
- readiness / preflight needs / risk flags when relevant;
- implementation reports when work is submitted;
- reviews;
- final resolution when closed.
Keep long research dumps out of the item body. Put necessary artifacts under the ticket's `artifacts/` directory and summarize the conclusion in the thread.
Keep long research dumps out of the item body. Put necessary artifacts under the Ticket's `artifacts/` directory and summarize the conclusion in the thread.
## Workflow
Do not store secrets, credentials, private prompt contents, or raw logs containing secrets in Ticket bodies, thread entries, artifacts, diagnostics, or model-visible prompts.
Create or refine the work item before opening a separate implementation worktree. When using child Pods, the orchestrator should provide a scoped task and later verify concrete evidence: diff, tests, worktree status, and child output.
## Backend/maintainer CLI: `yoi ticket`
The product CLI exposes the typed Ticket backend for repository maintenance and validation. It operates on the configured `.yoi/tickets/` storage and is the preferred command-line surface when editing Tickets outside a Pod.
```sh
yoi ticket create --title "..." [--slug slug] [--kind task] [--priority P2] [--label a,b]
yoi ticket list [--status open|pending|closed|all]
yoi ticket show <id-or-slug>
yoi ticket comment <id-or-slug> [--role comment|plan|decision|implementation_report] [--file path]
yoi ticket review <id-or-slug> --approve|--request-changes [--file path]
yoi ticket status <id-or-slug> open|pending
yoi ticket close <id-or-slug> [--resolution text|--file path]
yoi ticket doctor
```
`yoi ticket status` intentionally does not close Tickets. Closing must use `yoi ticket close` so the backend writes the required `resolution.md` and passes `yoi ticket doctor`.
The current LocalTicketBackend stores records under:
```text
.yoi/tickets/{open,pending,closed}/<id>/
item.md
thread.md
artifacts/
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.
## Validation
Run at least:
```sh
yoi ticket doctor
git diff --check
```
Implementation Tickets usually also need focused tests and broader checks, for example:
```sh
cargo fmt --check
cargo check --workspace --all-targets
cargo test -p <crate> <filter>
nix build .#yoi --no-link
```
Record validation commands and results in the implementation report or resolution.
Closing a ticket means the repository records are ready, not merely that a child Pod announced completion.

View File

@ -40,7 +40,7 @@ rustPlatform.buildRustPackage rec {
filter = sourceFilter;
};
cargoHash = "sha256-+eIKCBT0NR8OJn8IxuJl2nc7M6OxlPQ+9RHncSz9K2M=";
cargoHash = "sha256-yk3cLEqIfLfjRpLM3Iaa7jJyV4inigD994QdUn/3iXY=";
depsExtraArgs = {
# Older fetchCargoVendor utilities used crates.io's API download endpoint,

539
tickets.sh Executable file
View File

@ -0,0 +1,539 @@
#!/bin/sh
set -eu
WORK_ITEMS_DIR=${WORK_ITEMS_DIR:-work-items}
STATUSES="open pending closed"
REQUIRED_FIELDS="id slug title status kind priority labels created_at updated_at assignee legacy_ticket"
usage() {
cat <<'EOF'
tickets.sh - repository-local WorkItem / Thread helper
Usage:
./tickets.sh help
./tickets.sh --help
./tickets.sh list [--status open|pending|closed|all]
./tickets.sh show <id-or-slug>
./tickets.sh create --title <title> [--slug <slug>] [--kind <kind>] [--priority P2] [--label a,b]
./tickets.sh comment <id-or-slug> [--role comment|plan|decision|implementation_report] [--author <name>] [--file <path>]
./tickets.sh review <id-or-slug> --approve|--request-changes [--author <name>] [--file <path>]
./tickets.sh status <id-or-slug> open|pending|closed
./tickets.sh close <id-or-slug> [--resolution <text>|--file <path>]
./tickets.sh doctor
Backend:
work-items/{open,pending,closed}/<id>/item.md
work-items/{open,pending,closed}/<id>/thread.md
work-items/{open,pending,closed}/<id>/artifacts/
Migration policy:
work-items/ is the canonical backend after migration. TODO.md is only a
legacy/generated-view notice. Open items must not remain as tickets/*.md, and
review notes must be appended to thread.md instead of tickets/*.review.md.
EOF
}
die() {
printf 'error: %s\n' "$*" >&2
exit 1
}
now_utc() {
date -u '+%Y-%m-%dT%H:%M:%SZ'
}
compact_date() {
date -u '+%Y%m%d-%H%M%S'
}
ensure_backend_dirs() {
mkdir -p "$WORK_ITEMS_DIR/open" "$WORK_ITEMS_DIR/pending" "$WORK_ITEMS_DIR/closed"
}
is_status() {
case "$1" in
open|pending|closed) return 0 ;;
*) return 1 ;;
esac
}
slugify() {
# ASCII-focused slugifier for POSIX shell. Non-ASCII titles should pass --slug.
printf '%s' "$1" |
tr '[:upper:]' '[:lower:]' |
sed 's/[^a-z0-9][^a-z0-9]*/-/g; s/^-//; s/-$//; s/--*/-/g'
}
field_value() {
file=$1
field=$2
awk -v key="$field" '
NR == 1 && $0 == "---" { in_fm = 1; next }
in_fm && $0 == "---" { exit }
in_fm {
prefix = key ":"
if (index($0, prefix) == 1) {
value = substr($0, length(prefix) + 1)
sub(/^[[:space:]]*/, "", value)
print value
exit
}
}
' "$file"
}
set_frontmatter_field() {
file=$1
field=$2
value=$3
tmp=${TMPDIR:-/tmp}/tickets-sh.$$.tmp
awk -v key="$field" -v new_value="$value" '
NR == 1 && $0 == "---" { in_fm = 1; print; next }
in_fm && $0 == "---" { in_fm = 0; print; next }
in_fm {
prefix = key ":"
if (index($0, prefix) == 1) {
print key ": " new_value
next
}
}
{ print }
' "$file" > "$tmp"
mv "$tmp" "$file"
}
find_item_dir() {
query=$1
matches=${TMPDIR:-/tmp}/tickets-sh.matches.$$
: > "$matches"
for status in $STATUSES; do
for dir in "$WORK_ITEMS_DIR/$status"/*; do
[ -d "$dir" ] || continue
item=$dir/item.md
[ -f "$item" ] || continue
id=$(field_value "$item" id || true)
slug=$(field_value "$item" slug || true)
if [ "$query" = "$id" ] || [ "$query" = "$slug" ]; then
printf '%s\n' "$dir" >> "$matches"
fi
done
done
count=$(wc -l < "$matches" | tr -d ' ')
if [ "$count" -eq 0 ]; then
rm -f "$matches"
die "work item not found: $query"
fi
if [ "$count" -gt 1 ]; then
cat "$matches" >&2
rm -f "$matches"
die "ambiguous work item: $query"
fi
sed -n '1p' "$matches"
rm -f "$matches"
}
item_status_from_dir() {
case "$1" in
*/open/*) printf 'open\n' ;;
*/pending/*) printf 'pending\n' ;;
*/closed/*) printf 'closed\n' ;;
*) printf 'unknown\n' ;;
esac
}
labels_yaml() {
labels=$1
if [ -z "$labels" ]; then
printf '[]'
return
fi
old_ifs=$IFS
IFS=,
first=1
printf '['
for label in $labels; do
clean=$(printf '%s' "$label" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
[ -n "$clean" ] || continue
if [ "$first" -eq 0 ]; then
printf ', '
fi
printf '%s' "$clean"
first=0
done
IFS=$old_ifs
printf ']'
}
read_body_to_file() {
input_file=$1
output_file=$2
if [ -n "$input_file" ]; then
[ -f "$input_file" ] || die "file not found: $input_file"
cat "$input_file" > "$output_file"
return
fi
if [ -t 0 ]; then
printf 'No body provided.\n' > "$output_file"
else
cat > "$output_file"
fi
}
append_thread_event() {
dir=$1
event=$2
heading=$3
author=$4
status_value=$5
body_file=$6
at=$(now_utc)
thread=$dir/thread.md
[ -f "$thread" ] || : > "$thread"
{
printf '\n<!-- event: %s author: %s at: %s' "$event" "$author" "$at"
if [ -n "$status_value" ]; then
printf ' status: %s' "$status_value"
fi
printf ' -->\n\n'
printf '## %s\n\n' "$heading"
cat "$body_file"
printf '\n\n---\n'
} >> "$thread"
set_frontmatter_field "$dir/item.md" updated_at "$at"
}
cmd_list() {
status_filter=open
while [ "$#" -gt 0 ]; do
case "$1" in
--status)
[ "$#" -ge 2 ] || die "--status requires a value"
status_filter=$2
shift 2
;;
*) die "unknown list argument: $1" ;;
esac
done
case "$status_filter" in
open|pending|closed|all) ;;
*) die "invalid status: $status_filter" ;;
esac
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' status id slug title kind priority updated_at
for status in $STATUSES; do
if [ "$status_filter" != all ] && [ "$status_filter" != "$status" ]; then
continue
fi
for dir in "$WORK_ITEMS_DIR/$status"/*; do
[ -d "$dir" ] || continue
item=$dir/item.md
[ -f "$item" ] || continue
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
"$(field_value "$item" status)" \
"$(field_value "$item" id)" \
"$(field_value "$item" slug)" \
"$(field_value "$item" title)" \
"$(field_value "$item" kind)" \
"$(field_value "$item" priority)" \
"$(field_value "$item" updated_at)"
done
done
}
cmd_show() {
[ "$#" -eq 1 ] || die "show requires <id-or-slug>"
dir=$(find_item_dir "$1")
item=$dir/item.md
thread=$dir/thread.md
printf '# %s\n\n' "$(field_value "$item" title)"
printf 'Path: %s\n' "$dir"
printf 'Status: %s\n' "$(field_value "$item" status)"
printf 'ID: %s\n' "$(field_value "$item" id)"
printf 'Slug: %s\n\n' "$(field_value "$item" slug)"
printf '## item.md\n\n'
cat "$item"
printf '\n\n## thread.md\n\n'
if [ -f "$thread" ]; then
tail -n 80 "$thread"
else
printf '(missing thread.md)\n'
fi
}
cmd_create() {
title=
slug=
kind=task
priority=P2
labels=
while [ "$#" -gt 0 ]; do
case "$1" in
--title) [ "$#" -ge 2 ] || die "--title requires a value"; title=$2; shift 2 ;;
--slug) [ "$#" -ge 2 ] || die "--slug requires a value"; slug=$2; shift 2 ;;
--kind) [ "$#" -ge 2 ] || die "--kind requires a value"; kind=$2; shift 2 ;;
--priority) [ "$#" -ge 2 ] || die "--priority requires a value"; priority=$2; shift 2 ;;
--label) [ "$#" -ge 2 ] || die "--label requires a value"; labels=$2; shift 2 ;;
*) die "unknown create argument: $1" ;;
esac
done
[ -n "$title" ] || die "create requires --title"
if [ -z "$slug" ]; then
slug=$(slugify "$title")
else
slug=$(slugify "$slug")
fi
[ -n "$slug" ] || slug=item
ensure_backend_dirs
stamp=$(compact_date)
id=$stamp-$slug
dir=$WORK_ITEMS_DIR/open/$id
if [ -e "$dir" ]; then
id=$id-$$
dir=$WORK_ITEMS_DIR/open/$id
fi
created=$(now_utc)
mkdir -p "$dir/artifacts"
: > "$dir/artifacts/.gitkeep"
cat > "$dir/item.md" <<EOF
---
id: $id
slug: $slug
title: $title
status: open
kind: $kind
priority: $priority
labels: $(labels_yaml "$labels")
created_at: $created
updated_at: $created
assignee: null
legacy_ticket: null
---
## Background
Created by tickets.sh.
## Acceptance criteria
- TBD
EOF
cat > "$dir/thread.md" <<EOF
<!-- event: create author: tickets.sh at: $created -->
## Created
Created by tickets.sh create.
---
EOF
printf '%s\n' "$id"
}
cmd_comment() {
[ "$#" -ge 1 ] || die "comment requires <id-or-slug>"
query=$1
shift
role=comment
author=${USER:-unknown}
file=
while [ "$#" -gt 0 ]; do
case "$1" in
--role) [ "$#" -ge 2 ] || die "--role requires a value"; role=$2; shift 2 ;;
--author) [ "$#" -ge 2 ] || die "--author requires a value"; author=$2; shift 2 ;;
--file) [ "$#" -ge 2 ] || die "--file requires a value"; file=$2; shift 2 ;;
*) die "unknown comment argument: $1" ;;
esac
done
dir=$(find_item_dir "$query")
body=${TMPDIR:-/tmp}/tickets-sh.body.$$
read_body_to_file "$file" "$body"
heading=$role
case "$role" in
comment) heading="Comment" ;;
plan) heading="Plan" ;;
decision) heading="Decision" ;;
implementation_report) heading="Implementation report" ;;
esac
append_thread_event "$dir" "$role" "$heading" "$author" "" "$body"
rm -f "$body"
}
cmd_review() {
[ "$#" -ge 1 ] || die "review requires <id-or-slug>"
query=$1
shift
author=${USER:-unknown}
file=
review_status=
while [ "$#" -gt 0 ]; do
case "$1" in
--approve) review_status=approve; shift ;;
--request-changes) review_status=request_changes; shift ;;
--author) [ "$#" -ge 2 ] || die "--author requires a value"; author=$2; shift 2 ;;
--file) [ "$#" -ge 2 ] || die "--file requires a value"; file=$2; shift 2 ;;
*) die "unknown review argument: $1" ;;
esac
done
[ -n "$review_status" ] || die "review requires --approve or --request-changes"
dir=$(find_item_dir "$query")
body=${TMPDIR:-/tmp}/tickets-sh.body.$$
read_body_to_file "$file" "$body"
if [ "$review_status" = approve ]; then
heading="Review: approve"
else
heading="Review: request changes"
fi
append_thread_event "$dir" review "$heading" "$author" "$review_status" "$body"
rm -f "$body"
}
cmd_status() {
[ "$#" -eq 2 ] || die "status requires <id-or-slug> <open|pending|closed>"
query=$1
new_status=$2
is_status "$new_status" || die "invalid status: $new_status"
dir=$(find_item_dir "$query")
id=$(field_value "$dir/item.md" id)
new_dir=$WORK_ITEMS_DIR/$new_status/$id
ensure_backend_dirs
if [ "$dir" != "$new_dir" ]; then
[ ! -e "$new_dir" ] || die "target already exists: $new_dir"
mv "$dir" "$new_dir"
dir=$new_dir
fi
set_frontmatter_field "$dir/item.md" status "$new_status"
set_frontmatter_field "$dir/item.md" updated_at "$(now_utc)"
}
cmd_close() {
[ "$#" -ge 1 ] || die "close requires <id-or-slug>"
query=$1
shift
resolution=
close_file=
while [ "$#" -gt 0 ]; do
case "$1" in
--resolution) [ "$#" -ge 2 ] || die "--resolution requires a value"; resolution=$2; shift 2 ;;
--file) [ "$#" -ge 2 ] || die "--file requires a value"; close_file=$2; shift 2 ;;
*) die "unknown close argument: $1" ;;
esac
done
cmd_status "$query" closed
dir=$(find_item_dir "$query")
body=${TMPDIR:-/tmp}/tickets-sh.body.$$
if [ -n "$close_file" ]; then
read_body_to_file "$close_file" "$body"
elif [ -n "$resolution" ]; then
printf '%s\n' "$resolution" > "$body"
else
printf 'Closed.\n' > "$body"
fi
cp "$body" "$dir/resolution.md"
append_thread_event "$dir" close "Closed" "${USER:-unknown}" closed "$body"
rm -f "$body"
}
doctor_error() {
printf 'doctor: %s\n' "$*" >&2
DOCTOR_ERRORS=$((DOCTOR_ERRORS + 1))
}
cmd_doctor() {
DOCTOR_ERRORS=0
for status in $STATUSES; do
[ -d "$WORK_ITEMS_DIR/$status" ] || doctor_error "missing directory: $WORK_ITEMS_DIR/$status"
done
ids=${TMPDIR:-/tmp}/tickets-sh.ids.$$
slugs=${TMPDIR:-/tmp}/tickets-sh.slugs.$$
: > "$ids"
: > "$slugs"
for status in $STATUSES; do
[ -d "$WORK_ITEMS_DIR/$status" ] || continue
for dir in "$WORK_ITEMS_DIR/$status"/*; do
[ -d "$dir" ] || continue
item=$dir/item.md
thread=$dir/thread.md
artifacts=$dir/artifacts
[ -f "$item" ] || { doctor_error "missing item.md: $dir"; continue; }
[ -f "$thread" ] || doctor_error "missing thread.md: $dir"
[ -d "$artifacts" ] || doctor_error "missing artifacts/: $dir"
first=$(sed -n '1p' "$item")
[ "$first" = "---" ] || doctor_error "item.md missing frontmatter opener: $item"
for field in $REQUIRED_FIELDS; do
value=$(field_value "$item" "$field" || true)
[ -n "$value" ] || doctor_error "missing required field '$field': $item"
done
id=$(field_value "$item" id || true)
slug=$(field_value "$item" slug || true)
fm_status=$(field_value "$item" status || true)
if [ -n "$id" ]; then
printf '%s\t%s\n' "$id" "$item" >> "$ids"
base=$(basename "$dir")
[ "$base" = "$id" ] || doctor_error "directory id mismatch: $dir has id $id"
fi
if [ -n "$slug" ]; then
printf '%s\t%s\n' "$slug" "$item" >> "$slugs"
fi
if [ "$fm_status" != "$status" ]; then
doctor_error "status mismatch: $item has '$fm_status' under '$status'"
fi
done
done
dup_ids=$(cut -f1 "$ids" | sort | uniq -d)
if [ -n "$dup_ids" ]; then
old_ifs=$IFS
IFS='
'
for dup in $dup_ids; do
[ -n "$dup" ] && doctor_error "duplicate id: $dup"
done
IFS=$old_ifs
fi
dup_slugs=$(cut -f1 "$slugs" | sort | uniq -d)
if [ -n "$dup_slugs" ]; then
old_ifs=$IFS
IFS='
'
for dup in $dup_slugs; do
[ -n "$dup" ] && doctor_error "duplicate slug: $dup"
done
IFS=$old_ifs
fi
rm -f "$ids" "$slugs"
if [ -f TODO.md ] && grep -Eq 'tickets/[^][ )]+\.md|tickets/.*\.review\.md' TODO.md; then
doctor_error "TODO.md still references legacy tickets/*.md"
fi
for f in tickets/*.md tickets/*.review.md; do
[ -e "$f" ] || continue
doctor_error "legacy ticket file remains: $f"
done
if [ "$DOCTOR_ERRORS" -eq 0 ]; then
printf 'doctor: ok\n'
return 0
fi
printf 'doctor: %s error(s)\n' "$DOCTOR_ERRORS" >&2
return 1
}
main() {
cmd=${1:-help}
case "$cmd" in
help|--help|-h) usage ;;
list) shift; cmd_list "$@" ;;
show) shift; cmd_show "$@" ;;
create) shift; cmd_create "$@" ;;
comment) shift; cmd_comment "$@" ;;
review) shift; cmd_review "$@" ;;
status) shift; cmd_status "$@" ;;
close) shift; cmd_close "$@" ;;
doctor) shift; [ "$#" -eq 0 ] || die "doctor takes no arguments"; cmd_doctor ;;
*) usage >&2; die "unknown command: $cmd" ;;
esac
}
main "$@"

96
work-items/README.md Normal file
View File

@ -0,0 +1,96 @@
# Work Items backend
`work-items/` is the canonical file backend for repository work after the migration from `TODO.md` and `tickets/*.md`.
## Layout
```text
work-items/
README.md
open/
<id>/
item.md
thread.md
artifacts/
pending/
<id>/
item.md
thread.md
artifacts/
closed/
<id>/
item.md
thread.md
resolution.md
artifacts/
```
`<id>` is timestamp based: `YYYYMMDD-HHMMSS-<slug>`. There is no central sequence file.
## `item.md` schema
`item.md` is Markdown with YAML-like frontmatter. The MVP parser intentionally supports only simple `key: value` lines.
Required fields:
```yaml
---
id: 20260527-000001-example
slug: example
title: Human-readable title
status: open
kind: feature
priority: P2
labels: [maintainer, workflow]
created_at: 2026-05-27T00:00:01Z
updated_at: 2026-05-27T00:00:01Z
assignee: null
legacy_ticket: tickets/example.md
---
```
`status` must match the containing directory (`open`, `pending`, or `closed`). `legacy_ticket` records the migrated source path when one existed; use `null` for items created from a TODO-only entry or new work.
## `thread.md` schema
`thread.md` is an append-only Markdown event log. `tickets.sh comment`, `tickets.sh review`, and `tickets.sh close` append an HTML-comment event header, a Markdown heading, the event body, and a `---` separator.
Example:
```md
<!-- event: review author: orchestrator at: 2026-05-27T12:00:00Z status: request_changes -->
## Review: request changes
Review notes.
---
```
Review state belongs in `thread.md`; new `tickets/*.review.md` files should not be created.
## Commands
Use `../tickets.sh --help` from this repository root for the command reference. The script performs file operations only; it does not run `git add` or `git commit`.
Common commands:
```sh
./tickets.sh list --status all
./tickets.sh show <id-or-slug>
./tickets.sh create --title "Title" --slug title
./tickets.sh comment <id-or-slug> --role plan --file notes.md
./tickets.sh review <id-or-slug> --approve --file review.md
./tickets.sh close <id-or-slug> --resolution "Done"
./tickets.sh doctor
```
## Migration policy
The migration moved unfinished `TODO.md` entries and `tickets/*.md` bodies into `work-items/open/*/item.md`. Existing review files, when present, must be represented as `review` events in `thread.md`. After migration:
- `work-items/` is the source of truth.
- `TODO.md` is only a legacy/generated-view notice and must not carry open ticket state.
- `tickets/*.md` and `tickets/*.review.md` must not remain as canonical unfinished items.
- Closed items are moved to `work-items/closed/` instead of being deleted.
- `./tickets.sh doctor` checks schema, status placement, duplicate IDs/slugs, and leftover legacy ticket files.

Some files were not shown because too many files have changed in this diff Show More