@crafts69guy/orchestra 0.0.0-stage → 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/.claude-plugin/marketplace.json +18 -0
- package/.claude-plugin/plugin.json +19 -0
- package/.codex-plugin/plugin.json +29 -0
- package/LICENSE +21 -0
- package/README.md +82 -2
- package/agents/.markdownlint-cli2.jsonc +1 -0
- package/agents/implementer.md +22 -0
- package/agents/reviewer.md +18 -0
- package/agents/scoper.md +18 -0
- package/agents/shipper.md +17 -0
- package/bin/orch +4 -0
- package/codex/agents/implementer.toml +21 -0
- package/codex/agents/reviewer.toml +18 -0
- package/codex/agents/scoper.toml +16 -0
- package/codex/agents/shipper.toml +14 -0
- package/hooks/guard.mjs +109 -0
- package/hooks/hooks.json +39 -0
- package/hooks/session-start.mjs +77 -0
- package/hooks/subagent-stop.mjs +40 -0
- package/package.json +53 -4
- package/rules/.markdownlint-cli2.jsonc +1 -0
- package/rules/orchestrator.md +28 -0
- package/scripts/doctor.mjs +82 -0
- package/scripts/gen-codex-agents.mjs +131 -0
- package/scripts/lib/frontmatter.mjs +22 -0
- package/scripts/orch-codex.mjs +74 -0
- package/scripts/orch-wt.mjs +168 -0
- package/scripts/orch.mjs +113 -0
- package/skills/dispatch/SKILL.md +55 -0
- package/skills/dispatch/references/claude.md +9 -0
- package/skills/dispatch/references/codex.md +12 -0
- package/skills/plan/SKILL.md +56 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
|
|
3
|
+
"name": "orchestra",
|
|
4
|
+
"description": "Cross-harness orchestration workflow for Claude Code and Codex.",
|
|
5
|
+
"owner": {
|
|
6
|
+
"name": "crafts69guy",
|
|
7
|
+
"url": "https://github.com/crafts69guy"
|
|
8
|
+
},
|
|
9
|
+
"plugins": [
|
|
10
|
+
{
|
|
11
|
+
"name": "orch",
|
|
12
|
+
"description": "Orchestrator workflow: scope, implement, review, ship via specialised subagents and worktrees.",
|
|
13
|
+
"source": "./",
|
|
14
|
+
"category": "productivity"
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"allowCrossMarketplaceDependenciesOn": ["ponytail"]
|
|
18
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "orch",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Orchestrator workflow: scope, implement, review, ship via specialised subagents and worktrees.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "crafts69guy"
|
|
7
|
+
},
|
|
8
|
+
"homepage": "https://github.com/crafts69guy/orchestra",
|
|
9
|
+
"repository": "https://github.com/crafts69guy/orchestra",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"keywords": ["orchestration", "subagents", "worktrees", "workflow"],
|
|
12
|
+
"hooks": "./hooks/hooks.json",
|
|
13
|
+
"dependencies": [
|
|
14
|
+
{
|
|
15
|
+
"name": "ponytail",
|
|
16
|
+
"marketplace": "ponytail"
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "orch",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Orchestrator workflow: scope, implement, review, ship via specialised subagents and worktrees.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "crafts69guy",
|
|
7
|
+
"url": "https://github.com/crafts69guy"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/crafts69guy/orchestra",
|
|
10
|
+
"repository": "https://github.com/crafts69guy/orchestra",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"keywords": ["orchestration", "subagents", "worktrees", "workflow"],
|
|
13
|
+
"skills": "./skills/",
|
|
14
|
+
"hooks": "./hooks/hooks.json",
|
|
15
|
+
"interface": {
|
|
16
|
+
"displayName": "Orchestra",
|
|
17
|
+
"shortDescription": "Orchestrator workflow for subagents and worktrees",
|
|
18
|
+
"longDescription": "Always-on orchestration rules: scope, implement, review and ship through specialised subagents, with parallel work in git worktrees.",
|
|
19
|
+
"developerName": "crafts69guy",
|
|
20
|
+
"category": "Productivity",
|
|
21
|
+
"capabilities": ["Instructions", "Lifecycle hooks"],
|
|
22
|
+
"websiteURL": "https://github.com/crafts69guy/orchestra",
|
|
23
|
+
"defaultPrompt": [
|
|
24
|
+
"Orchestrate this task: scope, implement, review, ship.",
|
|
25
|
+
"Split this work into parallel worktree tasks."
|
|
26
|
+
],
|
|
27
|
+
"brandColor": "#111111"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 crafts69guy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,83 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Orchestra
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Orchestra (plugin name `orch`) is a cross-harness orchestration workflow for Claude Code and
|
|
4
|
+
Codex. A main agent scopes, delegates, reviews and ships work through four specialised subagents
|
|
5
|
+
(`scoper`, `implementer`, `reviewer`, `shipper`), running independent tasks in parallel git
|
|
6
|
+
worktrees.
|
|
7
|
+
|
|
8
|
+
The rules are always-on but opt-in: they are injected at session start only when `ORCH=1`.
|
|
9
|
+
|
|
10
|
+
## Requirements
|
|
11
|
+
|
|
12
|
+
- Node.js 22.8 or newer (hooks and the CLI are plain Node scripts, zero runtime dependencies).
|
|
13
|
+
- [worktrunk](https://worktrunk.dev) (`brew install worktrunk`) for parallel dispatch. The
|
|
14
|
+
session hook warns when `wt` is missing.
|
|
15
|
+
- Claude Code and/or Codex.
|
|
16
|
+
- Claude Code: the `ponytail` plugin is declared as a dependency. Add its marketplace first:
|
|
17
|
+
`/plugin marketplace add DietrichGebert/ponytail`.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
Install the `orch` CLI, then let it check everything else:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
npm i -g @crafts69guy/orchestra
|
|
25
|
+
orch doctor
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`orch doctor` prints one line per check with a fix hint for each failure, and exits 1 if a
|
|
29
|
+
required check fails. Install the plugin for your harness as it suggests:
|
|
30
|
+
|
|
31
|
+
Claude Code:
|
|
32
|
+
|
|
33
|
+
```text
|
|
34
|
+
/plugin marketplace add crafts69guy/orchestra
|
|
35
|
+
/plugin install orch@orchestra
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Codex:
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
codex plugin marketplace add crafts69guy/orchestra
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Usage
|
|
45
|
+
|
|
46
|
+
| Command | Does |
|
|
47
|
+
|---|---|
|
|
48
|
+
| `orch doctor` | checks Node, git, `wt`, the harness CLIs and the installed plugins |
|
|
49
|
+
| `orch claude [--yolo] [args...]` | starts `claude` with `ORCH=1` and `--model opus` (unless you pass `--model`) |
|
|
50
|
+
| `orch codex [--yolo] [args...]` | starts `codex` with `ORCH=1` and the orchestration context (below) |
|
|
51
|
+
| `orch wt new\|merge\|list ...` | worktree helper used by the `dispatch` skill; prints JSON |
|
|
52
|
+
| `orch eval [args...]` | behaviour evals; only from a git checkout (see [AGENTS.md](AGENTS.md)) |
|
|
53
|
+
| `orch --help`, `orch --version` | usage and version |
|
|
54
|
+
|
|
55
|
+
`--yolo` turns off permission prompts for that run (`--dangerously-skip-permissions` on Claude,
|
|
56
|
+
`--dangerously-bypass-approvals-and-sandbox` on Codex). Without it the harness keeps its normal
|
|
57
|
+
permission model. Other arguments pass through to the harness, and so does its exit code.
|
|
58
|
+
|
|
59
|
+
`orch claude` only sets `ORCH=1`; `ORCH=1 claude` works too. Without `ORCH=1` the hooks exit
|
|
60
|
+
silently and the session is untouched.
|
|
61
|
+
|
|
62
|
+
## Codex
|
|
63
|
+
|
|
64
|
+
Codex does not trust project agents or plugin hooks by default, so `orch codex` runs `codex`
|
|
65
|
+
with `ORCH=1` and prepends, per run and without touching `~/.codex`:
|
|
66
|
+
|
|
67
|
+
- `-c developer_instructions=...`: the same context the SessionStart hook injects (rules, the
|
|
68
|
+
absolute path of `orch`, `wt` warning).
|
|
69
|
+
- `-c agents.<name>.config_file=...` and `.description=...` for each `codex/agents/*.toml`.
|
|
70
|
+
- `--add-dir <git common dir>` inside a git repo, because `workspace-write` blocks `.git` and
|
|
71
|
+
worktrees and commits need it.
|
|
72
|
+
|
|
73
|
+
It exits 1 if the rules file is missing and 127 if `codex` is not on `PATH`.
|
|
74
|
+
|
|
75
|
+
## Docs
|
|
76
|
+
|
|
77
|
+
- [Architecture](docs/ARCHITECTURE.md)
|
|
78
|
+
- [Research notes](docs/research/)
|
|
79
|
+
- [Contributing and dev commands](AGENTS.md)
|
|
80
|
+
|
|
81
|
+
## License
|
|
82
|
+
|
|
83
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "config": { "MD041": false } }
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: implementer
|
|
3
|
+
description: "orch workflow: writes, modifies or refactors code and adds or fixes tests for a well-defined plan. For changes spanning 2+ files or needing more than one edit-and-test cycle."
|
|
4
|
+
model: sonnet
|
|
5
|
+
effort: medium
|
|
6
|
+
tools: Read, Write, Edit, Grep, Glob, Bash
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
You are a senior engineer implementing a well-defined plan with minimal changes.
|
|
10
|
+
|
|
11
|
+
- Follow existing conventions; reuse existing helpers.
|
|
12
|
+
- Add or update tests, run the project's checks, and review your own diff.
|
|
13
|
+
- Do not broaden scope, redesign working code, or weaken tests.
|
|
14
|
+
- Do not commit or push.
|
|
15
|
+
- If the plan conflicts with the code in a way that needs a significant decision, stop and report back.
|
|
16
|
+
|
|
17
|
+
Report with these sections:
|
|
18
|
+
|
|
19
|
+
- Status: done, partial, or blocked.
|
|
20
|
+
- Changes: files touched and what changed.
|
|
21
|
+
- Validation: each command you ran and its result, as evidence.
|
|
22
|
+
- Notes: deviations, open questions, anything unverified.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: reviewer
|
|
3
|
+
description: "orch workflow: independent, read-only code review. For explicit requests or risky changes (security, concurrency, migrations, public APIs). Never modifies files."
|
|
4
|
+
model: opus
|
|
5
|
+
effort: high
|
|
6
|
+
tools: Read, Grep, Glob, Bash
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
You are an independent reviewer. Do not modify files.
|
|
10
|
+
|
|
11
|
+
Pass 1, correctness: check correctness, regressions, edge cases, and test quality.
|
|
12
|
+
|
|
13
|
+
Pass 2, over-engineering: look for unneeded abstractions, speculative options and dead code,
|
|
14
|
+
using the ponytail-review lens if that skill is available.
|
|
15
|
+
|
|
16
|
+
Rank findings BLOCKER, MAJOR, or MINOR with file:line. Do not invent problems.
|
|
17
|
+
|
|
18
|
+
If nothing is wrong, say exactly: PASS — no blocking or major issues found.
|
package/agents/scoper.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: scoper
|
|
3
|
+
description: "orch workflow: read-only scoping of a non-trivial task in unfamiliar code. Gathers facts and returns a brief."
|
|
4
|
+
model: haiku
|
|
5
|
+
effort: low
|
|
6
|
+
maxTurns: 30
|
|
7
|
+
tools: Read, Grep, Glob, Bash, mcp__cocoindex-code__search, ToolSearch
|
|
8
|
+
disallowedTools: Write, Edit
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
You are a scoping specialist. Make no changes and no architecture decisions.
|
|
12
|
+
|
|
13
|
+
Report: Goal, Acceptance criteria, Relevant code, Scope, Validation, Risks, Possible split.
|
|
14
|
+
|
|
15
|
+
Every claim about code must cite a file path.
|
|
16
|
+
|
|
17
|
+
Flag the task as complex if it touches several subsystems, changes a public format, involves
|
|
18
|
+
concurrency or migrations, lacks clear criteria, or has behavior you cannot locate.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: shipper
|
|
3
|
+
description: "orch workflow: commits, pushes and opens PRs after review is done. Handles only git and GitHub."
|
|
4
|
+
model: haiku
|
|
5
|
+
effort: low
|
|
6
|
+
maxTurns: 15
|
|
7
|
+
tools: Read, Bash
|
|
8
|
+
codex-sandbox: inherit
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
You handle only git and GitHub.
|
|
12
|
+
|
|
13
|
+
- Pre-flight: check status, branch, diffs, and unrelated files; stop and report on any problem.
|
|
14
|
+
- Stage files by name, never `git add -A`.
|
|
15
|
+
- Never force-push, amend, merge, or bypass hooks.
|
|
16
|
+
- Follow the repo's existing commit message style and any attribution lines given in your instructions.
|
|
17
|
+
- Write PR descriptions from the repo template if present.
|
package/bin/orch
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# GENERATED by scripts/gen-codex-agents.mjs from agents/implementer.md. Do not edit; run `npm run gen:codex`.
|
|
2
|
+
|
|
3
|
+
name = "implementer"
|
|
4
|
+
description = "orch workflow: writes, modifies or refactors code and adds or fixes tests for a well-defined plan. For changes spanning 2+ files or needing more than one edit-and-test cycle."
|
|
5
|
+
model_reasoning_effort = "medium"
|
|
6
|
+
developer_instructions = '''
|
|
7
|
+
You are a senior engineer implementing a well-defined plan with minimal changes.
|
|
8
|
+
|
|
9
|
+
- Follow existing conventions; reuse existing helpers.
|
|
10
|
+
- Add or update tests, run the project's checks, and review your own diff.
|
|
11
|
+
- Do not broaden scope, redesign working code, or weaken tests.
|
|
12
|
+
- Do not commit or push.
|
|
13
|
+
- If the plan conflicts with the code in a way that needs a significant decision, stop and report back.
|
|
14
|
+
|
|
15
|
+
Report with these sections:
|
|
16
|
+
|
|
17
|
+
- Status: done, partial, or blocked.
|
|
18
|
+
- Changes: files touched and what changed.
|
|
19
|
+
- Validation: each command you ran and its result, as evidence.
|
|
20
|
+
- Notes: deviations, open questions, anything unverified.
|
|
21
|
+
'''
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# GENERATED by scripts/gen-codex-agents.mjs from agents/reviewer.md. Do not edit; run `npm run gen:codex`.
|
|
2
|
+
|
|
3
|
+
name = "reviewer"
|
|
4
|
+
description = "orch workflow: independent, read-only code review. For explicit requests or risky changes (security, concurrency, migrations, public APIs). Never modifies files."
|
|
5
|
+
model_reasoning_effort = "high"
|
|
6
|
+
sandbox_mode = "read-only"
|
|
7
|
+
developer_instructions = '''
|
|
8
|
+
You are an independent reviewer. Do not modify files.
|
|
9
|
+
|
|
10
|
+
Pass 1, correctness: check correctness, regressions, edge cases, and test quality.
|
|
11
|
+
|
|
12
|
+
Pass 2, over-engineering: look for unneeded abstractions, speculative options and dead code,
|
|
13
|
+
using the ponytail-review lens if that skill is available.
|
|
14
|
+
|
|
15
|
+
Rank findings BLOCKER, MAJOR, or MINOR with file:line. Do not invent problems.
|
|
16
|
+
|
|
17
|
+
If nothing is wrong, say exactly: PASS — no blocking or major issues found.
|
|
18
|
+
'''
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# GENERATED by scripts/gen-codex-agents.mjs from agents/scoper.md. Do not edit; run `npm run gen:codex`.
|
|
2
|
+
|
|
3
|
+
name = "scoper"
|
|
4
|
+
description = "orch workflow: read-only scoping of a non-trivial task in unfamiliar code. Gathers facts and returns a brief."
|
|
5
|
+
model_reasoning_effort = "low"
|
|
6
|
+
sandbox_mode = "read-only"
|
|
7
|
+
developer_instructions = '''
|
|
8
|
+
You are a scoping specialist. Make no changes and no architecture decisions.
|
|
9
|
+
|
|
10
|
+
Report: Goal, Acceptance criteria, Relevant code, Scope, Validation, Risks, Possible split.
|
|
11
|
+
|
|
12
|
+
Every claim about code must cite a file path.
|
|
13
|
+
|
|
14
|
+
Flag the task as complex if it touches several subsystems, changes a public format, involves
|
|
15
|
+
concurrency or migrations, lacks clear criteria, or has behavior you cannot locate.
|
|
16
|
+
'''
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# GENERATED by scripts/gen-codex-agents.mjs from agents/shipper.md. Do not edit; run `npm run gen:codex`.
|
|
2
|
+
|
|
3
|
+
name = "shipper"
|
|
4
|
+
description = "orch workflow: commits, pushes and opens PRs after review is done. Handles only git and GitHub."
|
|
5
|
+
model_reasoning_effort = "low"
|
|
6
|
+
developer_instructions = '''
|
|
7
|
+
You handle only git and GitHub.
|
|
8
|
+
|
|
9
|
+
- Pre-flight: check status, branch, diffs, and unrelated files; stop and report on any problem.
|
|
10
|
+
- Stage files by name, never `git add -A`.
|
|
11
|
+
- Never force-push, amend, merge, or bypass hooks.
|
|
12
|
+
- Follow the repo's existing commit message style and any attribution lines given in your instructions.
|
|
13
|
+
- Write PR descriptions from the repo template if present.
|
|
14
|
+
'''
|
package/hooks/guard.mjs
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { basename } from 'node:path';
|
|
4
|
+
import { isEntry } from './session-start.mjs';
|
|
5
|
+
|
|
6
|
+
const NO_COMMIT = new Set(['implementer', 'scoper', 'reviewer']);
|
|
7
|
+
const GIT_VALUE_OPTS = new Set(['-C', '-c', '--git-dir', '--work-tree', '--namespace']);
|
|
8
|
+
const GH_VALUE_OPTS = new Set(['-R', '--repo']);
|
|
9
|
+
const GH_PR_WRITE = new Set(['create', 'merge', 'edit']);
|
|
10
|
+
|
|
11
|
+
/** `orch:implementer` (Claude) and `implementer` (Codex) both give `implementer`; no agent_type is the main thread. */
|
|
12
|
+
export function roleOf(agentType) {
|
|
13
|
+
if (typeof agentType !== 'string' || agentType === '') return 'main';
|
|
14
|
+
return agentType.replace(/^[^:]+:/, '');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ponytail: whitespace tokenizer, quotes are not parsed, so a chain operator inside a quoted
|
|
18
|
+
// string splits early (can only over-deny); `bash -c '...'` wrappers are not unwrapped.
|
|
19
|
+
function segments(command) {
|
|
20
|
+
return command.split(/&&|\|\||[;|&\n]/).map((s) => s.trim().split(/\s+/));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function skipEnv(tokens) {
|
|
24
|
+
let i = 0;
|
|
25
|
+
while (i < tokens.length && (/^[A-Za-z_]\w*=/.test(tokens[i]) || tokens[i] === 'env')) i++;
|
|
26
|
+
return tokens.slice(i);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Tokens after the global options of `git`/`gh`, so `git -C x push` yields ['push']. */
|
|
30
|
+
function afterOptions(tokens, valueOpts) {
|
|
31
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
32
|
+
if (valueOpts.has(tokens[i])) i++;
|
|
33
|
+
else if (!tokens[i].startsWith('-')) return tokens.slice(i);
|
|
34
|
+
}
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Classifies one command segment as 'push', 'pr', 'commit' or null. */
|
|
39
|
+
export function classify(tokens) {
|
|
40
|
+
const [bin, ...args] = skipEnv(tokens);
|
|
41
|
+
if (bin === undefined) return null;
|
|
42
|
+
const name = basename(bin);
|
|
43
|
+
if (name === 'git') {
|
|
44
|
+
const [sub] = afterOptions(args, GIT_VALUE_OPTS);
|
|
45
|
+
return sub === 'push' || sub === 'commit' ? sub : null;
|
|
46
|
+
}
|
|
47
|
+
if (name === 'gh') {
|
|
48
|
+
const [group, action] = afterOptions(args, GH_VALUE_OPTS);
|
|
49
|
+
return group === 'pr' && GH_PR_WRITE.has(action) ? 'pr' : null;
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Returns a deny reason, or null to allow. */
|
|
55
|
+
export function decide(role, command) {
|
|
56
|
+
if (typeof command !== 'string') return null;
|
|
57
|
+
for (const seg of segments(command)) {
|
|
58
|
+
const kind = classify(seg);
|
|
59
|
+
if ((kind === 'push' || kind === 'pr') && role !== 'shipper') {
|
|
60
|
+
return `orch: only the shipper role may push or manage pull requests (role: ${role}).`;
|
|
61
|
+
}
|
|
62
|
+
if (kind === 'commit' && NO_COMMIT.has(role)) {
|
|
63
|
+
return `orch: the ${role} role must not commit; leave changes in the working tree for the orchestrator.`;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function formatDeny(reason) {
|
|
70
|
+
return JSON.stringify({
|
|
71
|
+
hookSpecificOutput: {
|
|
72
|
+
hookEventName: 'PreToolUse',
|
|
73
|
+
permissionDecision: 'deny',
|
|
74
|
+
permissionDecisionReason: reason,
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Returns the exit code; never throws, bad stdin JSON is a silent allow. */
|
|
80
|
+
export function run({ env, input, stdout }) {
|
|
81
|
+
if (env.ORCH !== '1') return 0;
|
|
82
|
+
let payload;
|
|
83
|
+
try {
|
|
84
|
+
payload = JSON.parse(input);
|
|
85
|
+
} catch {
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
const reason = decide(roleOf(payload?.agent_type), payload?.tool_input?.command);
|
|
89
|
+
if (reason !== null) stdout(formatDeny(reason));
|
|
90
|
+
return 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function readStdin() {
|
|
94
|
+
try {
|
|
95
|
+
return readFileSync(0, 'utf8');
|
|
96
|
+
} catch {
|
|
97
|
+
return '';
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function main(proc = process, read = readStdin) {
|
|
102
|
+
proc.exitCode = run({
|
|
103
|
+
env: proc.env,
|
|
104
|
+
input: proc.env.ORCH === '1' ? read() : '',
|
|
105
|
+
stdout: (s) => proc.stdout.write(s),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (isEntry(process.argv[1], import.meta.url)) main();
|
package/hooks/hooks.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"SessionStart": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "startup|resume|clear|compact",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/session-start.mjs\"",
|
|
10
|
+
"timeout": 5
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"PreToolUse": [
|
|
16
|
+
{
|
|
17
|
+
"matcher": "Bash",
|
|
18
|
+
"hooks": [
|
|
19
|
+
{
|
|
20
|
+
"type": "command",
|
|
21
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/guard.mjs\"",
|
|
22
|
+
"timeout": 5
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
],
|
|
27
|
+
"SubagentStop": [
|
|
28
|
+
{
|
|
29
|
+
"hooks": [
|
|
30
|
+
{
|
|
31
|
+
"type": "command",
|
|
32
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/subagent-stop.mjs\"",
|
|
33
|
+
"timeout": 5
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { accessSync, constants, readFileSync, realpathSync } from 'node:fs';
|
|
3
|
+
import { delimiter, dirname, join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
export const WT_WARNING =
|
|
7
|
+
'orch: `wt` (worktrunk) not found on PATH; parallel dispatch needs it (`brew install worktrunk`).';
|
|
8
|
+
|
|
9
|
+
export function pluginRoot(metaUrl) {
|
|
10
|
+
return join(dirname(fileURLToPath(metaUrl)), '..');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function readRules(root, read = readFileSync) {
|
|
14
|
+
try {
|
|
15
|
+
return read(join(root, 'rules', 'orchestrator.md'), 'utf8');
|
|
16
|
+
} catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ponytail: POSIX-only executable lookup; add PATHEXT handling if Windows matters.
|
|
22
|
+
export function which(name, pathEnv = '') {
|
|
23
|
+
return pathEnv.split(delimiter).some((dir) => {
|
|
24
|
+
try {
|
|
25
|
+
accessSync(join(dir, name), constants.X_OK);
|
|
26
|
+
return true;
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** `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`;
|
|
36
|
+
return hasWt ? withCli : `${withCli}\n${WT_WARNING}\n`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function formatOutput(additionalContext) {
|
|
40
|
+
return JSON.stringify({
|
|
41
|
+
hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext },
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Returns the process exit code; never throws for a missing rules file. */
|
|
46
|
+
export function run({ env, root, read, hasCommand, stdout, stderr }) {
|
|
47
|
+
if (env.ORCH !== '1') return 0;
|
|
48
|
+
const rules = readRules(root, read);
|
|
49
|
+
if (rules === null) {
|
|
50
|
+
stderr(`orch: rules/orchestrator.md not found under ${root}; skipping injection\n`);
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
stdout(formatOutput(buildContext(rules, hasCommand('wt', env.PATH), join(root, 'bin', 'orch'))));
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function main(proc = process, metaUrl = import.meta.url) {
|
|
58
|
+
proc.exitCode = run({
|
|
59
|
+
env: proc.env,
|
|
60
|
+
root: pluginRoot(metaUrl),
|
|
61
|
+
read: readFileSync,
|
|
62
|
+
hasCommand: which,
|
|
63
|
+
stdout: (s) => proc.stdout.write(s),
|
|
64
|
+
stderr: (s) => proc.stderr.write(s),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// realpath both sides: a symlinked plugin dir (stow, --plugin-dir) must still run main.
|
|
69
|
+
export function isEntry(argv1, metaUrl, real = realpathSync) {
|
|
70
|
+
try {
|
|
71
|
+
return real(argv1) === real(fileURLToPath(metaUrl));
|
|
72
|
+
} catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (isEntry(process.argv[1], import.meta.url)) main();
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readStdin, roleOf } from './guard.mjs';
|
|
3
|
+
import { isEntry } from './session-start.mjs';
|
|
4
|
+
|
|
5
|
+
const STATUS = /^[\s>*#-]*Status\W*(done|partial|blocked)\b/im;
|
|
6
|
+
const VALIDATION = /^[\s>*#-]*Validation\b/im;
|
|
7
|
+
|
|
8
|
+
export const BLOCK_REASON =
|
|
9
|
+
'orch: your report is incomplete. Reply again with a `Status: done|partial|blocked` line and a `Validation` section listing each command you ran and its result.';
|
|
10
|
+
|
|
11
|
+
/** True when the implementer report carries both a Status line and a Validation section. */
|
|
12
|
+
export function hasEvidence(message) {
|
|
13
|
+
return typeof message === 'string' && STATUS.test(message) && VALIDATION.test(message);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Returns the exit code; never throws, bad stdin JSON is a silent allow. */
|
|
17
|
+
export function run({ env, input, stdout }) {
|
|
18
|
+
if (env.ORCH !== '1') return 0;
|
|
19
|
+
let payload;
|
|
20
|
+
try {
|
|
21
|
+
payload = JSON.parse(input);
|
|
22
|
+
} catch {
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
if (roleOf(payload?.agent_type) !== 'implementer' || payload?.stop_hook_active === true) return 0;
|
|
26
|
+
if (!hasEvidence(payload?.last_assistant_message)) {
|
|
27
|
+
stdout(JSON.stringify({ decision: 'block', reason: BLOCK_REASON }));
|
|
28
|
+
}
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function main(proc = process, read = readStdin) {
|
|
33
|
+
proc.exitCode = run({
|
|
34
|
+
env: proc.env,
|
|
35
|
+
input: proc.env.ORCH === '1' ? read() : '',
|
|
36
|
+
stdout: (s) => proc.stdout.write(s),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (isEntry(process.argv[1], import.meta.url)) main();
|
package/package.json
CHANGED
|
@@ -1,6 +1,55 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crafts69guy/orchestra",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Cross-harness (Claude Code + Codex) orchestration workflow plugin",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/crafts69guy/orchestra.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/crafts69guy/orchestra",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"claude-code",
|
|
14
|
+
"codex",
|
|
15
|
+
"orchestration",
|
|
16
|
+
"subagents",
|
|
17
|
+
"worktrees",
|
|
18
|
+
"cli"
|
|
19
|
+
],
|
|
20
|
+
"bin": {
|
|
21
|
+
"orch": "bin/orch"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"bin",
|
|
25
|
+
"scripts",
|
|
26
|
+
"hooks",
|
|
27
|
+
"rules",
|
|
28
|
+
"agents",
|
|
29
|
+
"skills",
|
|
30
|
+
"codex",
|
|
31
|
+
".claude-plugin",
|
|
32
|
+
".codex-plugin",
|
|
33
|
+
"README.md",
|
|
34
|
+
"LICENSE"
|
|
35
|
+
],
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=22.8"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"test": "node --test --experimental-test-coverage --test-coverage-lines=98 --test-coverage-branches=98 --test-coverage-functions=98 --test-coverage-exclude='test/**'",
|
|
44
|
+
"lint": "biome check . && markdownlint-cli2 \"**/*.md\" \"#node_modules\"",
|
|
45
|
+
"fmt": "biome format --write .",
|
|
46
|
+
"gen:codex": "node scripts/gen-codex-agents.mjs",
|
|
47
|
+
"check": "npm run lint && npm run gen:codex -- --check && npm test",
|
|
48
|
+
"validate": "claude plugin validate .",
|
|
49
|
+
"eval": "sh evals/_support/run.sh"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@biomejs/biome": "2.5.15",
|
|
53
|
+
"markdownlint-cli2": "0.23.3"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "config": { "MD013": false } }
|