@autodidacticon/orchestrator 0.1.0 → 0.3.0
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 +60 -33
- package/dist/src/agent/mcp-shim.js +1 -1
- package/dist/src/agent/server.js +11 -6
- package/dist/src/agent/server.js.map +1 -1
- package/dist/src/brief/compose.js +8 -1
- package/dist/src/brief/compose.js.map +1 -1
- package/dist/src/config.js +23 -3
- package/dist/src/config.js.map +1 -1
- package/dist/src/graph/load.js +4 -0
- package/dist/src/graph/load.js.map +1 -1
- package/dist/src/import/mapping.js +29 -5
- package/dist/src/import/mapping.js.map +1 -1
- package/dist/src/index.js +8 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/launch/launch.js +5 -1
- package/dist/src/launch/launch.js.map +1 -1
- package/dist/src/log.js +20 -0
- package/dist/src/log.js.map +1 -0
- package/dist/src/loop/orchestrator.js +94 -3
- package/dist/src/loop/orchestrator.js.map +1 -1
- package/dist/src/loop/tick.js +2 -1
- package/dist/src/loop/tick.js.map +1 -1
- package/dist/src/main.js +115 -33
- package/dist/src/main.js.map +1 -1
- package/dist/src/operator/cli.js +40 -2
- package/dist/src/operator/cli.js.map +1 -1
- package/dist/src/operator/mcp-server.js +41 -9
- package/dist/src/operator/mcp-server.js.map +1 -1
- package/dist/src/operator/mcp.js +1 -1
- package/dist/src/operator/routes.js +62 -5
- package/dist/src/operator/routes.js.map +1 -1
- package/dist/src/runner/claude-code.js +1 -1
- package/dist/src/runner/claude-code.js.map +1 -1
- package/dist/src/runner/command.js +24 -0
- package/dist/src/runner/command.js.map +1 -0
- package/dist/src/runner/selection.js +20 -0
- package/dist/src/runner/selection.js.map +1 -0
- package/dist/src/runner/subprocess.js +7 -1
- package/dist/src/runner/subprocess.js.map +1 -1
- package/dist/src/runs/childEnv.js +32 -0
- package/dist/src/runs/childEnv.js.map +1 -0
- package/dist/src/runs/lifecycle.js +102 -5
- package/dist/src/runs/lifecycle.js.map +1 -1
- package/dist/src/runs/resolve.js +217 -0
- package/dist/src/runs/resolve.js.map +1 -0
- package/dist/src/runs/runDir.js +63 -13
- package/dist/src/runs/runDir.js.map +1 -1
- package/dist/src/state/store.js.map +1 -1
- package/dist/src/toolchain/branch.js +19 -0
- package/dist/src/toolchain/branch.js.map +1 -0
- package/dist/src/toolchain/detect.js +115 -0
- package/dist/src/toolchain/detect.js.map +1 -0
- package/dist/src/toolchain/tools.js +56 -0
- package/dist/src/toolchain/tools.js.map +1 -0
- package/dist/src/verify/verifier.js +9 -3
- package/dist/src/verify/verifier.js.map +1 -1
- package/hosts/claude-code/.claude-plugin/plugin.json +1 -1
- package/hosts/claude-code/package-lock.json +1247 -0
- package/hosts/claude-code/package.json +1 -1
- package/hosts/claude-code/skills/import/SKILL.md +66 -14
- package/hosts/claude-code/skills/run/SKILL.md +83 -21
- package/hosts/claude-code/skills/work/SKILL.md +25 -6
- package/hosts/claude-code/workflows/work.js +23 -9
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: import
|
|
3
|
-
description: Import an epic or parent issue from Linear or
|
|
3
|
+
description: Import an epic or parent issue from Linear, Jira, or GitHub Issues into the orchestrator's run graph for this repository. Use when the user wants to turn a tracker epic, parent issue, project milestone, GitHub milestone, or list of issues into an orchestrated run.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Import an epic into the run graph
|
|
@@ -11,13 +11,21 @@ session and hand over normalized records.
|
|
|
11
11
|
|
|
12
12
|
## 1. Check the prerequisites
|
|
13
13
|
|
|
14
|
-
- A Linear or Atlassian (Jira) MCP server
|
|
15
|
-
with `mcp__plugin_linear`, `mcp__linear`, `mcp__plugin_atlassian`, or
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
- A tracker connector must be available: a Linear or Atlassian (Jira) MCP server (tools whose
|
|
15
|
+
names start with `mcp__plugin_linear`, `mcp__linear`, `mcp__plugin_atlassian`, or
|
|
16
|
+
`mcp__atlassian`), a GitHub MCP server, or the `gh` CLI for GitHub Issues: run
|
|
17
|
+
`gh auth status` with Bash and treat success as the connector. If none is available, stop
|
|
18
|
+
and tell the user: "No tracker connector is available in this session; connect the Linear or
|
|
19
|
+
Atlassian plugin, a GitHub MCP server, or log in with `gh auth login`, and run the import
|
|
20
|
+
again." Do not call `import_issues`. When more than one is available, use the one the
|
|
21
|
+
user's root belongs to (a GitHub URL or `#123` means GitHub).
|
|
19
22
|
- The orchestrator daemon must be running for this repository. Call the `start` tool; it is
|
|
20
|
-
idempotent and returns the run directory
|
|
23
|
+
idempotent and returns the run directory, the graph path, and the resolved configuration.
|
|
24
|
+
Report the toolchain in one line: the check command and the integration branch with the
|
|
25
|
+
source of each (for example "detected: npm test on main"). When `needsConfig` names
|
|
26
|
+
`checkCommand`, follow the run skill's section 1b (read `CLAUDE.md`, the README, and the
|
|
27
|
+
root manifests; set it with `configure` when it is stated unambiguously, otherwise ask) before
|
|
28
|
+
continuing, so that the first submission is not held.
|
|
21
29
|
|
|
22
30
|
## 2. Fetch the root and everything it references
|
|
23
31
|
|
|
@@ -32,6 +40,27 @@ For **Jira**: fetch the root (an epic or any parent); fetch the issues whose par
|
|
|
32
40
|
(or whose epic link is the root); for every child, read the issue links of type "is blocked
|
|
33
41
|
by"; fetch each blocker outside the child set.
|
|
34
42
|
|
|
43
|
+
For **GitHub**: the root is an issue (`#123`, `owner/repo#123`, or its URL) or a milestone
|
|
44
|
+
(by name or URL). Query the GraphQL API through `gh api graphql` (or the GitHub MCP server).
|
|
45
|
+
For an issue root fetch its `subIssues` as the children; for a milestone root fetch the issues
|
|
46
|
+
in the milestone. For every child read `blockedBy` and collect the blockers; fetch each blocker
|
|
47
|
+
outside the child set for its state. One query shape covers every issue:
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
query($owner:String!, $repo:String!, $n:Int!) { repository(owner:$owner, name:$repo) { issue(number:$n) {
|
|
51
|
+
number url title body state stateReason
|
|
52
|
+
labels(first:50) { nodes { name } }
|
|
53
|
+
milestone { number url title state }
|
|
54
|
+
parent { number url repository { nameWithOwner } }
|
|
55
|
+
subIssues(first:100) { nodes { number url title body state stateReason labels(first:50) { nodes { name } } repository { nameWithOwner } } }
|
|
56
|
+
blockedBy(first:50) { nodes { number url state stateReason repository { nameWithOwner } } }
|
|
57
|
+
} } }
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
If the root has no sub-issues (or the milestone no issues), say so and stop: this skill does
|
|
61
|
+
not parse task lists or "blocked by" text in issue bodies, and an empty epic is not imported.
|
|
62
|
+
Repositories on plans without sub-issues or dependencies need the parent to be a milestone.
|
|
63
|
+
|
|
35
64
|
## 3. Build normalized records
|
|
36
65
|
|
|
37
66
|
One record per issue you fetched, including the root:
|
|
@@ -41,7 +70,9 @@ One record per issue you fetched, including the root:
|
|
|
41
70
|
```
|
|
42
71
|
|
|
43
72
|
- `id` is the tracker key (`ENG-123`, `PROJ-45`).
|
|
44
|
-
- `state` is one of `open`, `inProgress`, `done`, `cancelled`.
|
|
73
|
+
- `state` is one of `open`, `inProgress`, `done`, `cancelled`. A `done` or `cancelled` child
|
|
74
|
+
becomes an external node that already counts as satisfied: it neither holds the milestone
|
|
75
|
+
nor blocks a dependant. Map tracker states: anything
|
|
45
76
|
unstarted or backlog is `open`; started or in review is `inProgress`; completed is `done`;
|
|
46
77
|
cancelled, duplicate, or won't-do is `cancelled`.
|
|
47
78
|
- `parent` is the root id for each child.
|
|
@@ -55,7 +86,20 @@ One record per issue you fetched, including the root:
|
|
|
55
86
|
break ties between ready tasks with equally long chains behind them. Linear: send the
|
|
56
87
|
issue's `priority` field as is (its 0, meaning none, is dropped). Jira: send the position
|
|
57
88
|
of the issue's priority in the site's priority list, counted from the most urgent as 1.
|
|
58
|
-
|
|
89
|
+
GitHub: the number from a label `P0`..`P4` or `priority:<n>`, otherwise omit it; the label
|
|
90
|
+
stays in `labels`. Omit it when the tracker has no priority.
|
|
91
|
+
- GitHub records differ in three ways. `id` is `GH-<number>` for an issue in the tracker's
|
|
92
|
+
repository, `GH-<owner>-<repo>-<number>` for an issue in another repository, and `GH-M<number>`
|
|
93
|
+
for a milestone root (GitHub's own key, `owner/repo#123`, is not usable: ids travel through
|
|
94
|
+
URL paths and git branch names); always set `url`, which is what the run skill acts on.
|
|
95
|
+
`state` is `open` for an open issue, `inProgress` for an open issue carrying the in-progress
|
|
96
|
+
label when one is configured, `done` for a closed issue whose `stateReason` is `COMPLETED`,
|
|
97
|
+
and `cancelled` for `NOT_PLANNED` or `DUPLICATE`. `parent` is the root's id for every child.
|
|
98
|
+
- `labels` are passed through. Two label shapes choose the worker for the task: `model:<name>`
|
|
99
|
+
(for example `model:claude-sonnet-5-5`) and `effort:<level>` (`low`, `medium`, `high`,
|
|
100
|
+
`xhigh`, or `max`). Without them a task runs with the run's defaults (`claude-opus-5-5` at
|
|
101
|
+
`medium`, changeable with `configure`). A label whose value is not recognised is left as a
|
|
102
|
+
plain label and reported under `ignoredLabels`.
|
|
59
103
|
|
|
60
104
|
## 4. Import
|
|
61
105
|
|
|
@@ -65,14 +109,22 @@ taken over; set `task` to orchestrate them), `gated` (the root milestone waits f
|
|
|
65
109
|
review, default true).
|
|
66
110
|
|
|
67
111
|
Always pass `tracker`: `{ kind: "linear" }` when you fetched through the Linear tools,
|
|
68
|
-
`{ kind: "atlassian" }` through the Atlassian tools,
|
|
69
|
-
workspace (its URL key)
|
|
70
|
-
|
|
71
|
-
in progress
|
|
112
|
+
`{ kind: "atlassian" }` through the Atlassian tools, `{ kind: "github" }` through `gh` or a
|
|
113
|
+
GitHub MCP server, plus `workspace` naming the Linear workspace (its URL key), the Jira site,
|
|
114
|
+
or the GitHub repository as `owner/repo`. For GitHub also pass `inProgressLabel` when the
|
|
115
|
+
repository uses a label for work in progress: GitHub has no started state, so this label is
|
|
116
|
+
both how an open issue reads as in progress at import and what the run skill adds when a task
|
|
117
|
+
starts. When no label was given and the repository has one that looks like it (`in-progress`,
|
|
118
|
+
`in progress`, `status: in progress`), offer it to the user before importing; without one, in
|
|
119
|
+
progress is simply not mirrored. The daemon records the tracker and from then on mirrors the
|
|
120
|
+
run back through the `run` skill: issues move to in progress, done, or cancelled, and comments
|
|
121
|
+
appear where a human is needed.
|
|
72
122
|
|
|
73
123
|
## 5. Report
|
|
74
124
|
|
|
75
|
-
Show the user the summary: counts of tasks, external nodes, and milestones;
|
|
125
|
+
Show the user the summary: counts of tasks, external nodes, and milestones; any
|
|
126
|
+
`ignoredLabels` (say which issue's `model:` or `effort:` label was not recognised and that the
|
|
127
|
+
task will use the run default); the tasks missing
|
|
76
128
|
acceptance criteria, which will block at ready until someone adds them; any unknown external
|
|
77
129
|
blockers; and whether the graph was accepted by the daemon. If the import was rejected as an
|
|
78
130
|
incompatible reload, show the reasons and suggest cancelling the affected tasks first.
|
|
@@ -11,13 +11,45 @@ never raw event lists.
|
|
|
11
11
|
|
|
12
12
|
## 1. Start
|
|
13
13
|
|
|
14
|
-
Call `start`. It returns the run directory, graph path, URL,
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
Call `start`. It returns the run directory, graph path, URL, the runner `mode` in force, and
|
|
15
|
+
the resolved configuration: `config` (every key as `{ value, source }`, the source being
|
|
16
|
+
`argument`, `file`, `pinned`, `detected`, `default`, or `unresolved`), `toolchain` (what
|
|
17
|
+
detection found in the repository root), and `needsConfig` (keys no layer supplied). It is safe
|
|
18
|
+
to repeat. A run started from this plugin defaults to `pull`: the daemon prepares each launch
|
|
19
|
+
and this session's own subagents do the work (section 1a), so the user sees every agent here.
|
|
20
|
+
`spawn` (the daemon runs headless agents, for unattended runs) and `mixed` are explicit
|
|
21
|
+
choices: `start` with `mode`, or `runner.mode` in `orchestrator.yaml`, which wins.
|
|
22
|
+
|
|
23
|
+
Tell the user who is executing the work, and report the toolchain in one line: the check
|
|
24
|
+
command, the integration branch, and the worker model and effort (`config.model` and
|
|
25
|
+
`config.effort`, default `claude-opus-5-5` at `medium`) with the source of each and, when the
|
|
26
|
+
daemon spawns agents (`spawn`, or `mixed` after the claim window), the toolchains whose
|
|
27
|
+
commands agents may run, for example
|
|
28
|
+
"detected: npm test on main; workers claude-opus-5-5 at medium (default); agents may run npm and node".
|
|
29
|
+
When this session's subagents work the launches (`pull`), omit the allow-list part: workers
|
|
30
|
+
run under this session's own permissions.
|
|
31
|
+
When `config.model.applied` is false (a `runnerCommand` without the `{model}` slot), omit the
|
|
32
|
+
model part. A task can choose its own model or effort: `model` and `effort` in its payload, or
|
|
33
|
+
`model:<name>` and `effort:<level>` labels on its issue; `status.liveLaunches` shows what each
|
|
34
|
+
launch runs with. If `needsConfig` is not empty, resolve it now (section 1b). Then call
|
|
35
|
+
`status`. If no graph is loaded, say so and suggest the `import` skill.
|
|
36
|
+
|
|
37
|
+
## 1b. Resolve what detection could not
|
|
38
|
+
|
|
39
|
+
When `needsConfig` names `checkCommand`, in the `start` result or in a later `wait` digest,
|
|
40
|
+
submitted work is held under `verifying` until a check command is set: no attempt is spent and
|
|
41
|
+
the sealed work is kept, but nothing is published. Never leave it unresolved while a task is
|
|
42
|
+
`submitted` without having asked the user. Look for the test command in `CLAUDE.md`, the
|
|
43
|
+
README, and the manifests in the repository root (`package.json` scripts, `Makefile` targets,
|
|
44
|
+
and their counterparts). When one is stated unambiguously, call `configure` with `checkCommand`
|
|
45
|
+
and tell the user the command and where you found it ("set checkCommand to `make check` from
|
|
46
|
+
CLAUDE.md"). Otherwise propose your best reading, or say you found none, and call `configure`
|
|
47
|
+
only with what the user confirms. `configure` also takes `capacity`, `allowedTools` (rules
|
|
48
|
+
appended to the derived list for spawned agents, such as `Bash(docker:*)`), `runnerCommand`,
|
|
49
|
+
and `model` and `effort` (the worker defaults for launches prepared from then on; effort is
|
|
50
|
+
`low`, `medium`, `high`, `xhigh`, or `max`); a null clears an override so the value falls back
|
|
51
|
+
to the file, detection, or the default. The integration branch is start-only: pass `integrationBranch` to `start` before
|
|
52
|
+
the run holds any launch; a running daemon refuses it.
|
|
21
53
|
|
|
22
54
|
## 1a. Work the run with session subagents
|
|
23
55
|
|
|
@@ -26,13 +58,18 @@ with a worker count no larger than the run's `capacity` (from `status`), usually
|
|
|
26
58
|
a run tag such as `work-1`:
|
|
27
59
|
|
|
28
60
|
```
|
|
29
|
-
Workflow({ scriptPath: "<plugin dir>/workflows/work.js", args: { workers: 2, runTag: "work-1" } })
|
|
61
|
+
Workflow({ scriptPath: "<plugin dir>/workflows/work.js", args: { workers: 2, runTag: "work-1", capacity: <status.config.capacity.value> } })
|
|
30
62
|
```
|
|
31
63
|
|
|
32
|
-
Each worker loop
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
64
|
+
Each worker loop runs a cheap claimer agent (`claude-haiku-5-5` at `low` by default; pass
|
|
65
|
+
`claimModel` and `claimEffort` only when the user asks otherwise) that claims a launch, polling
|
|
66
|
+
up to three times with 90-second waits when nothing is claimable, then runs a worker subagent
|
|
67
|
+
on its brief in the prepared worktree with the launch's model and effort, reports through the
|
|
68
|
+
worker tools, and claims again until nothing is claimable. Agents are labelled `claim
|
|
69
|
+
<workerId>` and `work <workerId> <taskId>`. Workers edit files in worktrees under the run
|
|
70
|
+
directory, outside the repository, so an `acceptEdits` session prompts for each file there
|
|
71
|
+
unless it was started with `--add-dir ~/.orchestrator`. The workflow runs in the background; do
|
|
72
|
+
not wait for its completion notice, monitor with `wait` (section 2). When the workflow has ended while launches are still claimable (a question was
|
|
36
73
|
answered after it stopped, or a task was retried), re-run it with the same run tag: a worker
|
|
37
74
|
with the same id claims its launch back. In `spawn` mode run no workflow; the daemon's agents
|
|
38
75
|
do the work and you only monitor.
|
|
@@ -47,7 +84,15 @@ answers (section 2b), then act on the digest:
|
|
|
47
84
|
|
|
48
85
|
- **openQuestions** non-empty: for each task, call `status` or `logs` only if you need
|
|
49
86
|
context, then present the question to the user verbatim and wait for their reply. Send the
|
|
50
|
-
reply with `answer`. Do not invent answers to questions that need a human decision.
|
|
87
|
+
reply with `answer`. Do not invent answers to questions that need a human decision. When the
|
|
88
|
+
question's kind is `needsAccess` and it names a command (from `status`, under the task's
|
|
89
|
+
blocked or waiting reason), offer three things: adding a rule through `configure`
|
|
90
|
+
(`allowedTools: ["Bash(<command>:*)"]`) so launches spawned afterwards can run it, saying
|
|
91
|
+
that the live agent cannot gain the rule; running the command yourself on the agent's behalf
|
|
92
|
+
in its worktree and sending the result with `answer`, which is what unblocks the live agent
|
|
93
|
+
now; or answering with an alternative. Do what the user picks.
|
|
94
|
+
- **needsConfig** names `checkCommand`: submitted tasks are held under `verifying` until a
|
|
95
|
+
check command is set. Follow section 1b now; do not call the run slow or stalled.
|
|
51
96
|
- **awaitingReview** non-empty: invoke the `review` skill for each milestone.
|
|
52
97
|
- **finished** true: report the final state (done tasks, milestones) and stop.
|
|
53
98
|
- **stalled** true: report `stallRoots` with their reasons. Offer the options that apply:
|
|
@@ -65,7 +110,10 @@ answers (section 2b), then act on the digest:
|
|
|
65
110
|
user's decision. On acceptance in a run imported from a tracker, first create the issue
|
|
66
111
|
with this session's tracker tools: a child of the epic, blocked by the proposing task's
|
|
67
112
|
issue, with the acceptance criteria under the configured heading in its description; then
|
|
68
|
-
call `accept_proposal` with the new issue's key as `nodeId`.
|
|
113
|
+
call `accept_proposal` with the new issue's key as `nodeId`. GitHub: `gh issue create
|
|
114
|
+
--repo <owner/repo> --parent <root url> --title ... --body-file -` creates the sub-issue;
|
|
115
|
+
add the dependency with the GraphQL mutation `addBlockedBy(input: { issueId, blockingIssueId })`
|
|
116
|
+
(node ids from `gh api graphql`); the `nodeId` is `GH-<new number>`. In a file-based run call
|
|
69
117
|
`accept_proposal` without `nodeId` and report the new node id. On rejection call
|
|
70
118
|
`reject_proposal` with the user's note. Never accept a proposal the user has not approved.
|
|
71
119
|
- **pausedReason** is `costBudget`: the run cost reached its ceiling. Report `cost.runUsd`
|
|
@@ -92,12 +140,25 @@ tracker tools in this session, acknowledging each one before the next:
|
|
|
92
140
|
type `started` for `inProgress`, `completed` for `done`, `canceled` for `cancelled`. Jira:
|
|
93
141
|
transition to a status in category In Progress or Done; for `cancelled`, a canceled-type
|
|
94
142
|
status when the workflow has one. When the tracker has no canceled-type state, leave the
|
|
95
|
-
status, post a comment saying the task was cancelled, and acknowledge.
|
|
96
|
-
`
|
|
97
|
-
|
|
98
|
-
`
|
|
99
|
-
|
|
100
|
-
|
|
143
|
+
status, post a comment saying the task was cancelled, and acknowledge. GitHub (act on the
|
|
144
|
+
effect's `url` with `gh`, never on the `GH-` id):
|
|
145
|
+
`done`: `gh issue close <url> --reason completed`;
|
|
146
|
+
`cancelled`: `gh issue close <url> --reason "not planned"`;
|
|
147
|
+
both followed by `gh issue edit <url> --remove-label <policy.tracker.inProgressLabel>` when
|
|
148
|
+
the policy has a label (a no-op when the issue does not carry it);
|
|
149
|
+
`inProgress`: `gh issue reopen <url>` when the issue is closed, then
|
|
150
|
+
`gh issue edit <url> --add-label <policy.tracker.inProgressLabel>`; when the policy has no
|
|
151
|
+
`inProgressLabel`, call `ack { id, result: { skipped: true } }` and change nothing.
|
|
152
|
+
A milestone root (`GH-M<n>`, the effect's `url` is a milestone): `done` closes the milestone
|
|
153
|
+
with `gh api`; acknowledge `inProgress` as skipped. On success call
|
|
154
|
+
`ack { id, result: { stateId } }` (for GitHub, `{ state }`).
|
|
155
|
+
- `comment`: list the issue's recent comments first (GitHub: `gh issue view <url> --comments`
|
|
156
|
+
or the GraphQL `comments` connection). If one already contains the effect's `marker`, call
|
|
157
|
+
`ack { id, result: { commentId } }` with that comment's id and do not post again (a previous
|
|
158
|
+
session posted it and crashed before acknowledging). Otherwise post `body`, a blank line,
|
|
159
|
+
and the `marker` as the last line (GitHub: `gh issue comment <url> --body-file -`), then
|
|
160
|
+
`ack { id, result: { commentId } }`. A GitHub milestone root takes no comments: acknowledge
|
|
161
|
+
the effect as skipped.
|
|
101
162
|
- On a tracker error: `ack { id, error: "<message>" }`, then skip the remaining effects for
|
|
102
163
|
that issue in this drain; they come back next time. The daemon retries a rejected effect
|
|
103
164
|
up to its cap and then lists it in `trackerErrors`; when the digest shows `trackerErrors`,
|
|
@@ -109,7 +170,8 @@ conversation.
|
|
|
109
170
|
### 2b. Relay answers from the tracker
|
|
110
171
|
|
|
111
172
|
When `policy.answersFromComments` is true and the digest lists `openQuestions`: for each
|
|
112
|
-
task, list its issue's comments
|
|
173
|
+
task, list its issue's comments (GitHub: `gh issue view <url> --comments`); ignore any
|
|
174
|
+
containing "— orchestrator run"; take the first
|
|
113
175
|
human comment posted after the latest marked comment that you have not relayed before; send
|
|
114
176
|
it with `answer`; remember that comment's id so you never relay it twice. If the daemon
|
|
115
177
|
answers 409, the task is no longer waiting or blocked (someone answered first): tell the
|
|
@@ -14,7 +14,15 @@ the workflow hands back: questions, reviews, and the end.
|
|
|
14
14
|
## 1. Prerequisites
|
|
15
15
|
|
|
16
16
|
- A graph must be loaded: call `start` and then `status`; if no graph is loaded, use the
|
|
17
|
-
`import` skill first.
|
|
17
|
+
`import` skill first. `start` also returns the resolved configuration: report the check
|
|
18
|
+
command, the integration branch, and the worker model and effort (`config.model`,
|
|
19
|
+
`config.effort`; default `claude-opus-5-5` at `medium`, changeable with `configure`, and
|
|
20
|
+
overridable per task by `model`/`effort` in its payload or `model:`/`effort:` labels) with
|
|
21
|
+
their sources in one line, for example "detected: npm test on main; workers claude-opus-5-5
|
|
22
|
+
at medium (default)", and when `needsConfig`
|
|
23
|
+
names `checkCommand`, follow the run skill's section 1b (find the test command in
|
|
24
|
+
`CLAUDE.md`, the README, or the root manifests and set it with `configure`, or ask the user)
|
|
25
|
+
before running workers, since submitted work is held until it is set.
|
|
18
26
|
- `start` reports the mode in force. A plugin-started run is already `pull`. To work a run in
|
|
19
27
|
`spawn` mode, call `start` with `mode: "pull"` (workers only) or `mode: "mixed"` (the
|
|
20
28
|
daemon spawns whatever nobody claims within its claim window); this switches a daemon
|
|
@@ -23,15 +31,26 @@ the workflow hands back: questions, reviews, and the end.
|
|
|
23
31
|
## 2. Run the workflow
|
|
24
32
|
|
|
25
33
|
Pick a worker count no larger than the run's `capacity` (from `status`), usually 2 to 4, and a
|
|
26
|
-
run tag such as `work-1`. Run the plugin's workflow `work.js` with the Workflow tool
|
|
34
|
+
run tag such as `work-1`. Run the plugin's workflow `work.js` with the Workflow tool, passing
|
|
35
|
+
the capacity so the workflow clamps the count itself:
|
|
27
36
|
|
|
28
37
|
```
|
|
29
|
-
Workflow({ scriptPath: "<plugin dir>/workflows/work.js", args: { workers: 2, runTag: "work-1" } })
|
|
38
|
+
Workflow({ scriptPath: "<plugin dir>/workflows/work.js", args: { workers: 2, runTag: "work-1", capacity: <status.config.capacity.value> } })
|
|
30
39
|
```
|
|
31
40
|
|
|
32
|
-
Each worker loop claims a launch with the worker id `<runTag>-<n
|
|
33
|
-
|
|
34
|
-
|
|
41
|
+
Each worker loop runs a claimer agent that claims a launch with the worker id `<runTag>-<n>`;
|
|
42
|
+
when nothing is claimable it waits up to 90 seconds, reads `status`, and tries again, up to
|
|
43
|
+
three polls before returning. The claimer makes no decisions, so it runs on a fixed cheap
|
|
44
|
+
model: `claude-haiku-5-5` at `low` effort. Pass `claimModel` and `claimEffort` in `args` only
|
|
45
|
+
when the user asks for a different claimer. On a claim the loop runs a worker agent on the
|
|
46
|
+
brief with the model and effort the claim carries (the task's choice, else the run default),
|
|
47
|
+
then claims again until nothing is claimable. In the agent view the claimer is labelled
|
|
48
|
+
`claim <workerId>` and the worker `work <workerId> <taskId>`, so the two agents of a loop
|
|
49
|
+
share the worker id. The workflow runs in the background and returns short outcomes per
|
|
50
|
+
launch; never paste briefs or logs into the conversation. Workers edit files in worktrees under
|
|
51
|
+
the run directory (`~/.orchestrator/runs/<id>/worktrees`, outside the repository), so a session
|
|
52
|
+
in `acceptEdits` mode prompts for each file there unless it was started with
|
|
53
|
+
`--add-dir ~/.orchestrator`; tell the user when the first such prompt appears.
|
|
35
54
|
|
|
36
55
|
## 3. Monitor while it runs
|
|
37
56
|
|
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
export const meta = {
|
|
2
2
|
name: 'orchestrator-work',
|
|
3
|
-
description: 'Fan out N worker loops that claim prepared launches from the orchestrator daemon, do the work in the prepared worktree, and report; stop when nothing is claimable.',
|
|
4
|
-
whenToUse: 'When the work skill runs a pull-mode or mixed-mode orchestrator run inside this session.',
|
|
3
|
+
description: 'Fan out N worker loops that claim prepared launches from the orchestrator daemon (a cheap claimer polls; the worker runs with the launch\'s model and effort), do the work in the prepared worktree, and report; stop when nothing is claimable.',
|
|
4
|
+
whenToUse: 'When the work skill runs a pull-mode or mixed-mode orchestrator run inside this session. Args: workers, runTag, capacity (clamps workers), claimModel and claimEffort (the claimer; default claude-haiku-5-5 at low).',
|
|
5
5
|
phases: [{ title: 'Claim' }, { title: 'Work' }],
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
// The claimer makes no decision (claim, wait, status, a spelled-out stop rule), so it runs on a fixed cheap model unless the user asks otherwise. See specs/host-integration "Work skill and workflow".
|
|
9
|
+
const EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max']
|
|
10
|
+
const claimModel = String((args && args.claimModel) || 'claude-haiku-5-5')
|
|
11
|
+
const claimEffort = EFFORTS.includes(args && args.claimEffort) ? args.claimEffort : 'low'
|
|
12
|
+
const CLAIM_POLLS = 3
|
|
13
|
+
const CLAIM_WAIT_MS = 90000
|
|
14
|
+
const requested = Math.max(1, Math.min(16, Number((args && args.workers) || 2)))
|
|
15
|
+
const capacity = Number(args && args.capacity)
|
|
16
|
+
// A loop beyond the daemon's capacity would only poll for a slot that is never filled.
|
|
17
|
+
const workers = Number.isInteger(capacity) && capacity > 0 && capacity < requested ? capacity : requested
|
|
18
|
+
if (workers !== requested) log(`workers clamped from ${requested} to the run's capacity ${workers}`)
|
|
9
19
|
const runTag = String((args && args.runTag) || 'work')
|
|
10
20
|
const held = {}
|
|
11
21
|
const outcomes = []
|
|
@@ -24,6 +34,8 @@ const CLAIM_SCHEMA = {
|
|
|
24
34
|
briefMarkdown: { type: 'string' },
|
|
25
35
|
pendingSeq: { type: 'number' },
|
|
26
36
|
pendingAnswer: { type: 'string' },
|
|
37
|
+
model: { type: 'string' },
|
|
38
|
+
effort: { type: 'string', enum: ['low', 'medium', 'high', 'xhigh', 'max'] },
|
|
27
39
|
finished: { type: 'boolean' },
|
|
28
40
|
stalled: { type: 'boolean' },
|
|
29
41
|
openQuestions: { type: 'array', items: { type: 'string' } },
|
|
@@ -45,11 +57,12 @@ const OUTCOME_SCHEMA = {
|
|
|
45
57
|
|
|
46
58
|
function claimPrompt(workerId, heldCount) {
|
|
47
59
|
return [
|
|
48
|
-
`You are the claimer for orchestrator worker "${workerId}". Use the orchestrator operator MCP tools (names end in claim, wait, status; load them with ToolSearch if needed).`,
|
|
60
|
+
`You are the claimer for orchestrator worker "${workerId}". Use the orchestrator operator MCP tools (names end in claim, wait, status; load them with ToolSearch if needed). You make no decisions: follow these steps exactly.`,
|
|
49
61
|
`1. Call claim with workerId "${workerId}" and access "local".`,
|
|
50
|
-
'2. If it returns a launch (status 200): read the markdown brief at briefPath and return { claimed: true, launchId, taskId, token, worktree, briefPath, briefMarkdown: <the full brief file>, pendingSeq, pendingAnswer } (pending fields only when the response carries pending).',
|
|
51
|
-
|
|
62
|
+
'2. If it returns a launch (status 200): read the markdown brief at briefPath and return { claimed: true, launchId, taskId, token, worktree, briefPath, briefMarkdown: <the full brief file>, model, effort, pendingSeq, pendingAnswer } (model and effort exactly as the response gives them; pending fields only when the response carries pending).',
|
|
63
|
+
`3. If it returns 404 (nothing claimable or paused): call wait with until "any" and timeoutMs ${CLAIM_WAIT_MS}, then call status, and compute stop:`,
|
|
52
64
|
` stop is true when finished is true, or stalled is true, or (claimable is 0 and ${heldCount} launches are held by other loops is false and (openQuestions is non-empty or awaitingReview is non-empty)). Otherwise stop is false.`,
|
|
65
|
+
` If stop is false and you have done fewer than ${CLAIM_POLLS} polls (a poll is one claim followed by one wait and one status), go back to step 1. Otherwise return { claimed: false, stop, finished, stalled, openQuestions, awaitingReview, claimable } from the last status.`,
|
|
53
66
|
'4. On any other error return { claimed: false, stop: true }.',
|
|
54
67
|
'Return only the structured output; do not do any task work.',
|
|
55
68
|
].join('\n')
|
|
@@ -80,7 +93,7 @@ async function loop(i) {
|
|
|
80
93
|
const workerId = `${runTag}-${i + 1}`
|
|
81
94
|
for (let cycle = 0; cycle < 200; cycle++) {
|
|
82
95
|
const heldCount = Object.keys(held).filter((k) => k !== workerId).length
|
|
83
|
-
const c = await agent(claimPrompt(workerId, heldCount), { label: `claim
|
|
96
|
+
const c = await agent(claimPrompt(workerId, heldCount), { label: `claim ${workerId}`, phase: 'Claim', schema: CLAIM_SCHEMA, model: claimModel, effort: claimEffort })
|
|
84
97
|
if (!c) break
|
|
85
98
|
if (!c.claimed) {
|
|
86
99
|
lastDigest = { finished: c.finished, stalled: c.stalled, openQuestions: c.openQuestions || [], awaitingReview: c.awaitingReview || [], claimable: c.claimable || 0 }
|
|
@@ -89,7 +102,8 @@ async function loop(i) {
|
|
|
89
102
|
}
|
|
90
103
|
held[workerId] = c.taskId
|
|
91
104
|
log(`${workerId}: working ${c.taskId}`)
|
|
92
|
-
|
|
105
|
+
// The launch's model and effort were fixed when the daemon prepared it; the worker agent runs with them. See specs/model-selection.
|
|
106
|
+
const r = await agent(workerPrompt(c), { label: `work ${workerId} ${c.taskId}`, phase: 'Work', schema: OUTCOME_SCHEMA, ...(c.model ? { model: c.model } : {}), ...(c.effort ? { effort: c.effort } : {}) })
|
|
93
107
|
delete held[workerId]
|
|
94
108
|
const outcome = r || { taskId: c.taskId, outcome: 'failed', summary: 'worker agent returned nothing' }
|
|
95
109
|
outcomes.push({ worker: workerId, ...outcome })
|
|
@@ -98,4 +112,4 @@ async function loop(i) {
|
|
|
98
112
|
}
|
|
99
113
|
|
|
100
114
|
await parallel(Array.from({ length: workers }, (_, i) => () => loop(i)))
|
|
101
|
-
return { runTag, workers, outcomes, digest: lastDigest }
|
|
115
|
+
return { runTag, workers, claimModel, claimEffort, outcomes, digest: lastDigest }
|