From a9c5c3a8214eee1b0430d1b7f26086a51bd96db4 Mon Sep 17 00:00:00 2001 From: Hare Date: Sat, 6 Jun 2026 07:03:08 +0900 Subject: [PATCH] ticket: remove tickets shell shim --- .yoi/workflow/multi-agent-workflow.md | 2 +- .yoi/workflow/ticket-intake-workflow.md | 2 +- .yoi/workflow/ticket-preflight-workflow.md | 8 +- AGENTS.md | 28 +- README.md | 4 +- crates/lint-common/README.md | 2 +- crates/ticket/src/lib.rs | 75 +-- crates/ticket/src/tool.rs | 6 +- crates/workflow/README.md | 2 +- docs/development/validation.md | 4 +- docs/development/work-items.md | 30 +- tickets.sh | 540 --------------------- work-items/README.md | 2 +- 13 files changed, 56 insertions(+), 649 deletions(-) delete mode 100755 tickets.sh diff --git a/.yoi/workflow/multi-agent-workflow.md b/.yoi/workflow/multi-agent-workflow.md index 0e2435c2..706be104 100644 --- a/.yoi/workflow/multi-agent-workflow.md +++ b/.yoi/workflow/multi-agent-workflow.md @@ -268,7 +268,7 @@ Validation: - cargo fmt --check - cargo check --workspace - cargo test ... -- ./tickets.sh doctor +- yoi ticket doctor Parent decision needed: - none / specific question diff --git a/.yoi/workflow/ticket-intake-workflow.md b/.yoi/workflow/ticket-intake-workflow.md index 9f9e7fdd..687172af 100644 --- a/.yoi/workflow/ticket-intake-workflow.md +++ b/.yoi/workflow/ticket-intake-workflow.md @@ -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 できない」と報告し、必要なら `tickets.sh` を使う人間/親 workflow に戻す。 +Ticket tools が利用できない環境では、勝手に file write で代替しない。ユーザーまたは Orchestrator に「Ticket tools がないため materialize できない」と報告し、必要なら `yoi ticket` を使える人間/親 workflow に戻す。 ## 手順 diff --git a/.yoi/workflow/ticket-preflight-workflow.md b/.yoi/workflow/ticket-preflight-workflow.md index 5b02c900..0e228d01 100644 --- a/.yoi/workflow/ticket-preflight-workflow.md +++ b/.yoi/workflow/ticket-preflight-workflow.md @@ -1,5 +1,5 @@ --- -description: ticket を実装委譲する前に、要件・前提・設計境界・反証観点を同期し、tickets.sh に記録する preflight フロー +description: ticket を実装委譲する前に、要件・前提・設計境界・反証観点を同期し、Ticket thread に記録する preflight フロー model_invokation: true user_invocable: true requires: [] @@ -23,12 +23,12 @@ yoi プロジェクトで ticket を実装に渡す前に、要件・前提・ 小さなバグ修正や仕様が明確な局所変更では、この 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` を更新する。 -- 調査結果・実装前 plan は `./tickets.sh comment --role plan --file ` で残す。 +- 調査結果・実装前 plan は `TicketComment` または `yoi ticket comment --role plan --file ` で残す。 - 採用/却下した設計判断、実装停止判断、仕様同期の結論は `--role decision` で残す。 - 実装に入ってよい状態になったら、その根拠を intent packet として ticket thread に残す。 - 仕様が未決定なら、実装 ticket にせず requirements-sync / spike / design ticket として切り分ける。 diff --git a/AGENTS.md b/AGENTS.md index 159598bd..31825f67 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -47,20 +47,20 @@ docs-only など Nix build の価値が低い変更で省略する場合は、 ## 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]` -- 一覧: `./tickets.sh list [--status open|pending|closed|all]` -- 詳細: `./tickets.sh show ` -- コメント / 計画 / 判断 / 実装報告: `./tickets.sh comment [--role comment|plan|decision|implementation_report] [--file path]` -- レビュー記録: `./tickets.sh review --approve|--request-changes [--file path]` -- 状態変更: `./tickets.sh status open|pending|closed` -- 完了: `./tickets.sh close [--resolution text|--file path]` -- 整合性確認: `./tickets.sh doctor` +- 新規作成: `yoi ticket create --title "..." [--slug slug] [--kind task] [--priority P2] [--label a,b]` +- 一覧: `yoi ticket list [--status open|pending|closed|all]` +- 詳細: `yoi ticket show ` +- コメント / 計画 / 判断 / 実装報告: `yoi ticket comment [--role comment|plan|decision|implementation_report] [--file path]` +- レビュー記録: `yoi ticket review --approve|--request-changes [--file path]` +- 状態変更: `yoi ticket status open|pending` +- 完了: `yoi ticket close [--resolution text|--file path]` +- 整合性確認: `yoi ticket doctor` -`tickets.sh` は `.yoi/tickets/{open,pending,closed}//` 配下の `item.md`、`thread.md`、`artifacts/` を扱う。完了時は `resolution.md` も作られる。手でファイルを作るより、原則としてスクリプトを使うこと。 +`yoi ticket` は typed Ticket backend 経由で `.yoi/tickets/{open,pending,closed}//` 配下の `item.md`、`thread.md`、`artifacts/` を扱う。完了時は `resolution.md` も作られる。手でファイルを作るより、原則として `yoi ticket` または Ticket tools を使うこと。 ### Work item の粒度 @@ -71,10 +71,10 @@ docs-only など Nix build の価値が低い変更で省略する場合は、 ### ライフサイクル -- 作成: `./tickets.sh create ...` で `.yoi/tickets/open/...` を作成し、必要な前提を書いて commit する。 -- 詳細化・前提変更: `item.md` を更新し、必要に応じて `./tickets.sh comment` で `thread.md` に経緯を残して commit する。 -- レビュー: `./tickets.sh review --approve|--request-changes` で `thread.md` にレビュー結果を追記して commit する。 -- 完了: `./tickets.sh close ` で `.yoi/tickets/closed/...` に移動し、`resolution.md` と完了状態を commit する。 +- 作成: `yoi ticket create ...` で `.yoi/tickets/open/...` を作成し、必要な前提を書いて commit する。 +- 詳細化・前提変更: `item.md` を更新し、必要に応じて `yoi ticket comment` で `thread.md` に経緯を残して commit する。 +- レビュー: `yoi ticket review --approve|--request-changes` で `thread.md` にレビュー結果を追記して commit する。 +- 完了: `yoi ticket close ` で `.yoi/tickets/closed/...` に移動し、`resolution.md` と完了状態を commit する。 worktree と併用して作業を進める場合、必ずブランチを切る前に対象 work item を作成・詳細化して commit してから切ること。 diff --git a/README.md b/README.md index f6e48134..ea5d57ce 100644 --- a/README.md +++ b/README.md @@ -68,12 +68,12 @@ Key docs: ## 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: ```sh -./tickets.sh doctor +yoi ticket doctor git diff --check cargo fmt --check cargo check --workspace --all-targets diff --git a/crates/lint-common/README.md b/crates/lint-common/README.md index e9e815ee..ddccdb51 100644 --- a/crates/lint-common/README.md +++ b/crates/lint-common/README.md @@ -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 diff --git a/crates/ticket/src/lib.rs b/crates/ticket/src/lib.rs index d3c473c9..42206d40 100644 --- a/crates/ticket/src/lib.rs +++ b/crates/ticket/src/lib.rs @@ -2,7 +2,7 @@ //! //! The public domain name is **Ticket**. `LocalTicketBackend` preserves the //! repository's current `.yoi/tickets/{open,pending,closed}//` 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::fmt; @@ -1330,9 +1330,9 @@ fn parse_thread(path: &Path) -> Result> { } fn parse_event_comment(comment: &str) -> BTreeMap { - // 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. + // 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. let mut attrs = BTreeMap::new(); let mut iter = comment.split_whitespace().peekable(); while let Some(token) = iter.next() { @@ -1528,31 +1528,6 @@ mod tests { 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] fn parses_item_frontmatter_and_optional_fields() { let item = r#"--- @@ -1586,7 +1561,7 @@ action_required: none } #[test] - fn create_writes_tickets_sh_compatible_layout() { + fn create_writes_local_ticket_layout() { let tmp = TempDir::new().unwrap(); let backend = backend(&tmp); let mut input = NewTicket::new("Example Ticket"); @@ -1597,11 +1572,12 @@ action_required: none assert!(dir.join("thread.md").exists()); assert!(dir.join("artifacts/.gitkeep").exists()); 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] - 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 backend = backend(&tmp); let ticket = backend.create(NewTicket::new("Flow Ticket")).unwrap(); @@ -1638,39 +1614,8 @@ action_required: none assert!(thread.contains("\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 " - 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" < "$dir/thread.md" < - -## Created - -Created by tickets.sh create. - ---- -EOF - printf '%s\n' "$id" -} - -cmd_comment() { - [ "$#" -ge 1 ] || die "comment requires " - 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 " - 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 " - 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 " - 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 "$@" diff --git a/work-items/README.md b/work-items/README.md index f8e04fc6..1dff0b1b 100644 --- a/work-items/README.md +++ b/work-items/README.md @@ -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. -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.