Improve orch status reconciliation view

This commit is contained in:
2026-03-20 17:57:58 +08:00
parent 693a79345b
commit cf3c3cbe60
11 changed files with 374 additions and 21 deletions
+3 -1
View File
@@ -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. - The leader may use `inbox` directly for inspection or manual repair.
- Workers should use `inbox` only. - Workers should use `inbox` only.
- Workers should not use `orch`. - 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. - User-facing discussion stays with the leader.
- Code-writing workers should run in `orch`-assigned Git worktrees, not in the user's primary checkout. - 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 assigned worktree path should be stored in attempt metadata and inbox task payload
- the worker runtime should execute inside that worktree - the worker runtime should execute inside that worktree
- strict mode should require a committed base revision - 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. 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. 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` - `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 - `council-review` skill: used when the user explicitly wants a structured three-reviewer brainstorm or review with grouped and tallied recommendations
+18 -6
View File
@@ -11,6 +11,7 @@ In normal operation:
- leaders use `orch` - leaders use `orch`
- `orch` creates and monitors `inbox` threads - `orch` creates and monitors `inbox` threads
- workers continue using `inbox` - workers continue using `inbox`
- a separate worker runtime or worker agent must still consume the assigned inbox thread after `dispatch`
## Responsibilities ## Responsibilities
@@ -34,6 +35,7 @@ In normal operation:
- worker claiming - worker claiming
- direct worker polling - direct worker polling
- automatic worker-runtime launch
- raw message append storage - raw message append storage
- low-level thread history management - low-level thread history management
@@ -93,12 +95,13 @@ The normal leader loop is:
3. add dependencies 3. add dependencies
4. inspect `ready` 4. inspect `ready`
5. `dispatch` tasks 5. `dispatch` tasks
6. `reconcile` inbox state back into task state 6. arrange or launch a separate worker runtime that consumes the assigned inbox threads
7. inspect `blocked` 7. use `status` for the current operational view; it reconciles first and includes latest attempt and message context
8. answer blocked questions 8. inspect `blocked`
9. if nothing is actionable, call `wait` 9. answer blocked questions
10. retry or reassign failures when needed 10. if nothing is actionable, call `wait`
11. finish when all required tasks are `done` 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`. The leader should block on `orch wait`, not on ad hoc `sleep`.
@@ -194,6 +197,7 @@ Behavior:
- creates or links an `inbox` thread - creates or links an `inbox` thread
- writes workspace metadata into attempt storage and task payload - writes workspace metadata into attempt storage and task payload
- moves the task to `dispatched` - moves the task to `dispatched`
- does not start a worker runtime on its own
Strict-mode recommendation: Strict-mode recommendation:
@@ -316,6 +320,14 @@ Suggested flags:
- `--run RUN_ID` - `--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` ### `orch show`
Show one task with dependencies, attempts, and inbox mapping. Show one task with dependencies, attempts, and inbox mapping.
+9 -6
View File
@@ -18,12 +18,13 @@ It is not a replacement for automated Go tests.
Snapshot date: Snapshot date:
- `2026-03-19` - `2026-03-20`
Current state: Current state:
- `orch` CLI is implemented for the current scheduler, strict worktree, wait, and council review surfaces - `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 - 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` - 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 - 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 - 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`: Progress summary for planned test-plan documents, excluding `ROADMAP.md`:
- planned document files: `64` - planned document files: `65`
- authored document files: `64` - authored document files: `65`
- planned case slugs in this roadmap: `46` - planned case slugs in this roadmap: `47`
- authored case slugs in this roadmap: `46` - authored case slugs in this roadmap: `47`
## Scope ## 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/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/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-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/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-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 | | `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-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-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/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-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-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 | | `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 |
+2 -1
View File
@@ -4,4 +4,5 @@
| Case Slug | File | Coverage Note | | 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 |
@@ -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 与问题摘要,减少二次查询
@@ -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` 数组
- `data.tasks[0].task_id == "T1"` - `data.tasks[0].task_id == "T1"`
- `data.tasks[0].status == "done"` - `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 级收口检查 - `status``run show` 更完整,适合做 run 级收口检查
- 任务清单与 run 聚合状态应保持一致,不应出现 run 已完成而任务仍显示旧状态的结果 - 任务清单与 run 聚合状态应保持一致,不应出现 run 已完成而任务仍显示旧状态的结果
- leader 不必再单独查询 attempt 或 thread 历史,常见收口信息可直接从 `status` 拿到
+127
View File
@@ -72,6 +72,19 @@ type RunOverview struct {
Tasks []Task `json:"tasks,omitempty"` 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 { type CreateRunInput struct {
RunID string RunID string
Goal string Goal string
@@ -1781,6 +1794,52 @@ func (s *OrchStore) GetRunOverview(ctx context.Context, runID string) (RunOvervi
}, nil }, 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) { func (s *OrchStore) WaitForEvents(ctx context.Context, input WaitInput) (WaitResult, error) {
if strings.TrimSpace(input.RunID) == "" { if strings.TrimSpace(input.RunID) == "" {
return WaitResult{}, fmt.Errorf("%w: run id is required", ErrInvalidInput) 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 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) { func (s *OrchStore) findRunEventsAfter(ctx context.Context, runID string, afterEventID int64, eventTypes []string) ([]RunEvent, int64, bool, error) {
args := []any{runID, afterEventID} args := []any{runID, afterEventID}
query := `SELECT query := `SELECT
@@ -2194,6 +2294,33 @@ func selectLatestQuestionMessage(ctx context.Context, db queryRowsAndRower, thre
return message, nil 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 { type queryRowsAndRower interface {
queryRower queryRower
QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
@@ -375,6 +375,120 @@ func TestOrchStatusReturnsRunSummaryAndTaskList(t *testing.T) {
if got, _ := task["status"].(string); got != "done" { if got, _ := task["status"].(string); got != "done" {
t.Fatalf("expected task status done, got %#v", task["status"]) 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) { func TestOrchReconcileMapsFailedThreadToTerminalTaskState(t *testing.T) {
@@ -28,7 +28,12 @@ func newStatusCmd(root *rootOptions) *cobra.Command {
} }
defer sqlDB.Close() 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 { if err != nil {
return err return err
} }
+47 -4
View File
@@ -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`. 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 ## Quick Start
- Invoke `./assets/orch` relative to this skill directory. - Invoke `./assets/orch` relative to this skill directory.
- Pass `--db` explicitly for every command. - Pass `--db` explicitly for every command.
- Prefer `--json` whenever another agent or script will read the output. - 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. - Use this skill for leader-side scheduling and control-plane actions, not worker-side lease or progress updates.
## Rules ## Rules
- Prefer `orch` over hand-written `inbox send` for normal leader operations. - 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. - 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. - 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 `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 `retry` or `reassign` only after checking the latest task and attempt state.
- Use `inbox` directly only for inspection or manual repair, not routine scheduling. - 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 ## Typical Commands
```bash ```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 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 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 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 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 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 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 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." ./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 - `dep add`: record a task dependency
- `ready`: list tasks that are ready for dispatch - `ready`: list tasks that are ready for dispatch
- `dispatch`: create an attempt and inbox thread for a ready task - `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 - `wait`: block until matching run events arrive
- `blocked`: list blocked tasks and their latest questions - `blocked`: list blocked tasks and their latest questions
- `answer`: send a leader answer into the active blocked attempt - `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 - `reassign`: cancel the current attempt and dispatch a new one to another worker
- `cancel`: cancel a task or entire run - `cancel`: cancel a task or entire run
- `cleanup`: remove completed or abandoned worktrees - `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 ## Notes
- `dispatch` supports `--repo-path`, `--workspace-root`, `--strict-worktree`, and `--base-ref` for worktree-backed code execution. - `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. - `answer` supports `--payload-json` for structured decisions, not just freeform text.
- `status` is the full run view; `run show` is the lighter aggregate view. - `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. - If the bundled binary cannot execute on the current host, stop and report the compatibility issue instead of guessing a replacement path or workflow.
+1 -1
View File
@@ -1,7 +1,7 @@
interface: interface:
display_name: "Orch CLI" display_name: "Orch CLI"
short_description: "Leader-side orchestration 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: policy:
allow_implicit_invocation: true allow_implicit_invocation: true