@tingrudeng/worker-review-orchestrator-cli 0.1.0-beta.1 → 0.1.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -25,11 +25,13 @@ This package is meant to pair with the `worker-review-orchestrator` skill, but i
25
25
  - `dispatch`
26
26
  - POST a dispatch payload to `/api/dispatches`
27
27
  - Supports `--target-worker-id` to pin all tasks in the payload to one worker
28
+ - Supports `--require-existing-worker` to verify the target worker or worker pool is already online in `/api/dashboard/snapshot` before posting
28
29
  - `dispatch-task`
29
30
  - Build and POST a single-task dispatch payload from CLI flags
30
31
  - Preferred when the control layer wants to send one focused task without hand-writing `dispatch.json`
31
32
  - Supports `--worker-prompt` and `--context-markdown` for inline string inputs
32
- - Does NOT support file paths for prompt or context; use `--worker-prompt` or `--context-markdown` with literal strings
33
+ - Supports `--worker-prompt-file` and `--context-markdown-file` for file-backed prompt/context inputs
34
+ - Supports `--require-existing-worker` to reject dispatch when no matching online worker exists
33
35
  - `watch`
34
36
  - Poll `/api/dashboard/snapshot` until a task reaches `review`, `failed`, `merged`, or `blocked`
35
37
  - `decide`
@@ -52,6 +54,7 @@ This package is meant to pair with the `worker-review-orchestrator` skill, but i
52
54
  ```bash
53
55
  forgeflow-review-orchestrator dispatch --dispatcher-url http://127.0.0.1:8787 --input dispatch.json
54
56
  forgeflow-review-orchestrator dispatch --dispatcher-url http://127.0.0.1:8787 --input dispatch.json --target-worker-id trae-remote-forgeflow
57
+ forgeflow-review-orchestrator dispatch --dispatcher-url http://127.0.0.1:8787 --input dispatch.json --target-worker-id trae-local-forgeflow --require-existing-worker
55
58
  forgeflow-review-orchestrator dispatch-task --dispatcher-url http://127.0.0.1:8787 --repo TingRuDeng/ForgeFlow --default-branch main --task-id task-1 --title "Update docs" --pool trae --branch-name ai/trae/task-1 --allowed-paths docs/**,README.md --acceptance "pnpm typecheck,git diff --check" --target-worker-id trae-remote-forgeflow
56
59
  forgeflow-review-orchestrator dispatch-task --dispatcher-url http://127.0.0.1:8787 --repo TingRuDeng/ForgeFlow --default-branch main --task-id task-1 --title "Refactor auth" --pool codex --branch-name ai/codex/auth --allowed-paths "packages/auth/**" --acceptance "pnpm typecheck" --worker-prompt "You are a codex worker. Refactor the auth module." --context-markdown "# Context\n\nFocus on packages/auth only."
57
60
  forgeflow-review-orchestrator watch --dispatcher-url http://127.0.0.1:8787 --task-id dispatch-1:task-1
@@ -63,6 +66,8 @@ forgeflow-review-orchestrator inspect --dispatcher-url http://127.0.0.1:8787 --t
63
66
 
64
67
  When `--target-worker-id` is set, the CLI injects that worker id into every task and assignment package before posting the dispatch. This keeps the existing `dispatch.json` shape but gives the control layer an explicit way to target `trae-local-forgeflow` or `trae-remote-forgeflow`.
65
68
 
69
+ When `--require-existing-worker` is set, the CLI fetches `/api/dashboard/snapshot` before dispatch and hard-fails if the pinned target worker is offline or if no online worker exists for the required task pool. Use this when the control layer must reuse an already running dispatcher + worker runtime instead of implicitly relying on a later bootstrap step.
70
+
66
71
  Use `dispatch-task` for the common case of one worker task with one branch, one `allowedPaths` list, and one `acceptance` list. Keep `dispatch --input` for more complex multi-task or prebuilt payloads.
67
72
 
68
73
  For control-layer review flow, prefer this CLI over calling lower-level review-decision scripts directly. Keep the scripts as compatibility and implementation detail, but use `forgeflow-review-orchestrator decide` as the default operator entrypoint.
package/dist/cli.js CHANGED
@@ -51,7 +51,9 @@ function printHelp() {
51
51
  Usage:
52
52
  forgeflow-review-orchestrator dispatch --dispatcher-url http://127.0.0.1:8787 --input dispatch.json
53
53
  forgeflow-review-orchestrator dispatch --dispatcher-url http://127.0.0.1:8787 --input dispatch.json --target-worker-id trae-remote-forgeflow
54
+ forgeflow-review-orchestrator dispatch --dispatcher-url http://127.0.0.1:8787 --input dispatch.json --require-existing-worker
54
55
  forgeflow-review-orchestrator dispatch-task --dispatcher-url http://127.0.0.1:8787 --repo TingRuDeng/ForgeFlow --default-branch main --task-id task-1 --title "Update docs" --pool trae --branch-name ai/trae/task-1 --allowed-paths docs/**,README.md --acceptance "pnpm typecheck,git diff --check"
56
+ forgeflow-review-orchestrator dispatch-task --dispatcher-url http://127.0.0.1:8787 --repo TingRuDeng/ForgeFlow --default-branch main --task-id task-1 --title "Update docs" --pool trae --branch-name ai/trae/task-1 --target-worker-id trae-local-forgeflow --require-existing-worker
55
57
  forgeflow-review-orchestrator dispatch-task --dispatcher-url http://127.0.0.1:8787 --repo TingRuDeng/ForgeFlow --default-branch main --task-id task-1 --title "Update docs" --pool trae --branch-name ai/trae/task-1 --worker-prompt-file prompts/worker.md --context-markdown-file context/task.md
56
58
  forgeflow-review-orchestrator watch --dispatcher-url http://127.0.0.1:8787 --task-id dispatch-1:task-1
57
59
  forgeflow-review-orchestrator watch --dispatcher-url http://127.0.0.1:8787 --task-id dispatch-1:task-1 --summary
@@ -89,6 +91,7 @@ export async function runCli(argv, partialDeps = {}) {
89
91
  const result = await deps.runDispatch({
90
92
  dispatcherUrl,
91
93
  input,
94
+ requireExistingWorker: options.requireExistingWorker === true,
92
95
  targetWorkerId: typeof options.targetWorkerId === "string" ? options.targetWorkerId : undefined,
93
96
  requestTimeoutMs: typeof options.requestTimeoutMs === "number" ? options.requestTimeoutMs : undefined,
94
97
  });
@@ -136,6 +139,7 @@ export async function runCli(argv, partialDeps = {}) {
136
139
  dispatcherUrl,
137
140
  input: "-",
138
141
  payload,
142
+ requireExistingWorker: options.requireExistingWorker === true,
139
143
  requestTimeoutMs: typeof options.requestTimeoutMs === "number" ? options.requestTimeoutMs : undefined,
140
144
  });
141
145
  deps.log(JSON.stringify(result, null, 2));
@@ -4,6 +4,7 @@ export interface DispatchOptions {
4
4
  input: string;
5
5
  payload?: DispatchInput;
6
6
  targetWorkerId?: string;
7
+ requireExistingWorker?: boolean;
7
8
  requestTimeoutMs?: number;
8
9
  fetchImpl?: typeof globalThis.fetch;
9
10
  readStdin?: () => Promise<string>;
package/dist/dispatch.js CHANGED
@@ -90,10 +90,67 @@ export function applyDispatchTargetWorker(payload, targetWorkerId) {
90
90
  packages: applyTargetWorkerToRecords(payload.packages, normalizedTargetWorkerId),
91
91
  };
92
92
  }
93
+ function normalizeString(value) {
94
+ if (typeof value !== "string") {
95
+ return undefined;
96
+ }
97
+ const trimmed = value.trim();
98
+ return trimmed ? trimmed : undefined;
99
+ }
100
+ function isOnlineWorkerStatus(status) {
101
+ return status === "idle" || status === "busy";
102
+ }
103
+ function formatWorkerInventory(workers) {
104
+ if (workers.length === 0) {
105
+ return "none";
106
+ }
107
+ return workers.map((worker) => {
108
+ const id = normalizeString(worker.id) || "<unknown>";
109
+ const pool = normalizeString(worker.pool) || "<unknown-pool>";
110
+ const status = normalizeString(worker.status) || "<unknown-status>";
111
+ return `${id}(${pool},${status})`;
112
+ }).join(", ");
113
+ }
114
+ async function ensureExistingWorkersAvailable(payload, options) {
115
+ const client = createJsonHttpClient(options.dispatcherUrl, {
116
+ fetchImpl: options.fetchImpl,
117
+ requestTimeoutMs: options.requestTimeoutMs,
118
+ });
119
+ const snapshot = await client.request("/api/dashboard/snapshot");
120
+ const workers = Array.isArray(snapshot.workers) ? snapshot.workers : [];
121
+ const onlineWorkers = workers.filter((worker) => isOnlineWorkerStatus(worker.status));
122
+ if (onlineWorkers.length === 0) {
123
+ throw new Error(`require-existing-worker check failed: dispatcher snapshot has no online workers. Snapshot workers: ${formatWorkerInventory(workers)}`);
124
+ }
125
+ const tasks = Array.isArray(payload.tasks) ? payload.tasks : [];
126
+ const targetWorkerIds = [...new Set(tasks.map((task) => normalizeString(task.targetWorkerId) || normalizeString(task.target_worker_id)).filter((value) => Boolean(value)))];
127
+ if (targetWorkerIds.length > 0) {
128
+ for (const targetWorkerId of targetWorkerIds) {
129
+ const matchedWorker = onlineWorkers.find((worker) => normalizeString(worker.id) === targetWorkerId);
130
+ if (!matchedWorker) {
131
+ throw new Error(`require-existing-worker check failed: target worker "${targetWorkerId}" is not online. Online workers: ${formatWorkerInventory(onlineWorkers)}`);
132
+ }
133
+ }
134
+ return;
135
+ }
136
+ const requiredPools = [...new Set(tasks.map((task) => normalizeString(task.pool)).filter((value) => Boolean(value)))];
137
+ if (requiredPools.length === 0) {
138
+ throw new Error("require-existing-worker check failed: every dispatch task must declare a pool or targetWorkerId");
139
+ }
140
+ for (const pool of requiredPools) {
141
+ const hasOnlineWorker = onlineWorkers.some((worker) => normalizeString(worker.pool) === pool);
142
+ if (!hasOnlineWorker) {
143
+ throw new Error(`require-existing-worker check failed: no online worker available for pool "${pool}". Online workers: ${formatWorkerInventory(onlineWorkers)}`);
144
+ }
145
+ }
146
+ }
93
147
  export async function runDispatch(options) {
94
148
  const payload = options.payload
95
149
  ? applyDispatchTargetWorker(options.payload, options.targetWorkerId)
96
150
  : applyDispatchTargetWorker(await loadDispatchInput(options.input, options.readStdin), options.targetWorkerId);
151
+ if (options.requireExistingWorker) {
152
+ await ensureExistingWorkersAvailable(payload, options);
153
+ }
97
154
  const client = createJsonHttpClient(options.dispatcherUrl, {
98
155
  fetchImpl: options.fetchImpl,
99
156
  requestTimeoutMs: options.requestTimeoutMs,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tingrudeng/worker-review-orchestrator-cli",
3
- "version": "0.1.0-beta.1",
3
+ "version": "0.1.0-beta.3",
4
4
  "private": false,
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -17,7 +17,7 @@
17
17
  "main": "./dist/index.js",
18
18
  "types": "./dist/index.d.ts",
19
19
  "bin": {
20
- "forgeflow-review-orchestrator": "./dist/cli.js"
20
+ "forgeflow-review-orchestrator": "dist/cli.js"
21
21
  },
22
22
  "exports": {
23
23
  ".": {