merge: remove tickets shell shim
This commit is contained in:
commit
da760e646d
|
|
@ -268,7 +268,7 @@ Validation:
|
||||||
- cargo fmt --check
|
- cargo fmt --check
|
||||||
- cargo check --workspace
|
- cargo check --workspace
|
||||||
- cargo test ...
|
- cargo test ...
|
||||||
- ./tickets.sh doctor
|
- yoi ticket doctor
|
||||||
|
|
||||||
Parent decision needed:
|
Parent decision needed:
|
||||||
- none / specific question
|
- none / specific question
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ Intake は以下を行う。
|
||||||
|
|
||||||
Intake は `TicketReview`, `TicketStatus`, `TicketClose` を通常使わない。review / status transition / close は Orchestrator または reviewer / maintainer workflow の責務である。
|
Intake は `TicketReview`, `TicketStatus`, `TicketClose` を通常使わない。review / status transition / close は Orchestrator または reviewer / maintainer workflow の責務である。
|
||||||
|
|
||||||
Ticket tools が利用できない環境では、勝手に file write で代替しない。ユーザーまたは Orchestrator に「Ticket tools がないため materialize できない」と報告し、必要なら `tickets.sh` を使う人間/親 workflow に戻す。
|
Ticket tools が利用できない環境では、勝手に file write で代替しない。ユーザーまたは Orchestrator に「Ticket tools がないため materialize できない」と報告し、必要なら `yoi ticket` を使える人間/親 workflow に戻す。
|
||||||
|
|
||||||
## 手順
|
## 手順
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
description: ticket を実装委譲する前に、要件・前提・設計境界・反証観点を同期し、tickets.sh に記録する preflight フロー
|
description: ticket を実装委譲する前に、要件・前提・設計境界・反証観点を同期し、Ticket thread に記録する preflight フロー
|
||||||
model_invokation: true
|
model_invokation: true
|
||||||
user_invocable: true
|
user_invocable: true
|
||||||
requires: []
|
requires: []
|
||||||
|
|
@ -23,12 +23,12 @@ yoi プロジェクトで ticket を実装に渡す前に、要件・前提・
|
||||||
|
|
||||||
小さなバグ修正や仕様が明確な局所変更では、この Workflow は省略してよい。ただし省略理由が曖昧な場合は preflight する。
|
小さなバグ修正や仕様が明確な局所変更では、この Workflow は省略してよい。ただし省略理由が曖昧な場合は preflight する。
|
||||||
|
|
||||||
## tickets.sh 運用方針
|
## Ticket 記録方針
|
||||||
|
|
||||||
作業管理の authority は `work-items/` と `tickets.sh` である。preflight の結果は、口頭の会話だけで終わらせず、ticket の `thread.md` または `item.md` に残す。
|
作業管理の authority は `.yoi/tickets/` に保存される Ticket と git history である。preflight の結果は、口頭の会話だけで終わらせず、Ticket tool または `yoi ticket ...` で ticket の `thread.md` または `item.md` に残す。
|
||||||
|
|
||||||
- 新規の前提・要件・受け入れ条件は、必要に応じて `item.md` を更新する。
|
- 新規の前提・要件・受け入れ条件は、必要に応じて `item.md` を更新する。
|
||||||
- 調査結果・実装前 plan は `./tickets.sh comment <ticket> --role plan --file <file>` で残す。
|
- 調査結果・実装前 plan は `TicketComment` または `yoi ticket comment <ticket> --role plan --file <file>` で残す。
|
||||||
- 採用/却下した設計判断、実装停止判断、仕様同期の結論は `--role decision` で残す。
|
- 採用/却下した設計判断、実装停止判断、仕様同期の結論は `--role decision` で残す。
|
||||||
- 実装に入ってよい状態になったら、その根拠を intent packet として ticket thread に残す。
|
- 実装に入ってよい状態になったら、その根拠を intent packet として ticket thread に残す。
|
||||||
- 仕様が未決定なら、実装 ticket にせず requirements-sync / spike / design ticket として切り分ける。
|
- 仕様が未決定なら、実装 ticket にせず requirements-sync / spike / design ticket として切り分ける。
|
||||||
|
|
|
||||||
28
AGENTS.md
28
AGENTS.md
|
|
@ -47,20 +47,20 @@ docs-only など Nix build の価値が低い変更で省略する場合は、
|
||||||
|
|
||||||
## Work item / Ticket の運用について
|
## Work item / Ticket の運用について
|
||||||
|
|
||||||
作業管理は `.yoi/tickets/` と `tickets.sh` を正とする。時系列・状態遷移の最終的な根拠は git history なので、work item の作成・更新・レビュー・完了はファイル操作と commit で表現する。
|
作業管理は `.yoi/tickets/` に保存される Ticket と `yoi ticket ...` コマンド / Ticket tools を正とする。時系列・状態遷移の最終的な根拠は git history なので、work item の作成・更新・レビュー・完了は Ticket 操作と commit で表現する。
|
||||||
|
|
||||||
### 基本コマンド
|
### 基本コマンド
|
||||||
|
|
||||||
- 新規作成: `./tickets.sh create --title "..." [--slug slug] [--kind task] [--priority P2] [--label a,b]`
|
- 新規作成: `yoi ticket create --title "..." [--slug slug] [--kind task] [--priority P2] [--label a,b]`
|
||||||
- 一覧: `./tickets.sh list [--status open|pending|closed|all]`
|
- 一覧: `yoi ticket list [--status open|pending|closed|all]`
|
||||||
- 詳細: `./tickets.sh show <id-or-slug>`
|
- 詳細: `yoi ticket show <id-or-slug>`
|
||||||
- コメント / 計画 / 判断 / 実装報告: `./tickets.sh comment <id-or-slug> [--role comment|plan|decision|implementation_report] [--file path]`
|
- コメント / 計画 / 判断 / 実装報告: `yoi ticket comment <id-or-slug> [--role comment|plan|decision|implementation_report] [--file path]`
|
||||||
- レビュー記録: `./tickets.sh review <id-or-slug> --approve|--request-changes [--file path]`
|
- レビュー記録: `yoi ticket review <id-or-slug> --approve|--request-changes [--file path]`
|
||||||
- 状態変更: `./tickets.sh status <id-or-slug> open|pending|closed`
|
- 状態変更: `yoi ticket status <id-or-slug> open|pending`
|
||||||
- 完了: `./tickets.sh close <id-or-slug> [--resolution text|--file path]`
|
- 完了: `yoi ticket close <id-or-slug> [--resolution text|--file path]`
|
||||||
- 整合性確認: `./tickets.sh doctor`
|
- 整合性確認: `yoi ticket doctor`
|
||||||
|
|
||||||
`tickets.sh` は `.yoi/tickets/{open,pending,closed}/<id>/` 配下の `item.md`、`thread.md`、`artifacts/` を扱う。完了時は `resolution.md` も作られる。手でファイルを作るより、原則としてスクリプトを使うこと。
|
`yoi ticket` は typed Ticket backend 経由で `.yoi/tickets/{open,pending,closed}/<id>/` 配下の `item.md`、`thread.md`、`artifacts/` を扱う。完了時は `resolution.md` も作られる。手でファイルを作るより、原則として `yoi ticket` または Ticket tools を使うこと。
|
||||||
|
|
||||||
### Work item の粒度
|
### Work item の粒度
|
||||||
|
|
||||||
|
|
@ -71,10 +71,10 @@ docs-only など Nix build の価値が低い変更で省略する場合は、
|
||||||
|
|
||||||
### ライフサイクル
|
### ライフサイクル
|
||||||
|
|
||||||
- 作成: `./tickets.sh create ...` で `.yoi/tickets/open/...` を作成し、必要な前提を書いて commit する。
|
- 作成: `yoi ticket create ...` で `.yoi/tickets/open/...` を作成し、必要な前提を書いて commit する。
|
||||||
- 詳細化・前提変更: `item.md` を更新し、必要に応じて `./tickets.sh comment` で `thread.md` に経緯を残して commit する。
|
- 詳細化・前提変更: `item.md` を更新し、必要に応じて `yoi ticket comment` で `thread.md` に経緯を残して commit する。
|
||||||
- レビュー: `./tickets.sh review <id-or-slug> --approve|--request-changes` で `thread.md` にレビュー結果を追記して commit する。
|
- レビュー: `yoi ticket review <id-or-slug> --approve|--request-changes` で `thread.md` にレビュー結果を追記して commit する。
|
||||||
- 完了: `./tickets.sh close <id-or-slug>` で `.yoi/tickets/closed/...` に移動し、`resolution.md` と完了状態を commit する。
|
- 完了: `yoi ticket close <id-or-slug>` で `.yoi/tickets/closed/...` に移動し、`resolution.md` と完了状態を commit する。
|
||||||
|
|
||||||
worktree と併用して作業を進める場合、必ずブランチを切る前に対象 work item を作成・詳細化して commit してから切ること。
|
worktree と併用して作業を進める場合、必ずブランチを切る前に対象 work item を作成・詳細化して commit してから切ること。
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -68,12 +68,12 @@ Key docs:
|
||||||
|
|
||||||
## 5. Development
|
## 5. Development
|
||||||
|
|
||||||
This repository dogfoods Yoi to develop Yoi. Work is tracked through `.yoi/tickets/` and `./tickets.sh`; git history plus Ticket files are the authoritative project record.
|
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.
|
||||||
|
|
||||||
Common checks:
|
Common checks:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
./tickets.sh doctor
|
yoi ticket doctor
|
||||||
git diff --check
|
git diff --check
|
||||||
cargo fmt --check
|
cargo fmt --check
|
||||||
cargo check --workspace --all-targets
|
cargo check --workspace --all-targets
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,8 @@ Does not own:
|
||||||
|
|
||||||
- memory-specific policy (`memory`)
|
- memory-specific policy (`memory`)
|
||||||
- workflow-specific policy (`workflow`)
|
- workflow-specific policy (`workflow`)
|
||||||
|
- Ticket backend policy (`ticket`)
|
||||||
- product CLI command shape (`yoi`)
|
- product CLI command shape (`yoi`)
|
||||||
- work item script behavior (`tickets.sh`)
|
|
||||||
|
|
||||||
## Design notes
|
## Design notes
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
//!
|
//!
|
||||||
//! The public domain name is **Ticket**. `LocalTicketBackend` preserves the
|
//! The public domain name is **Ticket**. `LocalTicketBackend` preserves the
|
||||||
//! repository's current `.yoi/tickets/{open,pending,closed}/<id>/` layout and the
|
//! repository's current `.yoi/tickets/{open,pending,closed}/<id>/` layout and the
|
||||||
//! event format used by `tickets.sh` while exposing typed Rust operations.
|
//! event/thread format while exposing typed Rust operations.
|
||||||
|
|
||||||
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
@ -1330,9 +1330,9 @@ fn parse_thread(path: &Path) -> Result<Vec<TicketEvent>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_event_comment(comment: &str) -> BTreeMap<String, String> {
|
fn parse_event_comment(comment: &str) -> BTreeMap<String, String> {
|
||||||
// tickets.sh emits unquoted `key: value` pairs separated by spaces. Values
|
// Thread event comments use unquoted `key: value` pairs separated by spaces.
|
||||||
// currently do not contain spaces; this parser intentionally preserves the
|
// Values currently do not contain spaces; this parser intentionally preserves
|
||||||
// compatibility shape instead of treating thread.md as strict YAML.
|
// the local file format instead of treating thread.md as strict YAML.
|
||||||
let mut attrs = BTreeMap::new();
|
let mut attrs = BTreeMap::new();
|
||||||
let mut iter = comment.split_whitespace().peekable();
|
let mut iter = comment.split_whitespace().peekable();
|
||||||
while let Some(token) = iter.next() {
|
while let Some(token) = iter.next() {
|
||||||
|
|
@ -1528,31 +1528,6 @@ mod tests {
|
||||||
LocalTicketBackend::new(dir.path().join("tickets"))
|
LocalTicketBackend::new(dir.path().join("tickets"))
|
||||||
}
|
}
|
||||||
|
|
||||||
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]
|
#[test]
|
||||||
fn parses_item_frontmatter_and_optional_fields() {
|
fn parses_item_frontmatter_and_optional_fields() {
|
||||||
let item = r#"---
|
let item = r#"---
|
||||||
|
|
@ -1586,7 +1561,7 @@ action_required: none
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn create_writes_tickets_sh_compatible_layout() {
|
fn create_writes_local_ticket_layout() {
|
||||||
let tmp = TempDir::new().unwrap();
|
let tmp = TempDir::new().unwrap();
|
||||||
let backend = backend(&tmp);
|
let backend = backend(&tmp);
|
||||||
let mut input = NewTicket::new("Example Ticket");
|
let mut input = NewTicket::new("Example Ticket");
|
||||||
|
|
@ -1597,11 +1572,12 @@ action_required: none
|
||||||
assert!(dir.join("thread.md").exists());
|
assert!(dir.join("thread.md").exists());
|
||||||
assert!(dir.join("artifacts/.gitkeep").exists());
|
assert!(dir.join("artifacts/.gitkeep").exists());
|
||||||
assert_eq!(ticket.slug, "example-ticket");
|
assert_eq!(ticket.slug, "example-ticket");
|
||||||
assert_script_ok(&tmp.path().join("tickets"), &["doctor"]);
|
let report = backend.doctor().unwrap();
|
||||||
|
assert!(report.is_ok(), "{:?}", report.diagnostics);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn add_event_review_status_and_close_are_script_compatible() {
|
fn add_event_review_status_and_close_preserve_local_layout() {
|
||||||
let tmp = TempDir::new().unwrap();
|
let tmp = TempDir::new().unwrap();
|
||||||
let backend = backend(&tmp);
|
let backend = backend(&tmp);
|
||||||
let ticket = backend.create(NewTicket::new("Flow Ticket")).unwrap();
|
let ticket = backend.create(NewTicket::new("Flow Ticket")).unwrap();
|
||||||
|
|
@ -1638,39 +1614,8 @@ action_required: none
|
||||||
assert!(thread.contains("<!-- event: review"));
|
assert!(thread.contains("<!-- event: review"));
|
||||||
assert!(thread.contains("status: approve"));
|
assert!(thread.contains("status: approve"));
|
||||||
assert!(thread.contains("<!-- event: close"));
|
assert!(thread.contains("<!-- event: close"));
|
||||||
assert_script_ok(&tmp.path().join("tickets"), &["doctor"]);
|
let report = backend.doctor().unwrap();
|
||||||
}
|
assert!(report.is_ok(), "{:?}", report.diagnostics);
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn reads_ticket_created_by_tickets_sh_and_script_mutates_rust_ticket() {
|
|
||||||
let tmp = TempDir::new().unwrap();
|
|
||||||
let work_items = tmp.path().join("tickets");
|
|
||||||
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]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ pub const TICKET_TOOL_NAMES: [&str; 8] = [
|
||||||
|
|
||||||
const CREATE_DESCRIPTION: &str = "Create a Ticket through the configured typed Ticket backend. \
|
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 \
|
Inputs mirror the Ticket `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.";
|
backend assigns the id and writes the local Ticket file layout under the configured backend root.";
|
||||||
const LIST_DESCRIPTION: &str = "List Tickets from the configured typed Ticket backend. Filter by \
|
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 \
|
status (`open`, `pending`, `closed`, or `all`) and optionally kind/priority/label. Output is a \
|
||||||
bounded JSON summary list, not full ticket bodies.";
|
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.";
|
`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 \
|
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 \
|
Ticket backend. Use `TicketClose` for closing because closed Tickets require a resolution accepted \
|
||||||
by `tickets.sh doctor`.";
|
by `yoi ticket doctor`.";
|
||||||
const CLOSE_DESCRIPTION: &str = "Close a Ticket with a Markdown resolution through the typed Ticket \
|
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 \
|
backend. The backend moves the Ticket to closed/, writes resolution.md, updates item.md, and appends \
|
||||||
a close event.";
|
a close event.";
|
||||||
const DOCTOR_DESCRIPTION: &str = "Run typed Ticket backend consistency checks and return bounded \
|
const DOCTOR_DESCRIPTION: &str = "Run typed Ticket backend consistency checks and return bounded \
|
||||||
diagnostics. This does not shell out to tickets.sh.";
|
diagnostics through the typed backend without shelling out to external commands.";
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, schemars::JsonSchema)]
|
#[derive(Debug, Deserialize, schemars::JsonSchema)]
|
||||||
struct TicketCreateParams {
|
struct TicketCreateParams {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ Owns:
|
||||||
Does not own:
|
Does not own:
|
||||||
|
|
||||||
- generated memory records (`memory`)
|
- generated memory records (`memory`)
|
||||||
- Ticket file lifecycle (`tickets.sh`, `.yoi/tickets/`)
|
- Ticket file lifecycle (`crates/ticket`, `.yoi/tickets/`)
|
||||||
- Pod orchestration decisions (`pod`, workflows executed by agents)
|
- Pod orchestration decisions (`pod`, workflows executed by agents)
|
||||||
- product CLI command shape (`yoi`)
|
- product CLI command shape (`yoi`)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ Validation should match the change. Do not run expensive broad checks just to lo
|
||||||
Minimum checks:
|
Minimum checks:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
./tickets.sh doctor
|
yoi ticket doctor
|
||||||
git diff --check
|
git diff --check
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -37,7 +37,7 @@ Avoid repository-wide formatting churn when a validation failure is caused by pr
|
||||||
Run:
|
Run:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
./tickets.sh doctor
|
yoi ticket doctor
|
||||||
```
|
```
|
||||||
|
|
||||||
Use work item review to verify that the implementation satisfies the ticket, not only that the diff looks plausible.
|
Use work item review to verify that the implementation satisfies the ticket, not only that the diff looks plausible.
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ Use the highest-level interface that matches the work:
|
||||||
- Inside Pods, use typed Ticket tools to create, inspect, comment, review, and close Tickets.
|
- 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.
|
- For multi-step work, follow the Ticket Intake, Orchestrator Routing, Preflight, and Multi-agent workflows.
|
||||||
|
|
||||||
The local `.yoi/tickets/` files and `tickets.sh` compatibility CLI are backend/maintenance surfaces. They are documented later for maintainers, tests, and compatibility work, but normal user instructions should not start there.
|
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
|
## Ticket tools inside Pods
|
||||||
|
|
||||||
|
|
@ -200,7 +200,7 @@ Implementation normally happens in a child git worktree created by the Orchestra
|
||||||
|
|
||||||
Reviewer Pods should be sibling Pods, not children of coder Pods. They should read the Ticket, intent packet, diff, implementation report, and validation evidence.
|
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 compatibility CLI documented later.
|
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.
|
Blockers must be fixed or explicitly escalated before merge-ready submission.
|
||||||
|
|
||||||
|
|
@ -372,21 +372,23 @@ Keep long research dumps out of the item body. Put necessary artifacts under the
|
||||||
|
|
||||||
Do not store secrets, credentials, private prompt contents, or raw logs containing secrets in Ticket bodies, thread entries, artifacts, diagnostics, or model-visible prompts.
|
Do not store secrets, credentials, private prompt contents, or raw logs containing secrets in Ticket bodies, thread entries, artifacts, diagnostics, or model-visible prompts.
|
||||||
|
|
||||||
## Backend/maintainer compatibility: local Ticket CLI
|
## Backend/maintainer CLI: `yoi ticket`
|
||||||
|
|
||||||
`./tickets.sh` is the local-file compatibility and maintainer CLI for the current `.yoi/tickets/` backend. It is useful for repository maintenance, tests, migration/debugging, and low-level recovery, but it is not the primary user-facing path.
|
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
|
```sh
|
||||||
./tickets.sh create --title "..." [--slug slug] [--kind task] [--priority P2] [--label a,b]
|
yoi ticket create --title "..." [--slug slug] [--kind task] [--priority P2] [--label a,b]
|
||||||
./tickets.sh list [--status open|pending|closed|all]
|
yoi ticket list [--status open|pending|closed|all]
|
||||||
./tickets.sh show <id-or-slug>
|
yoi ticket show <id-or-slug>
|
||||||
./tickets.sh comment <id-or-slug> [--role comment|plan|decision|implementation_report] [--file path]
|
yoi ticket comment <id-or-slug> [--role comment|plan|decision|implementation_report] [--file path]
|
||||||
./tickets.sh review <id-or-slug> --approve|--request-changes [--file path]
|
yoi ticket review <id-or-slug> --approve|--request-changes [--file path]
|
||||||
./tickets.sh status <id-or-slug> open|pending|closed
|
yoi ticket status <id-or-slug> open|pending
|
||||||
./tickets.sh close <id-or-slug> [--resolution text|--file path]
|
yoi ticket close <id-or-slug> [--resolution text|--file path]
|
||||||
./tickets.sh doctor
|
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:
|
The current LocalTicketBackend stores records under:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
|
@ -397,14 +399,14 @@ The current LocalTicketBackend stores records under:
|
||||||
resolution.md # closed Tickets only
|
resolution.md # closed Tickets only
|
||||||
```
|
```
|
||||||
|
|
||||||
Backend integrations must preserve this format until an explicit migration changes it. The repository-root `work-items/` directory is a legacy notice only, not a live mutable backend. Human users should prefer TUI role actions or Ticket tools; maintainers may use `tickets.sh` when working directly with repository records.
|
Backend integrations must preserve this format until an explicit migration changes it. The repository-root `work-items/` directory is a legacy notice only, not a live mutable backend. Human users should prefer TUI role actions or Ticket tools; maintainers may use `yoi ticket ...` when working directly with repository records.
|
||||||
|
|
||||||
## Validation
|
## Validation
|
||||||
|
|
||||||
Run at least:
|
Run at least:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
./tickets.sh doctor
|
yoi ticket doctor
|
||||||
git diff --check
|
git diff --check
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
540
tickets.sh
540
tickets.sh
|
|
@ -1,540 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
WORK_ITEMS_DIR=${WORK_ITEMS_DIR:-.yoi/tickets}
|
|
||||||
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 - transitional repository-local Ticket 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:
|
|
||||||
.yoi/tickets/{open,pending,closed}/<id>/item.md
|
|
||||||
.yoi/tickets/{open,pending,closed}/<id>/thread.md
|
|
||||||
.yoi/tickets/{open,pending,closed}/<id>/artifacts/
|
|
||||||
|
|
||||||
Transition policy:
|
|
||||||
.yoi/tickets/ is the active built-in local Ticket backend. This script remains
|
|
||||||
a maintainer shim until yoi ticket fully replaces it; WORK_ITEMS_DIR may be
|
|
||||||
set for one-off legacy/recovery checks, but do not maintain a second live root.
|
|
||||||
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 "$@"
|
|
||||||
|
|
@ -4,4 +4,4 @@ Active Yoi Ticket storage has moved to `.yoi/tickets/`.
|
||||||
|
|
||||||
This directory is intentionally not a live mutable backend. It remains only as a compatibility notice for older references and migration history. Do not create `open/`, `pending/`, or `closed/` Ticket records here.
|
This directory is intentionally not a live mutable backend. It remains only as a compatibility notice for older references and migration history. Do not create `open/`, `pending/`, or `closed/` Ticket records here.
|
||||||
|
|
||||||
Use `yoi ticket ...`, Ticket tools, or the transitional `./tickets.sh` shim from the repository root; all default to `.yoi/tickets/`.
|
Use `yoi ticket ...` or Ticket tools; both operate on the active `.yoi/tickets/` storage.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user