diff --git a/docs/architecture.md b/docs/architecture.md index ba7932e..aafca92 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -36,6 +36,7 @@ If `inbox` is reduced to pure chat storage, the scheduler must reconstruct state - The leader may use `inbox` directly for inspection or manual repair. - Workers should use `inbox` only. - Workers should not use `orch`. +- `orch dispatch` creates handoff state, not execution. Leaders still need a separate worker runtime or worker agent to consume the assigned inbox thread. - User-facing discussion stays with the leader. - Code-writing workers should run in `orch`-assigned Git worktrees, not in the user's primary checkout. @@ -59,6 +60,7 @@ For code tasks, execution should be isolated from the user's primary checkout. - the assigned worktree path should be stored in attempt metadata and inbox task payload - the worker runtime should execute inside that worktree - strict mode should require a committed base revision +- non-code tasks may stay on a thread-only dispatch path with no worktree, but they still require a separate worker runtime to claim the inbox thread See [worktree-execution.md](/home/kurihada/project/ai-workflow-skill/docs/worktree-execution.md) for the full lifecycle. @@ -156,5 +158,5 @@ Do not put these into `orch`: The intended skill split mirrors the CLI split. - `inbox` skill: used when an agent needs to fetch work, claim a thread, send progress, ask blocked questions, reply, or return results through `inbox` -- `orch` skill: used when the leader needs to create runs, decompose tasks, manage dependencies, dispatch ready work, inspect blocks, answer them, retry failures, or reassign work through `orch` +- `orch` skill: used when the leader needs to create runs, decompose tasks, manage dependencies, dispatch ready work, inspect blocks, answer them, retry failures, or reassign work through `orch`; it is not itself the worker launcher - `council-review` skill: used when the user explicitly wants a structured three-reviewer brainstorm or review with grouped and tallied recommendations diff --git a/docs/orch-cli.md b/docs/orch-cli.md index 2195b99..6cec71a 100644 --- a/docs/orch-cli.md +++ b/docs/orch-cli.md @@ -11,6 +11,7 @@ In normal operation: - leaders use `orch` - `orch` creates and monitors `inbox` threads - workers continue using `inbox` +- a separate worker runtime or worker agent must still consume the assigned inbox thread after `dispatch` ## Responsibilities @@ -34,6 +35,7 @@ In normal operation: - worker claiming - direct worker polling +- automatic worker-runtime launch - raw message append storage - low-level thread history management @@ -93,12 +95,13 @@ The normal leader loop is: 3. add dependencies 4. inspect `ready` 5. `dispatch` tasks -6. `reconcile` inbox state back into task state -7. inspect `blocked` -8. answer blocked questions -9. if nothing is actionable, call `wait` -10. retry or reassign failures when needed -11. finish when all required tasks are `done` +6. arrange or launch a separate worker runtime that consumes the assigned inbox threads +7. use `status` for the current operational view; it reconciles first and includes latest attempt and message context +8. inspect `blocked` +9. answer blocked questions +10. if nothing is actionable, call `wait` +11. retry or reassign failures when needed +12. finish when all required tasks are `done` The leader should block on `orch wait`, not on ad hoc `sleep`. @@ -194,6 +197,7 @@ Behavior: - creates or links an `inbox` thread - writes workspace metadata into attempt storage and task payload - moves the task to `dispatched` +- does not start a worker runtime on its own Strict-mode recommendation: @@ -316,6 +320,14 @@ Suggested flags: - `--run RUN_ID` +Behavior: + +- reconciles inbox thread state before returning the view +- returns run aggregate counts plus per-task detail +- includes the latest attempt for each task when one exists +- includes the latest thread message for each task when one exists +- includes the latest blocked question for blocked tasks so the leader can inspect the current issue without a separate `blocked` call in the common case + ### `orch show` Show one task with dependencies, attempts, and inbox mapping. diff --git a/docs/tests/orch/ROADMAP.md b/docs/tests/orch/ROADMAP.md index 0bf0269..ac30928 100644 --- a/docs/tests/orch/ROADMAP.md +++ b/docs/tests/orch/ROADMAP.md @@ -18,12 +18,13 @@ It is not a replacement for automated Go tests. Snapshot date: -- `2026-03-19` +- `2026-03-20` Current state: - `orch` CLI is implemented for the current scheduler, strict worktree, wait, and council review surfaces - automated Go tests now cover every currently documented `orch` command case and workflow case, combining the original integration suite with focused contract tests for run/task/ready/dispatch/blocked/answer/cleanup/status/reconcile/workflow/council-report edges +- `status` coverage now also documents the richer leader view: auto-reconcile plus latest attempt, latest message, and blocked-question context - this roadmap now exists under `docs/tests/orch/ROADMAP.md` - all planned global, shared, workflow, command-index, and command-case Markdown documents in the current `orch` test-plan set have been authored - every implemented `orch` leaf-command folder now uses `README.md` as an index plus one Markdown file per planned case @@ -31,10 +32,10 @@ Current state: Progress summary for planned test-plan documents, excluding `ROADMAP.md`: -- planned document files: `64` -- authored document files: `64` -- planned case slugs in this roadmap: `46` -- authored case slugs in this roadmap: `46` +- planned document files: `65` +- authored document files: `65` +- planned case slugs in this roadmap: `47` +- authored case slugs in this roadmap: `47` ## Scope @@ -270,6 +271,7 @@ docs/tests/orch/ | `docs/tests/orch/cleanup/cleanup-returns-no-matching-work-when-filters-miss.md` | `cleanup` command case | 1 | 1 | done | | `docs/tests/orch/status/README.md` | `status` command case index | 0 | 0 | done | | `docs/tests/orch/status/status-returns-run-summary-and-task-list.md` | `status` command case | 1 | 1 | done | +| `docs/tests/orch/status/status-auto-reconciles-and-includes-blocked-context.md` | `status` command case | 1 | 1 | done | | `docs/tests/orch/council-start/README.md` | `council start` command case index | 0 | 0 | done | | `docs/tests/orch/council-start/council-start-dispatches-three-reviewers.md` | `council start` command case | 1 | 1 | done | | `docs/tests/orch/council-wait/README.md` | `council wait` command case index | 0 | 0 | done | @@ -333,7 +335,8 @@ docs/tests/orch/ | `docs/tests/orch/cleanup/cleanup-removes-completed-worktree.md` | `cleanup-removes-completed-worktree` | cleanup removes completed attempt worktree artifacts | done | | `docs/tests/orch/cleanup/cleanup-rejects-attempt-without-task.md` | `cleanup-rejects-attempt-without-task` | cleanup enforces `--task` when `--attempt` is specified | done | | `docs/tests/orch/cleanup/cleanup-returns-no-matching-work-when-filters-miss.md` | `cleanup-returns-no-matching-work-when-filters-miss` | cleanup returns no_matching_work when selectors find no candidates | done | -| `docs/tests/orch/status/status-returns-run-summary-and-task-list.md` | `status-returns-run-summary-and-task-list` | status reports aggregate run state and per-task statuses | done | +| `docs/tests/orch/status/status-returns-run-summary-and-task-list.md` | `status-returns-run-summary-and-task-list` | status reports aggregate run state, per-task statuses, and latest attempt context | done | +| `docs/tests/orch/status/status-auto-reconciles-and-includes-blocked-context.md` | `status-auto-reconciles-and-includes-blocked-context` | status auto-reconciles inbox state and exposes blocked-task question context | done | | `docs/tests/orch/council-start/council-start-dispatches-three-reviewers.md` | `council-start-dispatches-three-reviewers` | council start creates and dispatches three fixed reviewer tasks | done | | `docs/tests/orch/council-wait/council-wait-wakes-when-all-reviewers-complete.md` | `council-wait-wakes-when-all-reviewers-complete` | council wait wakes when all reviewer tasks complete | done | | `docs/tests/orch/council-wait/council-wait-times-out-when-reviewers-incomplete.md` | `council-wait-times-out-when-reviewers-incomplete` | council wait timeout stays machine-readable | done | diff --git a/docs/tests/orch/status/README.md b/docs/tests/orch/status/README.md index 4965a2a..71eb3ef 100644 --- a/docs/tests/orch/status/README.md +++ b/docs/tests/orch/status/README.md @@ -4,4 +4,5 @@ | Case Slug | File | Coverage Note | | --- | --- | --- | -| `status-returns-run-summary-and-task-list` | [status-returns-run-summary-and-task-list.md](./status-returns-run-summary-and-task-list.md) | returns aggregate run status plus the per-task status list | +| `status-returns-run-summary-and-task-list` | [status-returns-run-summary-and-task-list.md](./status-returns-run-summary-and-task-list.md) | returns aggregate run status plus the per-task status list and latest attempt context | +| `status-auto-reconciles-and-includes-blocked-context` | [status-auto-reconciles-and-includes-blocked-context.md](./status-auto-reconciles-and-includes-blocked-context.md) | auto-reconciles inbox state and exposes blocked-task attempt and question context | diff --git a/docs/tests/orch/status/status-auto-reconciles-and-includes-blocked-context.md b/docs/tests/orch/status/status-auto-reconciles-and-includes-blocked-context.md new file mode 100644 index 0000000..786aafe --- /dev/null +++ b/docs/tests/orch/status/status-auto-reconciles-and-includes-blocked-context.md @@ -0,0 +1,41 @@ +# Case: `status-auto-reconciles-and-includes-blocked-context` + +## 用例意义 + +验证 `status` 在返回结果前会先 reconcile 当前 inbox 线程状态,并附带 blocked 任务的 latest attempt、latest message 与 latest blocked question 上下文,方便 leader 直接判断谁在执行、卡在什么问题上。 + +## 前置条件 + +- 已存在 run `run_blog_002` +- 任务 `T1` 已 dispatch 到 `worker-a` +- `worker-a` 已 `claim` 对应线程,并写入一次 `blocked` 问题 +- leader 尚未显式执行 `reconcile` + +## 输入 + +```bash +orch --db TMPDIR/coord.db --json run init --run run_blog_002 --goal "Build blog MVP" +orch --db TMPDIR/coord.db --json task add --run run_blog_002 --task T1 --title "Implement retry policy" --default-to worker-a +orch --db TMPDIR/coord.db --json dispatch --run run_blog_002 --task T1 --body "Implement retry handling for the HTTP client." +inbox --db TMPDIR/coord.db --json claim --agent worker-a --thread THREAD_ID +inbox --db TMPDIR/coord.db --json update --agent worker-a --thread THREAD_ID --status blocked --summary "Need logging decision" --payload-json '{"question":"Should retry attempts be logged?"}' +orch --db TMPDIR/coord.db --json status --run run_blog_002 +``` + +## 预期输出 + +- `status` 退出码为 `0` +- `data.run.status == "blocked"` +- `data.tasks[0].status == "blocked"` +- `data.tasks[0].latest_attempt.thread_id == THREAD_ID` +- `data.tasks[0].latest_attempt.status == "blocked"` +- `data.tasks[0].latest_message.kind == "question"` +- `data.tasks[0].latest_message.summary == "Need logging decision"` +- `data.tasks[0].blocked_question.kind == "question"` +- `data.tasks[0].blocked_question.summary == "Need logging decision"` + +## 断言结论 + +- `status` 现在是更偏 operational 的 leader 视图,而不是只读的任务列表查询 +- leader 在常见排障场景里,不必先手工 `reconcile` 再额外跑 `blocked` +- enriched task context 能直接暴露当前 attempt 与问题摘要,减少二次查询 diff --git a/docs/tests/orch/status/status-returns-run-summary-and-task-list.md b/docs/tests/orch/status/status-returns-run-summary-and-task-list.md index 72e8586..0bae3bf 100644 --- a/docs/tests/orch/status/status-returns-run-summary-and-task-list.md +++ b/docs/tests/orch/status/status-returns-run-summary-and-task-list.md @@ -2,7 +2,7 @@ ## 用例意义 -验证 `status` 会返回 run 聚合视图以及任务明细列表,是 leader 端的完整状态检查入口。 +验证 `status` 会返回 run 聚合视图、任务明细列表以及最新 attempt/message 上下文,是 leader 端的完整状态检查入口。 ## 前置条件 @@ -30,8 +30,13 @@ orch --db TMPDIR/coord.db --json status --run run_blog_001 - 返回 `data.tasks` 数组 - `data.tasks[0].task_id == "T1"` - `data.tasks[0].status == "done"` +- `data.tasks[0].latest_attempt.assigned_to == "worker-a"` +- `data.tasks[0].latest_attempt.status == "done"` +- `data.tasks[0].latest_message.kind == "result"` +- `data.tasks[0].latest_message.summary == "Retry policy implemented"` ## 断言结论 - `status` 比 `run show` 更完整,适合做 run 级收口检查 - 任务清单与 run 聚合状态应保持一致,不应出现 run 已完成而任务仍显示旧状态的结果 +- leader 不必再单独查询 attempt 或 thread 历史,常见收口信息可直接从 `status` 拿到 diff --git a/packages/coord-core/store/orch.go b/packages/coord-core/store/orch.go index d2dd19b..ae3fa5c 100644 --- a/packages/coord-core/store/orch.go +++ b/packages/coord-core/store/orch.go @@ -72,6 +72,19 @@ type RunOverview struct { Tasks []Task `json:"tasks,omitempty"` } +type RunStatusTask struct { + Task + LatestAttempt *TaskAttempt `json:"latest_attempt,omitempty"` + LatestMessage *Message `json:"latest_message,omitempty"` + BlockedQuestion *Message `json:"blocked_question,omitempty"` +} + +type RunStatusView struct { + Run Run `json:"run"` + TaskCounts map[string]int `json:"task_counts"` + Tasks []RunStatusTask `json:"tasks,omitempty"` +} + type CreateRunInput struct { RunID string Goal string @@ -1781,6 +1794,52 @@ func (s *OrchStore) GetRunOverview(ctx context.Context, runID string) (RunOvervi }, nil } +func (s *OrchStore) GetRunStatusView(ctx context.Context, runID string) (RunStatusView, error) { + if strings.TrimSpace(runID) == "" { + return RunStatusView{}, fmt.Errorf("%w: run id is required", ErrInvalidInput) + } + + now := nowUTC() + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return RunStatusView{}, fmt.Errorf("begin run status transaction: %w", err) + } + defer tx.Rollback() + + if _, err := selectRun(ctx, tx, runID); err != nil { + return RunStatusView{}, err + } + if err := refreshReadyStates(ctx, tx, runID, now); err != nil { + return RunStatusView{}, err + } + if err := updateRunAggregateStatus(ctx, tx, runID, now); err != nil { + return RunStatusView{}, err + } + + run, err := selectRun(ctx, tx, runID) + if err != nil { + return RunStatusView{}, err + } + taskCounts, err := collectTaskCounts(ctx, tx, runID) + if err != nil { + return RunStatusView{}, err + } + tasks, err := listRunStatusTasks(ctx, tx, runID) + if err != nil { + return RunStatusView{}, err + } + + if err := tx.Commit(); err != nil { + return RunStatusView{}, fmt.Errorf("commit run status transaction: %w", err) + } + + return RunStatusView{ + Run: run, + TaskCounts: taskCounts, + Tasks: tasks, + }, nil +} + func (s *OrchStore) WaitForEvents(ctx context.Context, input WaitInput) (WaitResult, error) { if strings.TrimSpace(input.RunID) == "" { return WaitResult{}, fmt.Errorf("%w: run id is required", ErrInvalidInput) @@ -1893,6 +1952,47 @@ func listTasksForRun(ctx context.Context, db queryRowsContexter, runID string) ( return tasks, nil } +func listRunStatusTasks(ctx context.Context, db queryRowsAndRower, runID string) ([]RunStatusTask, error) { + tasks, err := listTasksForRun(ctx, db, runID) + if err != nil { + return nil, err + } + + items := make([]RunStatusTask, 0, len(tasks)) + for _, task := range tasks { + item := RunStatusTask{Task: task} + if task.LatestAttemptNo > 0 { + attempt, err := selectAttempt(ctx, db, runID, task.TaskID, task.LatestAttemptNo) + if err != nil { + return nil, err + } + attemptCopy := attempt + item.LatestAttempt = &attemptCopy + + if strings.TrimSpace(attempt.ThreadID) != "" { + latestMessage, err := selectLatestThreadMessage(ctx, db, attempt.ThreadID) + if err != nil { + return nil, err + } + latestMessageCopy := latestMessage + item.LatestMessage = &latestMessageCopy + + if task.Status == "blocked" { + question, err := selectLatestQuestionMessage(ctx, db, attempt.ThreadID) + if err != nil { + return nil, err + } + questionCopy := question + item.BlockedQuestion = &questionCopy + } + } + } + items = append(items, item) + } + + return items, nil +} + func (s *OrchStore) findRunEventsAfter(ctx context.Context, runID string, afterEventID int64, eventTypes []string) ([]RunEvent, int64, bool, error) { args := []any{runID, afterEventID} query := `SELECT @@ -2194,6 +2294,33 @@ func selectLatestQuestionMessage(ctx context.Context, db queryRowsAndRower, thre return message, nil } +func selectLatestThreadMessage(ctx context.Context, db queryRowsAndRower, threadID string) (Message, error) { + row := db.QueryRowContext( + ctx, + `SELECT + message_id, thread_id, from_agent, to_agent, kind, summary, body, + payload_json, created_at + FROM messages + WHERE thread_id = ? + ORDER BY created_at DESC + LIMIT 1`, + threadID, + ) + message, err := scanMessage(row) + if errors.Is(err, sql.ErrNoRows) { + return Message{}, fmt.Errorf("%w: thread %s has no messages", ErrInvalidState, threadID) + } + if err != nil { + return Message{}, err + } + artifactsByMessageID, err := loadArtifactsForMessageIDsFromQueryer(ctx, db, []string{message.MessageID}) + if err != nil { + return Message{}, err + } + message.Artifacts = artifactsByMessageID[message.MessageID] + return message, nil +} + type queryRowsAndRower interface { queryRower QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) diff --git a/packages/orch-runtime/internal/cli/orch/command_contracts_remaining_test.go b/packages/orch-runtime/internal/cli/orch/command_contracts_remaining_test.go index 5c093cf..336c62e 100644 --- a/packages/orch-runtime/internal/cli/orch/command_contracts_remaining_test.go +++ b/packages/orch-runtime/internal/cli/orch/command_contracts_remaining_test.go @@ -375,6 +375,120 @@ func TestOrchStatusReturnsRunSummaryAndTaskList(t *testing.T) { if got, _ := task["status"].(string); got != "done" { t.Fatalf("expected task status done, got %#v", task["status"]) } + if got := nestedString(t, task, "latest_attempt", "assigned_to"); got != "worker-a" { + t.Fatalf("expected latest_attempt.assigned_to worker-a, got %q", got) + } + if got := nestedString(t, task, "latest_attempt", "status"); got != "done" { + t.Fatalf("expected latest_attempt.status done, got %q", got) + } + if got := nestedString(t, task, "latest_message", "kind"); got != "result" { + t.Fatalf("expected latest_message.kind result, got %q", got) + } + if got := nestedString(t, task, "latest_message", "summary"); got != "Retry policy implemented" { + t.Fatalf("expected latest_message.summary to match result summary, got %q", got) + } +} + +func TestOrchStatusAutoReconcilesAndIncludesBlockedContext(t *testing.T) { + t.Parallel() + + dbPath := filepath.Join(t.TempDir(), "coord.db") + + runOrchCommand( + t, + "--db", dbPath, + "--json", + "run", "init", + "--run", "run_blog_status_002", + "--goal", "Build blog MVP", + ) + runOrchCommand( + t, + "--db", dbPath, + "--json", + "task", "add", + "--run", "run_blog_status_002", + "--task", "T1", + "--title", "Implement retry policy", + "--default-to", "worker-a", + ) + + dispatchOut := runOrchCommand( + t, + "--db", dbPath, + "--json", + "dispatch", + "--run", "run_blog_status_002", + "--task", "T1", + "--body", "Implement retry handling for the HTTP client.", + ) + + var dispatchResp map[string]any + mustDecodeJSON(t, dispatchOut, &dispatchResp) + threadID := nestedString(t, dispatchResp, "data", "attempt", "thread_id") + + runInboxCommand( + t, + "--db", dbPath, + "--json", + "claim", + "--agent", "worker-a", + "--thread", threadID, + ) + runInboxCommand( + t, + "--db", dbPath, + "--json", + "update", + "--agent", "worker-a", + "--thread", threadID, + "--status", "blocked", + "--summary", "Need logging decision", + "--payload-json", `{"question":"Should retry attempts be logged?"}`, + ) + + statusOut := runOrchCommand( + t, + "--db", dbPath, + "--json", + "status", + "--run", "run_blog_status_002", + ) + + var statusResp map[string]any + mustDecodeJSON(t, statusOut, &statusResp) + if got := nestedString(t, statusResp, "data", "run", "status"); got != "blocked" { + t.Fatalf("expected run status blocked after status auto-reconcile, got %q", got) + } + tasks := nestedArray(t, statusResp, "data", "tasks") + if len(tasks) != 1 { + t.Fatalf("expected one task in status response, got %#v", tasks) + } + task, ok := tasks[0].(map[string]any) + if !ok { + t.Fatalf("expected task object, got %#v", tasks[0]) + } + if got := nestedString(t, task, "status"); got != "blocked" { + t.Fatalf("expected task status blocked, got %q", got) + } + if got := nestedString(t, task, "latest_attempt", "status"); got != "blocked" { + t.Fatalf("expected latest_attempt.status blocked, got %q", got) + } + if got := nestedString(t, task, "latest_attempt", "thread_id"); got != threadID { + t.Fatalf("expected latest_attempt.thread_id %q, got %q", threadID, got) + } + if got := nestedString(t, task, "latest_message", "kind"); got != "question" { + t.Fatalf("expected latest_message.kind question, got %q", got) + } + if got := nestedString(t, task, "latest_message", "summary"); got != "Need logging decision" { + t.Fatalf("expected latest_message.summary to match blocked update, got %q", got) + } + if got := nestedString(t, task, "blocked_question", "summary"); got != "Need logging decision" { + t.Fatalf("expected blocked_question.summary to match latest question, got %q", got) + } + if got := nestedString(t, task, "blocked_question", "kind"); got != "question" { + t.Fatalf("expected blocked_question.kind question, got %q", got) + } } func TestOrchReconcileMapsFailedThreadToTerminalTaskState(t *testing.T) { diff --git a/packages/orch-runtime/internal/cli/orch/status.go b/packages/orch-runtime/internal/cli/orch/status.go index 4c93b61..e655bb6 100644 --- a/packages/orch-runtime/internal/cli/orch/status.go +++ b/packages/orch-runtime/internal/cli/orch/status.go @@ -28,7 +28,12 @@ func newStatusCmd(root *rootOptions) *cobra.Command { } defer sqlDB.Close() - overview, err := store.NewOrchStore(sqlDB).GetRunOverview(ctx, opts.runID) + orchStore := store.NewOrchStore(sqlDB) + if _, err := orchStore.ReconcileRun(ctx, opts.runID); err != nil { + return err + } + + overview, err := orchStore.GetRunStatusView(ctx, opts.runID) if err != nil { return err } diff --git a/skills/orch/SKILL.md b/skills/orch/SKILL.md index 72f462c..f2267bc 100644 --- a/skills/orch/SKILL.md +++ b/skills/orch/SKILL.md @@ -7,32 +7,74 @@ description: Leader-side orchestration through a bundled orch CLI. Use when an a Use the bundled `./assets/orch` CLI to control leader-side orchestration through `orch`. +## Mental Model + +- `orch` is the leader control plane. It owns runs, tasks, attempts, dependencies, and scheduling state. +- `dispatch` creates an attempt and an `inbox` thread. It does not launch a worker by itself. +- After `dispatch`, a separate worker runtime or worker agent should consume the assigned thread through `skills/inbox/`. +- Use `orch` for planning and control. Use `inbox` for claim, progress, blocked questions, replies, and final results. + +## Good Fit + +- parallel work with `2` to `5` bounded subtasks +- runs that may block, retry, or need reassignment +- work where durable task state matters more than ad hoc chat coordination + +## Bad Fit + +- one tiny exploratory subtask +- work with no need for a run, thread, or retryable attempt record +- flows where leader-side scheduling would be more ceremony than value + ## Quick Start - Invoke `./assets/orch` relative to this skill directory. - Pass `--db` explicitly for every command. - Prefer `--json` whenever another agent or script will read the output. +- Initialize a new database path once through the bundled `inbox init` command before the first real run. +- Use `status` as the main operational view. It reconciles worker thread state first, then returns run, task, latest-attempt, and latest-message context. - Use this skill for leader-side scheduling and control-plane actions, not worker-side lease or progress updates. ## Rules - Prefer `orch` over hand-written `inbox send` for normal leader operations. -- Reconcile inbox state before making new dispatch decisions. +- Treat `dispatch` as handoff, not execution. After dispatch, arrange a separate worker runtime or worker agent to claim the mapped inbox thread. +- For analysis, review, or other read-only tasks, omit worktree flags so dispatch stays thread-only and light. - If nothing is actionable, use `wait` instead of manual sleep loops. - For code tasks, dispatch from a committed base and allocate a fresh worktree per attempt. - Use `blocked` and `answer` to resolve worker questions through the active attempt thread. - Use `retry` or `reassign` only after checking the latest task and attempt state. - Use `inbox` directly only for inspection or manual repair, not routine scheduling. +- If the local sandbox blocks SQLite writes, request escalation before the first real `orch` or `inbox` run against the shared DB. + +## Leader Loop + +1. Initialize the shared DB once with `../inbox/assets/inbox --db PATH --json init`. +2. Create the run and tasks through `run init`, `task add`, and `dep add`. +3. Use `ready` to find dispatchable work. +4. `dispatch` a task and capture the returned attempt metadata such as `thread_id`, `assigned_to`, and any worktree fields. +5. Launch or reuse a separate worker runtime or worker agent that uses `skills/inbox/` against the same DB path. +6. Use `status`, `wait`, `blocked`, `answer`, `retry`, `reassign`, and `cleanup` to operate the run until the required tasks are complete. + +## Worker Handoff Contract + +- The worker side should use `skills/inbox/`, not this skill. +- The leader should pass or preserve the `dispatch` result, especially `attempt.thread_id`, `attempt.assigned_to`, and worktree metadata when present. +- Code-writing workers should execute inside the assigned worktree path from the task payload or attempt metadata. +- Read-only or analysis workers can stay on the normal thread-only path with no worktree. ## Typical Commands ```bash +../inbox/assets/inbox --db ./coord.db --json init ./assets/orch --db ./coord.db --json run init --run blog_mvp_001 --goal "Build blog MVP" --summary "Public blog plus admin CRUD" ./assets/orch --db ./coord.db --json task add --run blog_mvp_001 --task T1 --title "Project skeleton" --summary "Initialize app structure and database wiring" --default-to foundation-worker +./assets/orch --db ./coord.db --json task add --run blog_mvp_001 --task T2 --title "Summarize flaky tests" --summary "Read logs and report next steps" --default-to qa-worker --acceptance-json '{"kind":"analysis"}' ./assets/orch --db ./coord.db --json dep add --run blog_mvp_001 --task T2 --depends-on T1 ./assets/orch --db ./coord.db --json ready --run blog_mvp_001 ./assets/orch --db ./coord.db --json dispatch --run blog_mvp_001 --task T1 --to foundation-worker --base-ref main --workspace-root .orch/worktrees --strict-worktree --body-file tasks/t1.md -./assets/orch --db ./coord.db --json reconcile --run blog_mvp_001 +./assets/orch --db ./coord.db --json dispatch --run blog_mvp_001 --task T2 --to qa-worker --body "Read the failing test logs and summarize the root cause." +./assets/orch --db ./coord.db --json status --run blog_mvp_001 ./assets/orch --db ./coord.db --json wait --run blog_mvp_001 --for task_blocked,task_done,task_failed --after-event 0 --timeout-seconds 900 ./assets/orch --db ./coord.db --json blocked --run blog_mvp_001 ./assets/orch --db ./coord.db --json answer --run blog_mvp_001 --task T2 --body "MVP supports draft and published only." @@ -48,7 +90,7 @@ Use the bundled `./assets/orch` CLI to control leader-side orchestration through - `dep add`: record a task dependency - `ready`: list tasks that are ready for dispatch - `dispatch`: create an attempt and inbox thread for a ready task -- `reconcile`: fold worker thread state back into orch task state +- `reconcile`: fold worker thread state back into orch task state explicitly - `wait`: block until matching run events arrive - `blocked`: list blocked tasks and their latest questions - `answer`: send a leader answer into the active blocked attempt @@ -56,11 +98,12 @@ Use the bundled `./assets/orch` CLI to control leader-side orchestration through - `reassign`: cancel the current attempt and dispatch a new one to another worker - `cancel`: cancel a task or entire run - `cleanup`: remove completed or abandoned worktrees -- `status`: inspect the full run summary and task list +- `status`: reconcile first, then inspect the full run summary, task list, latest attempt, and latest message context ## Notes - `dispatch` supports `--repo-path`, `--workspace-root`, `--strict-worktree`, and `--base-ref` for worktree-backed code execution. +- When worktree flags are omitted, code-like task metadata can still auto-enable strict worktree mode. Non-code tasks stay on the normal thread-only path. - `answer` supports `--payload-json` for structured decisions, not just freeform text. - `status` is the full run view; `run show` is the lighter aggregate view. - If the bundled binary cannot execute on the current host, stop and report the compatibility issue instead of guessing a replacement path or workflow. diff --git a/skills/orch/agents/openai.yaml b/skills/orch/agents/openai.yaml index d1ea20c..2cccc57 100644 --- a/skills/orch/agents/openai.yaml +++ b/skills/orch/agents/openai.yaml @@ -1,7 +1,7 @@ interface: display_name: "Orch CLI" short_description: "Leader-side orchestration CLI" - default_prompt: "Use $orch to manage orchestration runs through the bundled orch CLI and a SQLite orchestration database." + default_prompt: "Use $orch to manage leader-side orchestration runs through the bundled orch CLI and a SQLite orchestration database. Treat it as a control plane only: dispatch creates attempts and inbox threads, while separate workers consume them through inbox." policy: allow_implicit_invocation: true