@crafts69guy/orchestra 0.3.0 → 0.5.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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +3 -0
- package/agents/implementer.md +1 -0
- package/codex/agents/implementer.toml +1 -0
- package/hooks/session-start.mjs +21 -3
- package/hooks/subagent-stop.mjs +34 -2
- package/package.json +3 -2
- package/rules/orchestrator.md +4 -1
- package/scripts/orch-codex.mjs +6 -1
- package/scripts/orch-wt.mjs +17 -2
- package/skills/dispatch/SKILL.md +5 -3
- package/skills/plan/SKILL.md +12 -5
package/README.md
CHANGED
|
@@ -59,6 +59,9 @@ permission model. Other arguments pass through to the harness, and so does its e
|
|
|
59
59
|
`orch claude` only sets `ORCH=1`; `ORCH=1 claude` works too. Without `ORCH=1` the hooks exit
|
|
60
60
|
silently and the session is untouched.
|
|
61
61
|
|
|
62
|
+
The handoff brief to `implementer` includes a Docs/JSDoc acceptance criterion (new or changed exports
|
|
63
|
+
carry JSDoc, touched docs are updated). `ORCH_DOCS=0` turns it off; any other value keeps it on.
|
|
64
|
+
|
|
62
65
|
## Codex
|
|
63
66
|
|
|
64
67
|
Codex does not trust project agents or plugin hooks by default, so `orch codex` runs `codex`
|
package/agents/implementer.md
CHANGED
|
@@ -20,3 +20,4 @@ Report with these sections:
|
|
|
20
20
|
- Changes: files touched and what changed.
|
|
21
21
|
- Validation: each command you ran and its result, as evidence.
|
|
22
22
|
- Notes: deviations, open questions, anything unverified.
|
|
23
|
+
- Docs: if the brief includes Docs/JSDoc, list the JSDoc and docs you added.
|
|
@@ -18,4 +18,5 @@ Report with these sections:
|
|
|
18
18
|
- Changes: files touched and what changed.
|
|
19
19
|
- Validation: each command you ran and its result, as evidence.
|
|
20
20
|
- Notes: deviations, open questions, anything unverified.
|
|
21
|
+
- Docs: if the brief includes Docs/JSDoc, list the JSDoc and docs you added.
|
|
21
22
|
'''
|
package/hooks/session-start.mjs
CHANGED
|
@@ -30,9 +30,18 @@ export function which(name, pathEnv = '') {
|
|
|
30
30
|
});
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/** Lines ending in `<!-- orch:docs -->` are the Docs/JSDoc criterion: dropped when off, marker stripped when on. */
|
|
34
|
+
export function applyDocs(rules, docs) {
|
|
35
|
+
return rules
|
|
36
|
+
.split('\n')
|
|
37
|
+
.filter((l) => docs || !l.includes('<!-- orch:docs -->'))
|
|
38
|
+
.map((l) => l.replace(/ *<!-- orch:docs -->$/, ''))
|
|
39
|
+
.join('\n');
|
|
40
|
+
}
|
|
41
|
+
|
|
33
42
|
/** `orchPath` is the absolute `bin/orch`; plugin bin/ is not on PATH under Codex, so name it. */
|
|
34
|
-
export function buildContext(rules, hasWt, orchPath) {
|
|
35
|
-
const withCli = `${rules.trimEnd()}\n\norch CLI: ${orchPath} (use \`${orchPath} wt new|merge|list\`)\n`;
|
|
43
|
+
export function buildContext(rules, hasWt, orchPath, docs = true) {
|
|
44
|
+
const withCli = `${applyDocs(rules, docs).trimEnd()}\n\norch CLI: ${orchPath} (use \`${orchPath} wt new|merge|list\`)\n`;
|
|
36
45
|
return hasWt ? withCli : `${withCli}\n${WT_WARNING}\n`;
|
|
37
46
|
}
|
|
38
47
|
|
|
@@ -50,7 +59,16 @@ export function run({ env, root, read, hasCommand, stdout, stderr }) {
|
|
|
50
59
|
stderr(`orch: rules/orchestrator.md not found under ${root}; skipping injection\n`);
|
|
51
60
|
return 0;
|
|
52
61
|
}
|
|
53
|
-
stdout(
|
|
62
|
+
stdout(
|
|
63
|
+
formatOutput(
|
|
64
|
+
buildContext(
|
|
65
|
+
rules,
|
|
66
|
+
hasCommand('wt', env.PATH),
|
|
67
|
+
join(root, 'bin', 'orch'),
|
|
68
|
+
env.ORCH_DOCS !== '0',
|
|
69
|
+
),
|
|
70
|
+
),
|
|
71
|
+
);
|
|
54
72
|
return 0;
|
|
55
73
|
}
|
|
56
74
|
|
package/hooks/subagent-stop.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
2
3
|
import { readStdin, roleOf } from './guard.mjs';
|
|
3
4
|
import { isEntry } from './session-start.mjs';
|
|
4
5
|
|
|
@@ -13,8 +14,36 @@ export function hasEvidence(message) {
|
|
|
13
14
|
return typeof message === 'string' && STATUS.test(message) && VALIDATION.test(message);
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
/**
|
|
18
|
+
* The `message` of the last `SubagentHandback` tool call in a subagent transcript (JSONL), or
|
|
19
|
+
* undefined when the path is missing, unreadable or holds no handback. Never throws.
|
|
20
|
+
*/
|
|
21
|
+
export function handbackReport(path, readFile = (p) => readFileSync(p, 'utf8')) {
|
|
22
|
+
if (typeof path !== 'string' || path === '') return undefined;
|
|
23
|
+
let report;
|
|
24
|
+
try {
|
|
25
|
+
for (const line of readFile(path).split('\n')) {
|
|
26
|
+
let content;
|
|
27
|
+
try {
|
|
28
|
+
content = JSON.parse(line)?.message?.content;
|
|
29
|
+
} catch {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (!Array.isArray(content)) continue;
|
|
33
|
+
for (const part of content) {
|
|
34
|
+
if (part?.type === 'tool_use' && part.name === 'SubagentHandback') {
|
|
35
|
+
report = part.input?.message;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
return report;
|
|
43
|
+
}
|
|
44
|
+
|
|
16
45
|
/** Returns the exit code; never throws, bad stdin JSON is a silent allow. */
|
|
17
|
-
export function run({ env, input, stdout }) {
|
|
46
|
+
export function run({ env, input, stdout, readFile }) {
|
|
18
47
|
if (env.ORCH !== '1') return 0;
|
|
19
48
|
let payload;
|
|
20
49
|
try {
|
|
@@ -23,7 +52,10 @@ export function run({ env, input, stdout }) {
|
|
|
23
52
|
return 0;
|
|
24
53
|
}
|
|
25
54
|
if (roleOf(payload?.agent_type) !== 'implementer' || payload?.stop_hook_active === true) return 0;
|
|
26
|
-
if (
|
|
55
|
+
if (
|
|
56
|
+
!hasEvidence(payload?.last_assistant_message) &&
|
|
57
|
+
!hasEvidence(handbackReport(payload?.agent_transcript_path, readFile))
|
|
58
|
+
) {
|
|
27
59
|
stdout(JSON.stringify({ decision: 'block', reason: BLOCK_REASON }));
|
|
28
60
|
}
|
|
29
61
|
return 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crafts69guy/orchestra",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Cross-harness (Claude Code + Codex) orchestration workflow plugin",
|
|
6
6
|
"license": "MIT",
|
|
@@ -46,7 +46,8 @@
|
|
|
46
46
|
"gen:codex": "node scripts/gen-codex-agents.mjs",
|
|
47
47
|
"check": "npm run lint && npm run gen:codex -- --check && npm test",
|
|
48
48
|
"validate": "claude plugin validate .",
|
|
49
|
-
"eval": "sh evals/_support/run.sh"
|
|
49
|
+
"eval": "sh evals/_support/run.sh",
|
|
50
|
+
"eval:bench": "sh evals/_support/bench.sh"
|
|
50
51
|
},
|
|
51
52
|
"devDependencies": {
|
|
52
53
|
"@biomejs/biome": "2.5.15",
|
package/rules/orchestrator.md
CHANGED
|
@@ -10,8 +10,10 @@ Subagents: `scoper`, `implementer`, `reviewer`, `shipper`. On Claude Code call t
|
|
|
10
10
|
- **Scoping:** run `scoper` first on non-trivial work in unfamiliar code, unless you expect to read 3 or fewer new files. Spot-check its brief.
|
|
11
11
|
- **Implementation:** delegate to `implementer` when the change spans 2+ files, adds or fixes tests, or needs more than one edit-and-test cycle.
|
|
12
12
|
- **Planning:** for work with 2+ separable parts, use the `plan` skill to write `.scratch/<feature>/plan.md` before implementing.
|
|
13
|
-
- **Parallel:** when a plan has 2+ tasks
|
|
13
|
+
- **Parallel:** use the `dispatch` skill without asking when a plan has 2+ independent tasks (no `depends_on` path, disjoint `files`) that are each substantial: its own module or feature needing several edit-and-test cycles, roughly 100+ changed lines. Independent small tasks go to one `implementer` as a single batched task, done in order, not to dispatch. Never run two writers on the same checkout.
|
|
14
|
+
Measured: dispatch was no faster on 4 tiny modules (115 s vs 118 s, +45% cost) and 21% faster on 3 large ones (+22% cost).
|
|
14
15
|
- **Review:** review every implementer diff yourself and rerun its validation. Use `reviewer` (fresh context: diff, brief, acceptance criteria) for risky changes (security, concurrency, migrations, public APIs) or on request. At most 2 fix rounds.
|
|
16
|
+
Also check the Docs/JSDoc criterion. <!-- orch:docs -->
|
|
15
17
|
- **Shipping:** delegate commit, push and PR to `shipper` only after review passes and the user asked to ship.
|
|
16
18
|
- **Stays with you:** scope, architecture, cross-cutting decisions, root-cause analysis, merge conflicts, escalations.
|
|
17
19
|
|
|
@@ -19,6 +21,7 @@ Subagents: `scoper`, `implementer`, `reviewer`, `shipper`. On Claude Code call t
|
|
|
19
21
|
|
|
20
22
|
Subagents do not see this conversation. Decide interfaces, names and libraries before delegating, then send:
|
|
21
23
|
objective · decisions already made · files in scope and out of scope · constraints · validation command · stop conditions.
|
|
24
|
+
Docs/JSDoc: new or changed exported types, functions and their fields carry JSDoc in the repo's doc style, and docs the change touches (ADRs, glossary, guides) are updated; the orchestrator checks this when reviewing the diff. <!-- orch:docs -->
|
|
22
25
|
Point to the plan/brief file and section instead of restating it. Pass paths and commit SHAs, not "what we discussed".
|
|
23
26
|
|
|
24
27
|
## Results
|
package/scripts/orch-codex.mjs
CHANGED
|
@@ -63,7 +63,12 @@ export function run({ argv, env, root, cwd, read, readdir, hasCommand, gitDir, s
|
|
|
63
63
|
stderr(`orch codex: rules/orchestrator.md not found under ${root}\n`);
|
|
64
64
|
return 1;
|
|
65
65
|
}
|
|
66
|
-
const context = buildContext(
|
|
66
|
+
const context = buildContext(
|
|
67
|
+
rules,
|
|
68
|
+
hasCommand('wt', env.PATH),
|
|
69
|
+
join(root, 'bin', 'orch'),
|
|
70
|
+
env.ORCH_DOCS !== '0',
|
|
71
|
+
);
|
|
67
72
|
const prefix = buildArgs({
|
|
68
73
|
context,
|
|
69
74
|
agents: agentArgs(root, { read, readdir }),
|
package/scripts/orch-wt.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
|
-
import { appendFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { appendFileSync, existsSync, mkdirSync, rmdirSync } from 'node:fs';
|
|
3
3
|
import { resolve } from 'node:path';
|
|
4
4
|
import { parseArgs } from 'node:util';
|
|
5
5
|
|
|
@@ -122,9 +122,12 @@ export function merge(deps, cwd, feature, task, message, yes) {
|
|
|
122
122
|
}
|
|
123
123
|
if (tree.status !== 0) return fail(task, tree.stderr);
|
|
124
124
|
|
|
125
|
+
const yesArg = yes ? ['--yes'] : [];
|
|
126
|
+
// --no-remove: wt's own cleanup runs in the background and returns before the directory is
|
|
127
|
+
// gone (or never finishes when the caller's sandbox kills it), so remove in the foreground below.
|
|
125
128
|
const r = deps.exec(
|
|
126
129
|
'wt',
|
|
127
|
-
['-C', path, 'merge', feature, '--no-commit',
|
|
130
|
+
['-C', path, 'merge', feature, '--no-commit', '--no-remove', ...yesArg],
|
|
128
131
|
cwd,
|
|
129
132
|
);
|
|
130
133
|
if (r.status !== 0) {
|
|
@@ -135,6 +138,17 @@ export function merge(deps, cwd, feature, task, message, yes) {
|
|
|
135
138
|
return { code: 2, result: { status: 'conflict', task, files } };
|
|
136
139
|
}
|
|
137
140
|
const commit = git(deps, cwd, 'rev-parse', feature).stdout.trim();
|
|
141
|
+
const left = `merged as ${commit}, but ${path} is left`;
|
|
142
|
+
// -D: wt judges "merged" against the default branch, but the branch is now in `feature`.
|
|
143
|
+
const rm = deps.exec('wt', ['remove', branch, '--foreground', '-D', ...yesArg], cwd);
|
|
144
|
+
if (rm.status !== 0) return fail(task, `${left}: ${rm.stderr}`);
|
|
145
|
+
if (deps.exists(path)) {
|
|
146
|
+
try {
|
|
147
|
+
deps.rmdir(path); // only an empty leftover: rmdir refuses a non-empty directory
|
|
148
|
+
} catch (e) {
|
|
149
|
+
return fail(task, `${left}: ${e.message}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
138
152
|
return { code: 0, result: { status: 'merged', task, commit } };
|
|
139
153
|
}
|
|
140
154
|
|
|
@@ -164,5 +178,6 @@ export const realDeps = {
|
|
|
164
178
|
exec,
|
|
165
179
|
exists: existsSync,
|
|
166
180
|
mkdir: (p) => mkdirSync(p, { recursive: true }),
|
|
181
|
+
rmdir: rmdirSync,
|
|
167
182
|
append: appendFileSync,
|
|
168
183
|
};
|
package/skills/dispatch/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: dispatch
|
|
3
|
-
description: "orch workflow: run independent tasks from .scratch/<feature>/plan.md in parallel, one implementer per git worktree (worktrunk), then merge them into the feature branch one at a time."
|
|
3
|
+
description: "orch workflow: run independent substantial tasks from .scratch/<feature>/plan.md in parallel, one implementer per git worktree (worktrunk), then merge them into the feature branch one at a time."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Dispatch
|
|
@@ -21,8 +21,10 @@ absolute path. Use that path where the plugin's `bin/` is not on `PATH` (Codex).
|
|
|
21
21
|
|
|
22
22
|
## Each wave
|
|
23
23
|
|
|
24
|
-
1. **Pick** the ready tasks: all `depends_on` merged, `files`
|
|
25
|
-
4 per wave; queue the rest.
|
|
24
|
+
1. **Pick** the ready `substantial` tasks: all `depends_on` merged, `files`
|
|
25
|
+
disjoint. At most 4 per wave; queue the rest. `small` tasks are never
|
|
26
|
+
dispatched: run them as one batched implementer task on `F`, before any wave
|
|
27
|
+
that depends on them.
|
|
26
28
|
2. **Create** worktrees: `orch wt new <F> <T1> <T2> …` → JSON list of
|
|
27
29
|
`{task, branch, path}`.
|
|
28
30
|
3. **Spawn** one implementer per task, all in the **same message** so they run
|
package/skills/plan/SKILL.md
CHANGED
|
@@ -32,6 +32,7 @@ Base branch: <F> # the branch the tasks merge into
|
|
|
32
32
|
goal: <one sentence, observable outcome>
|
|
33
33
|
files: [<paths this task owns>]
|
|
34
34
|
depends_on: []
|
|
35
|
+
size: small # or substantial (rule below)
|
|
35
36
|
validate: <command that proves this task works>
|
|
36
37
|
decisions: [<task-specific choices>]
|
|
37
38
|
```
|
|
@@ -43,14 +44,20 @@ Base branch: <F> # the branch the tasks merge into
|
|
|
43
44
|
share a file. If they must, add a `depends_on` edge or merge the tasks.
|
|
44
45
|
- Shared files that everyone touches (lockfiles, barrel exports, registries) go
|
|
45
46
|
to one task that the others depend on.
|
|
46
|
-
- Each task is 1 implementer-sized unit
|
|
47
|
+
- Each task is 1 implementer-sized unit (one module or feature), not a project.
|
|
48
|
+
- `size` is `substantial` when the task is its own module or feature needing
|
|
49
|
+
several edit-and-test cycles, roughly 100+ changed lines; otherwise `small`.
|
|
47
50
|
- `validate` runs inside the task's own worktree and must not need other
|
|
48
51
|
unmerged tasks.
|
|
49
52
|
- Don't split one feature into many tiny tasks. Separate features stay separate
|
|
50
|
-
tasks,
|
|
53
|
+
tasks (each can land as its own commit), but small ones are batched, not
|
|
54
|
+
dispatched (below).
|
|
51
55
|
|
|
52
56
|
## After writing
|
|
53
57
|
|
|
54
|
-
Show the user the task table (id, goal, depends_on) in a few lines. If 2+
|
|
55
|
-
are
|
|
56
|
-
one
|
|
58
|
+
Show the user the task table (id, goal, size, depends_on) in a few lines. If 2+
|
|
59
|
+
independent tasks are `substantial`, continue with the `dispatch` skill for
|
|
60
|
+
them. Batch the `small` tasks into one implementer task (all of them, in order,
|
|
61
|
+
on the feature branch); run the rest (a lone or dependent substantial task) with
|
|
62
|
+
its own implementer, one at a time. Parallel worktrees only pay off when each
|
|
63
|
+
task is substantial.
|