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.
|
- 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
@@ -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.
|
||||||
|
|||||||
@@ -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 |
|
||||||
|
|||||||
@@ -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` 拿到
|
||||||
|
|||||||
@@ -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
@@ -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,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
|
||||||
|
|||||||
Reference in New Issue
Block a user