Improve orch status reconciliation view
This commit is contained in:
@@ -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
|
||||
|
||||
+18
-6
@@ -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.
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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[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` 拿到
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+47
-4
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user