refactor(monorepo): extract inbox and orch runtimes

This commit is contained in:
2026-03-20 13:08:33 +08:00
parent 1938eb8f07
commit 9b8e886289
78 changed files with 10516 additions and 77 deletions
+10 -4
View File
@@ -39,6 +39,7 @@ As of now:
- a repository-level skill workspace monorepo migration plan now exists under `docs/skill-workspace-monorepo.md`, defining the target split between runtime packages under `packages/`, agent-facing skill bundles under `skills/`, support apps under `apps/`, and package-based skill packaging flows
- the first migration phase for the skill workspace monorepo is now complete: root `go.work` exists, `pnpm-workspace.yaml` now discovers `packages/*`, empty runtime module roots now exist under `packages/`, and a declarative `scripts/skill-bundles.json` plus `scripts/package_skill_runtimes.sh` scaffold now define package-oriented skill bundle metadata from the repo root
- `packages/coord-core` now exists as the first real extracted runtime package, containing shared coordination DB/schema, protocol, and store code, and the active coordination runtimes now import `coord-core` instead of root `internal/db`, `internal/store`, and `internal/protocol`
- `packages/inbox-runtime` and `packages/orch-runtime` now exist as package-owned runtimes with their own `cmd/` entrypoints and package-local CLI wiring/tests, and the root skill packaging flow now builds `skills/inbox`, `skills/orch`, and `skills/council-review` from package entrypoints instead of root `cmd/` paths
- a repo-local `scripts/package_skill_clis.sh` packaging flow now builds bundled skill CLI assets for `inbox`, `orch`, and `council-review`
- `orch` now implements `run init/show`, `task add`, `dep add`, `ready`, `dispatch`, `reconcile`, `wait`, `blocked`, `answer`, `retry`, `reassign`, `cancel`, `cleanup`, and `status`
- `orch` can create runs, gate tasks through dependencies, dispatch work through `inbox`, reconcile worker thread state back into task state, answer blocked tasks, retry or reassign work, cancel tasks or runs, clean attempt worktrees, and create per-attempt Git worktrees during strict dispatch
@@ -501,10 +502,15 @@ Completed so far:
- `packages/coord-core/store` now owns the shared inbox, orch, and council store logic plus its coordination-domain tests
- root coordination runtimes under `cmd/`, `internal/cli/`, `internal/app/`, `internal/httpapi/`, and `internal/query/` now import `coord-core` instead of depending on root `internal/db`, `internal/store`, or root `internal/protocol`
- `go test ./...` still passes for the root module, and `go test ./...` passes inside `packages/coord-core`
- `packages/inbox-runtime/cmd/inbox` plus `packages/inbox-runtime/internal/cli/inbox` now provide a package-owned inbox runtime and pass `go test ./...`
- `packages/orch-runtime/cmd/orch` plus `packages/orch-runtime/internal/cli/orch` now provide a package-owned orch runtime and pass `go test ./...`
- `scripts/skill-bundles.json` now marks `inbox`, `orch`, and `council-review` as ready package-backed bundles
- `scripts/package_skill_runtimes.sh package` now builds and installs `skills/inbox/assets/inbox`, `skills/orch/assets/orch`, and `skills/council-review/assets/orch` from package entrypoints
- the legacy `scripts/package_skill_clis.sh` entrypoint now delegates to the declarative package-oriented packaging flow instead of hardcoding root `cmd/` paths
Remaining:
- extract `inbox`, `orch`, and `orchd` into package-owned runtimes
- extract `orchd` into a package-owned runtime
- import `repo-memory` as its own runtime package and add the corresponding skill bundle
- graduate the bundle scaffold into the primary packaging flow once package-owned runtime entrypoints exist
@@ -515,11 +521,11 @@ If a new agent is taking over now, the next concrete step should be:
1. treat `Milestone 9: Web Product Phase 2 Read-Only Operator UI` as complete for the initial operator surface and do not expand web feature scope further until the workspace split is decided package-by-package
2. treat the Phase 1 workspace bootstrap for `Milestone 10` as complete and keep the new `go.work`, `packages/`, and declarative bundle metadata as the baseline for all further migration steps
3. treat the shared coordination kernel extraction into `packages/coord-core` as complete and move `inbox` plus `orch` into package-owned runtimes next
4. keep the authored skill forward-test plans under `docs/tests/*-skill/` synchronized as runtime ownership moves from root paths to package paths
5. keep the legacy hardcoded packaging flow working temporarily, but evolve the new declarative bundle scaffold into the primary packaging path before adding `repo-memory`
4. treat `inbox-runtime` and `orch-runtime` as package-owned and move `orchd` into `packages/orchd-runtime` next so the web backend stops depending on root-owned runtime code
5. keep the authored skill forward-test plans under `docs/tests/*-skill/` synchronized as runtime ownership moves from root paths to package paths
6. import `repo-memory` only after the package-based runtime and skill packaging pattern exists
The inbox implementation and its human-readable test-plan set are already in place, `orch` supports the main scheduler loop plus the complete council start/wait/tally/report workflow, the web product now has its first real operator-facing read surfaces, and the repository has completed both the workspace bootstrap and the shared coordination-kernel extraction phases of the skill monorepo migration, so the next step should be runtime extraction rather than continuing to accrete new root-owned runtimes.
The inbox implementation and its human-readable test-plan set are already in place, `orch` supports the main scheduler loop plus the complete council start/wait/tally/report workflow, the web product now has its first real operator-facing read surfaces, and the repository has completed the workspace bootstrap, the shared coordination-kernel extraction, and the first package-owned runtime extraction phases of the skill monorepo migration, so the next step should be moving `orchd` and then importing `repo-memory`, not continuing to accrete new root-owned runtime paths.
## Recommended Driver Choices
@@ -27,7 +27,7 @@
- [x] create or adopt an active execution roadmap for the migration workstream
- [x] Phase 1: bootstrap `go.work`, expanded workspace manifests, package roots, and declarative skill bundle metadata
- [x] Phase 2: extract shared coordination code into `packages/coord-core`
- [ ] Phase 3: extract `inbox-runtime` and `orch-runtime`
- [x] Phase 3: extract `inbox-runtime` and `orch-runtime`
- [ ] Phase 4: extract `orchd-runtime`
- [ ] Phase 5: import `repo-memory-runtime` and add `skills/repo-memory`
- [ ] Phase 6: remove root runtime ownership and normalize package-based packaging
@@ -54,4 +54,4 @@
## Next Step
- start Phase 3 by moving `inbox` and `orch` into package-owned runtimes on top of the now-shared `packages/coord-core` kernel
- start Phase 4 by moving the HTTP/query/web backend runtime into `packages/orchd-runtime` on top of the extracted `coord-core`, `inbox-runtime`, and `orch-runtime` packages
+1
View File
@@ -3,6 +3,7 @@
"private": true,
"packageManager": "pnpm@10.25.0",
"scripts": {
"skills:bundle": "bash ./scripts/package_skill_runtimes.sh package",
"skills:bundle:plan": "bash ./scripts/package_skill_runtimes.sh plan",
"skills:bundle:validate": "bash ./scripts/package_skill_runtimes.sh validate",
"web:dev": "pnpm --filter @ai-workflow-skill/web dev",
+11
View File
@@ -0,0 +1,11 @@
package main
import (
"os"
inboxcli "ai-workflow-skill/packages/inbox-runtime/internal/cli/inbox"
)
func main() {
os.Exit(inboxcli.Execute(os.Args[1:], os.Stdout, os.Stderr))
}
+4
View File
@@ -1,3 +1,7 @@
module ai-workflow-skill/packages/inbox-runtime
go 1.26
require (
github.com/spf13/cobra v1.10.1
)
@@ -0,0 +1,78 @@
package inbox
import (
"strings"
"ai-workflow-skill/packages/coord-core/protocol"
"ai-workflow-skill/packages/coord-core/store"
"github.com/spf13/cobra"
)
type artifactOptions struct {
paths []string
kinds []string
metadataJSONs []string
}
func addArtifactFlags(cmd *cobra.Command, opts *artifactOptions) {
cmd.Flags().StringArrayVar(&opts.paths, "artifact", nil, "Artifact path to attach; may be repeated")
cmd.Flags().StringArrayVar(&opts.kinds, "artifact-kind", nil, "Artifact kind; one value applies to all, or match artifact count")
cmd.Flags().StringArrayVar(&opts.metadataJSONs, "artifact-metadata-json", nil, "Artifact metadata JSON; one value applies to all, or match artifact count")
}
func resolveArtifacts(opts artifactOptions) ([]store.ArtifactInput, error) {
if len(opts.paths) == 0 {
if len(opts.kinds) > 0 || len(opts.metadataJSONs) > 0 {
return nil, protocol.InvalidInput("artifact-kind and artifact-metadata-json require at least one artifact path", nil)
}
return nil, nil
}
kinds, err := expandArtifactValues(opts.kinds, len(opts.paths), "artifact-kind")
if err != nil {
return nil, err
}
metadataJSONs, err := expandArtifactValues(opts.metadataJSONs, len(opts.paths), "artifact-metadata-json")
if err != nil {
return nil, err
}
artifacts := make([]store.ArtifactInput, 0, len(opts.paths))
for i, path := range opts.paths {
if strings.TrimSpace(path) == "" {
return nil, protocol.InvalidInput("artifact path cannot be empty", nil)
}
artifact := store.ArtifactInput{
Path: path,
Kind: "file",
}
if len(kinds) > 0 {
artifact.Kind = kinds[i]
}
if len(metadataJSONs) > 0 {
artifact.MetadataJSON = metadataJSONs[i]
}
artifacts = append(artifacts, artifact)
}
return artifacts, nil
}
func expandArtifactValues(values []string, target int, flagName string) ([]string, error) {
switch len(values) {
case 0:
return nil, nil
case 1:
out := make([]string, target)
for i := range out {
out[i] = values[0]
}
return out, nil
case target:
return values, nil
default:
return nil, protocol.InvalidInput(flagName+" must be specified once or once per artifact", nil)
}
}
@@ -0,0 +1,22 @@
package inbox
import (
"os"
"ai-workflow-skill/packages/coord-core/protocol"
)
func resolveBodyValue(body, bodyFile string) (string, error) {
if body != "" && bodyFile != "" {
return "", protocol.InvalidInput("body and body-file are mutually exclusive", nil)
}
if bodyFile == "" {
return body, nil
}
content, err := os.ReadFile(bodyFile)
if err != nil {
return "", protocol.InvalidInput("failed to read body-file", err)
}
return string(content), nil
}
@@ -0,0 +1,83 @@
package inbox
import (
"fmt"
"ai-workflow-skill/packages/coord-core/protocol"
"ai-workflow-skill/packages/coord-core/store"
"github.com/spf13/cobra"
)
type cancelOptions struct {
agent string
threadID string
reason string
artifacts artifactOptions
}
func newCancelCmd(root *rootOptions) *cobra.Command {
opts := &cancelOptions{}
cmd := &cobra.Command{
Use: "cancel",
Short: "Cancel a thread",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
agent := opts.agent
if agent == "" {
agent = root.agent
}
if agent == "" {
return protocol.InvalidInput("agent is required", nil)
}
artifacts, err := resolveArtifacts(opts.artifacts)
if err != nil {
return err
}
sqlDB, err := openInboxDB(ctx, root.dbPath)
if err != nil {
return err
}
defer sqlDB.Close()
s := store.NewInboxStore(sqlDB)
thread, message, err := s.CancelThread(ctx, store.CancelInput{
ThreadID: opts.threadID,
Agent: agent,
Reason: opts.reason,
Artifacts: artifacts,
})
if err != nil {
return err
}
resp := protocol.Success{
OK: true,
Command: "cancel",
Data: map[string]any{
"thread": thread,
"message": message,
},
}
if root.json {
return protocol.WriteJSON(cmd.OutOrStdout(), resp)
}
_, err = fmt.Fprintf(cmd.OutOrStdout(), "cancelled thread %s\n", thread.ThreadID)
return err
},
}
cmd.Flags().StringVar(&opts.agent, "agent", "", "Acting agent")
cmd.Flags().StringVar(&opts.threadID, "thread", "", "Thread ID")
cmd.Flags().StringVar(&opts.reason, "reason", "", "Cancellation reason")
addArtifactFlags(cmd, &opts.artifacts)
_ = cmd.MarkFlagRequired("thread")
return cmd
}
@@ -0,0 +1,148 @@
package inbox
import (
"os"
"path/filepath"
"testing"
)
func TestCancelMarksThreadCancelled(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
runInboxCommand(t, "--db", dbPath, "--json", "init")
sendOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"send",
"--from", "leader",
"--to", "worker-a",
"--subject", "Implement cancellation",
"--summary", "Initial request",
)
var sendResp map[string]any
mustDecodeJSON(t, sendOut, &sendResp)
threadID := nestedString(t, sendResp, "data", "thread", "thread_id")
cancelOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"cancel",
"--agent", "leader",
"--thread", threadID,
"--reason", "Task superseded by a larger refactor",
)
var cancelResp map[string]any
mustDecodeJSON(t, cancelOut, &cancelResp)
if status := nestedString(t, cancelResp, "data", "thread", "status"); status != "cancelled" {
t.Fatalf("expected cancelled thread, got %q", status)
}
if kind := nestedString(t, cancelResp, "data", "message", "kind"); kind != "control" {
t.Fatalf("expected control message, got %q", kind)
}
}
func TestCancelPersistsReasonAndArtifact(t *testing.T) {
t.Parallel()
tempDir := t.TempDir()
dbPath := filepath.Join(tempDir, "coord.db")
cancelPath := filepath.Join(tempDir, "cancel.md")
if err := os.WriteFile(cancelPath, []byte("Cancelled by product decision"), 0o644); err != nil {
t.Fatalf("write cancel artifact: %v", err)
}
runInboxCommand(t, "--db", dbPath, "--json", "init")
sendOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"send",
"--from", "leader",
"--to", "worker-a",
"--subject", "Implement cancellation",
"--summary", "Initial request",
)
var sendResp map[string]any
mustDecodeJSON(t, sendOut, &sendResp)
threadID := nestedString(t, sendResp, "data", "thread", "thread_id")
runInboxCommand(
t,
"--db", dbPath,
"--json",
"cancel",
"--agent", "leader",
"--thread", threadID,
"--reason", "Task superseded by a larger refactor",
"--artifact", cancelPath,
"--artifact-kind", "brief",
)
showOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"show",
"--thread", threadID,
)
var showResp map[string]any
mustDecodeJSON(t, showOut, &showResp)
messages, ok := nestedValue(t, showResp, "data", "messages").([]any)
if !ok || len(messages) == 0 {
t.Fatalf("expected non-empty message history, got %#v", nestedValue(t, showResp, "data", "messages"))
}
lastMessage, ok := messages[len(messages)-1].(map[string]any)
if !ok {
t.Fatalf("expected message object, got %#v", messages[len(messages)-1])
}
if got := lastMessage["summary"]; got != "Task superseded by a larger refactor" {
t.Fatalf("expected cancel summary, got %#v", got)
}
if got := lastMessage["body"]; got != "Task superseded by a larger refactor" {
t.Fatalf("expected cancel body, got %#v", got)
}
artifacts, ok := lastMessage["artifacts"].([]any)
if !ok || len(artifacts) != 1 {
t.Fatalf("expected one cancel artifact, got %#v", lastMessage["artifacts"])
}
artifact, ok := artifacts[0].(map[string]any)
if !ok {
t.Fatalf("expected artifact object, got %#v", artifacts[0])
}
if got := artifact["path"]; got != cancelPath {
t.Fatalf("expected artifact path %q, got %#v", cancelPath, got)
}
if got := artifact["kind"]; got != "brief" {
t.Fatalf("expected artifact kind brief, got %#v", got)
}
}
func TestCancelRejectsWhenThreadMissing(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
runInboxCommand(t, "--db", dbPath, "--json", "init")
stdout, _, exitCode := executeInboxCommand(
"--db", dbPath,
"--agent", "leader",
"--json",
"cancel",
"--thread", "thr_missing",
)
if exitCode != 40 {
t.Fatalf("expected not-found exit code 40, got %d with %s", exitCode, stdout)
}
assertErrorJSON(t, stdout, "not_found")
}
@@ -0,0 +1,80 @@
package inbox
import (
"errors"
"fmt"
"ai-workflow-skill/packages/coord-core/protocol"
"ai-workflow-skill/packages/coord-core/store"
"github.com/spf13/cobra"
)
type claimOptions struct {
agent string
threadID string
leaseSeconds int
}
func newClaimCmd(root *rootOptions) *cobra.Command {
opts := &claimOptions{}
cmd := &cobra.Command{
Use: "claim",
Short: "Acquire a lease on a pending thread",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
agent := opts.agent
if agent == "" {
agent = root.agent
}
if agent == "" {
return protocol.InvalidInput("agent is required", nil)
}
sqlDB, err := openInboxDB(ctx, root.dbPath)
if err != nil {
return err
}
defer sqlDB.Close()
s := store.NewInboxStore(sqlDB)
result, err := s.ClaimThread(ctx, store.ClaimInput{
ThreadID: opts.threadID,
Agent: agent,
LeaseSeconds: opts.leaseSeconds,
})
if err != nil {
if errors.Is(err, store.ErrLeaseConflict) {
return fmt.Errorf("lease conflict: %w", err)
}
return err
}
resp := protocol.Success{
OK: true,
Command: "claim",
Data: map[string]any{
"thread": result.Thread,
"message": result.Message,
},
}
if root.json {
return protocol.WriteJSON(cmd.OutOrStdout(), resp)
}
_, err = fmt.Fprintf(cmd.OutOrStdout(), "claimed thread %s\n", result.Thread.ThreadID)
return err
},
}
cmd.Flags().StringVar(&opts.agent, "agent", "", "Claiming agent")
cmd.Flags().StringVar(&opts.threadID, "thread", "", "Thread ID")
cmd.Flags().IntVar(&opts.leaseSeconds, "lease-seconds", 900, "Lease duration in seconds")
_ = cmd.MarkFlagRequired("thread")
return cmd
}
@@ -0,0 +1,112 @@
package inbox
import "testing"
func TestClaimAcquiresThreadLease(t *testing.T) {
t.Parallel()
dbPath := initCommandTestDB(t)
threadID := sendPendingThread(t, dbPath, "leader", "worker-a", "Race claim", "Claim this task")
claimOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"claim",
"--agent", "worker-a",
"--thread", threadID,
"--lease-seconds", "300",
)
var claimResp map[string]any
mustDecodeJSON(t, claimOut, &claimResp)
if got := nestedString(t, claimResp, "data", "thread", "status"); got != "claimed" {
t.Fatalf("expected claimed status, got %q", got)
}
if got := nestedString(t, claimResp, "data", "thread", "assigned_to"); got != "worker-a" {
t.Fatalf("expected assigned_to worker-a, got %q", got)
}
if got := nestedString(t, claimResp, "data", "message", "kind"); got != "event" {
t.Fatalf("expected event message kind, got %q", got)
}
if got := nestedString(t, claimResp, "data", "message", "summary"); got != "thread claimed" {
t.Fatalf("expected summary thread claimed, got %q", got)
}
}
func TestClaimRejectsWhenThreadMissing(t *testing.T) {
t.Parallel()
dbPath := initCommandTestDB(t)
stdout, _, exitCode := executeInboxCommand(
"--db", dbPath,
"--json",
"claim",
"--agent", "worker-z",
"--thread", "thr_missing",
)
if exitCode != 40 {
t.Fatalf("expected exit code 40, got %d", exitCode)
}
assertErrorJSON(t, stdout, "not_found")
}
func TestClaimRejectsWhenThreadAlreadyClaimed(t *testing.T) {
t.Parallel()
dbPath := initCommandTestDB(t)
threadID := sendPendingThread(t, dbPath, "leader", "worker-z", "Claimed task", "Already claimed")
runInboxCommand(
t,
"--db", dbPath,
"--json",
"claim",
"--agent", "worker-z",
"--thread", threadID,
)
stdout, _, exitCode := executeInboxCommand(
"--db", dbPath,
"--json",
"claim",
"--agent", "worker-y",
"--thread", threadID,
)
if exitCode != 20 {
t.Fatalf("expected exit code 20, got %d", exitCode)
}
assertErrorJSON(t, stdout, "lease_conflict")
}
func TestClaimRecordsRequestedLeaseDuration(t *testing.T) {
t.Parallel()
dbPath := initCommandTestDB(t)
threadID := sendPendingThread(t, dbPath, "leader", "worker-a", "Lease payload", "Verify lease payload")
claimOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"claim",
"--agent", "worker-a",
"--thread", threadID,
"--lease-seconds", "300",
)
var claimResp map[string]any
mustDecodeJSON(t, claimOut, &claimResp)
payload, ok := nestedValue(t, claimResp, "data", "message", "payload_json").(map[string]any)
if !ok {
t.Fatalf("expected payload_json object, got %#v", nestedValue(t, claimResp, "data", "message", "payload_json"))
}
leaseSeconds, ok := payload["lease_seconds"].(float64)
if !ok || int(leaseSeconds) != 300 {
t.Fatalf("expected lease_seconds 300, got %#v", payload["lease_seconds"])
}
leaseToken, _ := payload["lease_token"].(string)
if leaseToken == "" {
t.Fatalf("expected non-empty lease_token, got %#v", payload["lease_token"])
}
}
@@ -0,0 +1,22 @@
package inbox
import (
"context"
"database/sql"
"ai-workflow-skill/packages/coord-core/db"
)
func openInboxDB(ctx context.Context, dbPath string) (*sql.DB, error) {
sqlDB, err := db.Open(ctx, dbPath)
if err != nil {
return nil, err
}
if err := db.ApplyMigrations(ctx, sqlDB); err != nil {
_ = sqlDB.Close()
return nil, err
}
return sqlDB, nil
}
@@ -0,0 +1,106 @@
package inbox
import (
"fmt"
"ai-workflow-skill/packages/coord-core/protocol"
"ai-workflow-skill/packages/coord-core/store"
"github.com/spf13/cobra"
)
type completeOptions struct {
agent string
threadID string
summary string
body string
bodyFile string
payloadJSON string
artifacts artifactOptions
}
func newDoneCmd(root *rootOptions) *cobra.Command {
return newCompleteCmd(root, "done")
}
func newFailCmd(root *rootOptions) *cobra.Command {
return newCompleteCmd(root, "fail")
}
func newCompleteCmd(root *rootOptions, mode string) *cobra.Command {
opts := &completeOptions{}
cmd := &cobra.Command{
Use: mode,
Short: map[string]string{"done": "Mark a thread complete", "fail": "Mark a thread failed"}[mode],
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
agent := opts.agent
if agent == "" {
agent = root.agent
}
if agent == "" {
return protocol.InvalidInput("agent is required", nil)
}
body, err := resolveBodyValue(opts.body, opts.bodyFile)
if err != nil {
return err
}
artifacts, err := resolveArtifacts(opts.artifacts)
if err != nil {
return err
}
sqlDB, err := openInboxDB(ctx, root.dbPath)
if err != nil {
return err
}
defer sqlDB.Close()
s := store.NewInboxStore(sqlDB)
thread, message, err := s.CompleteThread(ctx, store.CompleteInput{
ThreadID: opts.threadID,
Agent: agent,
Summary: opts.summary,
Body: body,
PayloadJSON: opts.payloadJSON,
Failed: mode == "fail",
Artifacts: artifacts,
})
if err != nil {
return err
}
resp := protocol.Success{
OK: true,
Command: mode,
Data: map[string]any{
"thread": thread,
"message": message,
},
}
if root.json {
return protocol.WriteJSON(cmd.OutOrStdout(), resp)
}
_, err = fmt.Fprintf(cmd.OutOrStdout(), "%s thread %s\n", mode, thread.ThreadID)
return err
},
}
cmd.Flags().StringVar(&opts.agent, "agent", "", "Acting agent")
cmd.Flags().StringVar(&opts.threadID, "thread", "", "Thread ID")
cmd.Flags().StringVar(&opts.summary, "summary", "", "Short completion summary")
cmd.Flags().StringVar(&opts.body, "body", "", "Completion body")
cmd.Flags().StringVar(&opts.bodyFile, "body-file", "", "Read completion body from file")
cmd.Flags().StringVar(&opts.payloadJSON, "payload-json", "", "Structured payload JSON string")
addArtifactFlags(cmd, &opts.artifacts)
_ = cmd.MarkFlagRequired("thread")
_ = cmd.MarkFlagRequired("summary")
return cmd
}
@@ -0,0 +1,140 @@
package inbox
import (
"os"
"path/filepath"
"testing"
)
func TestDoneMarksThreadTerminal(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
threadID := seedClaimedThreadForInboxTests(t, dbPath, "leader", "worker-a", "worker-a")
doneOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"done",
"--agent", "worker-a",
"--thread", threadID,
"--summary", "Retry policy implemented",
"--body", "The HTTP client now retries the selected transient failures.",
)
var doneResp map[string]any
mustDecodeJSON(t, doneOut, &doneResp)
if status := nestedString(t, doneResp, "data", "thread", "status"); status != "done" {
t.Fatalf("expected done thread status, got %q", status)
}
if kind := nestedString(t, doneResp, "data", "message", "kind"); kind != "result" {
t.Fatalf("expected result message kind, got %q", kind)
}
}
func TestDonePersistsResultBodyAndArtifact(t *testing.T) {
t.Parallel()
tempDir := t.TempDir()
dbPath := filepath.Join(tempDir, "coord.db")
resultPath := filepath.Join(tempDir, "result.md")
body := "Result from body file."
if err := os.WriteFile(resultPath, []byte(body), 0o644); err != nil {
t.Fatalf("write result file: %v", err)
}
threadID := seedClaimedThreadForInboxTests(t, dbPath, "leader", "worker-a", "worker-a")
runInboxCommand(
t,
"--db", dbPath,
"--json",
"done",
"--agent", "worker-a",
"--thread", threadID,
"--summary", "Retry policy implemented",
"--body-file", resultPath,
"--artifact", resultPath,
"--artifact-kind", "report",
)
showOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"show",
"--thread", threadID,
)
var showResp map[string]any
mustDecodeJSON(t, showOut, &showResp)
lastMessage := lastThreadMessageFromShow(t, showResp)
if gotBody, _ := lastMessage["body"].(string); gotBody != body {
t.Fatalf("expected body %q, got %#v", body, lastMessage["body"])
}
artifacts, ok := lastMessage["artifacts"].([]any)
if !ok || len(artifacts) != 1 {
t.Fatalf("expected one artifact, got %#v", lastMessage["artifacts"])
}
artifact, ok := artifacts[0].(map[string]any)
if !ok {
t.Fatalf("expected artifact object, got %#v", artifacts[0])
}
if gotPath, _ := artifact["path"].(string); gotPath != resultPath {
t.Fatalf("expected artifact path %q, got %#v", resultPath, artifact["path"])
}
if gotKind, _ := artifact["kind"].(string); gotKind != "report" {
t.Fatalf("expected artifact kind report, got %#v", artifact["kind"])
}
}
func TestDoneRejectsNonOwner(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
threadID := seedClaimedThreadForInboxTests(t, dbPath, "leader", "worker-a", "worker-a")
stdout, _, exitCode := executeInboxCommand(
"--db", dbPath,
"--json",
"done",
"--agent", "worker-b",
"--thread", threadID,
"--summary", "Retry policy implemented",
)
if exitCode != 20 {
t.Fatalf("expected exit code 20, got %d with output %s", exitCode, stdout)
}
assertErrorJSON(t, stdout, "lease_conflict")
}
func TestDoneRejectsOnTerminalThread(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
threadID := seedClaimedThreadForInboxTests(t, dbPath, "leader", "worker-a", "worker-a")
runInboxCommand(
t,
"--db", dbPath,
"--json",
"done",
"--agent", "worker-a",
"--thread", threadID,
"--summary", "Retry policy implemented",
)
stdout, _, exitCode := executeInboxCommand(
"--db", dbPath,
"--json",
"done",
"--agent", "worker-a",
"--thread", threadID,
"--summary", "Retry policy implemented",
)
if exitCode != 30 {
t.Fatalf("expected exit code 30, got %d with output %s", exitCode, stdout)
}
assertErrorJSON(t, stdout, "invalid_state")
}
@@ -0,0 +1,113 @@
package inbox
import (
"errors"
"fmt"
"io"
"strings"
"ai-workflow-skill/packages/coord-core/protocol"
"ai-workflow-skill/packages/coord-core/store"
)
func Execute(args []string, stdout, stderr io.Writer) int {
cmd := NewRootCmd()
cmd.SetOut(stdout)
cmd.SetErr(stderr)
cmd.SetArgs(args)
if err := cmd.Execute(); err != nil {
jsonOutput := hasJSONFlag(args)
renderError(stdout, stderr, jsonOutput, err)
return exitCodeForError(err)
}
return 0
}
func exitCodeForError(err error) int {
var cliErr *protocol.CLIError
if errors.As(err, &cliErr) {
return cliErr.ExitCode
}
switch {
case isUsageError(err):
return 30
case errors.Is(err, store.ErrLeaseConflict):
return 20
case errors.Is(err, store.ErrThreadNotFound), errors.Is(err, store.ErrMessageNotFound):
return 40
case errors.Is(err, store.ErrInvalidInput), errors.Is(err, store.ErrInvalidState), errors.Is(err, store.ErrNoActiveLease):
return 30
default:
return 50
}
}
func errorCodeForError(err error) string {
var cliErr *protocol.CLIError
if errors.As(err, &cliErr) {
return cliErr.Code
}
switch {
case isUsageError(err):
return "invalid_input"
case errors.Is(err, store.ErrLeaseConflict):
return "lease_conflict"
case errors.Is(err, store.ErrThreadNotFound), errors.Is(err, store.ErrMessageNotFound):
return "not_found"
case errors.Is(err, store.ErrInvalidInput):
return "invalid_input"
case errors.Is(err, store.ErrInvalidState), errors.Is(err, store.ErrNoActiveLease):
return "invalid_state"
default:
return "internal_error"
}
}
func renderError(stdout, stderr io.Writer, jsonOutput bool, err error) {
message := errorMessage(err)
if jsonOutput {
_ = protocol.WriteJSON(stdout, protocol.Error{
OK: false,
Error: protocol.ErrorPayload{
Code: errorCodeForError(err),
Message: message,
},
})
return
}
_, _ = fmt.Fprintln(stderr, message)
}
func errorMessage(err error) string {
var cliErr *protocol.CLIError
if errors.As(err, &cliErr) {
return cliErr.Message
}
return err.Error()
}
func hasJSONFlag(args []string) bool {
for _, arg := range args {
if arg == "--json" {
return true
}
if strings.HasPrefix(arg, "--json=") {
return !strings.HasSuffix(arg, "=false")
}
}
return false
}
func isUsageError(err error) bool {
message := err.Error()
return strings.HasPrefix(message, "required flag(s)") ||
strings.HasPrefix(message, "unknown flag:") ||
strings.HasPrefix(message, "unknown command ") ||
strings.Contains(message, " accepts ") ||
strings.Contains(message, "invalid argument ")
}
@@ -0,0 +1,143 @@
package inbox
import (
"os"
"path/filepath"
"testing"
)
func TestFailMarksThreadFailed(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
threadID := seedClaimedThreadForInboxTests(t, dbPath, "leader", "worker-b", "worker-b")
failOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"fail",
"--agent", "worker-b",
"--thread", threadID,
"--summary", "Migration failed",
"--body", "The migration cannot proceed because the prior schema is inconsistent.",
)
var failResp map[string]any
mustDecodeJSON(t, failOut, &failResp)
if status := nestedString(t, failResp, "data", "thread", "status"); status != "failed" {
t.Fatalf("expected failed thread status, got %q", status)
}
if kind := nestedString(t, failResp, "data", "message", "kind"); kind != "result" {
t.Fatalf("expected result message kind, got %q", kind)
}
if toAgent := nestedString(t, failResp, "data", "message", "to_agent"); toAgent != "leader" {
t.Fatalf("expected message to_agent leader, got %q", toAgent)
}
}
func TestFailPersistsFailureBodyAndArtifact(t *testing.T) {
t.Parallel()
tempDir := t.TempDir()
dbPath := filepath.Join(tempDir, "coord.db")
failurePath := filepath.Join(tempDir, "failure.md")
body := "Failure details from file."
if err := os.WriteFile(failurePath, []byte(body), 0o644); err != nil {
t.Fatalf("write failure file: %v", err)
}
threadID := seedClaimedThreadForInboxTests(t, dbPath, "leader", "worker-b", "worker-b")
runInboxCommand(
t,
"--db", dbPath,
"--json",
"fail",
"--agent", "worker-b",
"--thread", threadID,
"--summary", "Migration failed",
"--body-file", failurePath,
"--artifact", failurePath,
"--artifact-kind", "report",
)
showOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"show",
"--thread", threadID,
)
var showResp map[string]any
mustDecodeJSON(t, showOut, &showResp)
lastMessage := lastThreadMessageFromShow(t, showResp)
if gotBody, _ := lastMessage["body"].(string); gotBody != body {
t.Fatalf("expected body %q, got %#v", body, lastMessage["body"])
}
artifacts, ok := lastMessage["artifacts"].([]any)
if !ok || len(artifacts) != 1 {
t.Fatalf("expected one artifact, got %#v", lastMessage["artifacts"])
}
artifact, ok := artifacts[0].(map[string]any)
if !ok {
t.Fatalf("expected artifact object, got %#v", artifacts[0])
}
if gotPath, _ := artifact["path"].(string); gotPath != failurePath {
t.Fatalf("expected artifact path %q, got %#v", failurePath, artifact["path"])
}
if gotKind, _ := artifact["kind"].(string); gotKind != "report" {
t.Fatalf("expected artifact kind report, got %#v", artifact["kind"])
}
}
func TestFailRejectsNonOwner(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
threadID := seedClaimedThreadForInboxTests(t, dbPath, "leader", "worker-b", "worker-b")
stdout, _, exitCode := executeInboxCommand(
"--db", dbPath,
"--json",
"fail",
"--agent", "worker-x",
"--thread", threadID,
"--summary", "Migration failed",
)
if exitCode != 20 {
t.Fatalf("expected exit code 20, got %d with output %s", exitCode, stdout)
}
assertErrorJSON(t, stdout, "lease_conflict")
}
func TestFailRejectsOnTerminalThread(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
threadID := seedClaimedThreadForInboxTests(t, dbPath, "leader", "worker-b", "worker-b")
runInboxCommand(
t,
"--db", dbPath,
"--json",
"fail",
"--agent", "worker-b",
"--thread", threadID,
"--summary", "Migration failed",
)
stdout, _, exitCode := executeInboxCommand(
"--db", dbPath,
"--json",
"fail",
"--agent", "worker-b",
"--thread", threadID,
"--summary", "Migration failed",
)
if exitCode != 30 {
t.Fatalf("expected exit code 30, got %d with output %s", exitCode, stdout)
}
assertErrorJSON(t, stdout, "invalid_state")
}
@@ -0,0 +1,97 @@
package inbox
import (
"fmt"
"strings"
"ai-workflow-skill/packages/coord-core/protocol"
"ai-workflow-skill/packages/coord-core/store"
"github.com/spf13/cobra"
)
type fetchOptions struct {
agent string
statuses string
limit int
unread bool
}
func newFetchCmd(root *rootOptions) *cobra.Command {
opts := &fetchOptions{}
cmd := &cobra.Command{
Use: "fetch",
Short: "List candidate threads for an agent",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
agent := opts.agent
if agent == "" {
agent = root.agent
}
sqlDB, err := openInboxDB(ctx, root.dbPath)
if err != nil {
return err
}
defer sqlDB.Close()
s := store.NewInboxStore(sqlDB)
threads, err := s.FetchThreads(ctx, store.FetchInput{
Agent: agent,
Statuses: parseCSV(opts.statuses),
Limit: opts.limit,
Unread: opts.unread,
})
if err != nil {
return err
}
if len(threads) == 0 {
return protocol.NoMatchingWork("no matching work")
}
resp := protocol.Success{
OK: true,
Command: "fetch",
Data: map[string]any{
"threads": threads,
},
}
if root.json {
return protocol.WriteJSON(cmd.OutOrStdout(), resp)
}
for _, thread := range threads {
if _, err := fmt.Fprintf(cmd.OutOrStdout(), "%s\t%s\t%s\n", thread.ThreadID, thread.Status, thread.Subject); err != nil {
return err
}
}
return nil
},
}
cmd.Flags().StringVar(&opts.agent, "agent", "", "Assigned agent filter")
cmd.Flags().StringVar(&opts.statuses, "status", "pending", "Comma-separated status filter")
cmd.Flags().IntVar(&opts.limit, "limit", 20, "Maximum number of threads")
cmd.Flags().BoolVar(&opts.unread, "unread", false, "Only return threads whose latest message is unread by the agent")
return cmd
}
func parseCSV(value string) []string {
if strings.TrimSpace(value) == "" {
return nil
}
raw := strings.Split(value, ",")
out := make([]string, 0, len(raw))
for _, entry := range raw {
entry = strings.TrimSpace(entry)
if entry != "" {
out = append(out, entry)
}
}
return out
}
@@ -0,0 +1,187 @@
package inbox
import "testing"
func TestFetchReturnsPendingThreadForTargetAgent(t *testing.T) {
t.Parallel()
dbPath := initCommandTestDB(t)
sendPendingThread(t, dbPath, "leader", "worker-a", "Implement task", "Create API endpoint")
fetchOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"fetch",
"--agent", "worker-a",
"--status", "pending",
)
var fetchResp map[string]any
mustDecodeJSON(t, fetchOut, &fetchResp)
threads, ok := nestedValue(t, fetchResp, "data", "threads").([]any)
if !ok || len(threads) < 1 {
t.Fatalf("expected at least one fetched thread, got %#v", nestedValue(t, fetchResp, "data", "threads"))
}
thread, ok := threads[0].(map[string]any)
if !ok {
t.Fatalf("expected thread object, got %#v", threads[0])
}
if got, _ := thread["assigned_to"].(string); got != "worker-a" {
t.Fatalf("expected assigned_to worker-a, got %#v", thread["assigned_to"])
}
if got, _ := thread["status"].(string); got != "pending" {
t.Fatalf("expected pending status, got %#v", thread["status"])
}
}
func TestFetchRespectsStatusAndLimitFilters(t *testing.T) {
t.Parallel()
dbPath := initCommandTestDB(t)
sendPendingThread(t, dbPath, "leader", "worker-a", "Task A", "Pending task")
blockedThreadID := sendPendingThread(t, dbPath, "leader", "worker-a", "Task B", "Blocked task")
runInboxCommand(
t,
"--db", dbPath,
"--json",
"claim",
"--agent", "worker-a",
"--thread", blockedThreadID,
)
runInboxCommand(
t,
"--db", dbPath,
"--json",
"update",
"--agent", "worker-a",
"--thread", blockedThreadID,
"--status", "blocked",
"--summary", "Need decision",
"--payload-json", `{"question":"continue?"}`,
)
fetchOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"fetch",
"--agent", "worker-a",
"--status", "pending,blocked",
"--limit", "1",
)
var fetchResp map[string]any
mustDecodeJSON(t, fetchOut, &fetchResp)
threads, ok := nestedValue(t, fetchResp, "data", "threads").([]any)
if !ok {
t.Fatalf("expected threads array, got %#v", nestedValue(t, fetchResp, "data", "threads"))
}
if len(threads) > 1 {
t.Fatalf("expected at most one thread with limit=1, got %d", len(threads))
}
if len(threads) == 0 {
t.Fatalf("expected one thread with status filter, got empty result")
}
thread, ok := threads[0].(map[string]any)
if !ok {
t.Fatalf("expected thread object, got %#v", threads[0])
}
status, _ := thread["status"].(string)
if status != "pending" && status != "blocked" {
t.Fatalf("expected pending or blocked status, got %#v", thread["status"])
}
}
func TestFetchUnreadUsesReadCursor(t *testing.T) {
t.Parallel()
dbPath := initCommandTestDB(t)
threadID := sendPendingThread(t, dbPath, "leader", "worker-e", "Review navbar copy", "Check top nav wording")
firstFetchOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"fetch",
"--agent", "worker-e",
"--status", "pending",
"--unread",
)
var firstFetchResp map[string]any
mustDecodeJSON(t, firstFetchOut, &firstFetchResp)
firstThreads, ok := nestedValue(t, firstFetchResp, "data", "threads").([]any)
if !ok || len(firstThreads) != 1 {
t.Fatalf("expected one unread thread before mark-read, got %#v", nestedValue(t, firstFetchResp, "data", "threads"))
}
runInboxCommand(
t,
"--db", dbPath,
"--agent", "worker-e",
"--json",
"show",
"--thread", threadID,
"--mark-read",
)
stdout, _, exitCode := executeInboxCommand(
"--db", dbPath,
"--json",
"fetch",
"--agent", "worker-e",
"--status", "pending",
"--unread",
)
if exitCode != 10 {
t.Fatalf("expected unread fetch to return no_matching_work after mark-read, got exit=%d stdout=%s", exitCode, stdout)
}
assertErrorJSON(t, stdout, "no_matching_work")
runInboxCommand(
t,
"--db", dbPath,
"--json",
"send",
"--from", "leader",
"--to", "worker-e",
"--thread", threadID,
"--summary", "Use sentence case",
"--body", "Keep the nav labels in sentence case.",
)
thirdFetchOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"fetch",
"--agent", "worker-e",
"--status", "pending",
"--unread",
)
var thirdFetchResp map[string]any
mustDecodeJSON(t, thirdFetchOut, &thirdFetchResp)
thirdThreads, ok := nestedValue(t, thirdFetchResp, "data", "threads").([]any)
if !ok || len(thirdThreads) != 1 {
t.Fatalf("expected unread thread to reappear after new message, got %#v", nestedValue(t, thirdFetchResp, "data", "threads"))
}
}
func TestFetchReturnsNoMatchingWorkWhenEmpty(t *testing.T) {
t.Parallel()
dbPath := initCommandTestDB(t)
stdout, _, exitCode := executeInboxCommand(
"--db", dbPath,
"--json",
"fetch",
"--agent", "worker-z",
"--status", "pending",
)
if exitCode != 10 {
t.Fatalf("expected exit code 10, got %d", exitCode)
}
assertErrorJSON(t, stdout, "no_matching_work")
}
@@ -0,0 +1,41 @@
package inbox
import (
"fmt"
"ai-workflow-skill/packages/coord-core/protocol"
"github.com/spf13/cobra"
)
func newInitCmd(opts *rootOptions) *cobra.Command {
return &cobra.Command{
Use: "init",
Short: "Initialize the shared SQLite database schema",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
sqlDB, err := openInboxDB(ctx, opts.dbPath)
if err != nil {
return err
}
defer sqlDB.Close()
resp := protocol.Success{
OK: true,
Command: "init",
Data: map[string]any{
"db_path": opts.dbPath,
"status": "initialized",
},
}
if opts.json {
return protocol.WriteJSON(cmd.OutOrStdout(), resp)
}
_, err = fmt.Fprintf(cmd.OutOrStdout(), "initialized database: %s\n", opts.dbPath)
return err
},
}
}
@@ -0,0 +1,83 @@
package inbox
import (
"path/filepath"
"testing"
)
func TestInitCreatesSchemaOnEmptyDB(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
initOut := runInboxCommand(t, "--db", dbPath, "--json", "init")
var initResp map[string]any
mustDecodeJSON(t, initOut, &initResp)
if ok, _ := initResp["ok"].(bool); !ok {
t.Fatalf("expected ok=true, got %#v", initResp)
}
if cmd, _ := initResp["command"].(string); cmd != "init" {
t.Fatalf("expected command init, got %#v", initResp["command"])
}
if got := nestedString(t, initResp, "data", "db_path"); got != dbPath {
t.Fatalf("expected db_path %q, got %q", dbPath, got)
}
if got := nestedString(t, initResp, "data", "status"); got != "initialized" {
t.Fatalf("expected initialized status, got %q", got)
}
}
func TestInitIsIdempotentOnExistingDB(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
firstOut := runInboxCommand(t, "--db", dbPath, "--json", "init")
secondOut := runInboxCommand(t, "--db", dbPath, "--json", "init")
var firstResp map[string]any
var secondResp map[string]any
mustDecodeJSON(t, firstOut, &firstResp)
mustDecodeJSON(t, secondOut, &secondResp)
if got := nestedString(t, firstResp, "data", "status"); got != "initialized" {
t.Fatalf("expected first init status initialized, got %q", got)
}
if got := nestedString(t, secondResp, "data", "status"); got != "initialized" {
t.Fatalf("expected second init status initialized, got %q", got)
}
if got := nestedString(t, firstResp, "data", "db_path"); got != dbPath {
t.Fatalf("expected first db_path %q, got %q", dbPath, got)
}
if got := nestedString(t, secondResp, "data", "db_path"); got != dbPath {
t.Fatalf("expected second db_path %q, got %q", dbPath, got)
}
}
func initCommandTestDB(t *testing.T) string {
t.Helper()
dbPath := filepath.Join(t.TempDir(), "coord.db")
runInboxCommand(t, "--db", dbPath, "--json", "init")
return dbPath
}
func sendPendingThread(t *testing.T, dbPath, from, to, subject, summary string) string {
t.Helper()
sendOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"send",
"--from", from,
"--to", to,
"--subject", subject,
"--summary", summary,
)
var sendResp map[string]any
mustDecodeJSON(t, sendOut, &sendResp)
return nestedString(t, sendResp, "data", "thread", "thread_id")
}
@@ -0,0 +1,734 @@
package inbox
import (
"os"
"path/filepath"
"testing"
"time"
)
func TestInboxLifecycle(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
initOut := runInboxCommand(t, "--db", dbPath, "--json", "init")
var initResp map[string]any
mustDecodeJSON(t, initOut, &initResp)
if initResp["ok"] != true {
t.Fatalf("expected init ok=true, got %#v", initResp)
}
sendOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"send",
"--from", "leader",
"--to", "worker-a",
"--subject", "Implement feature X",
"--summary", "Add retry policy",
"--body", "Implement retry handling for the HTTP client.",
"--run", "run_blog_001",
"--task", "T1",
)
var sendResp map[string]any
mustDecodeJSON(t, sendOut, &sendResp)
threadID := nestedString(t, sendResp, "data", "thread", "thread_id")
threadStatus := nestedString(t, sendResp, "data", "thread", "status")
if threadStatus != "pending" {
t.Fatalf("expected pending thread, got %q", threadStatus)
}
fetchOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"fetch",
"--agent", "worker-a",
"--status", "pending",
)
var fetchResp map[string]any
mustDecodeJSON(t, fetchOut, &fetchResp)
threadsValue := nestedValue(t, fetchResp, "data", "threads")
threads, ok := threadsValue.([]any)
if !ok || len(threads) != 1 {
t.Fatalf("expected one fetched thread, got %#v", threadsValue)
}
claimOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"claim",
"--agent", "worker-a",
"--thread", threadID,
"--lease-seconds", "300",
)
var claimResp map[string]any
mustDecodeJSON(t, claimOut, &claimResp)
claimedStatus := nestedString(t, claimResp, "data", "thread", "status")
if claimedStatus != "claimed" {
t.Fatalf("expected claimed thread, got %q", claimedStatus)
}
updateOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"update",
"--agent", "worker-a",
"--thread", threadID,
"--status", "in_progress",
"--summary", "Implementation started",
"--body", "Scanning current HTTP client usage.",
)
var updateResp map[string]any
mustDecodeJSON(t, updateOut, &updateResp)
updatedStatus := nestedString(t, updateResp, "data", "thread", "status")
if updatedStatus != "in_progress" {
t.Fatalf("expected in_progress thread, got %q", updatedStatus)
}
blockedOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"update",
"--agent", "worker-a",
"--thread", threadID,
"--status", "blocked",
"--summary", "Need timeout decision",
"--payload-json", `{"question":"Should retries apply to read timeouts?"}`,
)
var blockedResp map[string]any
mustDecodeJSON(t, blockedOut, &blockedResp)
blockedStatus := nestedString(t, blockedResp, "data", "thread", "status")
if blockedStatus != "blocked" {
t.Fatalf("expected blocked thread, got %q", blockedStatus)
}
replyOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"reply",
"--from", "leader",
"--to", "worker-a",
"--thread", threadID,
"--summary", "Retry read timeouts",
"--body", "Yes, include read timeouts in the retry policy.",
)
var replyResp map[string]any
mustDecodeJSON(t, replyOut, &replyResp)
replyKind := nestedString(t, replyResp, "data", "message", "kind")
if replyKind != "answer" {
t.Fatalf("expected answer reply, got %q", replyKind)
}
doneOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"done",
"--agent", "worker-a",
"--thread", threadID,
"--summary", "Retry policy implemented",
"--body", "The HTTP client now retries the selected transient failures.",
)
var doneResp map[string]any
mustDecodeJSON(t, doneOut, &doneResp)
doneStatus := nestedString(t, doneResp, "data", "thread", "status")
if doneStatus != "done" {
t.Fatalf("expected done thread, got %q", doneStatus)
}
showOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"show",
"--thread", threadID,
)
var showResp map[string]any
mustDecodeJSON(t, showOut, &showResp)
showStatus := nestedString(t, showResp, "data", "thread", "status")
if showStatus != "done" {
t.Fatalf("expected show status done, got %q", showStatus)
}
messagesValue := nestedValue(t, showResp, "data", "messages")
messages, ok := messagesValue.([]any)
if !ok || len(messages) != 6 {
t.Fatalf("expected six messages in thread history, got %#v", messagesValue)
}
}
func TestInboxFailLifecycle(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
runInboxCommand(t, "--db", dbPath, "--json", "init")
sendOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"send",
"--from", "leader",
"--to", "worker-b",
"--subject", "Investigate failing migration",
"--summary", "Check migration failure",
"--run", "run_blog_002",
"--task", "T2",
)
var sendResp map[string]any
mustDecodeJSON(t, sendOut, &sendResp)
threadID := nestedString(t, sendResp, "data", "thread", "thread_id")
runInboxCommand(
t,
"--db", dbPath,
"--json",
"claim",
"--agent", "worker-b",
"--thread", threadID,
)
failOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"fail",
"--agent", "worker-b",
"--thread", threadID,
"--summary", "Migration failed",
"--body", "The migration cannot proceed because the prior schema is inconsistent.",
)
var failResp map[string]any
mustDecodeJSON(t, failOut, &failResp)
failStatus := nestedString(t, failResp, "data", "thread", "status")
if failStatus != "failed" {
t.Fatalf("expected failed thread, got %q", failStatus)
}
showOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"show",
"--thread", threadID,
)
var showResp map[string]any
mustDecodeJSON(t, showOut, &showResp)
showStatus := nestedString(t, showResp, "data", "thread", "status")
if showStatus != "failed" {
t.Fatalf("expected show status failed, got %q", showStatus)
}
}
func TestInboxRenewWaitReplyAndCancel(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
runInboxCommand(t, "--db", dbPath, "--json", "init")
sendOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"send",
"--from", "leader",
"--to", "worker-c",
"--subject", "Investigate auth edge case",
"--summary", "Check auth redirect behavior",
"--run", "run_blog_003",
"--task", "T3",
)
var sendResp map[string]any
mustDecodeJSON(t, sendOut, &sendResp)
threadID := nestedString(t, sendResp, "data", "thread", "thread_id")
runInboxCommand(
t,
"--db", dbPath,
"--json",
"claim",
"--agent", "worker-c",
"--thread", threadID,
"--lease-seconds", "300",
)
renewOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"renew",
"--agent", "worker-c",
"--thread", threadID,
"--lease-seconds", "600",
)
var renewResp map[string]any
mustDecodeJSON(t, renewOut, &renewResp)
if got := nestedString(t, renewResp, "data", "message", "summary"); got != "lease renewed" {
t.Fatalf("expected lease renewed summary, got %q", got)
}
blockedOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"update",
"--agent", "worker-c",
"--thread", threadID,
"--status", "blocked",
"--summary", "Need policy decision",
"--body", "Should guest users be redirected to login or shown a 403 page?",
)
var blockedResp map[string]any
mustDecodeJSON(t, blockedOut, &blockedResp)
blockedMessageID := nestedString(t, blockedResp, "data", "message", "message_id")
type commandResult struct {
stdout string
stderr string
exit int
}
waitCh := make(chan commandResult, 1)
go func() {
stdout, stderr, exitCode := executeInboxCommand(
"--db", dbPath,
"--agent", "worker-c",
"--json",
"wait-reply",
"--thread", threadID,
"--after-message", blockedMessageID,
"--timeout-seconds", "2",
)
waitCh <- commandResult{stdout: stdout, stderr: stderr, exit: exitCode}
}()
time.Sleep(200 * time.Millisecond)
runInboxCommand(
t,
"--db", dbPath,
"--json",
"reply",
"--from", "leader",
"--to", "worker-c",
"--thread", threadID,
"--summary", "Redirect to login",
"--body", "Redirect guests to login for the MVP.",
)
var waitResult commandResult
select {
case waitResult = <-waitCh:
case <-time.After(3 * time.Second):
t.Fatal("wait-reply command did not return")
}
if waitResult.exit != 0 {
t.Fatalf("wait-reply failed with exit=%d\nstderr:\n%s\nstdout:\n%s", waitResult.exit, waitResult.stderr, waitResult.stdout)
}
var waitResp map[string]any
mustDecodeJSON(t, waitResult.stdout, &waitResp)
if woke, ok := nestedValue(t, waitResp, "data", "woke").(bool); !ok || !woke {
t.Fatalf("expected wait-reply to wake, got %#v", nestedValue(t, waitResp, "data", "woke"))
}
if kind := nestedString(t, waitResp, "data", "message", "kind"); kind != "answer" {
t.Fatalf("expected answer wake message, got %q", kind)
}
stdout, _, exitCode := executeInboxCommand(
"--db", dbPath,
"--agent", "worker-c",
"--json",
"fetch",
"--status", "blocked",
"--unread",
)
if exitCode != 10 {
t.Fatalf("expected blocked unread list to be cleared after wait-reply, got exit %d with %s", exitCode, stdout)
}
cancelOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"cancel",
"--agent", "leader",
"--thread", threadID,
"--reason", "Task superseded by a larger refactor",
)
var cancelResp map[string]any
mustDecodeJSON(t, cancelOut, &cancelResp)
if status := nestedString(t, cancelResp, "data", "thread", "status"); status != "cancelled" {
t.Fatalf("expected cancelled thread, got %q", status)
}
}
func TestInboxWatchListUnreadAndAppend(t *testing.T) {
t.Parallel()
tempDir := t.TempDir()
dbPath := filepath.Join(tempDir, "coord.db")
bodyPath := filepath.Join(tempDir, "task.md")
if err := os.WriteFile(bodyPath, []byte("Implement the initial admin post editor."), 0o644); err != nil {
t.Fatalf("write body file: %v", err)
}
runInboxCommand(t, "--db", dbPath, "--json", "init")
type commandResult struct {
stdout string
stderr string
exit int
}
watchCh := make(chan commandResult, 1)
go func() {
stdout, stderr, exitCode := executeInboxCommand(
"--db", dbPath,
"--json",
"watch",
"--agent", "worker-d",
"--status", "pending",
"--timeout-seconds", "2",
)
watchCh <- commandResult{stdout: stdout, stderr: stderr, exit: exitCode}
}()
time.Sleep(200 * time.Millisecond)
sendOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"send",
"--from", "leader",
"--to", "worker-d",
"--subject", "Build admin editor",
"--summary", "Create the first editor screen",
"--body-file", bodyPath,
"--artifact", bodyPath,
"--artifact-kind", "brief",
"--artifact-metadata-json", `{"label":"task-brief"}`,
"--run", "run_blog_004",
"--task", "T4",
)
var sendResp map[string]any
mustDecodeJSON(t, sendOut, &sendResp)
threadID := nestedString(t, sendResp, "data", "thread", "thread_id")
var watchResult commandResult
select {
case watchResult = <-watchCh:
case <-time.After(3 * time.Second):
t.Fatal("watch command did not return")
}
if watchResult.exit != 0 {
t.Fatalf("watch failed with exit=%d\nstderr:\n%s\nstdout:\n%s", watchResult.exit, watchResult.stderr, watchResult.stdout)
}
var watchResp map[string]any
mustDecodeJSON(t, watchResult.stdout, &watchResp)
if woke, ok := nestedValue(t, watchResp, "data", "woke").(bool); !ok || !woke {
t.Fatalf("expected watch to wake, got %#v", nestedValue(t, watchResp, "data", "woke"))
}
if watchedThreadID := nestedString(t, watchResp, "data", "thread", "thread_id"); watchedThreadID != threadID {
t.Fatalf("expected watch on thread %s, got %s", threadID, watchedThreadID)
}
fetchOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"fetch",
"--agent", "worker-d",
"--status", "pending",
"--unread",
)
var fetchResp map[string]any
mustDecodeJSON(t, fetchOut, &fetchResp)
fetchedThreads, ok := nestedValue(t, fetchResp, "data", "threads").([]any)
if !ok || len(fetchedThreads) != 1 {
t.Fatalf("expected one unread pending thread, got %#v", nestedValue(t, fetchResp, "data", "threads"))
}
listOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"list",
"--assigned-to", "worker-d",
"--status", "pending",
)
var listResp map[string]any
mustDecodeJSON(t, listOut, &listResp)
listedThreads, ok := nestedValue(t, listResp, "data", "threads").([]any)
if !ok || len(listedThreads) != 1 {
t.Fatalf("expected one listed thread, got %#v", nestedValue(t, listResp, "data", "threads"))
}
runInboxCommand(
t,
"--db", dbPath,
"--json",
"send",
"--from", "leader",
"--to", "worker-d",
"--thread", threadID,
"--summary", "Use a markdown editor",
"--body", "Prefer a textarea-based markdown editor for v1.",
)
showOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"show",
"--thread", threadID,
)
var showResp map[string]any
mustDecodeJSON(t, showOut, &showResp)
messages, ok := nestedValue(t, showResp, "data", "messages").([]any)
if !ok || len(messages) != 2 {
t.Fatalf("expected two messages after append, got %#v", nestedValue(t, showResp, "data", "messages"))
}
firstMessage, ok := messages[0].(map[string]any)
if !ok {
t.Fatalf("expected first message object, got %#v", messages[0])
}
if firstMessage["body"] != "Implement the initial admin post editor." {
t.Fatalf("expected body-file content in first message, got %#v", firstMessage["body"])
}
artifacts, ok := firstMessage["artifacts"].([]any)
if !ok || len(artifacts) != 1 {
t.Fatalf("expected one artifact on first message, got %#v", firstMessage["artifacts"])
}
firstArtifact, ok := artifacts[0].(map[string]any)
if !ok {
t.Fatalf("expected artifact object, got %#v", artifacts[0])
}
if firstArtifact["path"] != bodyPath {
t.Fatalf("expected artifact path %q, got %#v", bodyPath, firstArtifact["path"])
}
if firstArtifact["kind"] != "brief" {
t.Fatalf("expected artifact kind brief, got %#v", firstArtifact["kind"])
}
}
func TestInboxUnreadReadCursor(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
runInboxCommand(t, "--db", dbPath, "--json", "init")
sendOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"send",
"--from", "leader",
"--to", "worker-e",
"--subject", "Review navbar copy",
"--summary", "Check top nav wording",
)
var sendResp map[string]any
mustDecodeJSON(t, sendOut, &sendResp)
threadID := nestedString(t, sendResp, "data", "thread", "thread_id")
fetchOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"fetch",
"--agent", "worker-e",
"--status", "pending",
"--unread",
)
var fetchResp map[string]any
mustDecodeJSON(t, fetchOut, &fetchResp)
threads, ok := nestedValue(t, fetchResp, "data", "threads").([]any)
if !ok || len(threads) != 1 {
t.Fatalf("expected one unread pending thread, got %#v", nestedValue(t, fetchResp, "data", "threads"))
}
runInboxCommand(
t,
"--db", dbPath,
"--agent", "worker-e",
"--json",
"show",
"--thread", threadID,
"--mark-read",
)
stdout, _, exitCode := executeInboxCommand(
"--db", dbPath,
"--json",
"fetch",
"--agent", "worker-e",
"--status", "pending",
"--unread",
)
if exitCode != 10 {
t.Fatalf("expected unread fetch to clear after mark-read, got exit %d with %s", exitCode, stdout)
}
runInboxCommand(
t,
"--db", dbPath,
"--json",
"send",
"--from", "leader",
"--to", "worker-e",
"--thread", threadID,
"--summary", "Use sentence case",
"--body", "Keep the nav labels in sentence case.",
)
fetchOut = runInboxCommand(
t,
"--db", dbPath,
"--json",
"fetch",
"--agent", "worker-e",
"--status", "pending",
"--unread",
)
mustDecodeJSON(t, fetchOut, &fetchResp)
threads, ok = nestedValue(t, fetchResp, "data", "threads").([]any)
if !ok || len(threads) != 1 {
t.Fatalf("expected unread thread to reappear after new message, got %#v", nestedValue(t, fetchResp, "data", "threads"))
}
}
func TestInboxJSONErrorsAndExitCodes(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
if _, _, exitCode := executeInboxCommand("--db", dbPath, "--json", "init"); exitCode != 0 {
t.Fatalf("expected init exit code 0, got %d", exitCode)
}
stdout, _, exitCode := executeInboxCommand(
"--db", dbPath,
"--json",
"fetch",
"--agent", "worker-z",
"--status", "pending",
)
if exitCode != 10 {
t.Fatalf("expected fetch no-match exit code 10, got %d", exitCode)
}
assertErrorJSON(t, stdout, "no_matching_work")
stdout, _, exitCode = executeInboxCommand(
"--db", dbPath,
"--json",
"claim",
"--agent", "worker-z",
"--thread", "thr_missing",
)
if exitCode != 40 {
t.Fatalf("expected claim missing-thread exit code 40, got %d", exitCode)
}
assertErrorJSON(t, stdout, "not_found")
sendOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"send",
"--from", "leader",
"--to", "worker-z",
"--subject", "Review cache settings",
"--summary", "Check cache config",
)
var sendResp map[string]any
mustDecodeJSON(t, sendOut, &sendResp)
threadID := nestedString(t, sendResp, "data", "thread", "thread_id")
runInboxCommand(
t,
"--db", dbPath,
"--json",
"claim",
"--agent", "worker-z",
"--thread", threadID,
)
stdout, _, exitCode = executeInboxCommand(
"--db", dbPath,
"--json",
"claim",
"--agent", "worker-y",
"--thread", threadID,
)
if exitCode != 20 {
t.Fatalf("expected lease conflict exit code 20, got %d", exitCode)
}
assertErrorJSON(t, stdout, "lease_conflict")
stdout, _, exitCode = executeInboxCommand(
"--db", dbPath,
"--json",
"send",
"--from", "leader",
"--to", "worker-z",
"--subject", "Invalid payload json",
"--payload-json", "not-json",
)
if exitCode != 30 {
t.Fatalf("expected invalid input exit code 30, got %d", exitCode)
}
assertErrorJSON(t, stdout, "invalid_input")
stdout, _, exitCode = executeInboxCommand(
"--db", dbPath,
"--json",
"send",
"--from", "leader",
"--to", "worker-z",
"--subject", "Invalid artifact json",
"--artifact", "/tmp/report.md",
"--artifact-metadata-json", "not-json",
)
if exitCode != 30 {
t.Fatalf("expected invalid artifact metadata exit code 30, got %d", exitCode)
}
assertErrorJSON(t, stdout, "invalid_input")
}
@@ -0,0 +1,83 @@
package inbox
import (
"fmt"
"ai-workflow-skill/packages/coord-core/protocol"
"ai-workflow-skill/packages/coord-core/store"
"github.com/spf13/cobra"
)
type listOptions struct {
agent string
statuses string
createdBy string
assignedTo string
limit int
}
func newListCmd(root *rootOptions) *cobra.Command {
opts := &listOptions{}
cmd := &cobra.Command{
Use: "list",
Short: "List threads with filters",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
agent := opts.agent
if agent == "" {
agent = root.agent
}
sqlDB, err := openInboxDB(ctx, root.dbPath)
if err != nil {
return err
}
defer sqlDB.Close()
s := store.NewInboxStore(sqlDB)
threads, err := s.ListThreads(ctx, store.ListInput{
Agent: agent,
Statuses: parseCSV(opts.statuses),
CreatedBy: opts.createdBy,
AssignedTo: opts.assignedTo,
Limit: opts.limit,
})
if err != nil {
return err
}
if len(threads) == 0 {
return protocol.NoMatchingWork("no matching work")
}
resp := protocol.Success{
OK: true,
Command: "list",
Data: map[string]any{
"threads": threads,
},
}
if root.json {
return protocol.WriteJSON(cmd.OutOrStdout(), resp)
}
for _, thread := range threads {
if _, err := fmt.Fprintf(cmd.OutOrStdout(), "%s\t%s\t%s\t%s\n", thread.ThreadID, thread.Status, thread.AssignedTo, thread.Subject); err != nil {
return err
}
}
return nil
},
}
cmd.Flags().StringVar(&opts.agent, "agent", "", "Assigned agent filter shortcut")
cmd.Flags().StringVar(&opts.statuses, "status", "", "Comma-separated status filter")
cmd.Flags().StringVar(&opts.createdBy, "created-by", "", "Created-by filter")
cmd.Flags().StringVar(&opts.assignedTo, "assigned-to", "", "Assigned-to filter")
cmd.Flags().IntVar(&opts.limit, "limit", 20, "Maximum number of threads")
return cmd
}
@@ -0,0 +1,183 @@
package inbox
import (
"path/filepath"
"testing"
"time"
)
func TestListFiltersByStatus(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
runInboxCommand(t, "--db", dbPath, "--json", "init")
createThreadForList(t, dbPath, "leader", "worker-d", "Task pending", "Pending task")
blockedThreadID := createThreadForList(t, dbPath, "leader", "worker-d", "Task blocked", "Blocked task")
runInboxCommand(t, "--db", dbPath, "--json", "claim", "--agent", "worker-d", "--thread", blockedThreadID)
runInboxCommand(
t,
"--db", dbPath,
"--json",
"update",
"--agent", "worker-d",
"--thread", blockedThreadID,
"--status", "blocked",
"--summary", "Need policy decision",
)
listOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"list",
"--agent", "worker-d",
"--status", "pending,blocked",
)
var listResp map[string]any
mustDecodeJSON(t, listOut, &listResp)
threads, ok := nestedValue(t, listResp, "data", "threads").([]any)
if !ok || len(threads) < 2 {
t.Fatalf("expected at least two matching threads, got %#v", nestedValue(t, listResp, "data", "threads"))
}
for _, raw := range threads {
thread, ok := raw.(map[string]any)
if !ok {
t.Fatalf("expected thread object, got %#v", raw)
}
status, _ := thread["status"].(string)
if status != "pending" && status != "blocked" {
t.Fatalf("expected status pending or blocked, got %#v", status)
}
}
}
func TestListFiltersByCreatedBy(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
runInboxCommand(t, "--db", dbPath, "--json", "init")
createThreadForList(t, dbPath, "leader", "worker-d", "Leader task", "From leader")
createThreadForList(t, dbPath, "planner", "worker-d", "Planner task", "From planner")
listOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"list",
"--created-by", "leader",
"--assigned-to", "worker-d",
)
var listResp map[string]any
mustDecodeJSON(t, listOut, &listResp)
threads, ok := nestedValue(t, listResp, "data", "threads").([]any)
if !ok || len(threads) == 0 {
t.Fatalf("expected matching leader-created threads, got %#v", nestedValue(t, listResp, "data", "threads"))
}
for _, raw := range threads {
thread, ok := raw.(map[string]any)
if !ok {
t.Fatalf("expected thread object, got %#v", raw)
}
if got := thread["created_by"]; got != "leader" {
t.Fatalf("expected created_by leader, got %#v", got)
}
}
}
func TestListFiltersByAssignedTo(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
runInboxCommand(t, "--db", dbPath, "--json", "init")
createThreadForList(t, dbPath, "leader", "worker-d", "Worker D task", "For worker-d")
createThreadForList(t, dbPath, "leader", "worker-e", "Worker E task", "For worker-e")
listOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"list",
"--assigned-to", "worker-d",
"--status", "pending",
)
var listResp map[string]any
mustDecodeJSON(t, listOut, &listResp)
threads, ok := nestedValue(t, listResp, "data", "threads").([]any)
if !ok || len(threads) == 0 {
t.Fatalf("expected assigned-to match, got %#v", nestedValue(t, listResp, "data", "threads"))
}
for _, raw := range threads {
thread, ok := raw.(map[string]any)
if !ok {
t.Fatalf("expected thread object, got %#v", raw)
}
if got := thread["assigned_to"]; got != "worker-d" {
t.Fatalf("expected assigned_to worker-d, got %#v", got)
}
if got := thread["status"]; got != "pending" {
t.Fatalf("expected pending status, got %#v", got)
}
}
}
func TestListRespectsLimit(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
runInboxCommand(t, "--db", dbPath, "--json", "init")
createThreadForList(t, dbPath, "leader", "worker-d", "Task 1", "Earlier task")
time.Sleep(20 * time.Millisecond)
createThreadForList(t, dbPath, "leader", "worker-d", "Task 2", "Latest task")
listOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"list",
"--assigned-to", "worker-d",
"--limit", "1",
)
var listResp map[string]any
mustDecodeJSON(t, listOut, &listResp)
threads, ok := nestedValue(t, listResp, "data", "threads").([]any)
if !ok {
t.Fatalf("expected threads array, got %#v", nestedValue(t, listResp, "data", "threads"))
}
if len(threads) != 1 {
t.Fatalf("expected exactly one row for limit=1, got %d", len(threads))
}
thread, ok := threads[0].(map[string]any)
if !ok {
t.Fatalf("expected thread object, got %#v", threads[0])
}
if got := thread["subject"]; got != "Task 2" {
t.Fatalf("expected latest thread subject Task 2, got %#v", got)
}
}
func createThreadForList(t *testing.T, dbPath, from, to, subject, summary string) string {
t.Helper()
sendOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"send",
"--from", from,
"--to", to,
"--subject", subject,
"--summary", summary,
)
var sendResp map[string]any
mustDecodeJSON(t, sendOut, &sendResp)
return nestedString(t, sendResp, "data", "thread", "thread_id")
}
@@ -0,0 +1,76 @@
package inbox
import (
"fmt"
"ai-workflow-skill/packages/coord-core/protocol"
"ai-workflow-skill/packages/coord-core/store"
"github.com/spf13/cobra"
)
type renewOptions struct {
agent string
threadID string
leaseSeconds int
}
func newRenewCmd(root *rootOptions) *cobra.Command {
opts := &renewOptions{}
cmd := &cobra.Command{
Use: "renew",
Short: "Extend an existing lease",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
agent := opts.agent
if agent == "" {
agent = root.agent
}
if agent == "" {
return protocol.InvalidInput("agent is required", nil)
}
sqlDB, err := openInboxDB(ctx, root.dbPath)
if err != nil {
return err
}
defer sqlDB.Close()
s := store.NewInboxStore(sqlDB)
result, err := s.RenewLease(ctx, store.RenewInput{
ThreadID: opts.threadID,
Agent: agent,
LeaseSeconds: opts.leaseSeconds,
})
if err != nil {
return err
}
resp := protocol.Success{
OK: true,
Command: "renew",
Data: map[string]any{
"thread": result.Thread,
"message": result.Message,
},
}
if root.json {
return protocol.WriteJSON(cmd.OutOrStdout(), resp)
}
_, err = fmt.Fprintf(cmd.OutOrStdout(), "renewed lease on thread %s\n", result.Thread.ThreadID)
return err
},
}
cmd.Flags().StringVar(&opts.agent, "agent", "", "Lease owner")
cmd.Flags().StringVar(&opts.threadID, "thread", "", "Thread ID")
cmd.Flags().IntVar(&opts.leaseSeconds, "lease-seconds", 900, "Lease duration in seconds")
_ = cmd.MarkFlagRequired("thread")
return cmd
}
@@ -0,0 +1,103 @@
package inbox
import "testing"
func TestRenewExtendsActiveLease(t *testing.T) {
t.Parallel()
dbPath := initCommandTestDB(t)
threadID := sendPendingThread(t, dbPath, "leader", "worker-c", "Renew lease", "Need renew coverage")
runInboxCommand(
t,
"--db", dbPath,
"--json",
"claim",
"--agent", "worker-c",
"--thread", threadID,
"--lease-seconds", "300",
)
renewOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"renew",
"--agent", "worker-c",
"--thread", threadID,
"--lease-seconds", "600",
)
var renewResp map[string]any
mustDecodeJSON(t, renewOut, &renewResp)
if got := nestedString(t, renewResp, "data", "thread", "status"); got != "claimed" {
t.Fatalf("expected status to stay claimed, got %q", got)
}
if got := nestedString(t, renewResp, "data", "message", "kind"); got != "event" {
t.Fatalf("expected event message kind, got %q", got)
}
if got := nestedString(t, renewResp, "data", "message", "summary"); got != "lease renewed" {
t.Fatalf("expected lease renewed summary, got %q", got)
}
payload, ok := nestedValue(t, renewResp, "data", "message", "payload_json").(map[string]any)
if !ok {
t.Fatalf("expected payload_json object, got %#v", nestedValue(t, renewResp, "data", "message", "payload_json"))
}
leaseSeconds, ok := payload["lease_seconds"].(float64)
if !ok || int(leaseSeconds) != 600 {
t.Fatalf("expected lease_seconds 600, got %#v", payload["lease_seconds"])
}
leaseToken, _ := payload["lease_token"].(string)
if leaseToken == "" {
t.Fatalf("expected non-empty lease_token, got %#v", payload["lease_token"])
}
}
func TestRenewRejectsNonOwner(t *testing.T) {
t.Parallel()
dbPath := initCommandTestDB(t)
threadID := sendPendingThread(t, dbPath, "leader", "worker-c", "Renew non-owner", "Reject non-owner renew")
runInboxCommand(
t,
"--db", dbPath,
"--json",
"claim",
"--agent", "worker-c",
"--thread", threadID,
)
stdout, _, exitCode := executeInboxCommand(
"--db", dbPath,
"--json",
"renew",
"--agent", "worker-x",
"--thread", threadID,
"--lease-seconds", "600",
)
if exitCode != 20 {
t.Fatalf("expected exit code 20, got %d", exitCode)
}
assertErrorJSON(t, stdout, "lease_conflict")
}
func TestRenewRejectsWithoutActiveLease(t *testing.T) {
t.Parallel()
dbPath := initCommandTestDB(t)
threadID := sendPendingThread(t, dbPath, "leader", "worker-c", "Renew without lease", "Should fail without active lease")
stdout, _, exitCode := executeInboxCommand(
"--db", dbPath,
"--json",
"renew",
"--agent", "worker-c",
"--thread", threadID,
"--lease-seconds", "600",
)
if exitCode != 30 {
t.Fatalf("expected exit code 30, got %d", exitCode)
}
assertErrorJSON(t, stdout, "invalid_state")
}
@@ -0,0 +1,104 @@
package inbox
import (
"fmt"
"ai-workflow-skill/packages/coord-core/protocol"
"ai-workflow-skill/packages/coord-core/store"
"github.com/spf13/cobra"
)
type replyOptions struct {
from string
to string
threadID string
kind string
summary string
body string
bodyFile string
payloadJSON string
artifacts artifactOptions
}
func newReplyCmd(root *rootOptions) *cobra.Command {
opts := &replyOptions{}
cmd := &cobra.Command{
Use: "reply",
Short: "Reply inside an existing thread",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
from := opts.from
if from == "" {
from = root.agent
}
if from == "" {
return protocol.InvalidInput("from agent is required", nil)
}
body, err := resolveBodyValue(opts.body, opts.bodyFile)
if err != nil {
return err
}
artifacts, err := resolveArtifacts(opts.artifacts)
if err != nil {
return err
}
sqlDB, err := openInboxDB(ctx, root.dbPath)
if err != nil {
return err
}
defer sqlDB.Close()
s := store.NewInboxStore(sqlDB)
thread, message, err := s.ReplyToThread(ctx, store.ReplyInput{
ThreadID: opts.threadID,
FromAgent: from,
ToAgent: opts.to,
Kind: opts.kind,
Summary: opts.summary,
Body: body,
PayloadJSON: opts.payloadJSON,
Artifacts: artifacts,
})
if err != nil {
return err
}
resp := protocol.Success{
OK: true,
Command: "reply",
Data: map[string]any{
"thread": thread,
"message": message,
},
}
if root.json {
return protocol.WriteJSON(cmd.OutOrStdout(), resp)
}
_, err = fmt.Fprintf(cmd.OutOrStdout(), "replied on thread %s\n", thread.ThreadID)
return err
},
}
cmd.Flags().StringVar(&opts.from, "from", "", "Replying agent")
cmd.Flags().StringVar(&opts.to, "to", "", "Receiving agent")
cmd.Flags().StringVar(&opts.threadID, "thread", "", "Thread ID")
cmd.Flags().StringVar(&opts.kind, "kind", "answer", "Reply kind")
cmd.Flags().StringVar(&opts.summary, "summary", "", "Short reply summary")
cmd.Flags().StringVar(&opts.body, "body", "", "Reply body")
cmd.Flags().StringVar(&opts.bodyFile, "body-file", "", "Read reply body from file")
cmd.Flags().StringVar(&opts.payloadJSON, "payload-json", "", "Structured payload JSON string")
addArtifactFlags(cmd, &opts.artifacts)
_ = cmd.MarkFlagRequired("thread")
_ = cmd.MarkFlagRequired("to")
_ = cmd.MarkFlagRequired("summary")
return cmd
}
@@ -0,0 +1,138 @@
package inbox
import (
"os"
"path/filepath"
"testing"
)
func TestReplyAddsAnswerMessage(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
threadID := seedThreadForInboxTests(t, dbPath, "leader", "worker-a")
replyOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"reply",
"--from", "leader",
"--to", "worker-a",
"--thread", threadID,
"--summary", "Retry read timeouts",
"--body", "Yes, include read timeouts in the retry policy.",
)
var replyResp map[string]any
mustDecodeJSON(t, replyOut, &replyResp)
if kind := nestedString(t, replyResp, "data", "message", "kind"); kind != "answer" {
t.Fatalf("expected answer message kind, got %q", kind)
}
if gotThreadID := nestedString(t, replyResp, "data", "thread", "thread_id"); gotThreadID != threadID {
t.Fatalf("expected thread_id %q, got %q", threadID, gotThreadID)
}
if status := nestedString(t, replyResp, "data", "thread", "status"); status != "pending" {
t.Fatalf("expected thread status pending, got %q", status)
}
}
func TestReplySupportsControlKind(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
threadID := seedThreadForInboxTests(t, dbPath, "leader", "worker-a")
replyOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"reply",
"--from", "leader",
"--to", "worker-a",
"--thread", threadID,
"--kind", "control",
"--summary", "Pause rollout",
"--body", "Pause rollout until QA confirms the fix.",
)
var replyResp map[string]any
mustDecodeJSON(t, replyOut, &replyResp)
if kind := nestedString(t, replyResp, "data", "message", "kind"); kind != "control" {
t.Fatalf("expected control message kind, got %q", kind)
}
}
func TestReplyAttachesArtifact(t *testing.T) {
t.Parallel()
tempDir := t.TempDir()
dbPath := filepath.Join(tempDir, "coord.db")
decisionPath := filepath.Join(tempDir, "decision.md")
if err := os.WriteFile(decisionPath, []byte("Decision note."), 0o644); err != nil {
t.Fatalf("write decision file: %v", err)
}
threadID := seedThreadForInboxTests(t, dbPath, "leader", "worker-a")
replyOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"reply",
"--from", "leader",
"--to", "worker-a",
"--thread", threadID,
"--summary", "Retry read timeouts",
"--artifact", decisionPath,
"--artifact-kind", "brief",
"--artifact-metadata-json", `{"label":"decision"}`,
)
var replyResp map[string]any
mustDecodeJSON(t, replyOut, &replyResp)
artifactsValue := nestedValue(t, replyResp, "data", "message", "artifacts")
artifacts, ok := artifactsValue.([]any)
if !ok || len(artifacts) != 1 {
t.Fatalf("expected one artifact, got %#v", artifactsValue)
}
artifact, ok := artifacts[0].(map[string]any)
if !ok {
t.Fatalf("expected artifact object, got %#v", artifacts[0])
}
if gotPath, _ := artifact["path"].(string); gotPath != decisionPath {
t.Fatalf("expected artifact path %q, got %#v", decisionPath, artifact["path"])
}
if gotKind, _ := artifact["kind"].(string); gotKind != "brief" {
t.Fatalf("expected artifact kind brief, got %#v", artifact["kind"])
}
metadata, ok := artifact["metadata_json"].(map[string]any)
if !ok {
t.Fatalf("expected metadata_json object, got %#v", artifact["metadata_json"])
}
if gotLabel := metadata["label"]; gotLabel != "decision" {
t.Fatalf("expected metadata label decision, got %#v", gotLabel)
}
}
func TestReplyRejectsInvalidPayloadJSON(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
threadID := seedThreadForInboxTests(t, dbPath, "leader", "worker-a")
stdout, _, exitCode := executeInboxCommand(
"--db", dbPath,
"--json",
"reply",
"--from", "leader",
"--to", "worker-a",
"--thread", threadID,
"--summary", "Retry read timeouts",
"--payload-json", "not-json",
)
if exitCode != 30 {
t.Fatalf("expected exit code 30, got %d with output %s", exitCode, stdout)
}
assertErrorJSON(t, stdout, "invalid_input")
}
@@ -0,0 +1,43 @@
package inbox
import (
"github.com/spf13/cobra"
)
type rootOptions struct {
dbPath string
json bool
agent string
}
func NewRootCmd() *cobra.Command {
opts := &rootOptions{}
cmd := &cobra.Command{
Use: "inbox",
Short: "Worker-facing durable coordination bus",
SilenceErrors: true,
SilenceUsage: true,
}
cmd.PersistentFlags().StringVar(&opts.dbPath, "db", ".agents/coord.db", "SQLite database path")
cmd.PersistentFlags().BoolVar(&opts.json, "json", false, "Emit machine-readable JSON")
cmd.PersistentFlags().StringVar(&opts.agent, "agent", "", "Agent identity")
cmd.AddCommand(newInitCmd(opts))
cmd.AddCommand(newSendCmd(opts))
cmd.AddCommand(newFetchCmd(opts))
cmd.AddCommand(newClaimCmd(opts))
cmd.AddCommand(newRenewCmd(opts))
cmd.AddCommand(newUpdateCmd(opts))
cmd.AddCommand(newReplyCmd(opts))
cmd.AddCommand(newDoneCmd(opts))
cmd.AddCommand(newFailCmd(opts))
cmd.AddCommand(newCancelCmd(opts))
cmd.AddCommand(newListCmd(opts))
cmd.AddCommand(newWatchCmd(opts))
cmd.AddCommand(newWaitReplyCmd(opts))
cmd.AddCommand(newShowCmd(opts))
return cmd
}
@@ -0,0 +1,117 @@
package inbox
import (
"fmt"
"ai-workflow-skill/packages/coord-core/protocol"
"ai-workflow-skill/packages/coord-core/store"
"github.com/spf13/cobra"
)
type sendOptions struct {
from string
to string
threadID string
runID string
taskID string
subject string
kind string
summary string
body string
bodyFile string
payloadJSON string
priority string
artifacts artifactOptions
}
func newSendCmd(root *rootOptions) *cobra.Command {
opts := &sendOptions{}
cmd := &cobra.Command{
Use: "send",
Short: "Create a thread with an initial directed message",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
from := opts.from
if from == "" {
from = root.agent
}
if from == "" {
return protocol.InvalidInput("from agent is required", nil)
}
if opts.threadID == "" && opts.subject == "" {
return protocol.InvalidInput("subject is required when creating a new thread", nil)
}
body, err := resolveBodyValue(opts.body, opts.bodyFile)
if err != nil {
return err
}
artifacts, err := resolveArtifacts(opts.artifacts)
if err != nil {
return err
}
sqlDB, err := openInboxDB(ctx, root.dbPath)
if err != nil {
return err
}
defer sqlDB.Close()
s := store.NewInboxStore(sqlDB)
thread, message, err := s.Send(ctx, store.SendInput{
ThreadID: opts.threadID,
RunID: opts.runID,
TaskID: opts.taskID,
Subject: opts.subject,
FromAgent: from,
ToAgent: opts.to,
Kind: opts.kind,
Summary: opts.summary,
Body: body,
PayloadJSON: opts.payloadJSON,
Priority: opts.priority,
Artifacts: artifacts,
})
if err != nil {
return err
}
resp := protocol.Success{
OK: true,
Command: "send",
Data: map[string]any{
"thread": thread,
"message": message,
},
}
if root.json {
return protocol.WriteJSON(cmd.OutOrStdout(), resp)
}
_, err = fmt.Fprintf(cmd.OutOrStdout(), "created thread %s\n", thread.ThreadID)
return err
},
}
cmd.Flags().StringVar(&opts.from, "from", "", "Sending agent")
cmd.Flags().StringVar(&opts.to, "to", "", "Receiving agent")
cmd.Flags().StringVar(&opts.threadID, "thread", "", "Optional thread ID override")
cmd.Flags().StringVar(&opts.runID, "run", "", "Optional run ID override")
cmd.Flags().StringVar(&opts.taskID, "task", "", "Optional task ID override")
cmd.Flags().StringVar(&opts.subject, "subject", "", "Thread subject")
cmd.Flags().StringVar(&opts.kind, "kind", "task", "Initial message kind")
cmd.Flags().StringVar(&opts.summary, "summary", "", "Short message summary")
cmd.Flags().StringVar(&opts.body, "body", "", "Message body")
cmd.Flags().StringVar(&opts.bodyFile, "body-file", "", "Read message body from file")
cmd.Flags().StringVar(&opts.payloadJSON, "payload-json", "", "Structured payload JSON string")
cmd.Flags().StringVar(&opts.priority, "priority", "normal", "Thread priority")
addArtifactFlags(cmd, &opts.artifacts)
_ = cmd.MarkFlagRequired("to")
return cmd
}
@@ -0,0 +1,230 @@
package inbox
import (
"os"
"path/filepath"
"testing"
)
func TestSendCreatesNewThread(t *testing.T) {
t.Parallel()
dbPath := initCommandTestDB(t)
sendOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"send",
"--from", "leader",
"--to", "worker-a",
"--subject", "Implement feature X",
"--summary", "Add retry policy",
"--body", "Implement retry handling for the HTTP client.",
"--run", "run_blog_001",
"--task", "T1",
)
var sendResp map[string]any
mustDecodeJSON(t, sendOut, &sendResp)
if got := nestedString(t, sendResp, "data", "thread", "thread_id"); got == "" {
t.Fatalf("expected thread_id, got empty")
}
if got := nestedString(t, sendResp, "data", "thread", "status"); got != "pending" {
t.Fatalf("expected pending status, got %q", got)
}
if got := nestedString(t, sendResp, "data", "thread", "created_by"); got != "leader" {
t.Fatalf("expected created_by leader, got %q", got)
}
if got := nestedString(t, sendResp, "data", "thread", "assigned_to"); got != "worker-a" {
t.Fatalf("expected assigned_to worker-a, got %q", got)
}
if got := nestedString(t, sendResp, "data", "message", "kind"); got != "task" {
t.Fatalf("expected message kind task, got %q", got)
}
}
func TestSendAppendsMessageToExistingThread(t *testing.T) {
t.Parallel()
dbPath := initCommandTestDB(t)
threadID := sendPendingThread(t, dbPath, "leader", "worker-d", "Build editor", "Create editor v1")
appendOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"send",
"--from", "leader",
"--to", "worker-d",
"--thread", threadID,
"--summary", "Use a markdown editor",
"--body", "Prefer a textarea-based markdown editor for v1.",
)
var appendResp map[string]any
mustDecodeJSON(t, appendOut, &appendResp)
if got := nestedString(t, appendResp, "data", "thread", "thread_id"); got != threadID {
t.Fatalf("expected same thread_id %q, got %q", threadID, got)
}
if got := nestedString(t, appendResp, "data", "thread", "status"); got != "pending" {
t.Fatalf("expected thread status to stay pending, got %q", got)
}
showOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"show",
"--thread", threadID,
)
var showResp map[string]any
mustDecodeJSON(t, showOut, &showResp)
messages, ok := nestedValue(t, showResp, "data", "messages").([]any)
if !ok || len(messages) != 2 {
t.Fatalf("expected two messages after append, got %#v", nestedValue(t, showResp, "data", "messages"))
}
}
func TestSendReadsBodyFromBodyFile(t *testing.T) {
t.Parallel()
tempDir := t.TempDir()
dbPath := filepath.Join(tempDir, "coord.db")
bodyPath := filepath.Join(tempDir, "task.md")
bodyContent := "Create the first editor screen.\nUse markdown syntax."
if err := os.WriteFile(bodyPath, []byte(bodyContent), 0o644); err != nil {
t.Fatalf("write body file: %v", err)
}
runInboxCommand(t, "--db", dbPath, "--json", "init")
sendOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"send",
"--from", "leader",
"--to", "worker-d",
"--subject", "Build admin editor",
"--summary", "Create the first editor screen",
"--body-file", bodyPath,
)
var sendResp map[string]any
mustDecodeJSON(t, sendOut, &sendResp)
threadID := nestedString(t, sendResp, "data", "thread", "thread_id")
showOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"show",
"--thread", threadID,
)
var showResp map[string]any
mustDecodeJSON(t, showOut, &showResp)
messages, ok := nestedValue(t, showResp, "data", "messages").([]any)
if !ok || len(messages) != 1 {
t.Fatalf("expected one message, got %#v", nestedValue(t, showResp, "data", "messages"))
}
message, ok := messages[0].(map[string]any)
if !ok {
t.Fatalf("expected message object, got %#v", messages[0])
}
if got, _ := message["body"].(string); got != bodyContent {
t.Fatalf("expected body %q, got %#v", bodyContent, message["body"])
}
}
func TestSendAttachesArtifactWithMetadata(t *testing.T) {
t.Parallel()
tempDir := t.TempDir()
dbPath := filepath.Join(tempDir, "coord.db")
artifactPath := filepath.Join(tempDir, "task.md")
if err := os.WriteFile(artifactPath, []byte("task brief"), 0o644); err != nil {
t.Fatalf("write artifact file: %v", err)
}
runInboxCommand(t, "--db", dbPath, "--json", "init")
sendOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"send",
"--from", "leader",
"--to", "worker-d",
"--subject", "Build admin editor",
"--summary", "Create the first editor screen",
"--artifact", artifactPath,
"--artifact-kind", "brief",
"--artifact-metadata-json", `{"label":"task-brief"}`,
)
var sendResp map[string]any
mustDecodeJSON(t, sendOut, &sendResp)
artifacts, ok := nestedValue(t, sendResp, "data", "message", "artifacts").([]any)
if !ok || len(artifacts) != 1 {
t.Fatalf("expected one artifact, got %#v", nestedValue(t, sendResp, "data", "message", "artifacts"))
}
artifact, ok := artifacts[0].(map[string]any)
if !ok {
t.Fatalf("expected artifact object, got %#v", artifacts[0])
}
if got, _ := artifact["path"].(string); got != artifactPath {
t.Fatalf("expected artifact path %q, got %#v", artifactPath, artifact["path"])
}
if got, _ := artifact["kind"].(string); got != "brief" {
t.Fatalf("expected artifact kind brief, got %#v", artifact["kind"])
}
metadata, ok := artifact["metadata_json"].(map[string]any)
if !ok {
t.Fatalf("expected metadata_json object, got %#v", artifact["metadata_json"])
}
if got, _ := metadata["label"].(string); got != "task-brief" {
t.Fatalf("expected metadata_json.label task-brief, got %#v", metadata["label"])
}
}
func TestSendRejectsInvalidPayloadJSON(t *testing.T) {
t.Parallel()
dbPath := initCommandTestDB(t)
stdout, _, exitCode := executeInboxCommand(
"--db", dbPath,
"--json",
"send",
"--from", "leader",
"--to", "worker-z",
"--subject", "Invalid payload json",
"--payload-json", "not-json",
)
if exitCode != 30 {
t.Fatalf("expected exit code 30, got %d", exitCode)
}
assertErrorJSON(t, stdout, "invalid_input")
}
func TestSendRejectsInvalidArtifactMetadataJSON(t *testing.T) {
t.Parallel()
dbPath := initCommandTestDB(t)
stdout, _, exitCode := executeInboxCommand(
"--db", dbPath,
"--json",
"send",
"--from", "leader",
"--to", "worker-z",
"--subject", "Invalid artifact json",
"--artifact", "/tmp/report.md",
"--artifact-metadata-json", "not-json",
)
if exitCode != 30 {
t.Fatalf("expected exit code 30, got %d", exitCode)
}
assertErrorJSON(t, stdout, "invalid_input")
}
@@ -0,0 +1,78 @@
package inbox
import (
"fmt"
"ai-workflow-skill/packages/coord-core/protocol"
"ai-workflow-skill/packages/coord-core/store"
"github.com/spf13/cobra"
)
type showOptions struct {
threadID string
markRead bool
}
func newShowCmd(root *rootOptions) *cobra.Command {
opts := &showOptions{}
cmd := &cobra.Command{
Use: "show",
Short: "Show one thread with message history",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
sqlDB, err := openInboxDB(ctx, root.dbPath)
if err != nil {
return err
}
defer sqlDB.Close()
s := store.NewInboxStore(sqlDB)
agent := root.agent
if opts.markRead && agent == "" {
return protocol.InvalidInput("agent is required when using --mark-read", nil)
}
detail, err := s.GetThreadForAgent(ctx, opts.threadID, agent, opts.markRead)
if err != nil {
return err
}
resp := protocol.Success{
OK: true,
Command: "show",
Data: map[string]any{
"thread": detail.Thread,
"messages": detail.Messages,
},
}
if root.json {
return protocol.WriteJSON(cmd.OutOrStdout(), resp)
}
if _, err := fmt.Fprintf(cmd.OutOrStdout(), "%s\t%s\t%s\n", detail.Thread.ThreadID, detail.Thread.Status, detail.Thread.Subject); err != nil {
return err
}
for _, message := range detail.Messages {
if _, err := fmt.Fprintf(cmd.OutOrStdout(), "- %s\t%s\t%s\n", message.MessageID, message.Kind, message.Summary); err != nil {
return err
}
for _, artifact := range message.Artifacts {
if _, err := fmt.Fprintf(cmd.OutOrStdout(), " artifact\t%s\t%s\n", artifact.Kind, artifact.Path); err != nil {
return err
}
}
}
return nil
},
}
cmd.Flags().StringVar(&opts.threadID, "thread", "", "Thread ID")
cmd.Flags().BoolVar(&opts.markRead, "mark-read", false, "Advance the caller's read cursor to the latest message")
_ = cmd.MarkFlagRequired("thread")
return cmd
}
@@ -0,0 +1,196 @@
package inbox
import (
"os"
"path/filepath"
"testing"
)
func TestShowReturnsThreadAndMessageHistory(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
runInboxCommand(t, "--db", dbPath, "--json", "init")
sendOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"send",
"--from", "leader",
"--to", "worker-a",
"--subject", "Implement feature X",
"--summary", "Initial request",
)
var sendResp map[string]any
mustDecodeJSON(t, sendOut, &sendResp)
threadID := nestedString(t, sendResp, "data", "thread", "thread_id")
runInboxCommand(
t,
"--db", dbPath,
"--json",
"send",
"--from", "leader",
"--to", "worker-a",
"--thread", threadID,
"--summary", "Follow-up request",
"--body", "Please include request logging.",
)
showOut := runInboxCommand(t, "--db", dbPath, "--json", "show", "--thread", threadID)
var showResp map[string]any
mustDecodeJSON(t, showOut, &showResp)
if got := nestedString(t, showResp, "data", "thread", "thread_id"); got != threadID {
t.Fatalf("expected thread %q, got %q", threadID, got)
}
messages, ok := nestedValue(t, showResp, "data", "messages").([]any)
if !ok || len(messages) != 2 {
t.Fatalf("expected two ordered messages, got %#v", nestedValue(t, showResp, "data", "messages"))
}
first, ok := messages[0].(map[string]any)
if !ok {
t.Fatalf("expected first message object, got %#v", messages[0])
}
second, ok := messages[1].(map[string]any)
if !ok {
t.Fatalf("expected second message object, got %#v", messages[1])
}
if got := first["summary"]; got != "Initial request" {
t.Fatalf("expected first summary Initial request, got %#v", got)
}
if got := second["summary"]; got != "Follow-up request" {
t.Fatalf("expected second summary Follow-up request, got %#v", got)
}
}
func TestShowIncludesArtifactsPerMessage(t *testing.T) {
t.Parallel()
tempDir := t.TempDir()
dbPath := filepath.Join(tempDir, "coord.db")
artifactPath := filepath.Join(tempDir, "task.md")
if err := os.WriteFile(artifactPath, []byte("task brief"), 0o644); err != nil {
t.Fatalf("write artifact file: %v", err)
}
runInboxCommand(t, "--db", dbPath, "--json", "init")
sendOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"send",
"--from", "leader",
"--to", "worker-a",
"--subject", "Artifact task",
"--summary", "Attach brief",
"--artifact", artifactPath,
"--artifact-kind", "brief",
"--artifact-metadata-json", `{"label":"task-brief"}`,
)
var sendResp map[string]any
mustDecodeJSON(t, sendOut, &sendResp)
threadID := nestedString(t, sendResp, "data", "thread", "thread_id")
showOut := runInboxCommand(t, "--db", dbPath, "--json", "show", "--thread", threadID)
var showResp map[string]any
mustDecodeJSON(t, showOut, &showResp)
messages, ok := nestedValue(t, showResp, "data", "messages").([]any)
if !ok || len(messages) == 0 {
t.Fatalf("expected messages with artifacts, got %#v", nestedValue(t, showResp, "data", "messages"))
}
first, ok := messages[0].(map[string]any)
if !ok {
t.Fatalf("expected message object, got %#v", messages[0])
}
artifacts, ok := first["artifacts"].([]any)
if !ok || len(artifacts) != 1 {
t.Fatalf("expected one artifact, got %#v", first["artifacts"])
}
artifact, ok := artifacts[0].(map[string]any)
if !ok {
t.Fatalf("expected artifact object, got %#v", artifacts[0])
}
if got := artifact["path"]; got != artifactPath {
t.Fatalf("expected artifact path %q, got %#v", artifactPath, got)
}
if got := artifact["kind"]; got != "brief" {
t.Fatalf("expected artifact kind brief, got %#v", got)
}
}
func TestShowMarkReadAdvancesReadCursor(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
runInboxCommand(t, "--db", dbPath, "--json", "init")
sendOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"send",
"--from", "leader",
"--to", "worker-e",
"--subject", "Review nav copy",
"--summary", "Check wording",
)
var sendResp map[string]any
mustDecodeJSON(t, sendOut, &sendResp)
threadID := nestedString(t, sendResp, "data", "thread", "thread_id")
runInboxCommand(
t,
"--db", dbPath,
"--json",
"fetch",
"--agent", "worker-e",
"--status", "pending",
"--unread",
)
runInboxCommand(
t,
"--db", dbPath,
"--agent", "worker-e",
"--json",
"show",
"--thread", threadID,
"--mark-read",
)
stdout, _, exitCode := executeInboxCommand(
"--db", dbPath,
"--json",
"fetch",
"--agent", "worker-e",
"--status", "pending",
"--unread",
)
if exitCode != 10 {
t.Fatalf("expected unread fetch to be empty after mark-read, got exit=%d with %s", exitCode, stdout)
}
assertErrorJSON(t, stdout, "no_matching_work")
}
func TestShowRejectsWhenThreadMissing(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
runInboxCommand(t, "--db", dbPath, "--json", "init")
stdout, _, exitCode := executeInboxCommand(
"--db", dbPath,
"--json",
"show",
"--thread", "thr_missing",
)
if exitCode != 40 {
t.Fatalf("expected not-found exit code 40, got %d with %s", exitCode, stdout)
}
assertErrorJSON(t, stdout, "not_found")
}
@@ -0,0 +1,78 @@
package inbox
import (
"bytes"
"encoding/json"
"testing"
)
func runInboxCommand(t *testing.T, args ...string) string {
t.Helper()
stdout, stderr, exitCode := executeInboxCommand(args...)
if exitCode != 0 {
t.Fatalf("execute inbox command %v: exit=%d\nstderr:\n%s\nstdout:\n%s", args, exitCode, stderr, stdout)
}
return stdout
}
func executeInboxCommand(args ...string) (string, string, int) {
var stdout bytes.Buffer
var stderr bytes.Buffer
exitCode := Execute(args, &stdout, &stderr)
return stdout.String(), stderr.String(), exitCode
}
func mustDecodeJSON(t *testing.T, raw string, target any) {
t.Helper()
if err := json.Unmarshal([]byte(raw), target); err != nil {
t.Fatalf("decode json %q: %v", raw, err)
}
}
func nestedString(t *testing.T, value map[string]any, keys ...string) string {
t.Helper()
current := nestedValue(t, value, keys...)
str, ok := current.(string)
if !ok {
t.Fatalf("expected string at %v, got %#v", keys, current)
}
return str
}
func nestedValue(t *testing.T, value map[string]any, keys ...string) any {
t.Helper()
var current any = value
for _, key := range keys {
obj, ok := current.(map[string]any)
if !ok {
t.Fatalf("expected object at %q in %v, got %#v", key, keys, current)
}
current, ok = obj[key]
if !ok {
t.Fatalf("missing key %q in %v", key, keys)
}
}
return current
}
func assertErrorJSON(t *testing.T, raw string, expectedCode string) {
t.Helper()
var payload map[string]any
mustDecodeJSON(t, raw, &payload)
if ok, _ := payload["ok"].(bool); ok {
t.Fatalf("expected ok=false error payload, got %#v", payload)
}
errorValue, ok := payload["error"].(map[string]any)
if !ok {
t.Fatalf("expected error object, got %#v", payload["error"])
}
if code, _ := errorValue["code"].(string); code != expectedCode {
t.Fatalf("expected error code %q, got %#v", expectedCode, errorValue["code"])
}
}
@@ -0,0 +1,101 @@
package inbox
import (
"fmt"
"ai-workflow-skill/packages/coord-core/protocol"
"ai-workflow-skill/packages/coord-core/store"
"github.com/spf13/cobra"
)
type updateOptions struct {
agent string
threadID string
status string
summary string
body string
bodyFile string
payloadJSON string
artifacts artifactOptions
}
func newUpdateCmd(root *rootOptions) *cobra.Command {
opts := &updateOptions{}
cmd := &cobra.Command{
Use: "update",
Short: "Append a progress or blocked update to a thread",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
agent := opts.agent
if agent == "" {
agent = root.agent
}
if agent == "" {
return protocol.InvalidInput("agent is required", nil)
}
body, err := resolveBodyValue(opts.body, opts.bodyFile)
if err != nil {
return err
}
artifacts, err := resolveArtifacts(opts.artifacts)
if err != nil {
return err
}
sqlDB, err := openInboxDB(ctx, root.dbPath)
if err != nil {
return err
}
defer sqlDB.Close()
s := store.NewInboxStore(sqlDB)
thread, message, err := s.UpdateThreadStatus(ctx, store.UpdateInput{
ThreadID: opts.threadID,
Agent: agent,
Status: opts.status,
Summary: opts.summary,
Body: body,
PayloadJSON: opts.payloadJSON,
Artifacts: artifacts,
})
if err != nil {
return err
}
resp := protocol.Success{
OK: true,
Command: "update",
Data: map[string]any{
"thread": thread,
"message": message,
},
}
if root.json {
return protocol.WriteJSON(cmd.OutOrStdout(), resp)
}
_, err = fmt.Fprintf(cmd.OutOrStdout(), "updated thread %s to %s\n", thread.ThreadID, thread.Status)
return err
},
}
cmd.Flags().StringVar(&opts.agent, "agent", "", "Updating agent")
cmd.Flags().StringVar(&opts.threadID, "thread", "", "Thread ID")
cmd.Flags().StringVar(&opts.status, "status", "", "New status: in_progress or blocked")
cmd.Flags().StringVar(&opts.summary, "summary", "", "Short update summary")
cmd.Flags().StringVar(&opts.body, "body", "", "Update body")
cmd.Flags().StringVar(&opts.bodyFile, "body-file", "", "Read update body from file")
cmd.Flags().StringVar(&opts.payloadJSON, "payload-json", "", "Structured payload JSON string")
addArtifactFlags(cmd, &opts.artifacts)
_ = cmd.MarkFlagRequired("thread")
_ = cmd.MarkFlagRequired("status")
_ = cmd.MarkFlagRequired("summary")
return cmd
}
@@ -0,0 +1,226 @@
package inbox
import (
"os"
"path/filepath"
"testing"
)
func seedThreadForInboxTests(t *testing.T, dbPath, from, to string) string {
t.Helper()
runInboxCommand(t, "--db", dbPath, "--json", "init")
sendOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"send",
"--from", from,
"--to", to,
"--subject", "Implement feature X",
"--summary", "Add retry policy",
)
var sendResp map[string]any
mustDecodeJSON(t, sendOut, &sendResp)
return nestedString(t, sendResp, "data", "thread", "thread_id")
}
func seedClaimedThreadForInboxTests(t *testing.T, dbPath, from, to, claimer string) string {
t.Helper()
threadID := seedThreadForInboxTests(t, dbPath, from, to)
runInboxCommand(
t,
"--db", dbPath,
"--json",
"claim",
"--agent", claimer,
"--thread", threadID,
"--lease-seconds", "300",
)
return threadID
}
func lastThreadMessageFromShow(t *testing.T, showResp map[string]any) map[string]any {
t.Helper()
messagesValue := nestedValue(t, showResp, "data", "messages")
messages, ok := messagesValue.([]any)
if !ok || len(messages) == 0 {
t.Fatalf("expected non-empty messages, got %#v", messagesValue)
}
lastMessage, ok := messages[len(messages)-1].(map[string]any)
if !ok {
t.Fatalf("expected message object, got %#v", messages[len(messages)-1])
}
return lastMessage
}
func TestUpdateMovesThreadToInProgress(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
threadID := seedClaimedThreadForInboxTests(t, dbPath, "leader", "worker-a", "worker-a")
updateOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"update",
"--agent", "worker-a",
"--thread", threadID,
"--status", "in_progress",
"--summary", "Implementation started",
"--body", "Scanning current HTTP client usage.",
)
var updateResp map[string]any
mustDecodeJSON(t, updateOut, &updateResp)
if status := nestedString(t, updateResp, "data", "thread", "status"); status != "in_progress" {
t.Fatalf("expected in_progress thread status, got %q", status)
}
if kind := nestedString(t, updateResp, "data", "message", "kind"); kind != "progress" {
t.Fatalf("expected progress message kind, got %q", kind)
}
if toAgent := nestedString(t, updateResp, "data", "message", "to_agent"); toAgent != "leader" {
t.Fatalf("expected message to_agent leader, got %q", toAgent)
}
}
func TestUpdateMovesThreadToBlockedWithPayload(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
threadID := seedClaimedThreadForInboxTests(t, dbPath, "leader", "worker-a", "worker-a")
updateOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"update",
"--agent", "worker-a",
"--thread", threadID,
"--status", "blocked",
"--summary", "Need timeout decision",
"--payload-json", `{"question":"Should retries apply to read timeouts?"}`,
)
var updateResp map[string]any
mustDecodeJSON(t, updateOut, &updateResp)
if status := nestedString(t, updateResp, "data", "thread", "status"); status != "blocked" {
t.Fatalf("expected blocked thread status, got %q", status)
}
if kind := nestedString(t, updateResp, "data", "message", "kind"); kind != "question" {
t.Fatalf("expected question message kind, got %q", kind)
}
payload, ok := nestedValue(t, updateResp, "data", "message", "payload_json").(map[string]any)
if !ok {
t.Fatalf("expected payload_json object, got %#v", nestedValue(t, updateResp, "data", "message", "payload_json"))
}
if got := payload["question"]; got != "Should retries apply to read timeouts?" {
t.Fatalf("expected payload question, got %#v", got)
}
}
func TestUpdateAcceptsBodyFileAndArtifact(t *testing.T) {
t.Parallel()
tempDir := t.TempDir()
dbPath := filepath.Join(tempDir, "coord.db")
progressPath := filepath.Join(tempDir, "progress.md")
body := "Progress update from file."
if err := os.WriteFile(progressPath, []byte(body), 0o644); err != nil {
t.Fatalf("write progress file: %v", err)
}
threadID := seedClaimedThreadForInboxTests(t, dbPath, "leader", "worker-a", "worker-a")
runInboxCommand(
t,
"--db", dbPath,
"--json",
"update",
"--agent", "worker-a",
"--thread", threadID,
"--status", "in_progress",
"--summary", "Implementation started",
"--body-file", progressPath,
"--artifact", progressPath,
"--artifact-kind", "note",
)
showOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"show",
"--thread", threadID,
)
var showResp map[string]any
mustDecodeJSON(t, showOut, &showResp)
lastMessage := lastThreadMessageFromShow(t, showResp)
if gotBody, _ := lastMessage["body"].(string); gotBody != body {
t.Fatalf("expected body %q, got %#v", body, lastMessage["body"])
}
artifacts, ok := lastMessage["artifacts"].([]any)
if !ok || len(artifacts) != 1 {
t.Fatalf("expected one artifact, got %#v", lastMessage["artifacts"])
}
artifact, ok := artifacts[0].(map[string]any)
if !ok {
t.Fatalf("expected artifact object, got %#v", artifacts[0])
}
if gotPath, _ := artifact["path"].(string); gotPath != progressPath {
t.Fatalf("expected artifact path %q, got %#v", progressPath, artifact["path"])
}
if gotKind, _ := artifact["kind"].(string); gotKind != "note" {
t.Fatalf("expected artifact kind note, got %#v", artifact["kind"])
}
}
func TestUpdateRejectsInvalidPayloadJSON(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
threadID := seedClaimedThreadForInboxTests(t, dbPath, "leader", "worker-a", "worker-a")
stdout, _, exitCode := executeInboxCommand(
"--db", dbPath,
"--json",
"update",
"--agent", "worker-a",
"--thread", threadID,
"--status", "blocked",
"--summary", "Need timeout decision",
"--payload-json", "not-json",
)
if exitCode != 30 {
t.Fatalf("expected exit code 30, got %d with output %s", exitCode, stdout)
}
assertErrorJSON(t, stdout, "invalid_input")
}
func TestUpdateRejectsNonOwner(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
threadID := seedClaimedThreadForInboxTests(t, dbPath, "leader", "worker-a", "worker-a")
stdout, _, exitCode := executeInboxCommand(
"--db", dbPath,
"--json",
"update",
"--agent", "worker-b",
"--thread", threadID,
"--status", "in_progress",
"--summary", "Implementation started",
)
if exitCode != 20 {
t.Fatalf("expected exit code 20, got %d with output %s", exitCode, stdout)
}
assertErrorJSON(t, stdout, "lease_conflict")
}
@@ -0,0 +1,85 @@
package inbox
import (
"fmt"
"time"
"ai-workflow-skill/packages/coord-core/protocol"
"ai-workflow-skill/packages/coord-core/store"
"github.com/spf13/cobra"
)
type waitReplyOptions struct {
threadID string
afterMessageID string
afterEventID int64
kinds string
timeoutSeconds int
}
func newWaitReplyCmd(root *rootOptions) *cobra.Command {
opts := &waitReplyOptions{}
cmd := &cobra.Command{
Use: "wait-reply",
Short: "Block until a reply-like message appears in a thread",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
sqlDB, err := openInboxDB(ctx, root.dbPath)
if err != nil {
return err
}
defer sqlDB.Close()
s := store.NewInboxStore(sqlDB)
agent := root.agent
result, err := s.WaitReply(ctx, store.WaitReplyInput{
ThreadID: opts.threadID,
AfterMessageID: opts.afterMessageID,
AfterEventID: opts.afterEventID,
Kinds: parseCSV(opts.kinds),
Agent: agent,
Timeout: time.Duration(opts.timeoutSeconds) * time.Second,
})
if err != nil {
return err
}
if !result.Woke {
return protocol.NoMatchingWork("no matching reply before timeout")
}
data := map[string]any{
"woke": result.Woke,
"next_event_id": result.NextEventID,
}
if result.Message != nil {
data["message"] = result.Message
}
resp := protocol.Success{
OK: true,
Command: "wait-reply",
Data: data,
}
if root.json {
return protocol.WriteJSON(cmd.OutOrStdout(), resp)
}
_, err = fmt.Fprintf(cmd.OutOrStdout(), "reply received on thread %s at event %d\n", result.Message.ThreadID, result.NextEventID)
return err
},
}
cmd.Flags().StringVar(&opts.threadID, "thread", "", "Thread ID")
cmd.Flags().StringVar(&opts.afterMessageID, "after-message", "", "Resume after a known message ID")
cmd.Flags().Int64Var(&opts.afterEventID, "after-event", 0, "Resume after a known event ID")
cmd.Flags().StringVar(&opts.kinds, "kinds", "answer,control,result", "Comma-separated message kinds to wake on")
cmd.Flags().IntVar(&opts.timeoutSeconds, "timeout-seconds", 0, "Maximum time to wait; 0 waits forever")
_ = cmd.MarkFlagRequired("thread")
return cmd
}
@@ -0,0 +1,221 @@
package inbox
import (
"path/filepath"
"strconv"
"testing"
"time"
)
type waitReplyCommandResult struct {
stdout string
stderr string
exit int
}
func TestWaitReplyWakesOnAnswerAfterMessage(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
threadID, blockedMessageID := seedBlockedThreadForWaitReply(t, dbPath)
waitCh := make(chan waitReplyCommandResult, 1)
go func() {
stdout, stderr, exitCode := executeInboxCommand(
"--db", dbPath,
"--agent", "worker-c",
"--json",
"wait-reply",
"--thread", threadID,
"--after-message", blockedMessageID,
"--timeout-seconds", "2",
)
waitCh <- waitReplyCommandResult{stdout: stdout, stderr: stderr, exit: exitCode}
}()
time.Sleep(200 * time.Millisecond)
runInboxCommand(
t,
"--db", dbPath,
"--json",
"reply",
"--from", "leader",
"--to", "worker-c",
"--thread", threadID,
"--summary", "Redirect to login",
"--body", "Redirect guests to login for the MVP.",
)
var waitResult waitReplyCommandResult
select {
case waitResult = <-waitCh:
case <-time.After(3 * time.Second):
t.Fatal("wait-reply command did not return")
}
if waitResult.exit != 0 {
t.Fatalf("wait-reply failed with exit=%d\nstderr:\n%s\nstdout:\n%s", waitResult.exit, waitResult.stderr, waitResult.stdout)
}
var waitResp map[string]any
mustDecodeJSON(t, waitResult.stdout, &waitResp)
if woke, ok := nestedValue(t, waitResp, "data", "woke").(bool); !ok || !woke {
t.Fatalf("expected wait-reply wake, got %#v", nestedValue(t, waitResp, "data", "woke"))
}
if kind := nestedString(t, waitResp, "data", "message", "kind"); kind != "answer" {
t.Fatalf("expected answer wake message, got %q", kind)
}
}
func TestWaitReplyCanStartFromAfterEvent(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
threadID, blockedMessageID := seedBlockedThreadForWaitReply(t, dbPath)
runInboxCommand(
t,
"--db", dbPath,
"--json",
"reply",
"--from", "leader",
"--to", "worker-c",
"--thread", threadID,
"--summary", "First answer",
"--body", "First reply payload.",
)
firstWaitOut := runInboxCommand(
t,
"--db", dbPath,
"--agent", "worker-c",
"--json",
"wait-reply",
"--thread", threadID,
"--after-message", blockedMessageID,
"--timeout-seconds", "2",
)
var firstWaitResp map[string]any
mustDecodeJSON(t, firstWaitOut, &firstWaitResp)
firstEventIDFloat, ok := nestedValue(t, firstWaitResp, "data", "next_event_id").(float64)
if !ok {
t.Fatalf("expected numeric next_event_id, got %#v", nestedValue(t, firstWaitResp, "data", "next_event_id"))
}
firstEventID := int64(firstEventIDFloat)
waitCh := make(chan waitReplyCommandResult, 1)
go func() {
stdout, stderr, exitCode := executeInboxCommand(
"--db", dbPath,
"--agent", "worker-c",
"--json",
"wait-reply",
"--thread", threadID,
"--after-event", strconv.FormatInt(firstEventID, 10),
"--timeout-seconds", "2",
)
waitCh <- waitReplyCommandResult{stdout: stdout, stderr: stderr, exit: exitCode}
}()
time.Sleep(200 * time.Millisecond)
runInboxCommand(
t,
"--db", dbPath,
"--json",
"reply",
"--from", "leader",
"--to", "worker-c",
"--thread", threadID,
"--summary", "Second answer",
"--body", "Second reply payload.",
)
var waitResult waitReplyCommandResult
select {
case waitResult = <-waitCh:
case <-time.After(3 * time.Second):
t.Fatal("wait-reply after-event command did not return")
}
if waitResult.exit != 0 {
t.Fatalf("wait-reply after-event failed with exit=%d\nstderr:\n%s\nstdout:\n%s", waitResult.exit, waitResult.stderr, waitResult.stdout)
}
var waitResp map[string]any
mustDecodeJSON(t, waitResult.stdout, &waitResp)
if got := nestedString(t, waitResp, "data", "message", "summary"); got != "Second answer" {
t.Fatalf("expected second answer wake message, got %q", got)
}
secondEventIDFloat, ok := nestedValue(t, waitResp, "data", "next_event_id").(float64)
if !ok {
t.Fatalf("expected numeric next_event_id, got %#v", nestedValue(t, waitResp, "data", "next_event_id"))
}
if int64(secondEventIDFloat) <= firstEventID {
t.Fatalf("expected second event id > first event id, got %d <= %d", int64(secondEventIDFloat), firstEventID)
}
}
func TestWaitReplyTimesOutWhenNoReply(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
threadID, _ := seedBlockedThreadForWaitReply(t, dbPath)
stdout, _, exitCode := executeInboxCommand(
"--db", dbPath,
"--agent", "worker-c",
"--json",
"wait-reply",
"--thread", threadID,
"--timeout-seconds", "1",
)
if exitCode != 10 {
t.Fatalf("expected wait-reply timeout exit code 10, got %d with %s", exitCode, stdout)
}
assertErrorJSON(t, stdout, "no_matching_work")
}
func seedBlockedThreadForWaitReply(t *testing.T, dbPath string) (threadID string, blockedMessageID string) {
t.Helper()
runInboxCommand(t, "--db", dbPath, "--json", "init")
sendOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"send",
"--from", "leader",
"--to", "worker-c",
"--subject", "Investigate auth edge case",
"--summary", "Check auth redirect behavior",
)
var sendResp map[string]any
mustDecodeJSON(t, sendOut, &sendResp)
threadID = nestedString(t, sendResp, "data", "thread", "thread_id")
runInboxCommand(
t,
"--db", dbPath,
"--json",
"claim",
"--agent", "worker-c",
"--thread", threadID,
)
blockedOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"update",
"--agent", "worker-c",
"--thread", threadID,
"--status", "blocked",
"--summary", "Need policy decision",
)
var blockedResp map[string]any
mustDecodeJSON(t, blockedOut, &blockedResp)
blockedMessageID = nestedString(t, blockedResp, "data", "message", "message_id")
return threadID, blockedMessageID
}
@@ -0,0 +1,90 @@
package inbox
import (
"fmt"
"time"
"ai-workflow-skill/packages/coord-core/protocol"
"ai-workflow-skill/packages/coord-core/store"
"github.com/spf13/cobra"
)
type watchOptions struct {
agent string
statuses string
timeoutSeconds int
afterEventID int64
}
func newWatchCmd(root *rootOptions) *cobra.Command {
opts := &watchOptions{}
cmd := &cobra.Command{
Use: "watch",
Short: "Block until new matching activity appears",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
agent := opts.agent
if agent == "" {
agent = root.agent
}
sqlDB, err := openInboxDB(ctx, root.dbPath)
if err != nil {
return err
}
defer sqlDB.Close()
s := store.NewInboxStore(sqlDB)
result, err := s.WatchThreads(ctx, store.WatchInput{
Agent: agent,
Statuses: parseCSV(opts.statuses),
AfterEventID: opts.afterEventID,
StartFromNow: !cmd.Flags().Changed("after-event"),
Timeout: time.Duration(opts.timeoutSeconds) * time.Second,
})
if err != nil {
return err
}
if !result.Woke {
return protocol.NoMatchingWork("no matching work before watch timeout")
}
data := map[string]any{
"woke": result.Woke,
"next_event_id": result.NextEventID,
}
if result.Thread != nil {
data["thread"] = result.Thread
}
if result.Message != nil {
data["message"] = result.Message
}
if result.Event != nil {
data["event"] = result.Event
}
resp := protocol.Success{
OK: true,
Command: "watch",
Data: data,
}
if root.json {
return protocol.WriteJSON(cmd.OutOrStdout(), resp)
}
_, err = fmt.Fprintf(cmd.OutOrStdout(), "watch woke on thread %s at event %d\n", result.Thread.ThreadID, result.NextEventID)
return err
},
}
cmd.Flags().StringVar(&opts.agent, "agent", "", "Assigned agent filter")
cmd.Flags().StringVar(&opts.statuses, "status", "pending,blocked,done,failed", "Comma-separated status filter")
cmd.Flags().IntVar(&opts.timeoutSeconds, "timeout-seconds", 0, "Maximum time to wait; 0 waits forever")
cmd.Flags().Int64Var(&opts.afterEventID, "after-event", 0, "Resume after a known event ID")
return cmd
}
@@ -0,0 +1,171 @@
package inbox
import (
"path/filepath"
"testing"
"time"
)
type watchCommandResult struct {
stdout string
stderr string
exit int
}
func TestWatchWakesOnMatchingThread(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
runInboxCommand(t, "--db", dbPath, "--json", "init")
watchCh := make(chan watchCommandResult, 1)
go func() {
stdout, stderr, exitCode := executeInboxCommand(
"--db", dbPath,
"--json",
"watch",
"--agent", "worker-d",
"--status", "pending",
"--timeout-seconds", "2",
)
watchCh <- watchCommandResult{stdout: stdout, stderr: stderr, exit: exitCode}
}()
time.Sleep(200 * time.Millisecond)
sendOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"send",
"--from", "leader",
"--to", "worker-d",
"--subject", "Build admin editor",
"--summary", "Create the first editor screen",
)
var sendResp map[string]any
mustDecodeJSON(t, sendOut, &sendResp)
threadID := nestedString(t, sendResp, "data", "thread", "thread_id")
var watchResult watchCommandResult
select {
case watchResult = <-watchCh:
case <-time.After(3 * time.Second):
t.Fatal("watch command did not return")
}
if watchResult.exit != 0 {
t.Fatalf("watch failed with exit=%d\nstderr:\n%s\nstdout:\n%s", watchResult.exit, watchResult.stderr, watchResult.stdout)
}
var watchResp map[string]any
mustDecodeJSON(t, watchResult.stdout, &watchResp)
if woke, ok := nestedValue(t, watchResp, "data", "woke").(bool); !ok || !woke {
t.Fatalf("expected watch to wake, got %#v", nestedValue(t, watchResp, "data", "woke"))
}
if got := nestedString(t, watchResp, "data", "thread", "thread_id"); got != threadID {
t.Fatalf("expected woken thread %q, got %q", threadID, got)
}
nextEventID, ok := nestedValue(t, watchResp, "data", "next_event_id").(float64)
if !ok {
t.Fatalf("expected numeric next_event_id, got %#v", nestedValue(t, watchResp, "data", "next_event_id"))
}
eventID, ok := nestedValue(t, watchResp, "data", "event", "event_id").(float64)
if !ok {
t.Fatalf("expected numeric event_id, got %#v", nestedValue(t, watchResp, "data", "event", "event_id"))
}
if nextEventID != eventID {
t.Fatalf("expected next_event_id == event.event_id, got %v vs %v", nextEventID, eventID)
}
}
func TestWatchRespectsStatusFilter(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
runInboxCommand(t, "--db", dbPath, "--json", "init")
sendOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"send",
"--from", "leader",
"--to", "worker-c",
"--subject", "Investigate policy edge case",
"--summary", "Initial request",
)
var sendResp map[string]any
mustDecodeJSON(t, sendOut, &sendResp)
threadID := nestedString(t, sendResp, "data", "thread", "thread_id")
watchCh := make(chan watchCommandResult, 1)
go func() {
stdout, stderr, exitCode := executeInboxCommand(
"--db", dbPath,
"--json",
"watch",
"--agent", "worker-c",
"--status", "blocked",
"--timeout-seconds", "2",
)
watchCh <- watchCommandResult{stdout: stdout, stderr: stderr, exit: exitCode}
}()
time.Sleep(200 * time.Millisecond)
runInboxCommand(
t,
"--db", dbPath,
"--json",
"claim",
"--agent", "worker-c",
"--thread", threadID,
)
runInboxCommand(
t,
"--db", dbPath,
"--json",
"update",
"--agent", "worker-c",
"--thread", threadID,
"--status", "blocked",
"--summary", "Need policy decision",
)
var watchResult watchCommandResult
select {
case watchResult = <-watchCh:
case <-time.After(3 * time.Second):
t.Fatal("watch command did not return")
}
if watchResult.exit != 0 {
t.Fatalf("watch failed with exit=%d\nstderr:\n%s\nstdout:\n%s", watchResult.exit, watchResult.stderr, watchResult.stdout)
}
var watchResp map[string]any
mustDecodeJSON(t, watchResult.stdout, &watchResp)
if status := nestedString(t, watchResp, "data", "thread", "status"); status != "blocked" {
t.Fatalf("expected blocked status wake, got %q", status)
}
}
func TestWatchTimesOutWithNoActivity(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
runInboxCommand(t, "--db", dbPath, "--json", "init")
stdout, _, exitCode := executeInboxCommand(
"--db", dbPath,
"--json",
"watch",
"--agent", "worker-d",
"--status", "pending",
"--timeout-seconds", "1",
)
if exitCode != 10 {
t.Fatalf("expected watch timeout exit code 10, got %d with %s", exitCode, stdout)
}
assertErrorJSON(t, stdout, "no_matching_work")
}
+11
View File
@@ -0,0 +1,11 @@
package main
import (
"os"
orchcli "ai-workflow-skill/packages/orch-runtime/internal/cli/orch"
)
func main() {
os.Exit(orchcli.Execute(os.Args[1:], os.Stdout, os.Stderr))
}
+4
View File
@@ -1,3 +1,7 @@
module ai-workflow-skill/packages/orch-runtime
go 1.26
require (
github.com/spf13/cobra v1.10.1
)
@@ -0,0 +1,77 @@
package orch
import (
"fmt"
"ai-workflow-skill/packages/coord-core/protocol"
"ai-workflow-skill/packages/coord-core/store"
"github.com/spf13/cobra"
)
type answerOptions struct {
runID string
taskID string
body string
bodyFile string
payloadJSON string
}
func newAnswerCmd(root *rootOptions) *cobra.Command {
opts := &answerOptions{}
cmd := &cobra.Command{
Use: "answer",
Short: "Answer the active blocked question for a task",
RunE: func(cmd *cobra.Command, args []string) error {
body, err := resolveBodyValue(opts.body, opts.bodyFile)
if err != nil {
return err
}
ctx := cmd.Context()
sqlDB, err := openOrchDB(ctx, root.dbPath)
if err != nil {
return err
}
defer sqlDB.Close()
result, err := store.NewOrchStore(sqlDB).AnswerTask(ctx, store.AnswerInput{
RunID: opts.runID,
TaskID: opts.taskID,
Body: body,
PayloadJSON: opts.payloadJSON,
})
if err != nil {
return err
}
resp := protocol.Success{
OK: true,
Command: "answer",
Data: map[string]any{
"task": result.Task,
"attempt": result.Attempt,
"thread": result.Thread,
"message": result.Message,
},
}
if root.json {
return protocol.WriteJSON(cmd.OutOrStdout(), resp)
}
_, err = fmt.Fprintf(cmd.OutOrStdout(), "answered task %s on thread %s\n", result.Task.TaskID, result.Thread.ThreadID)
return err
},
}
cmd.Flags().StringVar(&opts.runID, "run", "", "Run ID")
cmd.Flags().StringVar(&opts.taskID, "task", "", "Task ID")
cmd.Flags().StringVar(&opts.body, "body", "", "Answer body")
cmd.Flags().StringVar(&opts.bodyFile, "body-file", "", "Read answer body from file")
cmd.Flags().StringVar(&opts.payloadJSON, "payload-json", "", "Structured payload JSON string")
_ = cmd.MarkFlagRequired("run")
_ = cmd.MarkFlagRequired("task")
return cmd
}
@@ -0,0 +1,64 @@
package orch
import (
"fmt"
"ai-workflow-skill/packages/coord-core/protocol"
"ai-workflow-skill/packages/coord-core/store"
"github.com/spf13/cobra"
)
type blockedOptions struct {
runID string
}
func newBlockedCmd(root *rootOptions) *cobra.Command {
opts := &blockedOptions{}
cmd := &cobra.Command{
Use: "blocked",
Short: "List blocked tasks and their latest question",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
sqlDB, err := openOrchDB(ctx, root.dbPath)
if err != nil {
return err
}
defer sqlDB.Close()
blocked, err := store.NewOrchStore(sqlDB).ListBlockedTasks(ctx, opts.runID)
if err != nil {
return err
}
resp := protocol.Success{
OK: true,
Command: "blocked",
Data: map[string]any{
"blocked": blocked,
},
}
if root.json {
return protocol.WriteJSON(cmd.OutOrStdout(), resp)
}
if len(blocked) == 0 {
_, err = fmt.Fprintln(cmd.OutOrStdout(), "no blocked tasks")
return err
}
for _, item := range blocked {
if _, err := fmt.Fprintf(cmd.OutOrStdout(), "%s\t%s\n", item.Task.TaskID, item.Question.Summary); err != nil {
return err
}
}
return nil
},
}
cmd.Flags().StringVar(&opts.runID, "run", "", "Run ID")
_ = cmd.MarkFlagRequired("run")
return cmd
}
@@ -0,0 +1,22 @@
package orch
import (
"os"
"ai-workflow-skill/packages/coord-core/protocol"
)
func resolveBodyValue(body, bodyFile string) (string, error) {
if body != "" && bodyFile != "" {
return "", protocol.InvalidInput("body and body-file are mutually exclusive", nil)
}
if bodyFile == "" {
return body, nil
}
content, err := os.ReadFile(bodyFile)
if err != nil {
return "", protocol.InvalidInput("failed to read body-file", err)
}
return string(content), nil
}
@@ -0,0 +1,69 @@
package orch
import (
"fmt"
"ai-workflow-skill/packages/coord-core/protocol"
"ai-workflow-skill/packages/coord-core/store"
"github.com/spf13/cobra"
)
type cancelOptions struct {
runID string
taskID string
reason string
}
func newCancelCmd(root *rootOptions) *cobra.Command {
opts := &cancelOptions{}
cmd := &cobra.Command{
Use: "cancel",
Short: "Cancel a task or an entire run",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
sqlDB, err := openOrchDB(ctx, root.dbPath)
if err != nil {
return err
}
defer sqlDB.Close()
result, err := store.NewOrchStore(sqlDB).Cancel(ctx, store.CancelControlInput{
RunID: opts.runID,
TaskID: opts.taskID,
Reason: opts.reason,
})
if err != nil {
return err
}
resp := protocol.Success{
OK: true,
Command: "cancel",
Data: map[string]any{
"run": result.Run,
"cancelled_tasks": result.CancelledTasks,
},
}
if root.json {
return protocol.WriteJSON(cmd.OutOrStdout(), resp)
}
if opts.taskID != "" {
_, err = fmt.Fprintf(cmd.OutOrStdout(), "cancelled task %s in run %s\n", opts.taskID, opts.runID)
return err
}
_, err = fmt.Fprintf(cmd.OutOrStdout(), "cancelled run %s (%d tasks)\n", opts.runID, len(result.CancelledTasks))
return err
},
}
cmd.Flags().StringVar(&opts.runID, "run", "", "Run ID")
cmd.Flags().StringVar(&opts.taskID, "task", "", "Optional task ID")
cmd.Flags().StringVar(&opts.reason, "reason", "", "Cancellation reason")
_ = cmd.MarkFlagRequired("run")
return cmd
}
@@ -0,0 +1,84 @@
package orch
import (
"fmt"
"ai-workflow-skill/packages/coord-core/protocol"
"ai-workflow-skill/packages/coord-core/store"
"github.com/spf13/cobra"
)
type cleanupOptions struct {
runID string
taskID string
attemptNo int
allCompleted bool
force bool
}
func newCleanupCmd(root *rootOptions) *cobra.Command {
opts := &cleanupOptions{}
cmd := &cobra.Command{
Use: "cleanup",
Short: "Remove completed or abandoned attempt worktrees",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
sqlDB, err := openOrchDB(ctx, root.dbPath)
if err != nil {
return err
}
defer sqlDB.Close()
s := store.NewOrchStore(sqlDB)
candidates, err := s.ListCleanupCandidates(ctx, store.CleanupInput{
RunID: opts.runID,
TaskID: opts.taskID,
AttemptNo: opts.attemptNo,
AllCompleted: opts.allCompleted,
Force: opts.force,
})
if err != nil {
return err
}
records := make([]store.CleanupRecord, 0, len(candidates))
for _, candidate := range candidates {
if err := cleanupAttemptWorktree(ctx, candidate.Attempt, opts.force); err != nil {
return err
}
records = append(records, store.CleanupRecord{Attempt: candidate.Attempt})
}
cleaned, err := s.MarkAttemptsCleaned(ctx, records)
if err != nil {
return err
}
resp := protocol.Success{
OK: true,
Command: "cleanup",
Data: map[string]any{
"cleaned": cleaned,
},
}
if root.json {
return protocol.WriteJSON(cmd.OutOrStdout(), resp)
}
_, err = fmt.Fprintf(cmd.OutOrStdout(), "cleaned %d worktrees\n", len(cleaned))
return err
},
}
cmd.Flags().StringVar(&opts.runID, "run", "", "Run ID")
cmd.Flags().StringVar(&opts.taskID, "task", "", "Optional task ID")
cmd.Flags().IntVar(&opts.attemptNo, "attempt", 0, "Specific attempt number")
cmd.Flags().BoolVar(&opts.allCompleted, "all-completed", false, "Clean all completed or abandoned worktrees in the run")
cmd.Flags().BoolVar(&opts.force, "force", false, "Force cleanup even for non-terminal worktrees")
_ = cmd.MarkFlagRequired("run")
return cmd
}
@@ -0,0 +1,245 @@
package orch
import (
"path/filepath"
"strings"
"testing"
)
func TestOrchRunShowReturnsRunSummaryAndTaskCounts(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
runOrchCommand(
t,
"--db", dbPath,
"--json",
"run", "init",
"--run", "run_blog_001",
"--goal", "Build blog MVP",
"--summary", "Public blog plus admin CRUD",
)
runOrchCommand(
t,
"--db", dbPath,
"--json",
"task", "add",
"--run", "run_blog_001",
"--task", "T1",
"--title", "Implement retry policy",
"--summary", "Add retry policy to HTTP client",
"--default-to", "worker-a",
)
showOut := runOrchCommand(
t,
"--db", dbPath,
"--json",
"run", "show",
"--run", "run_blog_001",
)
var showResp map[string]any
mustDecodeJSON(t, showOut, &showResp)
if got := nestedString(t, showResp, "data", "run", "run_id"); got != "run_blog_001" {
t.Fatalf("expected run id run_blog_001, got %q", got)
}
if got := nestedString(t, showResp, "data", "run", "status"); got != "ready" {
t.Fatalf("expected run status ready, got %q", got)
}
taskCounts, ok := nestedValue(t, showResp, "data", "task_counts").(map[string]any)
if !ok {
t.Fatalf("expected task_counts object, got %#v", nestedValue(t, showResp, "data", "task_counts"))
}
if got, _ := taskCounts["ready"].(float64); got < 1 {
t.Fatalf("expected ready task count >= 1, got %#v", taskCounts["ready"])
}
data, ok := showResp["data"].(map[string]any)
if !ok {
t.Fatalf("expected data object, got %#v", showResp["data"])
}
if _, exists := data["tasks"]; exists {
t.Fatalf("did not expect tasks array in run show response, got %#v", data["tasks"])
}
}
func TestOrchRunShowRejectsMissingRun(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
stdout, _, exitCode := executeOrchCommand(
"--db", dbPath,
"--json",
"run", "show",
"--run", "run_blog_missing",
)
if exitCode != 40 {
t.Fatalf("expected not_found exit code 40, got %d\nstdout:\n%s", exitCode, stdout)
}
assertErrorJSON(t, stdout, "not_found")
}
func TestOrchTaskAddRejectsInvalidAcceptanceJSON(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
runOrchCommand(
t,
"--db", dbPath,
"--json",
"run", "init",
"--run", "run_blog_003",
"--goal", "Validate task add input guards",
)
stdout, _, exitCode := executeOrchCommand(
"--db", dbPath,
"--json",
"task", "add",
"--run", "run_blog_003",
"--task", "T1",
"--title", "Implement retry policy",
"--acceptance-json", `{"done":true`,
)
if exitCode != 30 {
t.Fatalf("expected invalid_input exit code 30, got %d\nstdout:\n%s", exitCode, stdout)
}
assertErrorJSON(t, stdout, "invalid_input")
assertErrorMessageContains(t, stdout, "acceptance-json must be valid JSON")
}
func TestOrchTaskAddRejectsInvalidPriority(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
runOrchCommand(
t,
"--db", dbPath,
"--json",
"run", "init",
"--run", "run_blog_004",
"--goal", "Validate task priority input",
)
stdout, _, exitCode := executeOrchCommand(
"--db", dbPath,
"--json",
"task", "add",
"--run", "run_blog_004",
"--task", "T1",
"--title", "Implement retry policy",
"--priority", "urgent",
)
if exitCode != 30 {
t.Fatalf("expected invalid_input exit code 30, got %d\nstdout:\n%s", exitCode, stdout)
}
assertErrorJSON(t, stdout, "invalid_input")
assertErrorMessageContains(t, stdout, "priority must be one of low, normal, high")
}
func TestOrchReadyOrdersByPriorityAndRespectsLimit(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
runOrchCommand(
t,
"--db", dbPath,
"--json",
"run", "init",
"--run", "run_blog_005",
"--goal", "Validate ready ordering",
)
runOrchCommand(
t,
"--db", dbPath,
"--json",
"task", "add",
"--run", "run_blog_005",
"--task", "T1",
"--title", "Low priority task",
"--priority", "low",
)
runOrchCommand(
t,
"--db", dbPath,
"--json",
"task", "add",
"--run", "run_blog_005",
"--task", "T2",
"--title", "Normal priority task",
"--priority", "normal",
)
runOrchCommand(
t,
"--db", dbPath,
"--json",
"task", "add",
"--run", "run_blog_005",
"--task", "T3",
"--title", "High priority task",
"--priority", "high",
)
readyOut := runOrchCommand(
t,
"--db", dbPath,
"--json",
"ready",
"--run", "run_blog_005",
"--limit", "2",
)
var readyResp map[string]any
mustDecodeJSON(t, readyOut, &readyResp)
readyTasks := nestedArray(t, readyResp, "data", "tasks")
if len(readyTasks) != 2 {
t.Fatalf("expected two ready tasks with limit 2, got %#v", readyTasks)
}
firstTask, ok := readyTasks[0].(map[string]any)
if !ok {
t.Fatalf("expected first ready task object, got %#v", readyTasks[0])
}
secondTask, ok := readyTasks[1].(map[string]any)
if !ok {
t.Fatalf("expected second ready task object, got %#v", readyTasks[1])
}
if got, _ := firstTask["task_id"].(string); got != "T3" {
t.Fatalf("expected first ready task T3, got %#v", firstTask["task_id"])
}
if got, _ := secondTask["task_id"].(string); got != "T2" {
t.Fatalf("expected second ready task T2, got %#v", secondTask["task_id"])
}
for _, item := range readyTasks {
task, ok := item.(map[string]any)
if !ok {
t.Fatalf("expected ready task object, got %#v", item)
}
if got, _ := task["task_id"].(string); got == "T1" {
t.Fatalf("did not expect low-priority task T1 within limited ready results")
}
}
}
func assertErrorMessageContains(t *testing.T, raw string, want string) {
t.Helper()
var payload map[string]any
mustDecodeJSON(t, raw, &payload)
errorValue, ok := payload["error"].(map[string]any)
if !ok {
t.Fatalf("expected error object, got %#v", payload["error"])
}
message, _ := errorValue["message"].(string)
if !strings.Contains(message, want) {
t.Fatalf("expected error message to contain %q, got %q", want, message)
}
}
@@ -0,0 +1,219 @@
package orch
import (
"path/filepath"
"testing"
)
func TestOrchAnswerAcceptsPayloadJSONWithoutBody(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
threadID := seedBlockedTaskForAnswerCleanupEdgeTests(t, dbPath, "run_blog_answer_001", "T2", "worker-b")
answerOut := runOrchCommand(
t,
"--db", dbPath,
"--json",
"answer",
"--run", "run_blog_answer_001",
"--task", "T2",
"--payload-json", `{"decision":"stdout","source":"leader"}`,
)
var answerResp map[string]any
mustDecodeJSON(t, answerOut, &answerResp)
message, ok := nestedValue(t, answerResp, "data", "message").(map[string]any)
if !ok {
t.Fatalf("expected answer message object, got %#v", nestedValue(t, answerResp, "data", "message"))
}
if got, _ := message["kind"].(string); got != "answer" {
t.Fatalf("expected answer message kind, got %#v", message["kind"])
}
payload, ok := message["payload_json"].(map[string]any)
if !ok {
t.Fatalf("expected payload_json object, got %#v", message["payload_json"])
}
if got, _ := payload["decision"].(string); got != "stdout" {
t.Fatalf("expected payload decision stdout, got %#v", payload["decision"])
}
if got, _ := payload["source"].(string); got != "leader" {
t.Fatalf("expected payload source leader, got %#v", payload["source"])
}
showOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"show",
"--thread", threadID,
)
var showResp map[string]any
mustDecodeJSON(t, showOut, &showResp)
messages := nestedArray(t, showResp, "data", "messages")
if len(messages) == 0 {
t.Fatalf("expected messages in thread %s", threadID)
}
lastMessage, ok := messages[len(messages)-1].(map[string]any)
if !ok {
t.Fatalf("expected last message object, got %#v", messages[len(messages)-1])
}
if got, _ := lastMessage["kind"].(string); got != "answer" {
t.Fatalf("expected latest message kind answer, got %#v", lastMessage["kind"])
}
lastPayload, ok := lastMessage["payload_json"].(map[string]any)
if !ok {
t.Fatalf("expected latest payload_json object, got %#v", lastMessage["payload_json"])
}
if got, _ := lastPayload["decision"].(string); got != "stdout" {
t.Fatalf("expected latest payload decision stdout, got %#v", lastPayload["decision"])
}
}
func TestOrchAnswerRejectsEmptyBodyAndPayload(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
_ = seedBlockedTaskForAnswerCleanupEdgeTests(t, dbPath, "run_blog_answer_002", "T2", "worker-b")
stdout, _, exitCode := executeOrchCommand(
"--db", dbPath,
"--json",
"answer",
"--run", "run_blog_answer_002",
"--task", "T2",
)
if exitCode != 30 {
t.Fatalf("expected exit code 30, got %d\nstdout:\n%s", exitCode, stdout)
}
assertErrorJSON(t, stdout, "invalid_input")
}
func TestOrchCleanupRejectsAttemptWithoutTask(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
runOrchCommand(
t,
"--db", dbPath,
"--json",
"run", "init",
"--run", "run_blog_cleanup_002",
"--goal", "Validate cleanup selectors",
)
stdout, _, exitCode := executeOrchCommand(
"--db", dbPath,
"--json",
"cleanup",
"--run", "run_blog_cleanup_002",
"--attempt", "1",
)
if exitCode != 30 {
t.Fatalf("expected exit code 30, got %d\nstdout:\n%s", exitCode, stdout)
}
assertErrorJSON(t, stdout, "invalid_input")
}
func TestOrchCleanupReturnsNoMatchingWorkWhenFiltersMiss(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
runOrchCommand(
t,
"--db", dbPath,
"--json",
"run", "init",
"--run", "run_blog_cleanup_003",
"--goal", "Validate cleanup empty result",
)
runOrchCommand(
t,
"--db", dbPath,
"--json",
"task", "add",
"--run", "run_blog_cleanup_003",
"--task", "T1",
"--title", "Prepare cleanup target",
)
stdout, _, exitCode := executeOrchCommand(
"--db", dbPath,
"--json",
"cleanup",
"--run", "run_blog_cleanup_003",
"--task", "T1",
)
if exitCode != 10 {
t.Fatalf("expected exit code 10, got %d\nstdout:\n%s", exitCode, stdout)
}
assertErrorJSON(t, stdout, "no_matching_work")
}
func seedBlockedTaskForAnswerCleanupEdgeTests(t *testing.T, dbPath, runID, taskID, agent string) string {
t.Helper()
runOrchCommand(
t,
"--db", dbPath,
"--json",
"run", "init",
"--run", runID,
"--goal", "Prepare blocked task for answer edge tests",
)
runOrchCommand(
t,
"--db", dbPath,
"--json",
"task", "add",
"--run", runID,
"--task", taskID,
"--title", "Build frontend",
"--default-to", agent,
)
dispatchOut := runOrchCommand(
t,
"--db", dbPath,
"--json",
"dispatch",
"--run", runID,
"--task", taskID,
)
var dispatchResp map[string]any
mustDecodeJSON(t, dispatchOut, &dispatchResp)
threadID := nestedString(t, dispatchResp, "data", "attempt", "thread_id")
runInboxCommand(
t,
"--db", dbPath,
"--json",
"claim",
"--agent", agent,
"--thread", threadID,
)
runInboxCommand(
t,
"--db", dbPath,
"--json",
"update",
"--agent", agent,
"--thread", threadID,
"--status", "blocked",
"--summary", "Need logging decision",
"--payload-json", `{"question":"stdout or stderr?"}`,
)
runOrchCommand(
t,
"--db", dbPath,
"--json",
"reconcile",
"--run", runID,
)
return threadID
}
@@ -0,0 +1,723 @@
package orch
import (
"os"
"path/filepath"
"testing"
)
func TestOrchRunInitCreatesNewRun(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
initOut := runOrchCommand(
t,
"--db", dbPath,
"--json",
"run", "init",
"--run", "run_blog_init_001",
"--goal", "Build blog MVP",
"--summary", "Public blog plus admin CRUD",
)
var initResp map[string]any
mustDecodeJSON(t, initOut, &initResp)
if got := nestedString(t, initResp, "data", "run", "run_id"); got != "run_blog_init_001" {
t.Fatalf("expected run id run_blog_init_001, got %q", got)
}
if got := nestedString(t, initResp, "data", "run", "goal"); got != "Build blog MVP" {
t.Fatalf("expected goal Build blog MVP, got %q", got)
}
if got := nestedString(t, initResp, "data", "run", "summary"); got != "Public blog plus admin CRUD" {
t.Fatalf("expected summary to round-trip, got %q", got)
}
if got := nestedString(t, initResp, "data", "run", "status"); got != "active" {
t.Fatalf("expected new run status active, got %q", got)
}
assertNonEmptyNestedString(t, initResp, "data", "run", "created_at")
assertNonEmptyNestedString(t, initResp, "data", "run", "updated_at")
showOut := runOrchCommand(
t,
"--db", dbPath,
"--json",
"run", "show",
"--run", "run_blog_init_001",
)
var showResp map[string]any
mustDecodeJSON(t, showOut, &showResp)
if got := nestedString(t, showResp, "data", "run", "run_id"); got != "run_blog_init_001" {
t.Fatalf("expected persisted run id run_blog_init_001, got %q", got)
}
if got := nestedString(t, showResp, "data", "run", "status"); got != "active" {
t.Fatalf("expected persisted run status active, got %q", got)
}
}
func TestOrchDispatchCreatesAttemptAndThreadForReadyTask(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
runOrchCommand(
t,
"--db", dbPath,
"--json",
"run", "init",
"--run", "run_blog_dispatch_001",
"--goal", "Build blog MVP",
"--summary", "Public blog plus admin CRUD",
)
runOrchCommand(
t,
"--db", dbPath,
"--json",
"task", "add",
"--run", "run_blog_dispatch_001",
"--task", "T1",
"--title", "Implement retry policy",
"--summary", "Add retry policy to HTTP client",
"--default-to", "worker-a",
)
dispatchOut := runOrchCommand(
t,
"--db", dbPath,
"--json",
"dispatch",
"--run", "run_blog_dispatch_001",
"--task", "T1",
"--body", "Implement retry handling for the HTTP client.",
)
var dispatchResp map[string]any
mustDecodeJSON(t, dispatchOut, &dispatchResp)
if got := nestedString(t, dispatchResp, "data", "task", "status"); got != "dispatched" {
t.Fatalf("expected dispatched task status, got %q", got)
}
if got := nestedValue(t, dispatchResp, "data", "attempt", "attempt_no").(float64); got != 1 {
t.Fatalf("expected attempt_no 1, got %#v", got)
}
threadID := nestedString(t, dispatchResp, "data", "attempt", "thread_id")
if threadID == "" {
t.Fatal("expected non-empty attempt thread_id")
}
if got := nestedString(t, dispatchResp, "data", "attempt", "assigned_to"); got != "worker-a" {
t.Fatalf("expected assigned_to worker-a, got %q", got)
}
if got := nestedString(t, dispatchResp, "data", "thread", "thread_id"); got != threadID {
t.Fatalf("expected thread.thread_id %q, got %q", threadID, got)
}
if got := nestedString(t, dispatchResp, "data", "message", "kind"); got != "task" {
t.Fatalf("expected first dispatch message kind task, got %q", got)
}
}
func TestOrchBlockedListsLatestQuestionForBlockedTask(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
runOrchCommand(
t,
"--db", dbPath,
"--json",
"run", "init",
"--run", "run_blog_blocked_001",
"--goal", "Build dependency-aware workflow",
)
runOrchCommand(
t,
"--db", dbPath,
"--json",
"task", "add",
"--run", "run_blog_blocked_001",
"--task", "T1",
"--title", "Build backend",
"--summary", "Implement backend APIs",
"--default-to", "worker-a",
)
runOrchCommand(
t,
"--db", dbPath,
"--json",
"task", "add",
"--run", "run_blog_blocked_001",
"--task", "T2",
"--title", "Build frontend",
"--summary", "Implement frontend flows",
"--default-to", "worker-b",
)
runOrchCommand(
t,
"--db", dbPath,
"--json",
"dep", "add",
"--run", "run_blog_blocked_001",
"--task", "T2",
"--depends-on", "T1",
)
firstDispatch := runOrchCommand(
t,
"--db", dbPath,
"--json",
"dispatch",
"--run", "run_blog_blocked_001",
"--task", "T1",
)
var firstDispatchResp map[string]any
mustDecodeJSON(t, firstDispatch, &firstDispatchResp)
threadBackend := nestedString(t, firstDispatchResp, "data", "attempt", "thread_id")
runInboxCommand(
t,
"--db", dbPath,
"--json",
"claim",
"--agent", "worker-a",
"--thread", threadBackend,
)
runInboxCommand(
t,
"--db", dbPath,
"--json",
"done",
"--agent", "worker-a",
"--thread", threadBackend,
"--summary", "Backend complete",
)
runOrchCommand(
t,
"--db", dbPath,
"--json",
"reconcile",
"--run", "run_blog_blocked_001",
)
secondDispatch := runOrchCommand(
t,
"--db", dbPath,
"--json",
"dispatch",
"--run", "run_blog_blocked_001",
"--task", "T2",
)
var secondDispatchResp map[string]any
mustDecodeJSON(t, secondDispatch, &secondDispatchResp)
threadFrontend := nestedString(t, secondDispatchResp, "data", "attempt", "thread_id")
runInboxCommand(
t,
"--db", dbPath,
"--json",
"claim",
"--agent", "worker-b",
"--thread", threadFrontend,
)
runInboxCommand(
t,
"--db", dbPath,
"--json",
"update",
"--agent", "worker-b",
"--thread", threadFrontend,
"--status", "blocked",
"--summary", "Need logging decision",
"--payload-json", `{"question":"stdout or stderr?"}`,
)
runOrchCommand(
t,
"--db", dbPath,
"--json",
"reconcile",
"--run", "run_blog_blocked_001",
)
blockedOut := runOrchCommand(
t,
"--db", dbPath,
"--json",
"blocked",
"--run", "run_blog_blocked_001",
)
var blockedResp map[string]any
mustDecodeJSON(t, blockedOut, &blockedResp)
blockedTasks := nestedArray(t, blockedResp, "data", "blocked")
if len(blockedTasks) != 1 {
t.Fatalf("expected one blocked task, got %#v", blockedTasks)
}
blockedTask, ok := blockedTasks[0].(map[string]any)
if !ok {
t.Fatalf("expected blocked task object, got %#v", blockedTasks[0])
}
if got := nestedString(t, blockedTask, "task", "task_id"); got != "T2" {
t.Fatalf("expected blocked task T2, got %q", got)
}
if got := nestedString(t, blockedTask, "question", "kind"); got != "question" {
t.Fatalf("expected question.kind=question, got %q", got)
}
if got := nestedString(t, blockedTask, "question", "summary"); got != "Need logging decision" {
t.Fatalf("expected question summary to match latest blocked message, got %q", got)
}
questionPayload, ok := nestedValue(t, blockedTask, "question", "payload_json").(map[string]any)
if !ok {
t.Fatalf("expected question payload_json object, got %#v", nestedValue(t, blockedTask, "question", "payload_json"))
}
if got, _ := questionPayload["question"].(string); got != "stdout or stderr?" {
t.Fatalf("expected latest question payload, got %#v", questionPayload["question"])
}
}
func TestOrchStatusReturnsRunSummaryAndTaskList(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
runOrchCommand(
t,
"--db", dbPath,
"--json",
"run", "init",
"--run", "run_blog_status_001",
"--goal", "Build blog MVP",
)
runOrchCommand(
t,
"--db", dbPath,
"--json",
"task", "add",
"--run", "run_blog_status_001",
"--task", "T1",
"--title", "Implement retry policy",
"--default-to", "worker-a",
)
dispatchOut := runOrchCommand(
t,
"--db", dbPath,
"--json",
"dispatch",
"--run", "run_blog_status_001",
"--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",
"done",
"--agent", "worker-a",
"--thread", threadID,
"--summary", "Retry policy implemented",
"--body", "The HTTP client now retries transient failures.",
)
runOrchCommand(
t,
"--db", dbPath,
"--json",
"reconcile",
"--run", "run_blog_status_001",
)
statusOut := runOrchCommand(
t,
"--db", dbPath,
"--json",
"status",
"--run", "run_blog_status_001",
)
var statusResp map[string]any
mustDecodeJSON(t, statusOut, &statusResp)
if got := nestedString(t, statusResp, "data", "run", "run_id"); got != "run_blog_status_001" {
t.Fatalf("expected run_id run_blog_status_001, got %q", got)
}
if got := nestedString(t, statusResp, "data", "run", "status"); got != "done" {
t.Fatalf("expected run status done, got %q", got)
}
taskCounts, ok := nestedValue(t, statusResp, "data", "task_counts").(map[string]any)
if !ok {
t.Fatalf("expected task_counts object, got %#v", nestedValue(t, statusResp, "data", "task_counts"))
}
if got, _ := taskCounts["done"].(float64); got != 1 {
t.Fatalf("expected done task count 1, got %#v", taskCounts["done"])
}
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, _ := task["task_id"].(string); got != "T1" {
t.Fatalf("expected task_id T1, got %#v", task["task_id"])
}
if got, _ := task["status"].(string); got != "done" {
t.Fatalf("expected task status done, got %#v", task["status"])
}
}
func TestOrchReconcileMapsFailedThreadToTerminalTaskState(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
runOrchCommand(
t,
"--db", dbPath,
"--json",
"run", "init",
"--run", "run_blog_reconcile_001",
"--goal", "Build blog MVP",
)
runOrchCommand(
t,
"--db", dbPath,
"--json",
"task", "add",
"--run", "run_blog_reconcile_001",
"--task", "T1",
"--title", "Implement retry policy",
"--default-to", "worker-a",
)
dispatchOut := runOrchCommand(
t,
"--db", dbPath,
"--json",
"dispatch",
"--run", "run_blog_reconcile_001",
"--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",
"fail",
"--agent", "worker-a",
"--thread", threadID,
"--summary", "Retry policy failed",
"--body", "The HTTP client kept failing integration tests.",
)
reconcileOut := runOrchCommand(
t,
"--db", dbPath,
"--json",
"reconcile",
"--run", "run_blog_reconcile_001",
)
var reconcileResp map[string]any
mustDecodeJSON(t, reconcileOut, &reconcileResp)
updatedTasks := nestedArray(t, reconcileResp, "data", "updated_tasks")
if len(updatedTasks) != 1 {
t.Fatalf("expected one updated task after failed reconcile, got %#v", updatedTasks)
}
task, ok := updatedTasks[0].(map[string]any)
if !ok {
t.Fatalf("expected updated task object, got %#v", updatedTasks[0])
}
if got, _ := task["task_id"].(string); got != "T1" {
t.Fatalf("expected updated task T1, got %#v", task["task_id"])
}
if got, _ := task["status"].(string); got != "failed" {
t.Fatalf("expected reconciled task status failed, got %#v", task["status"])
}
taskCounts, ok := nestedValue(t, reconcileResp, "data", "task_counts").(map[string]any)
if !ok {
t.Fatalf("expected task_counts object, got %#v", nestedValue(t, reconcileResp, "data", "task_counts"))
}
if got, _ := taskCounts["failed"].(float64); got != 1 {
t.Fatalf("expected failed task count 1 after reconcile, got %#v", taskCounts["failed"])
}
statusOut := runOrchCommand(
t,
"--db", dbPath,
"--json",
"status",
"--run", "run_blog_reconcile_001",
)
var statusResp map[string]any
mustDecodeJSON(t, statusOut, &statusResp)
if got := nestedString(t, statusResp, "data", "run", "status"); got != "failed" {
t.Fatalf("expected run status failed after failed reconcile, got %q", got)
}
}
func TestOrchWorkflowStrictWorktreeDispatchToCleanup(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
repoPath := initGitRepo(t)
runOrchCommand(
t,
"--db", dbPath,
"--json",
"run", "init",
"--run", "run_blog_workflow_worktree_001",
"--goal", "Validate strict worktree dispatch",
)
runOrchCommand(
t,
"--db", dbPath,
"--json",
"task", "add",
"--run", "run_blog_workflow_worktree_001",
"--task", "T1",
"--title", "Implement backend",
"--default-to", "worker-a",
)
dispatchOut := runOrchCommand(
t,
"--db", dbPath,
"--json",
"dispatch",
"--run", "run_blog_workflow_worktree_001",
"--task", "T1",
"--repo-path", repoPath,
"--workspace-root", ".orch/worktrees",
"--strict-worktree",
"--body", "Implement inside isolated worktree.",
)
var dispatchResp map[string]any
mustDecodeJSON(t, dispatchOut, &dispatchResp)
threadID := nestedString(t, dispatchResp, "data", "attempt", "thread_id")
worktreePath := nestedString(t, dispatchResp, "data", "attempt", "worktree_path")
if worktreePath == "" {
t.Fatal("expected non-empty worktree_path for strict worktree workflow")
}
if got := nestedString(t, dispatchResp, "data", "attempt", "workspace_status"); got != "created" {
t.Fatalf("expected workspace_status created, got %q", got)
}
runInboxCommand(
t,
"--db", dbPath,
"--json",
"claim",
"--agent", "worker-a",
"--thread", threadID,
)
runInboxCommand(
t,
"--db", dbPath,
"--json",
"done",
"--agent", "worker-a",
"--thread", threadID,
"--summary", "Backend complete",
)
runOrchCommand(
t,
"--db", dbPath,
"--json",
"reconcile",
"--run", "run_blog_workflow_worktree_001",
)
cleanupOut := runOrchCommand(
t,
"--db", dbPath,
"--json",
"cleanup",
"--run", "run_blog_workflow_worktree_001",
"--task", "T1",
"--attempt", "1",
)
var cleanupResp map[string]any
mustDecodeJSON(t, cleanupOut, &cleanupResp)
cleaned := nestedArray(t, cleanupResp, "data", "cleaned")
if len(cleaned) != 1 {
t.Fatalf("expected one cleaned attempt, got %#v", cleaned)
}
cleanedAttempt, ok := cleaned[0].(map[string]any)
if !ok {
t.Fatalf("expected cleaned attempt object, got %#v", cleaned[0])
}
if got, _ := cleanedAttempt["workspace_status"].(string); got != "cleaned" {
t.Fatalf("expected cleaned workspace_status, got %#v", cleanedAttempt["workspace_status"])
}
if _, err := os.Stat(worktreePath); !os.IsNotExist(err) {
t.Fatalf("expected cleaned worktree path to be removed, err=%v", err)
}
}
func TestOrchWorkflowCouncilReviewEndToEnd(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
runID := "council_blog_workflow_001"
startOut := runOrchCommand(
t,
"--db", dbPath,
"--json",
"council", "start",
"--run", runID,
"--target", "Review the current blog architecture.",
)
var startResp map[string]any
mustDecodeJSON(t, startOut, &startResp)
reviewers := nestedArray(t, startResp, "data", "reviewers")
if len(reviewers) != 3 {
t.Fatalf("expected three reviewers from council start, got %#v", reviewers)
}
completeCouncilWorkflowReviewersForRemainingTests(t, dbPath, runID)
waitOut := runOrchCommand(
t,
"--db", dbPath,
"--json",
"council", "wait",
"--run", runID,
"--timeout-seconds", "2",
)
var waitResp map[string]any
mustDecodeJSON(t, waitOut, &waitResp)
if woke, _ := nestedValue(t, waitResp, "data", "woke").(bool); !woke {
t.Fatalf("expected council wait to wake, got %#v", waitResp)
}
if allComplete, _ := nestedValue(t, waitResp, "data", "all_complete").(bool); !allComplete {
t.Fatalf("expected all reviewers complete, got %#v", waitResp)
}
tallyOut := runOrchCommand(
t,
"--db", dbPath,
"--json",
"council", "tally",
"--run", runID,
"--similarity", "normal",
)
var tallyResp map[string]any
mustDecodeJSON(t, tallyOut, &tallyResp)
if got := nestedString(t, tallyResp, "data", "similarity"); got != "normal" {
t.Fatalf("expected normal tally similarity, got %q", got)
}
tallyCounts, ok := nestedValue(t, tallyResp, "data", "counts").(map[string]any)
if !ok {
t.Fatalf("expected tally counts object, got %#v", nestedValue(t, tallyResp, "data", "counts"))
}
if got, _ := tallyCounts["consensus"].(float64); got != 1 {
t.Fatalf("expected one consensus group, got %#v", tallyCounts["consensus"])
}
if got, _ := tallyCounts["majority"].(float64); got != 1 {
t.Fatalf("expected one majority group, got %#v", tallyCounts["majority"])
}
if got, _ := tallyCounts["minority"].(float64); got != 1 {
t.Fatalf("expected one minority group, got %#v", tallyCounts["minority"])
}
reportOut := runOrchCommand(
t,
"--db", dbPath,
"--json",
"council", "report",
"--run", runID,
)
var reportResp map[string]any
mustDecodeJSON(t, reportOut, &reportResp)
show := nestedArray(t, reportResp, "data", "show")
if len(show) != 2 || show[0] != "consensus" || show[1] != "majority" {
t.Fatalf("expected default report show [consensus majority], got %#v", show)
}
grouped := nestedArray(t, reportResp, "data", "grouped_recommendations")
if len(grouped) != 2 {
t.Fatalf("expected default report to include consensus and majority groups, got %#v", grouped)
}
artifacts := nestedArray(t, reportResp, "data", "report_artifacts")
if len(artifacts) != 1 {
t.Fatalf("expected one report artifact, got %#v", artifacts)
}
artifact, ok := artifacts[0].(map[string]any)
if !ok {
t.Fatalf("expected report artifact object, got %#v", artifacts[0])
}
reportPath, _ := artifact["path"].(string)
if reportPath == "" {
t.Fatalf("expected report artifact path, got %#v", artifact["path"])
}
if _, err := os.Stat(reportPath); err != nil {
t.Fatalf("expected report artifact to exist at %q: %v", reportPath, err)
}
}
func assertNonEmptyNestedString(t *testing.T, value map[string]any, keys ...string) {
t.Helper()
if got := nestedString(t, value, keys...); got == "" {
t.Fatalf("expected non-empty string at %v", keys)
}
}
func completeCouncilWorkflowReviewersForRemainingTests(t *testing.T, dbPath, runID string) {
t.Helper()
completeCouncilReviewer(
t,
dbPath,
runID,
"architecture-reviewer",
`{"reviewer_role":"architecture-reviewer","findings":[{"title":"Split contracts","summary":"Transport contracts are mixed into UI code.","proposal":"Move API contract definitions into a dedicated module.","rationale":"This lowers coupling.","confidence":"high","tags":["architecture"],"target_refs":{"repo_path":"."}},{"title":"Share helpers","summary":"Council report rendering paths are repeated.","proposal":"Introduce shared council coordinator helpers for report rendering.","rationale":"This keeps report assembly consistent.","confidence":"medium","tags":["reporting"],"target_refs":{"repo_path":"."}}]}`,
)
completeCouncilReviewer(
t,
dbPath,
runID,
"implementation-reviewer",
`{"reviewer_role":"implementation-reviewer","findings":[{"title":"Extract contracts","summary":"Shared transport shapes are duplicated.","proposal":"Move API contract definitions into dedicated module","rationale":"This reduces duplication.","confidence":"high","tags":["maintainability"],"target_refs":{"repo_path":"."}},{"title":"Reuse report helpers","summary":"Formatting logic should stay shared.","proposal":"Introduce shared council coordinator helpers for report rendering","rationale":"This avoids formatter drift.","confidence":"medium","tags":["reporting"],"target_refs":{"repo_path":"."}}]}`,
)
completeCouncilReviewer(
t,
dbPath,
runID,
"risk-reviewer",
`{"reviewer_role":"risk-reviewer","findings":[{"title":"Lock contracts","summary":"Contract drift becomes risky over time.","proposal":"Move API contract definitions into a dedicated module.","rationale":"This reduces integration regressions.","confidence":"high","tags":["risk"],"target_refs":{"repo_path":"."}},{"title":"Cover JSON output","summary":"The council report response should stay stable.","proposal":"Add regression tests for council report JSON output.","rationale":"This catches contract regressions earlier.","confidence":"high","tags":["testing"],"target_refs":{"repo_path":"."}}]}`,
)
}
@@ -0,0 +1,16 @@
package orch
import "github.com/spf13/cobra"
func newCouncilCmd(root *rootOptions) *cobra.Command {
cmd := &cobra.Command{
Use: "council",
Short: "Council review workflow commands",
}
cmd.AddCommand(newCouncilStartCmd(root))
cmd.AddCommand(newCouncilWaitCmd(root))
cmd.AddCommand(newCouncilTallyCmd(root))
cmd.AddCommand(newCouncilReportCmd(root))
return cmd
}
@@ -0,0 +1,111 @@
package orch
import (
"fmt"
"os"
"path/filepath"
"strings"
"ai-workflow-skill/packages/coord-core/protocol"
"ai-workflow-skill/packages/coord-core/store"
"github.com/spf13/cobra"
)
type councilReportOptions struct {
runID string
show string
}
func newCouncilReportCmd(root *rootOptions) *cobra.Command {
opts := &councilReportOptions{}
cmd := &cobra.Command{
Use: "report",
Short: "Render the final grouped council output",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
sqlDB, err := openOrchDB(ctx, root.dbPath)
if err != nil {
return err
}
defer sqlDB.Close()
orchStore := store.NewOrchStore(sqlDB)
result, err := orchStore.BuildCouncilReport(ctx, store.CouncilReportInput{
RunID: opts.runID,
Show: opts.show,
})
if err != nil {
return err
}
reportPath := councilReportArtifactPath(root.dbPath, result.RunID)
if err := os.MkdirAll(filepath.Dir(reportPath), 0o755); err != nil {
return fmt.Errorf("create council report directory: %w", err)
}
if err := os.WriteFile(reportPath, []byte(result.Markdown), 0o644); err != nil {
return fmt.Errorf("write council report artifact: %w", err)
}
result.ReportArtifacts = []store.CouncilReportArtifact{
{
Kind: "markdown",
Path: reportPath,
},
}
if err := orchStore.PersistCouncilReport(ctx, store.CouncilPersistReportInput{
RunID: result.RunID,
Show: result.Show,
Summary: result.Summary,
MarkdownPath: reportPath,
}); err != nil {
return err
}
resp := protocol.Success{
OK: true,
Command: "council report",
Data: map[string]any{
"run_id": result.RunID,
"show": result.Show,
"summary": result.Summary,
"grouped_recommendations": result.GroupedRecommendations,
"report_artifacts": result.ReportArtifacts,
},
}
if root.json {
return protocol.WriteJSON(cmd.OutOrStdout(), resp)
}
_, err = fmt.Fprint(cmd.OutOrStdout(), result.Markdown)
return err
},
}
cmd.Flags().StringVar(&opts.runID, "run", "", "Council run ID")
cmd.Flags().StringVar(&opts.show, "show", "", "Buckets to show: consensus,majority,minority,all")
_ = cmd.MarkFlagRequired("run")
return cmd
}
func councilReportArtifactPath(dbPath, runID string) string {
baseDir := "."
dbDir := filepath.Dir(dbPath)
switch {
case dbDir == "", dbDir == ".":
baseDir = "."
case filepath.Base(dbDir) == ".agents":
baseDir = filepath.Dir(dbDir)
default:
baseDir = dbDir
}
if baseDir == "" {
baseDir = "."
}
fileName := strings.ReplaceAll(strings.TrimSpace(runID), string(os.PathSeparator), "_") + ".md"
return filepath.Join(baseDir, ".orch", "reports", fileName)
}
@@ -0,0 +1,199 @@
package orch
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestOrchCouncilReportRejectsBeforeTally(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
runID := "council_blog_report_010"
runOrchCommand(
t,
"--db", dbPath,
"--json",
"council", "start",
"--run", runID,
"--target", "Review the council reporting flow.",
)
stdout, _, exitCode := executeOrchCommand(
"--db", dbPath,
"--json",
"council", "report",
"--run", runID,
)
if exitCode != 30 {
t.Fatalf("expected invalid_state exit code 30, got %d\nstdout:\n%s", exitCode, stdout)
}
assertErrorJSON(t, stdout, "invalid_state")
if msg := orchErrorMessage(t, stdout); !strings.Contains(msg, "run council tally first") {
t.Fatalf("expected error message to require council tally first, got %q", msg)
}
}
func TestOrchCouncilReportRejectsInvalidShow(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
runID := "council_blog_report_012"
seedCouncilReportRun(t, dbPath, runID)
stdout, _, exitCode := executeOrchCommand(
"--db", dbPath,
"--json",
"council", "report",
"--run", runID,
"--show", "consensus,invalid",
)
if exitCode != 30 {
t.Fatalf("expected invalid_input exit code 30, got %d\nstdout:\n%s", exitCode, stdout)
}
assertErrorJSON(t, stdout, "invalid_input")
msg := orchErrorMessage(t, stdout)
for _, expected := range []string{"consensus", "majority", "minority", "all"} {
if !strings.Contains(msg, expected) {
t.Fatalf("expected invalid --show message to mention %q, got %q", expected, msg)
}
}
}
func TestOrchCouncilReportDefaultsToConsensusForOnlyUnanimousRun(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
runID := "council_blog_report_011"
seedOnlyUnanimousCouncilReportRun(t, dbPath, runID)
reportOut := runOrchCommand(
t,
"--db", dbPath,
"--json",
"council", "report",
"--run", runID,
)
var reportResp map[string]any
mustDecodeJSON(t, reportOut, &reportResp)
if ok, _ := reportResp["ok"].(bool); !ok {
t.Fatalf("expected ok=true, got %#v", reportResp)
}
if got := nestedString(t, reportResp, "data", "run_id"); got != runID {
t.Fatalf("expected run id %q, got %q", runID, got)
}
show := nestedArray(t, reportResp, "data", "show")
if len(show) != 1 || show[0] != "consensus" {
t.Fatalf("expected unanimous-only default show bucket [consensus], got %#v", show)
}
summary, ok := nestedValue(t, reportResp, "data", "summary").(map[string]any)
if !ok {
t.Fatalf("expected summary object, got %#v", nestedValue(t, reportResp, "data", "summary"))
}
if got, _ := summary["consensus"].(float64); got != 1 {
t.Fatalf("expected one consensus group, got %#v", summary["consensus"])
}
if got, _ := summary["majority"].(float64); got != 1 {
t.Fatalf("expected one majority group, got %#v", summary["majority"])
}
if got, _ := summary["minority"].(float64); got != 1 {
t.Fatalf("expected one minority group, got %#v", summary["minority"])
}
groups := nestedArray(t, reportResp, "data", "grouped_recommendations")
if len(groups) != 1 {
t.Fatalf("expected one reported recommendation group, got %#v", groups)
}
group, ok := groups[0].(map[string]any)
if !ok {
t.Fatalf("expected grouped recommendation object, got %#v", groups[0])
}
if got, _ := group["bucket"].(string); got != "consensus" {
t.Fatalf("expected only reported bucket to be consensus, got %#v", group["bucket"])
}
artifacts := nestedArray(t, reportResp, "data", "report_artifacts")
if len(artifacts) != 1 {
t.Fatalf("expected one report artifact, got %#v", artifacts)
}
artifact, ok := artifacts[0].(map[string]any)
if !ok {
t.Fatalf("expected report artifact object, got %#v", artifacts[0])
}
reportPath, _ := artifact["path"].(string)
if reportPath == "" {
t.Fatalf("expected markdown artifact path, got %#v", artifact["path"])
}
if _, err := os.Stat(reportPath); err != nil {
t.Fatalf("expected markdown artifact to exist at %q: %v", reportPath, err)
}
}
func orchErrorMessage(t *testing.T, raw string) string {
t.Helper()
var payload map[string]any
mustDecodeJSON(t, raw, &payload)
errorValue, ok := payload["error"].(map[string]any)
if !ok {
t.Fatalf("expected error object, got %#v", payload["error"])
}
msg, ok := errorValue["message"].(string)
if !ok {
t.Fatalf("expected error message string, got %#v", errorValue["message"])
}
return msg
}
func seedOnlyUnanimousCouncilReportRun(t *testing.T, dbPath, runID string) {
t.Helper()
runOrchCommand(
t,
"--db", dbPath,
"--json",
"council", "start",
"--run", runID,
"--target", "Review the council reporting flow.",
"--only-unanimous",
)
completeCouncilReviewer(
t,
dbPath,
runID,
"architecture-reviewer",
`{"reviewer_role":"architecture-reviewer","findings":[{"title":"Split contracts","summary":"Transport contracts are mixed into UI code.","proposal":"Move API contract definitions into a dedicated module.","rationale":"This lowers coupling.","confidence":"high","tags":["architecture"],"target_refs":{"repo_path":"."}},{"title":"Share helpers","summary":"Council report rendering paths are repeated.","proposal":"Introduce shared council coordinator helpers for report rendering.","rationale":"This keeps report assembly consistent.","confidence":"medium","tags":["reporting"],"target_refs":{"repo_path":"."}}]}`,
)
completeCouncilReviewer(
t,
dbPath,
runID,
"implementation-reviewer",
`{"reviewer_role":"implementation-reviewer","findings":[{"title":"Extract contracts","summary":"Shared transport shapes are duplicated.","proposal":"Move API contract definitions into dedicated module","rationale":"This reduces duplication.","confidence":"high","tags":["maintainability"],"target_refs":{"repo_path":"."}},{"title":"Reuse report helpers","summary":"Formatting logic should stay shared.","proposal":"Introduce shared council coordinator helpers for report rendering","rationale":"This avoids formatter drift.","confidence":"medium","tags":["reporting"],"target_refs":{"repo_path":"."}}]}`,
)
completeCouncilReviewer(
t,
dbPath,
runID,
"risk-reviewer",
`{"reviewer_role":"risk-reviewer","findings":[{"title":"Lock contracts","summary":"Contract drift becomes risky over time.","proposal":"Move API contract definitions into a dedicated module.","rationale":"This reduces integration regressions.","confidence":"high","tags":["risk"],"target_refs":{"repo_path":"."}},{"title":"Cover JSON output","summary":"The council report response should stay stable.","proposal":"Add regression tests for council report JSON output.","rationale":"This catches contract regressions earlier.","confidence":"high","tags":["testing"],"target_refs":{"repo_path":"."}}]}`,
)
runOrchCommand(
t,
"--db", dbPath,
"--json",
"council", "tally",
"--run", runID,
"--similarity", "normal",
)
}
@@ -0,0 +1,88 @@
package orch
import (
"fmt"
"ai-workflow-skill/packages/coord-core/protocol"
"ai-workflow-skill/packages/coord-core/store"
"github.com/spf13/cobra"
)
type councilStartOptions struct {
runID string
target string
targetFile string
repoPath string
targetTaskID string
targetType string
mode string
outputMode string
onlyUnanimous bool
}
func newCouncilStartCmd(root *rootOptions) *cobra.Command {
opts := &councilStartOptions{}
cmd := &cobra.Command{
Use: "start",
Short: "Create and dispatch a three-reviewer council run",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
sqlDB, err := openOrchDB(ctx, root.dbPath)
if err != nil {
return err
}
defer sqlDB.Close()
result, err := store.NewOrchStore(sqlDB).StartCouncil(ctx, store.CouncilStartInput{
RunID: opts.runID,
Target: opts.target,
TargetFile: opts.targetFile,
RepoPath: opts.repoPath,
TargetTaskID: opts.targetTaskID,
TargetType: opts.targetType,
Mode: opts.mode,
OutputMode: opts.outputMode,
OnlyUnanimous: opts.onlyUnanimous,
})
if err != nil {
return err
}
resp := protocol.Success{
OK: true,
Command: "council start",
Data: map[string]any{
"run_id": result.Run.RunID,
"mode": result.Run.Mode,
"target_type": result.Run.TargetType,
"output": result.Run.OutputMode,
"only_unanimous": result.Run.OnlyUnanimous,
"target": result.Input,
"reviewers": result.Reviewers,
},
}
if root.json {
return protocol.WriteJSON(cmd.OutOrStdout(), resp)
}
_, err = fmt.Fprintf(cmd.OutOrStdout(), "started council run %s with %d reviewers\n", result.Run.RunID, len(result.Reviewers))
return err
},
}
cmd.Flags().StringVar(&opts.runID, "run", "", "Run ID")
cmd.Flags().StringVar(&opts.target, "target", "", "Inline target prompt")
cmd.Flags().StringVar(&opts.targetFile, "target-file", "", "Optional target context file")
cmd.Flags().StringVar(&opts.repoPath, "repo-path", "", "Optional repository path for review context")
cmd.Flags().StringVar(&opts.targetTaskID, "task-id", "", "Optional related task ID")
cmd.Flags().StringVar(&opts.targetType, "target-type", "mixed", "Target type: text, repo, or mixed")
cmd.Flags().StringVar(&opts.mode, "mode", "brainstorm", "Council mode: brainstorm or review")
cmd.Flags().StringVar(&opts.outputMode, "output", "both", "Output mode: markdown, json, or both")
cmd.Flags().BoolVar(&opts.onlyUnanimous, "only-unanimous", false, "Show only unanimous recommendations in downstream report defaults")
_ = cmd.MarkFlagRequired("run")
return cmd
}
@@ -0,0 +1,64 @@
package orch
import (
"fmt"
"ai-workflow-skill/packages/coord-core/protocol"
"ai-workflow-skill/packages/coord-core/store"
"github.com/spf13/cobra"
)
type councilTallyOptions struct {
runID string
similarity string
}
func newCouncilTallyCmd(root *rootOptions) *cobra.Command {
opts := &councilTallyOptions{}
cmd := &cobra.Command{
Use: "tally",
Short: "Group reviewer findings and compute council support counts",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
sqlDB, err := openOrchDB(ctx, root.dbPath)
if err != nil {
return err
}
defer sqlDB.Close()
result, err := store.NewOrchStore(sqlDB).TallyCouncil(ctx, store.CouncilTallyInput{
RunID: opts.runID,
Similarity: opts.similarity,
})
if err != nil {
return err
}
resp := protocol.Success{
OK: true,
Command: "council tally",
Data: map[string]any{
"run_id": result.RunID,
"similarity": result.Similarity,
"counts": result.Counts,
"grouped_recommendations": result.GroupedRecommendations,
},
}
if root.json {
return protocol.WriteJSON(cmd.OutOrStdout(), resp)
}
_, err = fmt.Fprintf(cmd.OutOrStdout(), "tallied council run %s into %d groups\n", result.RunID, len(result.GroupedRecommendations))
return err
},
}
cmd.Flags().StringVar(&opts.runID, "run", "", "Council run ID")
cmd.Flags().StringVar(&opts.similarity, "similarity", "normal", "Grouping mode: strict or normal")
_ = cmd.MarkFlagRequired("run")
return cmd
}
@@ -0,0 +1,69 @@
package orch
import (
"fmt"
"time"
"ai-workflow-skill/packages/coord-core/protocol"
"ai-workflow-skill/packages/coord-core/store"
"github.com/spf13/cobra"
)
type councilWaitOptions struct {
runID string
timeoutSeconds int
}
func newCouncilWaitCmd(root *rootOptions) *cobra.Command {
opts := &councilWaitOptions{}
cmd := &cobra.Command{
Use: "wait",
Short: "Block until all council reviewers complete or timeout is reached",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
sqlDB, err := openOrchDB(ctx, root.dbPath)
if err != nil {
return err
}
defer sqlDB.Close()
result, err := store.NewOrchStore(sqlDB).WaitForCouncil(ctx, store.CouncilWaitInput{
RunID: opts.runID,
Timeout: time.Duration(opts.timeoutSeconds) * time.Second,
})
if err != nil {
return err
}
resp := protocol.Success{
OK: true,
Command: "council wait",
Data: map[string]any{
"run_id": result.RunID,
"woke": result.Woke,
"all_complete": result.AllComplete,
"reviewers": result.ReviewerStatuses,
},
}
if root.json {
return protocol.WriteJSON(cmd.OutOrStdout(), resp)
}
if !result.Woke {
_, err = fmt.Fprintf(cmd.OutOrStdout(), "council wait timed out for run %s\n", result.RunID)
return err
}
_, err = fmt.Fprintf(cmd.OutOrStdout(), "all council reviewers completed for run %s\n", result.RunID)
return err
},
}
cmd.Flags().StringVar(&opts.runID, "run", "", "Council run ID")
cmd.Flags().IntVar(&opts.timeoutSeconds, "timeout-seconds", 0, "Maximum time to wait before timing out")
_ = cmd.MarkFlagRequired("run")
return cmd
}
@@ -0,0 +1,22 @@
package orch
import (
"context"
"database/sql"
"ai-workflow-skill/packages/coord-core/db"
)
func openOrchDB(ctx context.Context, dbPath string) (*sql.DB, error) {
sqlDB, err := db.Open(ctx, dbPath)
if err != nil {
return nil, err
}
if err := db.ApplyMigrations(ctx, sqlDB); err != nil {
_ = sqlDB.Close()
return nil, err
}
return sqlDB, nil
}
@@ -0,0 +1,76 @@
package orch
import (
"fmt"
"ai-workflow-skill/packages/coord-core/protocol"
"ai-workflow-skill/packages/coord-core/store"
"github.com/spf13/cobra"
)
type depAddOptions struct {
runID string
taskID string
dependsOn string
}
func newDepCmd(root *rootOptions) *cobra.Command {
cmd := &cobra.Command{
Use: "dep",
Short: "Task dependency commands",
}
cmd.AddCommand(newDepAddCmd(root))
return cmd
}
func newDepAddCmd(root *rootOptions) *cobra.Command {
opts := &depAddOptions{}
cmd := &cobra.Command{
Use: "add",
Short: "Add a dependency edge to a task",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
sqlDB, err := openOrchDB(ctx, root.dbPath)
if err != nil {
return err
}
defer sqlDB.Close()
dep, err := store.NewOrchStore(sqlDB).AddDependency(ctx, store.AddDependencyInput{
RunID: opts.runID,
TaskID: opts.taskID,
DependsOnTaskID: opts.dependsOn,
})
if err != nil {
return err
}
resp := protocol.Success{
OK: true,
Command: "dep add",
Data: map[string]any{
"dependency": dep,
},
}
if root.json {
return protocol.WriteJSON(cmd.OutOrStdout(), resp)
}
_, err = fmt.Fprintf(cmd.OutOrStdout(), "added dependency %s -> %s\n", dep.TaskID, dep.DependsOnTaskID)
return err
},
}
cmd.Flags().StringVar(&opts.runID, "run", "", "Run ID")
cmd.Flags().StringVar(&opts.taskID, "task", "", "Task ID")
cmd.Flags().StringVar(&opts.dependsOn, "depends-on", "", "Dependency task ID")
_ = cmd.MarkFlagRequired("run")
_ = cmd.MarkFlagRequired("task")
_ = cmd.MarkFlagRequired("depends-on")
return cmd
}
@@ -0,0 +1,93 @@
package orch
import (
"fmt"
"ai-workflow-skill/packages/coord-core/protocol"
"ai-workflow-skill/packages/coord-core/store"
"github.com/spf13/cobra"
)
type dispatchOptions struct {
runID string
taskID string
toAgent string
body string
bodyFile string
baseRef string
repoPath string
workspaceRoot string
strictWorktree bool
}
func newDispatchCmd(root *rootOptions) *cobra.Command {
opts := &dispatchOptions{}
cmd := &cobra.Command{
Use: "dispatch",
Short: "Dispatch a ready task to a worker through inbox",
RunE: func(cmd *cobra.Command, args []string) error {
body, err := resolveBodyValue(opts.body, opts.bodyFile)
if err != nil {
return err
}
ctx := cmd.Context()
sqlDB, err := openOrchDB(ctx, root.dbPath)
if err != nil {
return err
}
defer sqlDB.Close()
result, err := store.NewOrchStore(sqlDB).DispatchTask(ctx, store.DispatchInput{
RunID: opts.runID,
TaskID: opts.taskID,
ToAgent: opts.toAgent,
Body: body,
BaseRef: opts.baseRef,
PrepareWorkspace: newDispatchWorkspacePreparer(cmd, *opts),
})
if err != nil {
return err
}
resp := protocol.Success{
OK: true,
Command: "dispatch",
Data: map[string]any{
"task": result.Task,
"attempt": result.Attempt,
"thread": result.Thread,
"message": result.Message,
},
}
if root.json {
return protocol.WriteJSON(cmd.OutOrStdout(), resp)
}
_, err = fmt.Fprintf(
cmd.OutOrStdout(),
"dispatched task %s to %s as thread %s\n",
result.Task.TaskID,
result.Attempt.AssignedTo,
result.Attempt.ThreadID,
)
return err
},
}
cmd.Flags().StringVar(&opts.runID, "run", "", "Run ID")
cmd.Flags().StringVar(&opts.taskID, "task", "", "Task ID")
cmd.Flags().StringVar(&opts.toAgent, "to", "", "Worker agent override")
cmd.Flags().StringVar(&opts.body, "body", "", "Task message body")
cmd.Flags().StringVar(&opts.bodyFile, "body-file", "", "Read task message body from file")
cmd.Flags().StringVar(&opts.baseRef, "base-ref", "", "Optional base ref to record on the attempt")
cmd.Flags().StringVar(&opts.repoPath, "repo-path", "", "Source repository path for worktree dispatch")
cmd.Flags().StringVar(&opts.workspaceRoot, "workspace-root", "", "Workspace root for worktree dispatch")
cmd.Flags().BoolVar(&opts.strictWorktree, "strict-worktree", false, "Require strict worktree setup")
_ = cmd.MarkFlagRequired("run")
_ = cmd.MarkFlagRequired("task")
return cmd
}
@@ -0,0 +1,113 @@
package orch
import (
"errors"
"fmt"
"io"
"strings"
"ai-workflow-skill/packages/coord-core/protocol"
"ai-workflow-skill/packages/coord-core/store"
)
func Execute(args []string, stdout, stderr io.Writer) int {
cmd := NewRootCmd()
cmd.SetOut(stdout)
cmd.SetErr(stderr)
cmd.SetArgs(args)
if err := cmd.Execute(); err != nil {
jsonOutput := hasJSONFlag(args)
renderError(stdout, stderr, jsonOutput, err)
return exitCodeForError(err)
}
return 0
}
func exitCodeForError(err error) int {
var cliErr *protocol.CLIError
if errors.As(err, &cliErr) {
return cliErr.ExitCode
}
switch {
case isUsageError(err):
return 30
case errors.Is(err, store.ErrLeaseConflict):
return 20
case errors.Is(err, store.ErrRunNotFound), errors.Is(err, store.ErrTaskNotFound), errors.Is(err, store.ErrThreadNotFound), errors.Is(err, store.ErrMessageNotFound):
return 40
case errors.Is(err, store.ErrInvalidInput), errors.Is(err, store.ErrInvalidState), errors.Is(err, store.ErrNoActiveLease):
return 30
default:
return 50
}
}
func errorCodeForError(err error) string {
var cliErr *protocol.CLIError
if errors.As(err, &cliErr) {
return cliErr.Code
}
switch {
case isUsageError(err):
return "invalid_input"
case errors.Is(err, store.ErrLeaseConflict):
return "conflict"
case errors.Is(err, store.ErrRunNotFound), errors.Is(err, store.ErrTaskNotFound), errors.Is(err, store.ErrThreadNotFound), errors.Is(err, store.ErrMessageNotFound):
return "not_found"
case errors.Is(err, store.ErrInvalidInput):
return "invalid_input"
case errors.Is(err, store.ErrInvalidState), errors.Is(err, store.ErrNoActiveLease):
return "invalid_state"
default:
return "internal_error"
}
}
func renderError(stdout, stderr io.Writer, jsonOutput bool, err error) {
message := errorMessage(err)
if jsonOutput {
_ = protocol.WriteJSON(stdout, protocol.Error{
OK: false,
Error: protocol.ErrorPayload{
Code: errorCodeForError(err),
Message: message,
},
})
return
}
_, _ = fmt.Fprintln(stderr, message)
}
func errorMessage(err error) string {
var cliErr *protocol.CLIError
if errors.As(err, &cliErr) {
return cliErr.Message
}
return err.Error()
}
func hasJSONFlag(args []string) bool {
for _, arg := range args {
if arg == "--json" {
return true
}
if strings.HasPrefix(arg, "--json=") {
return !strings.HasSuffix(arg, "=false")
}
}
return false
}
func isUsageError(err error) bool {
message := err.Error()
return strings.HasPrefix(message, "required flag(s)") ||
strings.HasPrefix(message, "unknown flag:") ||
strings.HasPrefix(message, "unknown command ") ||
strings.Contains(message, " accepts ") ||
strings.Contains(message, "invalid argument ")
}
@@ -0,0 +1,53 @@
package orch
import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)
func initGitRepo(t *testing.T) string {
t.Helper()
repoPath := filepath.Join(t.TempDir(), "repo")
if err := os.MkdirAll(repoPath, 0o755); err != nil {
t.Fatalf("mkdir repo path: %v", err)
}
runGitCommand(t, repoPath, "init")
runGitCommand(t, repoPath, "config", "user.email", "test@example.com")
runGitCommand(t, repoPath, "config", "user.name", "Test User")
readmePath := filepath.Join(repoPath, "README.md")
if err := os.WriteFile(readmePath, []byte("hello\n"), 0o644); err != nil {
t.Fatalf("write README.md: %v", err)
}
runGitCommand(t, repoPath, "add", "README.md")
runGitCommand(t, repoPath, "commit", "-m", "init")
return repoPath
}
func gitHeadCommit(t *testing.T, repoPath string) string {
t.Helper()
cmd := exec.Command("git", "-C", repoPath, "rev-parse", "--verify", "HEAD^{commit}")
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("git rev-parse HEAD in %s: %v\n%s", repoPath, err, output)
}
return strings.TrimSpace(string(output))
}
func runGitCommand(t *testing.T, repoPath string, args ...string) {
t.Helper()
cmd := exec.Command("git", append([]string{"-C", repoPath}, args...)...)
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("git %v in %s: %v\n%s", args, repoPath, err, output)
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,69 @@
package orch
import (
"fmt"
"ai-workflow-skill/packages/coord-core/protocol"
"ai-workflow-skill/packages/coord-core/store"
"github.com/spf13/cobra"
)
type readyOptions struct {
runID string
limit int
}
func newReadyCmd(root *rootOptions) *cobra.Command {
opts := &readyOptions{}
cmd := &cobra.Command{
Use: "ready",
Short: "List tasks that are ready for dispatch",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
sqlDB, err := openOrchDB(ctx, root.dbPath)
if err != nil {
return err
}
defer sqlDB.Close()
tasks, err := store.NewOrchStore(sqlDB).ListReadyTasks(ctx, store.ListReadyInput{
RunID: opts.runID,
Limit: opts.limit,
})
if err != nil {
return err
}
resp := protocol.Success{
OK: true,
Command: "ready",
Data: map[string]any{
"tasks": tasks,
},
}
if root.json {
return protocol.WriteJSON(cmd.OutOrStdout(), resp)
}
if len(tasks) == 0 {
_, err = fmt.Fprintln(cmd.OutOrStdout(), "no ready tasks")
return err
}
for _, task := range tasks {
if _, err := fmt.Fprintf(cmd.OutOrStdout(), "%s\t%s\t%s\n", task.TaskID, task.Priority, task.Title); err != nil {
return err
}
}
return nil
},
}
cmd.Flags().StringVar(&opts.runID, "run", "", "Run ID")
cmd.Flags().IntVar(&opts.limit, "limit", 20, "Maximum number of tasks to list")
_ = cmd.MarkFlagRequired("run")
return cmd
}
@@ -0,0 +1,80 @@
package orch
import (
"fmt"
"ai-workflow-skill/packages/coord-core/protocol"
"ai-workflow-skill/packages/coord-core/store"
"github.com/spf13/cobra"
)
type reassignOptions struct {
runID string
taskID string
toAgent string
reason string
}
func newReassignCmd(root *rootOptions) *cobra.Command {
opts := &reassignOptions{}
cmd := &cobra.Command{
Use: "reassign",
Short: "Reassign a blocked or failed task to another worker",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
sqlDB, err := openOrchDB(ctx, root.dbPath)
if err != nil {
return err
}
defer sqlDB.Close()
s := store.NewOrchStore(sqlDB)
task, attempt, err := s.GetTaskWithLatestAttempt(ctx, opts.runID, opts.taskID)
if err != nil {
return err
}
result, err := s.ReassignTask(ctx, store.ReassignInput{
RunID: opts.runID,
TaskID: opts.taskID,
ToAgent: opts.toAgent,
Reason: opts.reason,
PrepareWorkspace: newAttemptReuseWorkspacePreparer(cmd, task, attempt),
})
if err != nil {
return err
}
resp := protocol.Success{
OK: true,
Command: "reassign",
Data: map[string]any{
"task": result.Task,
"attempt": result.Attempt,
"thread": result.Thread,
"message": result.Message,
"previous_attempt": result.PreviousAttempt,
},
}
if root.json {
return protocol.WriteJSON(cmd.OutOrStdout(), resp)
}
_, err = fmt.Fprintf(cmd.OutOrStdout(), "reassigned task %s to %s as attempt %d\n", result.Task.TaskID, result.Attempt.AssignedTo, result.Attempt.AttemptNo)
return err
},
}
cmd.Flags().StringVar(&opts.runID, "run", "", "Run ID")
cmd.Flags().StringVar(&opts.taskID, "task", "", "Task ID")
cmd.Flags().StringVar(&opts.toAgent, "to", "", "Destination worker agent")
cmd.Flags().StringVar(&opts.reason, "reason", "", "Reason for reassignment")
_ = cmd.MarkFlagRequired("run")
_ = cmd.MarkFlagRequired("task")
_ = cmd.MarkFlagRequired("to")
return cmd
}
@@ -0,0 +1,58 @@
package orch
import (
"fmt"
"ai-workflow-skill/packages/coord-core/protocol"
"ai-workflow-skill/packages/coord-core/store"
"github.com/spf13/cobra"
)
type reconcileOptions struct {
runID string
}
func newReconcileCmd(root *rootOptions) *cobra.Command {
opts := &reconcileOptions{}
cmd := &cobra.Command{
Use: "reconcile",
Short: "Reconcile inbox thread state back into orch task state",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
sqlDB, err := openOrchDB(ctx, root.dbPath)
if err != nil {
return err
}
defer sqlDB.Close()
result, err := store.NewOrchStore(sqlDB).ReconcileRun(ctx, opts.runID)
if err != nil {
return err
}
resp := protocol.Success{
OK: true,
Command: "reconcile",
Data: map[string]any{
"run": result.Run,
"task_counts": result.TaskCounts,
"updated_tasks": result.UpdatedTasks,
},
}
if root.json {
return protocol.WriteJSON(cmd.OutOrStdout(), resp)
}
_, err = fmt.Fprintf(cmd.OutOrStdout(), "reconciled run %s (%d updated tasks)\n", result.Run.RunID, len(result.UpdatedTasks))
return err
},
}
cmd.Flags().StringVar(&opts.runID, "run", "", "Run ID")
_ = cmd.MarkFlagRequired("run")
return cmd
}
@@ -0,0 +1,85 @@
package orch
import (
"fmt"
"ai-workflow-skill/packages/coord-core/protocol"
"ai-workflow-skill/packages/coord-core/store"
"github.com/spf13/cobra"
)
type retryOptions struct {
runID string
taskID string
toAgent string
body string
bodyFile string
}
func newRetryCmd(root *rootOptions) *cobra.Command {
opts := &retryOptions{}
cmd := &cobra.Command{
Use: "retry",
Short: "Retry a failed task by creating a new attempt",
RunE: func(cmd *cobra.Command, args []string) error {
body, err := resolveBodyValue(opts.body, opts.bodyFile)
if err != nil {
return err
}
ctx := cmd.Context()
sqlDB, err := openOrchDB(ctx, root.dbPath)
if err != nil {
return err
}
defer sqlDB.Close()
s := store.NewOrchStore(sqlDB)
task, attempt, err := s.GetTaskWithLatestAttempt(ctx, opts.runID, opts.taskID)
if err != nil {
return err
}
result, err := s.RetryTask(ctx, store.RetryInput{
RunID: opts.runID,
TaskID: opts.taskID,
ToAgent: opts.toAgent,
Body: body,
PrepareWorkspace: newAttemptReuseWorkspacePreparer(cmd, task, attempt),
})
if err != nil {
return err
}
resp := protocol.Success{
OK: true,
Command: "retry",
Data: map[string]any{
"task": result.Task,
"attempt": result.Attempt,
"thread": result.Thread,
"message": result.Message,
"previous_attempt": result.PreviousAttempt,
},
}
if root.json {
return protocol.WriteJSON(cmd.OutOrStdout(), resp)
}
_, err = fmt.Fprintf(cmd.OutOrStdout(), "retried task %s as attempt %d\n", result.Task.TaskID, result.Attempt.AttemptNo)
return err
},
}
cmd.Flags().StringVar(&opts.runID, "run", "", "Run ID")
cmd.Flags().StringVar(&opts.taskID, "task", "", "Task ID")
cmd.Flags().StringVar(&opts.toAgent, "to", "", "Optional worker agent override")
cmd.Flags().StringVar(&opts.body, "body", "", "Retry instruction body")
cmd.Flags().StringVar(&opts.bodyFile, "body-file", "", "Read retry instruction body from file")
_ = cmd.MarkFlagRequired("run")
_ = cmd.MarkFlagRequired("task")
return cmd
}
@@ -0,0 +1,42 @@
package orch
import (
"github.com/spf13/cobra"
)
type rootOptions struct {
dbPath string
json bool
}
func NewRootCmd() *cobra.Command {
opts := &rootOptions{}
cmd := &cobra.Command{
Use: "orch",
Short: "Leader-facing scheduler and control plane",
SilenceErrors: true,
SilenceUsage: true,
}
cmd.PersistentFlags().StringVar(&opts.dbPath, "db", ".agents/coord.db", "SQLite database path")
cmd.PersistentFlags().BoolVar(&opts.json, "json", false, "Emit machine-readable JSON")
cmd.AddCommand(newRunCmd(opts))
cmd.AddCommand(newTaskCmd(opts))
cmd.AddCommand(newDepCmd(opts))
cmd.AddCommand(newReadyCmd(opts))
cmd.AddCommand(newDispatchCmd(opts))
cmd.AddCommand(newReconcileCmd(opts))
cmd.AddCommand(newWaitCmd(opts))
cmd.AddCommand(newRetryCmd(opts))
cmd.AddCommand(newReassignCmd(opts))
cmd.AddCommand(newCancelCmd(opts))
cmd.AddCommand(newCleanupCmd(opts))
cmd.AddCommand(newCouncilCmd(opts))
cmd.AddCommand(newBlockedCmd(opts))
cmd.AddCommand(newAnswerCmd(opts))
cmd.AddCommand(newStatusCmd(opts))
return cmd
}
@@ -0,0 +1,124 @@
package orch
import (
"fmt"
"ai-workflow-skill/packages/coord-core/protocol"
"ai-workflow-skill/packages/coord-core/store"
"github.com/spf13/cobra"
)
type runInitOptions struct {
runID string
goal string
summary string
}
type runShowOptions struct {
runID string
}
func newRunCmd(root *rootOptions) *cobra.Command {
cmd := &cobra.Command{
Use: "run",
Short: "Run management commands",
}
cmd.AddCommand(newRunInitCmd(root))
cmd.AddCommand(newRunShowCmd(root))
return cmd
}
func newRunInitCmd(root *rootOptions) *cobra.Command {
opts := &runInitOptions{}
cmd := &cobra.Command{
Use: "init",
Short: "Create a new orchestration run",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
sqlDB, err := openOrchDB(ctx, root.dbPath)
if err != nil {
return err
}
defer sqlDB.Close()
run, err := store.NewOrchStore(sqlDB).CreateRun(ctx, store.CreateRunInput{
RunID: opts.runID,
Goal: opts.goal,
Summary: opts.summary,
})
if err != nil {
return err
}
resp := protocol.Success{
OK: true,
Command: "run init",
Data: map[string]any{
"run": run,
},
}
if root.json {
return protocol.WriteJSON(cmd.OutOrStdout(), resp)
}
_, err = fmt.Fprintf(cmd.OutOrStdout(), "created run %s\n", run.RunID)
return err
},
}
cmd.Flags().StringVar(&opts.runID, "run", "", "Run ID")
cmd.Flags().StringVar(&opts.goal, "goal", "", "Run goal")
cmd.Flags().StringVar(&opts.summary, "summary", "", "Run summary")
_ = cmd.MarkFlagRequired("run")
_ = cmd.MarkFlagRequired("goal")
return cmd
}
func newRunShowCmd(root *rootOptions) *cobra.Command {
opts := &runShowOptions{}
cmd := &cobra.Command{
Use: "show",
Short: "Show run metadata and aggregate state",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
sqlDB, err := openOrchDB(ctx, root.dbPath)
if err != nil {
return err
}
defer sqlDB.Close()
overview, err := store.NewOrchStore(sqlDB).GetRunOverview(ctx, opts.runID)
if err != nil {
return err
}
resp := protocol.Success{
OK: true,
Command: "run show",
Data: map[string]any{
"run": overview.Run,
"task_counts": overview.TaskCounts,
},
}
if root.json {
return protocol.WriteJSON(cmd.OutOrStdout(), resp)
}
_, err = fmt.Fprintf(cmd.OutOrStdout(), "run %s status %s\n", overview.Run.RunID, overview.Run.Status)
return err
},
}
cmd.Flags().StringVar(&opts.runID, "run", "", "Run ID")
_ = cmd.MarkFlagRequired("run")
return cmd
}
@@ -0,0 +1,65 @@
package orch
import (
"fmt"
"ai-workflow-skill/packages/coord-core/protocol"
"ai-workflow-skill/packages/coord-core/store"
"github.com/spf13/cobra"
)
type statusOptions struct {
runID string
}
func newStatusCmd(root *rootOptions) *cobra.Command {
opts := &statusOptions{}
cmd := &cobra.Command{
Use: "status",
Short: "Show task state summary for the run",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
sqlDB, err := openOrchDB(ctx, root.dbPath)
if err != nil {
return err
}
defer sqlDB.Close()
overview, err := store.NewOrchStore(sqlDB).GetRunOverview(ctx, opts.runID)
if err != nil {
return err
}
resp := protocol.Success{
OK: true,
Command: "status",
Data: map[string]any{
"run": overview.Run,
"task_counts": overview.TaskCounts,
"tasks": overview.Tasks,
},
}
if root.json {
return protocol.WriteJSON(cmd.OutOrStdout(), resp)
}
if _, err := fmt.Fprintf(cmd.OutOrStdout(), "run %s status %s\n", overview.Run.RunID, overview.Run.Status); err != nil {
return err
}
for _, task := range overview.Tasks {
if _, err := fmt.Fprintf(cmd.OutOrStdout(), "%s\t%s\t%s\n", task.TaskID, task.Status, task.Title); err != nil {
return err
}
}
return nil
},
}
cmd.Flags().StringVar(&opts.runID, "run", "", "Run ID")
_ = cmd.MarkFlagRequired("run")
return cmd
}
@@ -0,0 +1,88 @@
package orch
import (
"fmt"
"ai-workflow-skill/packages/coord-core/protocol"
"ai-workflow-skill/packages/coord-core/store"
"github.com/spf13/cobra"
)
type taskAddOptions struct {
runID string
taskID string
title string
summary string
defaultTo string
acceptanceJSON string
priority string
}
func newTaskCmd(root *rootOptions) *cobra.Command {
cmd := &cobra.Command{
Use: "task",
Short: "Task management commands",
}
cmd.AddCommand(newTaskAddCmd(root))
return cmd
}
func newTaskAddCmd(root *rootOptions) *cobra.Command {
opts := &taskAddOptions{}
cmd := &cobra.Command{
Use: "add",
Short: "Add a task to a run",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
sqlDB, err := openOrchDB(ctx, root.dbPath)
if err != nil {
return err
}
defer sqlDB.Close()
task, err := store.NewOrchStore(sqlDB).AddTask(ctx, store.AddTaskInput{
RunID: opts.runID,
TaskID: opts.taskID,
Title: opts.title,
Summary: opts.summary,
DefaultTo: opts.defaultTo,
AcceptanceJSON: opts.acceptanceJSON,
Priority: opts.priority,
})
if err != nil {
return err
}
resp := protocol.Success{
OK: true,
Command: "task add",
Data: map[string]any{
"task": task,
},
}
if root.json {
return protocol.WriteJSON(cmd.OutOrStdout(), resp)
}
_, err = fmt.Fprintf(cmd.OutOrStdout(), "added task %s to run %s\n", task.TaskID, task.RunID)
return err
},
}
cmd.Flags().StringVar(&opts.runID, "run", "", "Run ID")
cmd.Flags().StringVar(&opts.taskID, "task", "", "Task ID")
cmd.Flags().StringVar(&opts.title, "title", "", "Task title")
cmd.Flags().StringVar(&opts.summary, "summary", "", "Task summary")
cmd.Flags().StringVar(&opts.defaultTo, "default-to", "", "Default worker agent")
cmd.Flags().StringVar(&opts.acceptanceJSON, "acceptance-json", "", "Acceptance criteria JSON")
cmd.Flags().StringVar(&opts.priority, "priority", "normal", "Task priority")
_ = cmd.MarkFlagRequired("run")
_ = cmd.MarkFlagRequired("task")
_ = cmd.MarkFlagRequired("title")
return cmd
}
@@ -0,0 +1,131 @@
package orch
import (
"bytes"
"encoding/json"
"os/exec"
"path/filepath"
"runtime"
"testing"
)
func runOrchCommand(t *testing.T, args ...string) string {
t.Helper()
stdout, stderr, exitCode := executeOrchCommand(args...)
if exitCode != 0 {
t.Fatalf("execute orch command %v: exit=%d\nstderr:\n%s\nstdout:\n%s", args, exitCode, stderr, stdout)
}
return stdout
}
func executeOrchCommand(args ...string) (string, string, int) {
var stdout bytes.Buffer
var stderr bytes.Buffer
exitCode := Execute(args, &stdout, &stderr)
return stdout.String(), stderr.String(), exitCode
}
func runInboxCommand(t *testing.T, args ...string) string {
t.Helper()
stdout, stderr, exitCode := executeInboxCommand(args...)
if exitCode != 0 {
t.Fatalf("execute inbox command %v: exit=%d\nstderr:\n%s\nstdout:\n%s", args, exitCode, stderr, stdout)
}
return stdout
}
func executeInboxCommand(args ...string) (string, string, int) {
cmd := exec.Command("go", append([]string{"run", inboxCommandPath()}, args...)...)
var stdout bytes.Buffer
var stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if err == nil {
return stdout.String(), stderr.String(), 0
}
if exitError, ok := err.(*exec.ExitError); ok {
return stdout.String(), stderr.String(), exitError.ExitCode()
}
return stdout.String(), stderr.String(), 1
}
func inboxCommandPath() string {
_, file, _, ok := runtime.Caller(0)
if !ok {
panic("unable to determine orch test helper path")
}
return filepath.Join(filepath.Dir(file), "..", "..", "..", "..", "..", "cmd", "inbox")
}
func mustDecodeJSON(t *testing.T, raw string, target any) {
t.Helper()
if err := json.Unmarshal([]byte(raw), target); err != nil {
t.Fatalf("decode json %q: %v", raw, err)
}
}
func nestedString(t *testing.T, value map[string]any, keys ...string) string {
t.Helper()
current := nestedValue(t, value, keys...)
str, ok := current.(string)
if !ok {
t.Fatalf("expected string at %v, got %#v", keys, current)
}
return str
}
func nestedValue(t *testing.T, value map[string]any, keys ...string) any {
t.Helper()
var current any = value
for _, key := range keys {
obj, ok := current.(map[string]any)
if !ok {
t.Fatalf("expected object at %q in %v, got %#v", key, keys, current)
}
current, ok = obj[key]
if !ok {
t.Fatalf("missing key %q in %v", key, keys)
}
}
return current
}
func nestedArray(t *testing.T, value map[string]any, keys ...string) []any {
t.Helper()
current := nestedValue(t, value, keys...)
items, ok := current.([]any)
if !ok {
t.Fatalf("expected array at %v, got %#v", keys, current)
}
return items
}
func assertErrorJSON(t *testing.T, raw string, expectedCode string) {
t.Helper()
var payload map[string]any
mustDecodeJSON(t, raw, &payload)
if ok, _ := payload["ok"].(bool); ok {
t.Fatalf("expected ok=false error payload, got %#v", payload)
}
errorValue, ok := payload["error"].(map[string]any)
if !ok {
t.Fatalf("expected error object, got %#v", payload["error"])
}
if code, _ := errorValue["code"].(string); code != expectedCode {
t.Fatalf("expected error code %q, got %#v", expectedCode, errorValue["code"])
}
}
@@ -0,0 +1,97 @@
package orch
import (
"fmt"
"strings"
"time"
"ai-workflow-skill/packages/coord-core/protocol"
"ai-workflow-skill/packages/coord-core/store"
"github.com/spf13/cobra"
)
type waitOptions struct {
runID string
eventTypesRaw string
afterEventID int64
timeoutSeconds int
}
func newWaitCmd(root *rootOptions) *cobra.Command {
opts := &waitOptions{}
cmd := &cobra.Command{
Use: "wait",
Short: "Block until matching run-scoped task events become available",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
sqlDB, err := openOrchDB(ctx, root.dbPath)
if err != nil {
return err
}
defer sqlDB.Close()
result, err := store.NewOrchStore(sqlDB).WaitForEvents(ctx, store.WaitInput{
RunID: opts.runID,
EventTypes: splitCommaList(opts.eventTypesRaw),
AfterEventID: opts.afterEventID,
Timeout: time.Duration(opts.timeoutSeconds) * time.Second,
})
if err != nil {
return err
}
resp := protocol.Success{
OK: true,
Command: "wait",
Data: map[string]any{
"run_id": opts.runID,
"woke": result.Woke,
"next_event_id": result.NextEventID,
"events": result.Events,
},
}
if root.json {
return protocol.WriteJSON(cmd.OutOrStdout(), resp)
}
if !result.Woke {
_, err = fmt.Fprintf(cmd.OutOrStdout(), "wait timed out after event %d\n", result.NextEventID)
return err
}
for _, event := range result.Events {
if _, err := fmt.Fprintf(cmd.OutOrStdout(), "%d\t%s\t%s\t%s\n", event.EventID, event.Type, event.TaskID, event.Summary); err != nil {
return err
}
}
return nil
},
}
cmd.Flags().StringVar(&opts.runID, "run", "", "Run ID")
cmd.Flags().StringVar(&opts.eventTypesRaw, "for", "task_ready,task_blocked,task_done,task_failed", "Comma-separated event types to wait for")
cmd.Flags().Int64Var(&opts.afterEventID, "after-event", 0, "Only wait for events after this event ID")
cmd.Flags().IntVar(&opts.timeoutSeconds, "timeout-seconds", 0, "Maximum time to wait before timing out")
_ = cmd.MarkFlagRequired("run")
return cmd
}
func splitCommaList(value string) []string {
if strings.TrimSpace(value) == "" {
return nil
}
parts := strings.Split(value, ",")
result := make([]string, 0, len(parts))
for _, part := range parts {
part = strings.TrimSpace(part)
if part == "" {
continue
}
result = append(result, part)
}
return result
}
@@ -0,0 +1,503 @@
package orch
import (
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"ai-workflow-skill/packages/coord-core/protocol"
"ai-workflow-skill/packages/coord-core/store"
"github.com/spf13/cobra"
)
func newDispatchWorkspacePreparer(cmd *cobra.Command, opts dispatchOptions) store.DispatchWorkspacePreparer {
ctx := cmd.Context()
return func(task store.Task, attemptNo int) (store.DispatchWorkspace, func(), error) {
effectiveOpts, useWorktree := resolveDispatchWorktreeOptions(task, opts)
if !useWorktree {
return store.DispatchWorkspace{}, func() {}, nil
}
return provisionDispatchWorkspace(ctx, effectiveOpts, task, attemptNo)
}
}
func newAttemptReuseWorkspacePreparer(cmd *cobra.Command, task store.Task, attempt *store.TaskAttempt) store.DispatchWorkspacePreparer {
if attempt == nil || attempt.WorktreePath == "" {
return nil
}
workspaceRoot, ok := deriveWorkspaceRootFromAttempt(task.RunID, task.TaskID, attempt.WorktreePath)
if !ok {
return nil
}
baseRef := attempt.BaseRef
if strings.TrimSpace(baseRef) == "" {
baseRef = attempt.BaseCommit
}
opts := dispatchOptions{
repoPath: attempt.WorktreePath,
workspaceRoot: workspaceRoot,
strictWorktree: true,
baseRef: baseRef,
}
return newDispatchWorkspacePreparer(cmd, opts)
}
func dispatchUsesWorktree(opts dispatchOptions) bool {
return strings.TrimSpace(opts.workspaceRoot) != "" ||
opts.strictWorktree
}
func resolveDispatchWorktreeOptions(task store.Task, opts dispatchOptions) (dispatchOptions, bool) {
if dispatchUsesWorktree(opts) {
return opts, true
}
if !taskLooksLikeCodeWork(task) {
return opts, false
}
auto := opts
auto.strictWorktree = true
return auto, true
}
func taskLooksLikeCodeWork(task store.Task) bool {
if acceptanceJSONLooksCodeLike(task.AcceptanceJSON) {
return true
}
return roleLooksCodeLike(task.DefaultTo)
}
func acceptanceJSONLooksCodeLike(raw json.RawMessage) bool {
if len(raw) == 0 {
return false
}
var value any
if err := json.Unmarshal(raw, &value); err != nil {
return false
}
return acceptanceValueLooksCodeLike(value)
}
func acceptanceValueLooksCodeLike(value any) bool {
switch typed := value.(type) {
case map[string]any:
for key, raw := range typed {
lowerKey := strings.ToLower(strings.TrimSpace(key))
switch lowerKey {
case "code", "code_task", "writes_code", "worktree":
if boolValue, ok := raw.(bool); ok && boolValue {
return true
}
case "kind", "task_type", "mode", "type":
if stringValue, ok := raw.(string); ok && isCodeLikeMarker(stringValue) {
return true
}
}
if acceptanceValueLooksCodeLike(raw) {
return true
}
}
case []any:
for _, item := range typed {
if acceptanceValueLooksCodeLike(item) {
return true
}
}
case string:
return isCodeLikeMarker(typed)
}
return false
}
func roleLooksCodeLike(role string) bool {
role = strings.ToLower(strings.TrimSpace(role))
if role == "" {
return false
}
for _, token := range splitIdentifierTokens(role) {
switch token {
case "backend", "frontend", "front", "admin", "ui", "fullstack", "foundation", "db", "database", "mobile", "ios", "android", "web", "platform", "infra", "api":
return true
}
}
return false
}
func isCodeLikeMarker(value string) bool {
value = strings.ToLower(strings.TrimSpace(value))
switch value {
case "code", "code_task", "code-task", "code-change", "code_change", "implementation", "patch", "diff", "repo":
return true
default:
return false
}
}
func splitIdentifierTokens(value string) []string {
return strings.FieldsFunc(value, func(r rune) bool {
return !((r >= 'a' && r <= 'z') || (r >= '0' && r <= '9'))
})
}
func provisionDispatchWorkspace(ctx context.Context, opts dispatchOptions, task store.Task, attemptNo int) (store.DispatchWorkspace, func(), error) {
repoRoot, err := resolveRepoRoot(ctx, opts.repoPath)
if err != nil {
return store.DispatchWorkspace{}, nil, err
}
workspaceRoot := resolveWorkspaceRoot(repoRoot, opts.workspaceRoot)
if err := ensureWorkspaceRootIgnored(repoRoot, workspaceRoot); err != nil {
return store.DispatchWorkspace{}, nil, err
}
baseRef, baseCommit, err := resolveDispatchBase(ctx, repoRoot, workspaceRoot, opts.baseRef, opts.strictWorktree)
if err != nil {
return store.DispatchWorkspace{}, nil, err
}
branchName := buildAttemptBranchName(task.RunID, task.TaskID, attemptNo)
worktreePath := buildAttemptWorktreePath(workspaceRoot, task.RunID, task.TaskID, attemptNo)
if err := os.MkdirAll(filepath.Dir(worktreePath), 0o755); err != nil {
return store.DispatchWorkspace{}, nil, fmt.Errorf("create worktree parent dir: %w", err)
}
if _, err := os.Stat(worktreePath); err == nil {
return store.DispatchWorkspace{}, nil, fmt.Errorf("%w: worktree path already exists: %s", store.ErrInvalidState, worktreePath)
} else if err != nil && !os.IsNotExist(err) {
return store.DispatchWorkspace{}, nil, fmt.Errorf("stat worktree path: %w", err)
}
if _, _, err := runGit(ctx, repoRoot, "worktree", "add", "-b", branchName, worktreePath, baseCommit); err != nil {
return store.DispatchWorkspace{}, nil, err
}
cleanup := func() {
_, _, _ = runGit(context.Background(), repoRoot, "worktree", "remove", "--force", worktreePath)
_, _, _ = runGit(context.Background(), repoRoot, "branch", "-D", branchName)
_ = os.RemoveAll(worktreePath)
}
return store.DispatchWorkspace{
BaseRef: baseRef,
BaseCommit: baseCommit,
BranchName: branchName,
WorktreePath: worktreePath,
WorkspaceStatus: "created",
}, cleanup, nil
}
func resolveRepoRoot(ctx context.Context, repoPath string) (string, error) {
startPath := strings.TrimSpace(repoPath)
if startPath == "" {
var err error
startPath, err = os.Getwd()
if err != nil {
return "", fmt.Errorf("get current working directory: %w", err)
}
}
absPath, err := filepath.Abs(startPath)
if err != nil {
return "", fmt.Errorf("resolve repo path: %w", err)
}
if _, _, err := runGit(ctx, absPath, "rev-parse", "--show-toplevel"); err != nil {
return "", protocol.InvalidInput("repo-path must point to a Git worktree", err)
}
commonDir, err := resolveCommonGitDir(ctx, absPath)
if err != nil {
return "", protocol.InvalidInput("repo-path must point to a Git worktree", err)
}
return filepath.Dir(commonDir), nil
}
func resolveDispatchBase(ctx context.Context, repoRoot, workspaceRoot, requestedBaseRef string, strict bool) (string, string, error) {
baseRef := strings.TrimSpace(requestedBaseRef)
if baseRef != "" {
baseCommit, err := resolveCommit(ctx, repoRoot, baseRef)
if err != nil {
return "", "", protocol.InvalidInput("base-ref must resolve to a commit", err)
}
return baseRef, baseCommit, nil
}
if strict {
dirty, err := repoHasUncommittedChanges(ctx, repoRoot, workspaceRoot)
if err != nil {
return "", "", err
}
if dirty {
return "", "", fmt.Errorf("%w: repository has uncommitted changes; specify --base-ref or clean the repo", store.ErrInvalidState)
}
}
baseCommit, err := resolveCommit(ctx, repoRoot, "HEAD")
if err != nil {
return "", "", protocol.InvalidInput("failed to resolve HEAD commit", err)
}
return "HEAD", baseCommit, nil
}
func resolveCommit(ctx context.Context, repoRoot, ref string) (string, error) {
stdout, _, err := runGit(ctx, repoRoot, "rev-parse", "--verify", ref+"^{commit}")
if err != nil {
return "", err
}
return strings.TrimSpace(stdout), nil
}
func repoHasUncommittedChanges(ctx context.Context, repoRoot, workspaceRoot string) (bool, error) {
stdout, _, err := runGit(ctx, repoRoot, "status", "--porcelain")
if err != nil {
return false, fmt.Errorf("check repository status: %w", err)
}
for _, line := range strings.Split(strings.TrimSpace(stdout), "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
if len(line) >= 3 {
path := strings.TrimSpace(line[3:])
if shouldIgnoreStatusPath(repoRoot, workspaceRoot, path) {
continue
}
}
return true, nil
}
return false, nil
}
func resolveWorkspaceRoot(repoRoot, configuredRoot string) string {
root := strings.TrimSpace(configuredRoot)
if root == "" {
return filepath.Join(repoRoot, ".orch", "worktrees")
}
if filepath.IsAbs(root) {
return root
}
return filepath.Join(repoRoot, root)
}
func ensureWorkspaceRootIgnored(repoRoot, workspaceRoot string) error {
relative, err := filepath.Rel(repoRoot, workspaceRoot)
if err != nil {
return fmt.Errorf("resolve workspace root exclude path: %w", err)
}
if relative == "." || strings.HasPrefix(relative, "..") {
return nil
}
pattern := filepath.ToSlash(relative)
if !strings.HasSuffix(pattern, "/") {
pattern += "/"
}
excludePath := filepath.Join(repoRoot, ".git", "info", "exclude")
if err := os.MkdirAll(filepath.Dir(excludePath), 0o755); err != nil {
return fmt.Errorf("create git info dir: %w", err)
}
content, err := os.ReadFile(excludePath)
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("read git exclude file: %w", err)
}
if strings.Contains(string(content), pattern) {
return nil
}
appendContent := pattern + "\n"
if len(content) > 0 && !strings.HasSuffix(string(content), "\n") {
appendContent = "\n" + appendContent
}
file, err := os.OpenFile(excludePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
if err != nil {
return fmt.Errorf("open git exclude file: %w", err)
}
defer file.Close()
if _, err := file.WriteString(appendContent); err != nil {
return fmt.Errorf("append git exclude file: %w", err)
}
return nil
}
func shouldIgnoreStatusPath(repoRoot, workspaceRoot, statusPath string) bool {
relative, err := filepath.Rel(repoRoot, workspaceRoot)
if err != nil || relative == "." || strings.HasPrefix(relative, "..") {
return false
}
relative = filepath.ToSlash(relative)
statusPath = filepath.ToSlash(strings.Trim(statusPath, `"`))
return statusPath == relative || strings.HasPrefix(statusPath, relative+"/")
}
func buildAttemptBranchName(runID, taskID string, attemptNo int) string {
return fmt.Sprintf(
"orch/%s/%s/attempt-%d",
sanitizeGitSegment(runID),
sanitizeGitSegment(taskID),
attemptNo,
)
}
func buildAttemptWorktreePath(workspaceRoot, runID, taskID string, attemptNo int) string {
return filepath.Join(
workspaceRoot,
sanitizePathSegment(runID),
sanitizePathSegment(taskID),
fmt.Sprintf("attempt-%d", attemptNo),
)
}
func deriveWorkspaceRootFromAttempt(runID, taskID, worktreePath string) (string, bool) {
suffix := filepath.Join(
sanitizePathSegment(runID),
sanitizePathSegment(taskID),
filepath.Base(worktreePath),
)
parent := filepath.Dir(worktreePath)
if filepath.Base(parent) != sanitizePathSegment(taskID) {
return "", false
}
runDir := filepath.Dir(parent)
if filepath.Base(runDir) != sanitizePathSegment(runID) {
return "", false
}
root := filepath.Dir(runDir)
if filepath.Clean(filepath.Join(root, suffix)) != filepath.Clean(worktreePath) {
return "", false
}
return root, true
}
func sanitizeGitSegment(value string) string {
return sanitizeSegment(value)
}
func sanitizePathSegment(value string) string {
return sanitizeSegment(value)
}
func sanitizeSegment(value string) string {
value = strings.TrimSpace(value)
if value == "" {
return "item"
}
var b strings.Builder
lastDash := false
for _, r := range value {
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') {
b.WriteRune(r)
lastDash = false
continue
}
if r == '-' || r == '_' || r == '.' {
if !lastDash {
b.WriteByte('-')
lastDash = true
}
continue
}
if !lastDash {
b.WriteByte('-')
lastDash = true
}
}
result := strings.Trim(b.String(), "-.")
if result == "" {
return "item"
}
if strings.HasSuffix(result, ".lock") {
result = strings.TrimSuffix(result, ".lock") + "-lock"
}
return result
}
func runGit(ctx context.Context, repoRoot string, args ...string) (string, string, error) {
cmdArgs := append([]string{"-C", repoRoot}, args...)
cmd := exec.CommandContext(ctx, "git", cmdArgs...)
output, err := cmd.CombinedOutput()
if err == nil {
return string(output), "", nil
}
message := strings.TrimSpace(string(output))
if message == "" {
message = err.Error()
}
return "", message, fmt.Errorf("git %s: %s", strings.Join(args, " "), message)
}
func cleanupAttemptWorktree(ctx context.Context, attempt store.TaskAttempt, force bool) error {
if strings.TrimSpace(attempt.WorktreePath) == "" {
return nil
}
if _, err := os.Stat(attempt.WorktreePath); err != nil {
if os.IsNotExist(err) {
return nil
}
return fmt.Errorf("stat worktree path: %w", err)
}
repoRoot, err := resolveRepoRootFromExistingWorktree(ctx, attempt.WorktreePath)
if err != nil {
if force {
return os.RemoveAll(attempt.WorktreePath)
}
return err
}
args := []string{"worktree", "remove"}
if force {
args = append(args, "--force")
}
args = append(args, attempt.WorktreePath)
if _, _, err := runGit(ctx, repoRoot, args...); err != nil {
if force {
return os.RemoveAll(attempt.WorktreePath)
}
return err
}
return nil
}
func resolveRepoRootFromExistingWorktree(ctx context.Context, worktreePath string) (string, error) {
commonDir, err := resolveCommonGitDir(ctx, worktreePath)
if err != nil {
return "", err
}
return filepath.Dir(commonDir), nil
}
func resolveCommonGitDir(ctx context.Context, repoPath string) (string, error) {
stdout, _, err := runGit(ctx, repoPath, "rev-parse", "--path-format=absolute", "--git-common-dir")
if err != nil {
return "", err
}
commonDir := strings.TrimSpace(stdout)
if !filepath.IsAbs(commonDir) {
commonDir = filepath.Join(repoPath, commonDir)
}
return filepath.Clean(commonDir), nil
}
+1 -66
View File
@@ -3,70 +3,5 @@
set -euo pipefail
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
readonly BUILD_DIR="$(mktemp -d "${TMPDIR:-/tmp}/skill-clis.XXXXXX")"
cleanup() {
rm -rf "${BUILD_DIR}"
}
trap cleanup EXIT INT TERM
require_command() {
local cmd="$1"
if ! command -v "${cmd}" >/dev/null 2>&1; then
printf 'missing required command: %s\n' "${cmd}" >&2
exit 1
fi
}
build_binary() {
local name="$1"
local package_path="$2"
local output_path="${BUILD_DIR}/${name}"
printf 'building %s from %s\n' "${name}" "${package_path}" >&2
(
cd "${REPO_ROOT}"
go build -trimpath -o "${output_path}" "${package_path}"
)
printf '%s\n' "${output_path}"
}
install_binary() {
local source_path="$1"
shift
local destination_path
for destination_path in "$@"; do
mkdir -p "$(dirname "${destination_path}")"
install -m 0755 "${source_path}" "${destination_path}"
printf 'installed %s\n' "${destination_path}"
done
}
main() {
require_command go
require_command install
require_command mktemp
local inbox_binary
local orch_binary
inbox_binary="$(build_binary inbox ./cmd/inbox)"
orch_binary="$(build_binary orch ./cmd/orch)"
install_binary \
"${inbox_binary}" \
"${REPO_ROOT}/skills/inbox/assets/inbox"
install_binary \
"${orch_binary}" \
"${REPO_ROOT}/skills/orch/assets/orch" \
"${REPO_ROOT}/skills/council-review/assets/orch"
printf 'skill CLI packaging complete\n'
}
main "$@"
exec bash "${SCRIPT_DIR}/package_skill_runtimes.sh" package "$@"
+44 -2
View File
@@ -5,15 +5,17 @@ set -euo pipefail
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
readonly MANIFEST_PATH="${REPO_ROOT}/scripts/skill-bundles.json"
BUILD_DIR=""
usage() {
cat <<'EOF'
Usage:
package_skill_runtimes.sh plan
package_skill_runtimes.sh validate
package_skill_runtimes.sh package
Phase 1 only bootstraps declarative bundle metadata.
Actual runtime packaging will be added once package-owned entrypoints exist.
The bundle manifest is package-oriented. Bundles in state=planned are skipped by
the package command until their package-owned entrypoints exist.
EOF
}
@@ -44,6 +46,12 @@ emit_bundles() {
' "${MANIFEST_PATH}"
}
cleanup() {
if [[ -n "${BUILD_DIR}" && -d "${BUILD_DIR}" ]]; then
rm -rf "${BUILD_DIR}"
fi
}
plan() {
printf 'skill bundle plan from %s\n' "${MANIFEST_PATH}"
while IFS=$'\t' read -r skill type runtime_package entrypoint output build_state; do
@@ -98,6 +106,37 @@ validate() {
printf 'skill bundle manifest validated\n'
}
package_bundles() {
validate
require_command go
require_command install
require_command mktemp
BUILD_DIR="$(mktemp -d "${TMPDIR:-/tmp}/skill-runtimes.XXXXXX")"
trap cleanup EXIT INT TERM
while IFS=$'\t' read -r skill type runtime_package entrypoint output build_state; do
if [[ "${build_state}" != "ready" ]]; then
printf 'skipping %s (%s)\n' "${skill}" "${build_state}"
continue
fi
local output_path="${REPO_ROOT}/${output}"
local artifact_path="${BUILD_DIR}/${skill}"
printf 'building %s from %s\n' "${skill}" "${entrypoint}"
(
cd "${REPO_ROOT}"
go build -trimpath -o "${artifact_path}" "${entrypoint}"
)
mkdir -p "$(dirname "${output_path}")"
install -m 0755 "${artifact_path}" "${output_path}"
printf 'installed %s\n' "${output}"
done < <(emit_bundles)
}
main() {
require_command node
@@ -109,6 +148,9 @@ main() {
validate)
validate
;;
package)
package_bundles
;;
""|-h|--help|help)
usage
;;
+3 -3
View File
@@ -7,7 +7,7 @@
"runtimePackage": "./packages/inbox-runtime",
"entrypoint": "./packages/inbox-runtime/cmd/inbox",
"output": "skills/inbox/assets/inbox",
"buildState": "planned"
"buildState": "ready"
},
{
"skill": "orch",
@@ -15,7 +15,7 @@
"runtimePackage": "./packages/orch-runtime",
"entrypoint": "./packages/orch-runtime/cmd/orch",
"output": "skills/orch/assets/orch",
"buildState": "planned"
"buildState": "ready"
},
{
"skill": "council-review",
@@ -23,7 +23,7 @@
"runtimePackage": "./packages/orch-runtime",
"entrypoint": "./packages/orch-runtime/cmd/orch",
"output": "skills/council-review/assets/orch",
"buildState": "planned"
"buildState": "ready"
},
{
"skill": "repo-memory",
Binary file not shown.
Binary file not shown.
Binary file not shown.