@crafts69guy/orchestra 0.4.0 → 0.6.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 +4 -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 +1 -1
- package/rules/orchestrator.md +2 -0
- package/scripts/orch-codex.mjs +6 -1
- package/scripts/orch.mjs +35 -0
package/README.md
CHANGED
|
@@ -48,6 +48,7 @@ codex plugin marketplace add crafts69guy/orchestra
|
|
|
48
48
|
| `orch doctor` | checks Node, git, `wt`, the harness CLIs and the installed plugins |
|
|
49
49
|
| `orch claude [--yolo] [args...]` | starts `claude` with `ORCH=1` and `--model opus` (unless you pass `--model`) |
|
|
50
50
|
| `orch codex [--yolo] [args...]` | starts `codex` with `ORCH=1` and the orchestration context (below) |
|
|
51
|
+
| `orch update` | updates the npm CLI, then the plugin for each harness on `PATH` (`claude plugin update orch@orchestra`, `codex plugin marketplace upgrade orchestra`); skips npm from a git checkout |
|
|
51
52
|
| `orch wt new\|merge\|list ...` | worktree helper used by the `dispatch` skill; prints JSON |
|
|
52
53
|
| `orch eval [args...]` | behaviour evals; only from a git checkout (see [AGENTS.md](AGENTS.md)) |
|
|
53
54
|
| `orch --help`, `orch --version` | usage and version |
|
|
@@ -59,6 +60,9 @@ permission model. Other arguments pass through to the harness, and so does its e
|
|
|
59
60
|
`orch claude` only sets `ORCH=1`; `ORCH=1 claude` works too. Without `ORCH=1` the hooks exit
|
|
60
61
|
silently and the session is untouched.
|
|
61
62
|
|
|
63
|
+
The handoff brief to `implementer` includes a Docs/JSDoc acceptance criterion (new or changed exports
|
|
64
|
+
carry JSDoc, touched docs are updated). `ORCH_DOCS=0` turns it off; any other value keeps it on.
|
|
65
|
+
|
|
62
66
|
## Codex
|
|
63
67
|
|
|
64
68
|
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
package/rules/orchestrator.md
CHANGED
|
@@ -13,6 +13,7 @@ Subagents: `scoper`, `implementer`, `reviewer`, `shipper`. On Claude Code call t
|
|
|
13
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
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).
|
|
15
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 -->
|
|
16
17
|
- **Shipping:** delegate commit, push and PR to `shipper` only after review passes and the user asked to ship.
|
|
17
18
|
- **Stays with you:** scope, architecture, cross-cutting decisions, root-cause analysis, merge conflicts, escalations.
|
|
18
19
|
|
|
@@ -20,6 +21,7 @@ Subagents: `scoper`, `implementer`, `reviewer`, `shipper`. On Claude Code call t
|
|
|
20
21
|
|
|
21
22
|
Subagents do not see this conversation. Decide interfaces, names and libraries before delegating, then send:
|
|
22
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 -->
|
|
23
25
|
Point to the plan/brief file and section instead of restating it. Pass paths and commit SHAs, not "what we discussed".
|
|
24
26
|
|
|
25
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.mjs
CHANGED
|
@@ -13,6 +13,7 @@ commands:
|
|
|
13
13
|
codex [--yolo] [args] run codex with the orch workflow
|
|
14
14
|
wt new|merge|list ... manage task worktrees (prints JSON)
|
|
15
15
|
doctor check prerequisites
|
|
16
|
+
update update the npm CLI and the installed Claude/Codex plugins
|
|
16
17
|
eval [args] run the behaviour evals (git checkout only)
|
|
17
18
|
help, -h, --help show this help
|
|
18
19
|
-V, --version print the version
|
|
@@ -35,6 +36,26 @@ export function claudeArgs(args) {
|
|
|
35
36
|
];
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Steps of `orch update` as `[cmd, args]`. The npm step is skipped in a git checkout (update it
|
|
41
|
+
* with `git pull`); a harness step is skipped when that harness is not on PATH.
|
|
42
|
+
*/
|
|
43
|
+
export function updateSteps(io) {
|
|
44
|
+
const has = (cmd) => io.hasCommand(cmd, io.env.PATH ?? '');
|
|
45
|
+
return [
|
|
46
|
+
...(io.exists(join(io.root, '.git'))
|
|
47
|
+
? []
|
|
48
|
+
: [['npm', ['install', '-g', '@crafts69guy/orchestra@latest']]]),
|
|
49
|
+
...(has('claude')
|
|
50
|
+
? [
|
|
51
|
+
['claude', ['plugin', 'marketplace', 'update', 'orchestra']],
|
|
52
|
+
['claude', ['plugin', 'update', 'orch@orchestra']],
|
|
53
|
+
]
|
|
54
|
+
: []),
|
|
55
|
+
...(has('codex') ? [['codex', ['plugin', 'marketplace', 'upgrade', 'orchestra']]] : []),
|
|
56
|
+
];
|
|
57
|
+
}
|
|
58
|
+
|
|
38
59
|
/** Each command takes `(args, io)` and returns the exit code. */
|
|
39
60
|
export const commands = {
|
|
40
61
|
claude: (args, io) =>
|
|
@@ -62,6 +83,20 @@ export const commands = {
|
|
|
62
83
|
io.stdout(`${lines.join('\n')}\n`);
|
|
63
84
|
return code;
|
|
64
85
|
},
|
|
86
|
+
/** Runs every step even after a failure; returns the first non-zero code. */
|
|
87
|
+
update(_args, io) {
|
|
88
|
+
if (io.exists(join(io.root, '.git'))) {
|
|
89
|
+
io.stderr(`orch: ${io.root} is a git checkout; skipping npm (run git pull there)\n`);
|
|
90
|
+
}
|
|
91
|
+
let code = 0;
|
|
92
|
+
for (const [cmd, args] of updateSteps(io)) {
|
|
93
|
+
io.stdout(`$ ${cmd} ${args.join(' ')}\n`);
|
|
94
|
+
const c = spawnExit(io.spawn(cmd, args, { env: io.env }), cmd, io.stderr, 'skipping.');
|
|
95
|
+
if (c !== 0 && code === 0) code = c;
|
|
96
|
+
}
|
|
97
|
+
if (code === 0) io.stdout('orch: updated; restart Claude Code / Codex sessions to apply\n');
|
|
98
|
+
return code;
|
|
99
|
+
},
|
|
65
100
|
eval(args, io) {
|
|
66
101
|
const script = join(io.root, 'evals', '_support', 'run.sh');
|
|
67
102
|
if (!io.exists(script)) {
|