@kbediako/codex-orchestrator 0.1.18 → 0.1.20
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 +7 -6
- package/dist/bin/codex-orchestrator.js +33 -0
- package/dist/orchestrator/src/cli/init.js +1 -1
- package/dist/scripts/lib/pr-watch-merge.js +566 -0
- package/package.json +1 -1
- package/skills/collab-deliberation/SKILL.md +2 -0
- package/skills/collab-subagents-first/SKILL.md +170 -0
- package/skills/collab-subagents-first/references/subagent-brief-template.md +90 -0
- package/skills/delegation-usage/DELEGATION_GUIDE.md +5 -1
- package/skills/delegation-usage/SKILL.md +5 -1
- package/skills/release/SKILL.md +127 -0
package/README.md
CHANGED
|
@@ -55,15 +55,16 @@ Use this when you want Codex to drive work inside another repo with the CO defau
|
|
|
55
55
|
```bash
|
|
56
56
|
codex mcp add delegation -- codex-orchestrator delegate-server --repo /path/to/repo
|
|
57
57
|
```
|
|
58
|
-
3. Optional (
|
|
58
|
+
3. Optional (managed/pinned CLI path): set up a CO-managed Codex CLI:
|
|
59
59
|
```bash
|
|
60
60
|
codex-orchestrator codex setup
|
|
61
61
|
```
|
|
62
|
+
Use this when you want a pinned binary, build-from-source behavior, or a custom fork. Stock `codex` works for default flows.
|
|
62
63
|
4. Optional (fast refresh helper for downstream users):
|
|
63
64
|
```bash
|
|
64
|
-
scripts/codex-cli-refresh.sh --repo /path/to/codex
|
|
65
|
+
scripts/codex-cli-refresh.sh --repo /path/to/codex --align-only
|
|
65
66
|
```
|
|
66
|
-
Repo-only helper (not included in npm package). Set `CODEX_REPO` or `CODEX_CLI_SOURCE` to avoid passing `--repo` each time.
|
|
67
|
+
Repo-only helper (not included in npm package). Add `--no-push` when you only want local alignment and do not want to update `origin/main`. To refresh the CO-managed CLI, run a separate command with `--force-rebuild` (without `--align-only`). Set `CODEX_REPO` or `CODEX_CLI_SOURCE` to avoid passing `--repo` each time.
|
|
67
68
|
|
|
68
69
|
## Delegation MCP server
|
|
69
70
|
|
|
@@ -88,7 +89,7 @@ Delegation guard profile:
|
|
|
88
89
|
## Delegation + RLM flow
|
|
89
90
|
|
|
90
91
|
RLM (Recursive Language Model) is the long-horizon loop used by the `rlm` pipeline (`codex-orchestrator rlm "<goal>"` or `codex-orchestrator start rlm --goal "<goal>"`). Delegated runs only enter RLM when the child is launched with the `rlm` pipeline (or the rlm runner directly). In auto mode it resolves to symbolic when delegated, when `RLM_CONTEXT_PATH` is set, or when the context exceeds `RLM_SYMBOLIC_MIN_BYTES`; otherwise it stays iterative. The runner writes state to `.runs/<task-id>/cli/<run-id>/rlm/state.json` and stops when the validator passes or budgets are exhausted.
|
|
91
|
-
Symbolic subcalls can optionally use collab tools when `RLM_SYMBOLIC_COLLAB=1` (requires
|
|
92
|
+
Symbolic subcalls can optionally use collab tools when `RLM_SYMBOLIC_COLLAB=1` (requires `collab=true` in `codex features list`). Collab tool calls parsed from `codex exec --json --enable collab` are stored in `manifest.collab_tool_calls` (bounded by `CODEX_ORCHESTRATOR_COLLAB_MAX_EVENTS`, set to `0` to disable). `codex-orchestrator codex setup` remains available when you want a managed/pinned CLI path.
|
|
92
93
|
|
|
93
94
|
### Delegation flow
|
|
94
95
|
```mermaid
|
|
@@ -164,9 +165,9 @@ codex-orchestrator devtools setup
|
|
|
164
165
|
- `codex-orchestrator plan <pipeline>` — preview pipeline stages.
|
|
165
166
|
- `codex-orchestrator exec <cmd>` — run a one-off command with the exec runtime.
|
|
166
167
|
- `codex-orchestrator init codex` — install starter templates (`mcp-client.json`, `AGENTS.md`) into a repo.
|
|
167
|
-
- `codex-orchestrator init codex --codex-cli --yes --codex-source <path>` —
|
|
168
|
+
- `codex-orchestrator init codex --codex-cli --yes --codex-source <path>` — optionally provision a CO-managed Codex CLI binary (build-from-source default; set `CODEX_CLI_SOURCE` to avoid passing `--codex-source` every time).
|
|
168
169
|
- `codex-orchestrator init codex --codex-cli --yes --codex-download-url <url> --codex-download-sha256 <sha>` — opt-in to a prebuilt Codex CLI download.
|
|
169
|
-
- `codex-orchestrator codex setup` — plan/apply a CO-managed Codex CLI install (
|
|
170
|
+
- `codex-orchestrator codex setup` — plan/apply a CO-managed Codex CLI install (optional managed/pinned path; use `--download-url` + `--download-sha256` for prebuilts).
|
|
170
171
|
- `codex-orchestrator self-check --format json` — JSON health payload.
|
|
171
172
|
- `codex-orchestrator mcp serve` — Codex MCP stdio server.
|
|
172
173
|
|
|
@@ -7,6 +7,7 @@ import { CodexOrchestrator } from '../orchestrator/src/cli/orchestrator.js';
|
|
|
7
7
|
import { formatPlanPreview } from '../orchestrator/src/cli/utils/planFormatter.js';
|
|
8
8
|
import { executeExecCommand } from '../orchestrator/src/cli/exec/command.js';
|
|
9
9
|
import { resolveEnvironmentPaths } from '../scripts/lib/run-manifests.js';
|
|
10
|
+
import { runPrWatchMerge } from '../scripts/lib/pr-watch-merge.js';
|
|
10
11
|
import { normalizeEnvironmentPaths, sanitizeTaskId } from '../orchestrator/src/cli/run/environment.js';
|
|
11
12
|
import { RunEventEmitter } from '../orchestrator/src/cli/events/runEvents.js';
|
|
12
13
|
import { evaluateInteractiveGate } from '../orchestrator/src/cli/utils/interactive.js';
|
|
@@ -77,6 +78,9 @@ async function main() {
|
|
|
77
78
|
case 'mcp':
|
|
78
79
|
await handleMcp(args);
|
|
79
80
|
break;
|
|
81
|
+
case 'pr':
|
|
82
|
+
await handlePr(args);
|
|
83
|
+
break;
|
|
80
84
|
case 'delegate-server':
|
|
81
85
|
case 'delegation-server':
|
|
82
86
|
await handleDelegationServer(args);
|
|
@@ -627,6 +631,20 @@ async function handleMcp(rawArgs) {
|
|
|
627
631
|
const dryRun = Boolean(flags['dry-run']);
|
|
628
632
|
await serveMcp({ repoRoot, dryRun, extraArgs: positionals });
|
|
629
633
|
}
|
|
634
|
+
async function handlePr(rawArgs) {
|
|
635
|
+
if (rawArgs.length === 0 || rawArgs[0] === '--help' || rawArgs[0] === '-h' || rawArgs[0] === 'help') {
|
|
636
|
+
printPrHelp();
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
const [subcommand, ...subcommandArgs] = rawArgs;
|
|
640
|
+
if (subcommand !== 'watch-merge') {
|
|
641
|
+
throw new Error(`Unknown pr subcommand: ${subcommand}`);
|
|
642
|
+
}
|
|
643
|
+
const exitCode = await runPrWatchMerge(subcommandArgs, { usage: 'codex-orchestrator pr watch-merge' });
|
|
644
|
+
if (exitCode !== 0) {
|
|
645
|
+
process.exitCode = exitCode;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
630
648
|
async function handleDelegationServer(rawArgs) {
|
|
631
649
|
const { positionals, flags } = parseArgs(rawArgs);
|
|
632
650
|
if (isHelpRequest(positionals, flags)) {
|
|
@@ -931,6 +949,9 @@ Commands:
|
|
|
931
949
|
--codex-home <path> Override the target Codex home directory.
|
|
932
950
|
--format json Emit machine-readable output.
|
|
933
951
|
mcp serve [--repo <path>] [--dry-run] [-- <extra args>]
|
|
952
|
+
pr watch-merge [options]
|
|
953
|
+
Monitor PR checks/reviews with polling and optional auto-merge after a quiet window.
|
|
954
|
+
Use \`codex-orchestrator pr watch-merge --help\` for full options.
|
|
934
955
|
delegate-server Run the delegation MCP server (stdio).
|
|
935
956
|
--repo <path> Repo root for config + manifests (default cwd).
|
|
936
957
|
--mode <full|question_only> Limit tool surface for child runs.
|
|
@@ -989,3 +1010,15 @@ Options:
|
|
|
989
1010
|
--help Show this message.
|
|
990
1011
|
`);
|
|
991
1012
|
}
|
|
1013
|
+
function printPrHelp() {
|
|
1014
|
+
console.log(`Usage: codex-orchestrator pr <subcommand> [options]
|
|
1015
|
+
|
|
1016
|
+
Subcommands:
|
|
1017
|
+
watch-merge Monitor PR checks/reviews with polling and optional auto-merge.
|
|
1018
|
+
Supports PR_MONITOR_* env vars and standard flags (see: pr watch-merge --help).
|
|
1019
|
+
|
|
1020
|
+
Examples:
|
|
1021
|
+
codex-orchestrator pr watch-merge --pr 211 --dry-run --quiet-minutes 10
|
|
1022
|
+
codex-orchestrator pr watch-merge --pr 211 --auto-merge --merge-method squash
|
|
1023
|
+
`);
|
|
1024
|
+
}
|
|
@@ -62,6 +62,6 @@ export function formatInitSummary(result, cwd) {
|
|
|
62
62
|
}
|
|
63
63
|
lines.push('Next steps (recommended):');
|
|
64
64
|
lines.push(` - codex mcp add delegation -- codex-orchestrator delegate-server --repo ${cwd}`);
|
|
65
|
-
lines.push(' - codex-orchestrator codex setup # optional:
|
|
65
|
+
lines.push(' - codex-orchestrator codex setup # optional: managed/pinned Codex CLI (stock CLI works by default)');
|
|
66
66
|
return lines;
|
|
67
67
|
}
|
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
4
|
+
import { hasFlag, parseArgs } from './cli-args.js';
|
|
5
|
+
const DEFAULT_INTERVAL_SECONDS = 30;
|
|
6
|
+
const DEFAULT_QUIET_MINUTES = 15;
|
|
7
|
+
const DEFAULT_TIMEOUT_MINUTES = 180;
|
|
8
|
+
const DEFAULT_MERGE_METHOD = 'squash';
|
|
9
|
+
const CHECKRUN_PASS_CONCLUSIONS = new Set(['SUCCESS', 'SKIPPED', 'NEUTRAL']);
|
|
10
|
+
const STATUS_CONTEXT_PASS_STATES = new Set(['SUCCESS']);
|
|
11
|
+
const STATUS_CONTEXT_PENDING_STATES = new Set(['EXPECTED', 'PENDING']);
|
|
12
|
+
const MERGEABLE_STATES = new Set(['CLEAN', 'HAS_HOOKS', 'UNSTABLE']);
|
|
13
|
+
const BLOCKED_REVIEW_DECISIONS = new Set(['CHANGES_REQUESTED', 'REVIEW_REQUIRED']);
|
|
14
|
+
const DO_NOT_MERGE_LABEL = /do[\s_-]*not[\s_-]*merge/i;
|
|
15
|
+
const PR_QUERY = `
|
|
16
|
+
query($owner:String!, $repo:String!, $number:Int!) {
|
|
17
|
+
repository(owner:$owner, name:$repo) {
|
|
18
|
+
pullRequest(number:$number) {
|
|
19
|
+
number
|
|
20
|
+
url
|
|
21
|
+
state
|
|
22
|
+
isDraft
|
|
23
|
+
reviewDecision
|
|
24
|
+
mergeStateStatus
|
|
25
|
+
updatedAt
|
|
26
|
+
mergedAt
|
|
27
|
+
labels(first:50) {
|
|
28
|
+
nodes {
|
|
29
|
+
name
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
reviewThreads(first:100) {
|
|
33
|
+
nodes {
|
|
34
|
+
isResolved
|
|
35
|
+
isOutdated
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
commits(last:1) {
|
|
39
|
+
nodes {
|
|
40
|
+
commit {
|
|
41
|
+
oid
|
|
42
|
+
statusCheckRollup {
|
|
43
|
+
contexts(first:100) {
|
|
44
|
+
nodes {
|
|
45
|
+
__typename
|
|
46
|
+
... on CheckRun {
|
|
47
|
+
name
|
|
48
|
+
status
|
|
49
|
+
conclusion
|
|
50
|
+
detailsUrl
|
|
51
|
+
}
|
|
52
|
+
... on StatusContext {
|
|
53
|
+
context
|
|
54
|
+
state
|
|
55
|
+
targetUrl
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
`;
|
|
67
|
+
function normalizeEnum(value) {
|
|
68
|
+
return typeof value === 'string' ? value.trim().toUpperCase() : '';
|
|
69
|
+
}
|
|
70
|
+
function formatDuration(ms) {
|
|
71
|
+
if (ms <= 0) {
|
|
72
|
+
return '0s';
|
|
73
|
+
}
|
|
74
|
+
const seconds = Math.ceil(ms / 1000);
|
|
75
|
+
const minutes = Math.floor(seconds / 60);
|
|
76
|
+
const remainder = seconds % 60;
|
|
77
|
+
if (minutes === 0) {
|
|
78
|
+
return `${remainder}s`;
|
|
79
|
+
}
|
|
80
|
+
if (remainder === 0) {
|
|
81
|
+
return `${minutes}m`;
|
|
82
|
+
}
|
|
83
|
+
return `${minutes}m${remainder}s`;
|
|
84
|
+
}
|
|
85
|
+
function log(message) {
|
|
86
|
+
console.log(`[${new Date().toISOString()}] ${message}`);
|
|
87
|
+
}
|
|
88
|
+
function parseNumber(name, rawValue, fallback) {
|
|
89
|
+
if (rawValue === undefined || rawValue === null || rawValue === '') {
|
|
90
|
+
return fallback;
|
|
91
|
+
}
|
|
92
|
+
if (typeof rawValue === 'boolean') {
|
|
93
|
+
throw new Error(`--${name} requires a value.`);
|
|
94
|
+
}
|
|
95
|
+
const parsed = Number(rawValue);
|
|
96
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
97
|
+
throw new Error(`--${name} must be a number > 0 (received: ${rawValue})`);
|
|
98
|
+
}
|
|
99
|
+
return parsed;
|
|
100
|
+
}
|
|
101
|
+
function parseInteger(name, rawValue, fallback) {
|
|
102
|
+
if (rawValue === undefined || rawValue === null || rawValue === '') {
|
|
103
|
+
return fallback;
|
|
104
|
+
}
|
|
105
|
+
if (typeof rawValue === 'boolean') {
|
|
106
|
+
throw new Error(`--${name} requires a value.`);
|
|
107
|
+
}
|
|
108
|
+
const parsed = Number(rawValue);
|
|
109
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
110
|
+
throw new Error(`--${name} must be an integer > 0 (received: ${rawValue})`);
|
|
111
|
+
}
|
|
112
|
+
return parsed;
|
|
113
|
+
}
|
|
114
|
+
function envFlagEnabled(rawValue, fallback = false) {
|
|
115
|
+
if (rawValue === undefined || rawValue === null) {
|
|
116
|
+
return fallback;
|
|
117
|
+
}
|
|
118
|
+
const normalized = String(rawValue).trim().toLowerCase();
|
|
119
|
+
if (normalized.length === 0) {
|
|
120
|
+
return fallback;
|
|
121
|
+
}
|
|
122
|
+
if (normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on') {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
if (normalized === '0' || normalized === 'false' || normalized === 'no' || normalized === 'off') {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
return fallback;
|
|
129
|
+
}
|
|
130
|
+
function parseMergeMethod(rawValue) {
|
|
131
|
+
const normalized = (rawValue || DEFAULT_MERGE_METHOD).trim().toLowerCase();
|
|
132
|
+
if (normalized !== 'merge' && normalized !== 'squash' && normalized !== 'rebase') {
|
|
133
|
+
throw new Error(`--merge-method must be merge, squash, or rebase (received: ${rawValue})`);
|
|
134
|
+
}
|
|
135
|
+
return normalized;
|
|
136
|
+
}
|
|
137
|
+
export function printPrWatchMergeHelp(options = {}) {
|
|
138
|
+
const usageCommand = typeof options.usage === 'string' && options.usage.trim().length > 0
|
|
139
|
+
? options.usage.trim()
|
|
140
|
+
: 'codex-orchestrator pr watch-merge';
|
|
141
|
+
console.log(`Usage: ${usageCommand} [options]
|
|
142
|
+
|
|
143
|
+
Monitor PR checks/reviews with polling and optionally merge after a quiet window.
|
|
144
|
+
|
|
145
|
+
Options:
|
|
146
|
+
--pr <number> PR number (default: PR for current branch)
|
|
147
|
+
--owner <name> Repo owner (default: inferred via gh repo view)
|
|
148
|
+
--repo <name> Repo name (default: inferred via gh repo view)
|
|
149
|
+
--interval-seconds <n> Poll interval in seconds (default: ${DEFAULT_INTERVAL_SECONDS})
|
|
150
|
+
--quiet-minutes <n> Required quiet window after ready state (default: ${DEFAULT_QUIET_MINUTES})
|
|
151
|
+
--timeout-minutes <n> Max monitor duration before failing (default: ${DEFAULT_TIMEOUT_MINUTES})
|
|
152
|
+
--merge-method <method> merge|squash|rebase (default: ${DEFAULT_MERGE_METHOD})
|
|
153
|
+
--auto-merge Merge automatically after quiet window
|
|
154
|
+
--no-auto-merge Never merge automatically (monitor only)
|
|
155
|
+
--delete-branch Delete remote branch when merging
|
|
156
|
+
--no-delete-branch Keep remote branch after merge
|
|
157
|
+
--dry-run Never call gh pr merge (report only)
|
|
158
|
+
-h, --help Show this help message
|
|
159
|
+
|
|
160
|
+
Environment:
|
|
161
|
+
PR_MONITOR_AUTO_MERGE=1 Default auto-merge on
|
|
162
|
+
PR_MONITOR_DELETE_BRANCH=1 Default delete branch on merge
|
|
163
|
+
PR_MONITOR_QUIET_MINUTES=<n> Override quiet window default
|
|
164
|
+
PR_MONITOR_INTERVAL_SECONDS=<n>
|
|
165
|
+
PR_MONITOR_TIMEOUT_MINUTES=<n>
|
|
166
|
+
PR_MONITOR_MERGE_METHOD=<method>`);
|
|
167
|
+
}
|
|
168
|
+
async function runGh(args, { allowFailure = false } = {}) {
|
|
169
|
+
return await new Promise((resolve, reject) => {
|
|
170
|
+
const child = spawn('gh', args, {
|
|
171
|
+
env: {
|
|
172
|
+
...process.env,
|
|
173
|
+
GH_PAGER: process.env.GH_PAGER || 'cat',
|
|
174
|
+
// Harden all gh calls against interactive prompts (per `gh help environment`).
|
|
175
|
+
GH_PROMPT_DISABLED: process.env.GH_PROMPT_DISABLED || '1'
|
|
176
|
+
},
|
|
177
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
178
|
+
});
|
|
179
|
+
let stdout = '';
|
|
180
|
+
let stderr = '';
|
|
181
|
+
child.stdout?.on('data', (chunk) => {
|
|
182
|
+
stdout += chunk.toString();
|
|
183
|
+
});
|
|
184
|
+
child.stderr?.on('data', (chunk) => {
|
|
185
|
+
stderr += chunk.toString();
|
|
186
|
+
});
|
|
187
|
+
child.once('error', (error) => {
|
|
188
|
+
reject(new Error(`Failed to run gh ${args.join(' ')}: ${error.message}`));
|
|
189
|
+
});
|
|
190
|
+
child.once('close', (code) => {
|
|
191
|
+
const exitCode = typeof code === 'number' ? code : 1;
|
|
192
|
+
const result = {
|
|
193
|
+
exitCode,
|
|
194
|
+
stdout: stdout.trim(),
|
|
195
|
+
stderr: stderr.trim()
|
|
196
|
+
};
|
|
197
|
+
if (exitCode === 0 || allowFailure) {
|
|
198
|
+
resolve(result);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const detail = result.stderr || result.stdout || `exit code ${exitCode}`;
|
|
202
|
+
reject(new Error(`gh ${args.join(' ')} failed: ${detail}`));
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
async function runGhJson(args) {
|
|
207
|
+
const result = await runGh(args);
|
|
208
|
+
try {
|
|
209
|
+
return JSON.parse(result.stdout);
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
throw new Error(`Failed to parse JSON from gh ${args.join(' ')}: ${error instanceof Error ? error.message : String(error)}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
async function ensureGhAuth() {
|
|
216
|
+
const result = await runGh(['auth', 'status', '-h', 'github.com'], { allowFailure: true });
|
|
217
|
+
if (result.exitCode !== 0) {
|
|
218
|
+
throw new Error('GitHub CLI is not authenticated for github.com. Run `gh auth login` and retry.');
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
async function resolveRepo(ownerArg, repoArg) {
|
|
222
|
+
if (ownerArg && repoArg) {
|
|
223
|
+
return { owner: ownerArg, repo: repoArg };
|
|
224
|
+
}
|
|
225
|
+
if (ownerArg || repoArg) {
|
|
226
|
+
throw new Error('Provide both --owner and --repo, or neither.');
|
|
227
|
+
}
|
|
228
|
+
const response = await runGhJson(['repo', 'view', '--json', 'nameWithOwner']);
|
|
229
|
+
const nameWithOwner = response?.nameWithOwner;
|
|
230
|
+
if (typeof nameWithOwner !== 'string' || !nameWithOwner.includes('/')) {
|
|
231
|
+
throw new Error('Unable to infer repository owner/name from gh repo view.');
|
|
232
|
+
}
|
|
233
|
+
const [owner, repo] = nameWithOwner.split('/');
|
|
234
|
+
return { owner, repo };
|
|
235
|
+
}
|
|
236
|
+
async function resolvePrNumber(prArg) {
|
|
237
|
+
if (prArg !== undefined) {
|
|
238
|
+
return parseInteger('pr', prArg, null);
|
|
239
|
+
}
|
|
240
|
+
const response = await runGhJson(['pr', 'view', '--json', 'number']);
|
|
241
|
+
const number = response?.number;
|
|
242
|
+
if (!Number.isInteger(number) || number <= 0) {
|
|
243
|
+
throw new Error('Unable to infer PR number from current branch.');
|
|
244
|
+
}
|
|
245
|
+
return number;
|
|
246
|
+
}
|
|
247
|
+
function summarizeChecks(nodes) {
|
|
248
|
+
const summary = {
|
|
249
|
+
total: 0,
|
|
250
|
+
successCount: 0,
|
|
251
|
+
pending: [],
|
|
252
|
+
failed: []
|
|
253
|
+
};
|
|
254
|
+
for (const node of nodes) {
|
|
255
|
+
if (!node || typeof node !== 'object') {
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
const typeName = typeof node.__typename === 'string' ? node.__typename : '';
|
|
259
|
+
if (typeName === 'CheckRun') {
|
|
260
|
+
summary.total += 1;
|
|
261
|
+
const name = typeof node.name === 'string' && node.name.trim() ? node.name.trim() : 'check-run';
|
|
262
|
+
const status = normalizeEnum(node.status);
|
|
263
|
+
if (status !== 'COMPLETED') {
|
|
264
|
+
summary.pending.push(name);
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
const conclusion = normalizeEnum(node.conclusion);
|
|
268
|
+
if (CHECKRUN_PASS_CONCLUSIONS.has(conclusion)) {
|
|
269
|
+
summary.successCount += 1;
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
summary.failed.push({
|
|
273
|
+
name,
|
|
274
|
+
state: conclusion || 'UNKNOWN',
|
|
275
|
+
detailsUrl: typeof node.detailsUrl === 'string' ? node.detailsUrl : null
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (typeName === 'StatusContext') {
|
|
281
|
+
summary.total += 1;
|
|
282
|
+
const name = typeof node.context === 'string' && node.context.trim() ? node.context.trim() : 'status-context';
|
|
283
|
+
const state = normalizeEnum(node.state);
|
|
284
|
+
if (STATUS_CONTEXT_PENDING_STATES.has(state)) {
|
|
285
|
+
summary.pending.push(name);
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if (STATUS_CONTEXT_PASS_STATES.has(state)) {
|
|
289
|
+
summary.successCount += 1;
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
summary.failed.push({
|
|
293
|
+
name,
|
|
294
|
+
state: state || 'UNKNOWN',
|
|
295
|
+
detailsUrl: typeof node.targetUrl === 'string' ? node.targetUrl : null
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return summary;
|
|
301
|
+
}
|
|
302
|
+
function buildStatusSnapshot(response) {
|
|
303
|
+
const pr = response?.data?.repository?.pullRequest;
|
|
304
|
+
if (!pr) {
|
|
305
|
+
throw new Error('GraphQL response missing pullRequest payload.');
|
|
306
|
+
}
|
|
307
|
+
const labels = Array.isArray(pr.labels?.nodes)
|
|
308
|
+
? pr.labels.nodes
|
|
309
|
+
.map((item) => (item && typeof item.name === 'string' ? item.name.trim() : ''))
|
|
310
|
+
.filter(Boolean)
|
|
311
|
+
: [];
|
|
312
|
+
const hasDoNotMergeLabel = labels.some((label) => DO_NOT_MERGE_LABEL.test(label));
|
|
313
|
+
const threads = Array.isArray(pr.reviewThreads?.nodes) ? pr.reviewThreads.nodes : [];
|
|
314
|
+
const unresolvedThreadCount = threads.filter((thread) => thread && !thread.isResolved && !thread.isOutdated).length;
|
|
315
|
+
const contexts = pr.commits?.nodes?.[0]?.commit?.statusCheckRollup?.contexts?.nodes;
|
|
316
|
+
const checkNodes = Array.isArray(contexts) ? contexts : [];
|
|
317
|
+
const checks = summarizeChecks(checkNodes);
|
|
318
|
+
const reviewDecision = normalizeEnum(pr.reviewDecision);
|
|
319
|
+
const mergeStateStatus = normalizeEnum(pr.mergeStateStatus);
|
|
320
|
+
const state = normalizeEnum(pr.state);
|
|
321
|
+
const isDraft = Boolean(pr.isDraft);
|
|
322
|
+
const gateReasons = [];
|
|
323
|
+
if (state !== 'OPEN') {
|
|
324
|
+
gateReasons.push(`state=${state || 'UNKNOWN'}`);
|
|
325
|
+
}
|
|
326
|
+
if (isDraft) {
|
|
327
|
+
gateReasons.push('draft');
|
|
328
|
+
}
|
|
329
|
+
if (hasDoNotMergeLabel) {
|
|
330
|
+
gateReasons.push('label:do-not-merge');
|
|
331
|
+
}
|
|
332
|
+
if (checks.pending.length > 0) {
|
|
333
|
+
gateReasons.push(`checks_pending=${checks.pending.length}`);
|
|
334
|
+
}
|
|
335
|
+
if (!MERGEABLE_STATES.has(mergeStateStatus)) {
|
|
336
|
+
gateReasons.push(`merge_state=${mergeStateStatus || 'UNKNOWN'}`);
|
|
337
|
+
}
|
|
338
|
+
if (BLOCKED_REVIEW_DECISIONS.has(reviewDecision)) {
|
|
339
|
+
gateReasons.push(`review=${reviewDecision}`);
|
|
340
|
+
}
|
|
341
|
+
if (unresolvedThreadCount > 0) {
|
|
342
|
+
gateReasons.push(`unresolved_threads=${unresolvedThreadCount}`);
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
number: Number(pr.number),
|
|
346
|
+
url: typeof pr.url === 'string' ? pr.url : null,
|
|
347
|
+
state,
|
|
348
|
+
isDraft,
|
|
349
|
+
reviewDecision: reviewDecision || 'NONE',
|
|
350
|
+
mergeStateStatus: mergeStateStatus || 'UNKNOWN',
|
|
351
|
+
updatedAt: typeof pr.updatedAt === 'string' ? pr.updatedAt : null,
|
|
352
|
+
mergedAt: typeof pr.mergedAt === 'string' ? pr.mergedAt : null,
|
|
353
|
+
labels,
|
|
354
|
+
hasDoNotMergeLabel,
|
|
355
|
+
unresolvedThreadCount,
|
|
356
|
+
checks,
|
|
357
|
+
gateReasons,
|
|
358
|
+
readyToMerge: gateReasons.length === 0,
|
|
359
|
+
headOid: pr.commits?.nodes?.[0]?.commit?.oid || null
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
function formatStatusLine(snapshot, quietRemainingMs) {
|
|
363
|
+
const failedNames = snapshot.checks.failed.map((item) => `${item.name}:${item.state}`).join(', ') || '-';
|
|
364
|
+
const pendingNames = snapshot.checks.pending.join(', ') || '-';
|
|
365
|
+
const reasons = snapshot.gateReasons.join(', ') || 'none';
|
|
366
|
+
return [
|
|
367
|
+
`PR #${snapshot.number}`,
|
|
368
|
+
`state=${snapshot.state}`,
|
|
369
|
+
`merge_state=${snapshot.mergeStateStatus}`,
|
|
370
|
+
`review=${snapshot.reviewDecision}`,
|
|
371
|
+
`checks_ok=${snapshot.checks.successCount}/${snapshot.checks.total}`,
|
|
372
|
+
`checks_pending=${snapshot.checks.pending.length}`,
|
|
373
|
+
`checks_failed=${snapshot.checks.failed.length}`,
|
|
374
|
+
`unresolved_threads=${snapshot.unresolvedThreadCount}`,
|
|
375
|
+
`quiet_remaining=${formatDuration(quietRemainingMs)}`,
|
|
376
|
+
`blocked_by=${reasons}`,
|
|
377
|
+
`pending=[${pendingNames}]`,
|
|
378
|
+
`failed=[${failedNames}]`
|
|
379
|
+
].join(' | ');
|
|
380
|
+
}
|
|
381
|
+
async function fetchSnapshot(owner, repo, prNumber) {
|
|
382
|
+
const response = await runGhJson([
|
|
383
|
+
'api',
|
|
384
|
+
'graphql',
|
|
385
|
+
'-f',
|
|
386
|
+
`query=${PR_QUERY}`,
|
|
387
|
+
'-f',
|
|
388
|
+
`owner=${owner}`,
|
|
389
|
+
'-f',
|
|
390
|
+
`repo=${repo}`,
|
|
391
|
+
'-F',
|
|
392
|
+
`number=${prNumber}`
|
|
393
|
+
]);
|
|
394
|
+
return buildStatusSnapshot(response);
|
|
395
|
+
}
|
|
396
|
+
async function attemptMerge({ prNumber, mergeMethod, deleteBranch, headOid }) {
|
|
397
|
+
// gh pr merge has no --yes flag; rely on non-interactive stdio + explicit merge method.
|
|
398
|
+
const args = ['pr', 'merge', String(prNumber), `--${mergeMethod}`];
|
|
399
|
+
if (deleteBranch) {
|
|
400
|
+
args.push('--delete-branch');
|
|
401
|
+
}
|
|
402
|
+
if (headOid) {
|
|
403
|
+
args.push('--match-head-commit', headOid);
|
|
404
|
+
}
|
|
405
|
+
return await runGh(args, { allowFailure: true });
|
|
406
|
+
}
|
|
407
|
+
async function runPrWatchMergeOrThrow(argv, options) {
|
|
408
|
+
const { args, positionals } = parseArgs(argv);
|
|
409
|
+
if (hasFlag(args, 'h') || hasFlag(args, 'help')) {
|
|
410
|
+
printPrWatchMergeHelp(options);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
const knownFlags = new Set([
|
|
414
|
+
'pr',
|
|
415
|
+
'owner',
|
|
416
|
+
'repo',
|
|
417
|
+
'interval-seconds',
|
|
418
|
+
'quiet-minutes',
|
|
419
|
+
'timeout-minutes',
|
|
420
|
+
'merge-method',
|
|
421
|
+
'auto-merge',
|
|
422
|
+
'no-auto-merge',
|
|
423
|
+
'delete-branch',
|
|
424
|
+
'no-delete-branch',
|
|
425
|
+
'dry-run',
|
|
426
|
+
'h',
|
|
427
|
+
'help'
|
|
428
|
+
]);
|
|
429
|
+
const unknownFlags = Object.keys(args).filter((key) => !knownFlags.has(key));
|
|
430
|
+
if (unknownFlags.length > 0 || positionals.length > 0) {
|
|
431
|
+
const label = unknownFlags[0] ? `--${unknownFlags[0]}` : positionals[0];
|
|
432
|
+
throw new Error(`Unknown option: ${label}`);
|
|
433
|
+
}
|
|
434
|
+
const intervalSeconds = parseNumber('interval-seconds', typeof args['interval-seconds'] === 'string'
|
|
435
|
+
? args['interval-seconds']
|
|
436
|
+
: process.env.PR_MONITOR_INTERVAL_SECONDS, DEFAULT_INTERVAL_SECONDS);
|
|
437
|
+
const quietMinutes = parseNumber('quiet-minutes', typeof args['quiet-minutes'] === 'string'
|
|
438
|
+
? args['quiet-minutes']
|
|
439
|
+
: process.env.PR_MONITOR_QUIET_MINUTES, DEFAULT_QUIET_MINUTES);
|
|
440
|
+
const timeoutMinutes = parseNumber('timeout-minutes', typeof args['timeout-minutes'] === 'string'
|
|
441
|
+
? args['timeout-minutes']
|
|
442
|
+
: process.env.PR_MONITOR_TIMEOUT_MINUTES, DEFAULT_TIMEOUT_MINUTES);
|
|
443
|
+
const mergeMethod = parseMergeMethod(typeof args['merge-method'] === 'string'
|
|
444
|
+
? args['merge-method']
|
|
445
|
+
: process.env.PR_MONITOR_MERGE_METHOD || DEFAULT_MERGE_METHOD);
|
|
446
|
+
const defaultAutoMerge = envFlagEnabled(process.env.PR_MONITOR_AUTO_MERGE, false);
|
|
447
|
+
const defaultDeleteBranch = envFlagEnabled(process.env.PR_MONITOR_DELETE_BRANCH, true);
|
|
448
|
+
let autoMerge = defaultAutoMerge;
|
|
449
|
+
if (hasFlag(args, 'auto-merge')) {
|
|
450
|
+
autoMerge = true;
|
|
451
|
+
}
|
|
452
|
+
if (hasFlag(args, 'no-auto-merge')) {
|
|
453
|
+
autoMerge = false;
|
|
454
|
+
}
|
|
455
|
+
let deleteBranch = defaultDeleteBranch;
|
|
456
|
+
if (hasFlag(args, 'delete-branch')) {
|
|
457
|
+
deleteBranch = true;
|
|
458
|
+
}
|
|
459
|
+
if (hasFlag(args, 'no-delete-branch')) {
|
|
460
|
+
deleteBranch = false;
|
|
461
|
+
}
|
|
462
|
+
const dryRun = hasFlag(args, 'dry-run');
|
|
463
|
+
await ensureGhAuth();
|
|
464
|
+
const { owner, repo } = await resolveRepo(typeof args.owner === 'string' ? args.owner : undefined, typeof args.repo === 'string' ? args.repo : undefined);
|
|
465
|
+
const prNumber = await resolvePrNumber(args.pr);
|
|
466
|
+
const intervalMs = Math.round(intervalSeconds * 1000);
|
|
467
|
+
const quietMs = Math.round(quietMinutes * 60 * 1000);
|
|
468
|
+
const timeoutMs = Math.round(timeoutMinutes * 60 * 1000);
|
|
469
|
+
const deadline = Date.now() + timeoutMs;
|
|
470
|
+
log(`Monitoring ${owner}/${repo}#${prNumber} every ${intervalSeconds}s (quiet window ${quietMinutes}m, timeout ${timeoutMinutes}m, auto_merge=${autoMerge ? 'on' : 'off'}, dry_run=${dryRun ? 'on' : 'off'}).`);
|
|
471
|
+
let quietWindowStartedAt = null;
|
|
472
|
+
let quietWindowAnchorUpdatedAt = null;
|
|
473
|
+
let quietWindowAnchorHeadOid = null;
|
|
474
|
+
let lastMergeAttemptHeadOid = null;
|
|
475
|
+
while (Date.now() <= deadline) {
|
|
476
|
+
let snapshot;
|
|
477
|
+
try {
|
|
478
|
+
snapshot = await fetchSnapshot(owner, repo, prNumber);
|
|
479
|
+
}
|
|
480
|
+
catch (error) {
|
|
481
|
+
log(`Polling error: ${error instanceof Error ? error.message : String(error)} (retrying).`);
|
|
482
|
+
await sleep(intervalMs);
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
if (snapshot.state === 'MERGED' || snapshot.mergedAt) {
|
|
486
|
+
log(`PR #${prNumber} is merged.`);
|
|
487
|
+
if (snapshot.url) {
|
|
488
|
+
log(`URL: ${snapshot.url}`);
|
|
489
|
+
}
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
if (snapshot.state === 'CLOSED') {
|
|
493
|
+
throw new Error(`PR #${prNumber} was closed without merge.`);
|
|
494
|
+
}
|
|
495
|
+
if (snapshot.readyToMerge) {
|
|
496
|
+
const readyAnchorChanged = quietWindowStartedAt !== null &&
|
|
497
|
+
(snapshot.updatedAt !== quietWindowAnchorUpdatedAt || snapshot.headOid !== quietWindowAnchorHeadOid);
|
|
498
|
+
if (quietWindowStartedAt === null || readyAnchorChanged) {
|
|
499
|
+
quietWindowStartedAt = Date.now();
|
|
500
|
+
quietWindowAnchorUpdatedAt = snapshot.updatedAt;
|
|
501
|
+
quietWindowAnchorHeadOid = snapshot.headOid;
|
|
502
|
+
lastMergeAttemptHeadOid = null;
|
|
503
|
+
log(readyAnchorChanged
|
|
504
|
+
? 'Ready state changed; quiet window reset.'
|
|
505
|
+
: `Ready state reached; quiet window started (${quietMinutes}m).`);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
else if (quietWindowStartedAt !== null) {
|
|
509
|
+
quietWindowStartedAt = null;
|
|
510
|
+
quietWindowAnchorUpdatedAt = null;
|
|
511
|
+
quietWindowAnchorHeadOid = null;
|
|
512
|
+
lastMergeAttemptHeadOid = null;
|
|
513
|
+
log('Ready state lost; quiet window cleared.');
|
|
514
|
+
}
|
|
515
|
+
const quietElapsedMs = quietWindowStartedAt ? Date.now() - quietWindowStartedAt : 0;
|
|
516
|
+
const quietRemainingMs = quietWindowStartedAt ? Math.max(quietMs - quietElapsedMs, 0) : quietMs;
|
|
517
|
+
log(formatStatusLine(snapshot, quietRemainingMs));
|
|
518
|
+
if (snapshot.readyToMerge && quietWindowStartedAt !== null && quietElapsedMs >= quietMs) {
|
|
519
|
+
if (!autoMerge || dryRun) {
|
|
520
|
+
log(dryRun
|
|
521
|
+
? 'Dry run: merge conditions satisfied and quiet window elapsed.'
|
|
522
|
+
: 'Merge conditions satisfied and quiet window elapsed.');
|
|
523
|
+
if (snapshot.url) {
|
|
524
|
+
log(`Ready to merge: ${snapshot.url}`);
|
|
525
|
+
}
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
if (snapshot.headOid && snapshot.headOid === lastMergeAttemptHeadOid) {
|
|
529
|
+
log(`Merge already attempted for head ${snapshot.headOid}; waiting for PR state refresh.`);
|
|
530
|
+
}
|
|
531
|
+
else {
|
|
532
|
+
lastMergeAttemptHeadOid = snapshot.headOid;
|
|
533
|
+
log(`Attempting merge via gh pr merge --${mergeMethod}${deleteBranch ? ' --delete-branch' : ''}.`);
|
|
534
|
+
const mergeResult = await attemptMerge({
|
|
535
|
+
prNumber,
|
|
536
|
+
mergeMethod,
|
|
537
|
+
deleteBranch,
|
|
538
|
+
headOid: snapshot.headOid
|
|
539
|
+
});
|
|
540
|
+
if (mergeResult.exitCode === 0) {
|
|
541
|
+
log(`Merge command succeeded for PR #${prNumber}.`);
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
const details = mergeResult.stderr || mergeResult.stdout || `exit code ${mergeResult.exitCode}`;
|
|
545
|
+
log(`Merge attempt failed: ${details}`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
const remainingTimeMs = deadline - Date.now();
|
|
549
|
+
if (remainingTimeMs <= 0) {
|
|
550
|
+
break;
|
|
551
|
+
}
|
|
552
|
+
await sleep(Math.min(intervalMs, remainingTimeMs));
|
|
553
|
+
}
|
|
554
|
+
throw new Error(`Timed out after ${timeoutMinutes} minute(s) while monitoring PR #${prNumber}.`);
|
|
555
|
+
}
|
|
556
|
+
export async function runPrWatchMerge(argv, options = {}) {
|
|
557
|
+
try {
|
|
558
|
+
await runPrWatchMergeOrThrow(argv, options);
|
|
559
|
+
return 0;
|
|
560
|
+
}
|
|
561
|
+
catch (error) {
|
|
562
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
563
|
+
console.error(message);
|
|
564
|
+
return 1;
|
|
565
|
+
}
|
|
566
|
+
}
|
package/package.json
CHANGED
|
@@ -50,6 +50,7 @@ Use this skill when the user asks for brainstorming, tradeoffs, option compariso
|
|
|
50
50
|
2) Close critical context gaps.
|
|
51
51
|
- Ask up to 3 targeted questions only if answers could change the recommendation.
|
|
52
52
|
- If delegation is available, prefer a subagent for context gathering before asking the user.
|
|
53
|
+
- If collab spawning fails (for example `agent thread limit reached`), proceed solo and explicitly note the limitation; do not block on spawning.
|
|
53
54
|
|
|
54
55
|
3) Generate distinct options.
|
|
55
56
|
- Produce 3-5 materially different options.
|
|
@@ -83,3 +84,4 @@ Use this skill when the user asks for brainstorming, tradeoffs, option compariso
|
|
|
83
84
|
- Do not present uncertainty as certainty.
|
|
84
85
|
- Keep outputs concise and action-oriented.
|
|
85
86
|
- If collab subagents are used, close lifecycle loops per id (`spawn_agent` -> `wait` -> `close_agent`) before finishing.
|
|
87
|
+
- If you cannot close collab agents (missing ids) and spawn keeps failing, restart the session and re-run deliberation; keep work moving by doing solo deliberation meanwhile.
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: collab-subagents-first
|
|
3
|
+
description: Manage non-trivial tasks via focused collab subagents to save context and improve throughput. Use when work spans multiple files/components, can be split into independent streams, needs separate validation/review, or risks context bloat. Favor direct execution for trivial one-shot tasks.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Collab Subagents First
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
Delegate as a manager, not as a pass-through. Split work into narrow streams, give each subagent a rich brief, and keep parent context lean by collecting short structured summaries plus evidence paths.
|
|
11
|
+
|
|
12
|
+
Note: If a global `collab-subagents-first` skill is installed, prefer that and fall back to this bundled skill.
|
|
13
|
+
|
|
14
|
+
## Delegation gate
|
|
15
|
+
|
|
16
|
+
Use subagents when any condition is true:
|
|
17
|
+
- Task spans more than one subsystem or more than one file.
|
|
18
|
+
- Work naturally splits into independent streams (for example research + implementation + verification).
|
|
19
|
+
- Work likely exceeds about 5-10 minutes.
|
|
20
|
+
- Separate review/verification is required before handoff.
|
|
21
|
+
- Parent context is growing and summary compression is needed.
|
|
22
|
+
- You are unsure whether the task should be delegated.
|
|
23
|
+
|
|
24
|
+
Default rule:
|
|
25
|
+
- For any non-trivial task, spawn at least one subagent early (even if work is mostly single-stream) to offload execution and preserve parent context.
|
|
26
|
+
|
|
27
|
+
Skip subagents when all conditions are true:
|
|
28
|
+
- Single-file or tightly scoped change.
|
|
29
|
+
- No parallelizable stream exists.
|
|
30
|
+
- Execution and verification are straightforward in one pass.
|
|
31
|
+
- Expected duration is under about 5 minutes.
|
|
32
|
+
|
|
33
|
+
## Workflow
|
|
34
|
+
|
|
35
|
+
1) Define parent success criteria
|
|
36
|
+
- Write 3-6 acceptance bullets before spawning.
|
|
37
|
+
- Define "done" and required validation upfront.
|
|
38
|
+
|
|
39
|
+
2) Choose delegation shape
|
|
40
|
+
- Minimum for non-trivial work: 1 subagent (`implement` or `research`).
|
|
41
|
+
- Standard: 2 subagents (`implement` + `review/verify`).
|
|
42
|
+
- Complex/high-risk: 3-4 subagents (`research`, `implement`, `tests`, `review`).
|
|
43
|
+
- If uncertain, spawn a short-lived `scout` subagent first to propose decomposition and risks.
|
|
44
|
+
|
|
45
|
+
3) Split into narrow streams
|
|
46
|
+
- Prefer 1-4 streams based on the chosen shape.
|
|
47
|
+
- Assign one owner per stream and avoid overlapping file ownership.
|
|
48
|
+
- Good stream labels: `research`, `implement`, `tests`, `review`.
|
|
49
|
+
|
|
50
|
+
4) Send a rich brief to each subagent
|
|
51
|
+
- Use the required brief template from `references/subagent-brief-template.md`.
|
|
52
|
+
- Include objective, scope, constraints, acceptance criteria, and expected output format.
|
|
53
|
+
- Require concise summaries and evidence paths; avoid long logs in chat.
|
|
54
|
+
|
|
55
|
+
5) Run streams in parallel when independent
|
|
56
|
+
- Spawn multiple subagents for independent streams.
|
|
57
|
+
- Wait for all subagents to finish before final synthesis.
|
|
58
|
+
|
|
59
|
+
6) Synthesize with context compression
|
|
60
|
+
- Merge only decisions, findings, and evidence links into parent context.
|
|
61
|
+
- Keep full details in artifacts/files instead of long conversation dumps.
|
|
62
|
+
- Force summary discipline: keep each subagent synthesis to a short block with outcome, files, validation, findings, and open questions only.
|
|
63
|
+
|
|
64
|
+
7) Verify before handoff
|
|
65
|
+
- Run parent-level validation/tests.
|
|
66
|
+
- Run standalone review on merged changes (see review loop below).
|
|
67
|
+
|
|
68
|
+
8) Re-check delegation need at checkpoints
|
|
69
|
+
- Re-evaluate delegation after major context growth (for example every 6-8 parent messages, or after crossing about 8 touched files, or when the plan changes materially).
|
|
70
|
+
- If parent context starts bloating, spawn/redirect subagents instead of continuing in parent.
|
|
71
|
+
- Keep the delegation tree shallow. Prefer parent fan-out over subagent-of-subagent chains.
|
|
72
|
+
|
|
73
|
+
## Spawn payload + labels (current behavior)
|
|
74
|
+
|
|
75
|
+
- `spawn_agent` accepts exactly one input style:
|
|
76
|
+
- `message` (plain text), or
|
|
77
|
+
- `items` (structured input).
|
|
78
|
+
- Do not send both `message` and `items` in one spawn call.
|
|
79
|
+
- Use `items` when you need explicit structured context (for example `mention` paths like `app://...` or selected `skill` entries) instead of flattening everything into one long string.
|
|
80
|
+
- Spawn returns an `agent_id` (thread id). Collab event rendering/picker labels are id-based today; do not depend on custom visible agent names.
|
|
81
|
+
- To keep operator readability high despite id labels, encode the role clearly in your stream labels and first-line task brief (for example `review`, `tests`, `research`).
|
|
82
|
+
|
|
83
|
+
## Collab lifecycle hygiene (required)
|
|
84
|
+
|
|
85
|
+
When you use collab tools (`spawn_agent` / `wait` / `close_agent`):
|
|
86
|
+
- Keep a local list of every returned `agent_id`.
|
|
87
|
+
- For every successful `spawn_agent`, run `wait` and then `close_agent` for that same id.
|
|
88
|
+
- Always close agents on error/timeout paths; do a final cleanup pass before finishing so no id is left unclosed.
|
|
89
|
+
- If spawn fails with `agent thread limit reached`, stop spawning immediately, close any known ids, then retry once. If you still cannot spawn, proceed without collab (solo or via delegation) and explicitly note the degraded mode.
|
|
90
|
+
|
|
91
|
+
## Required subagent contract
|
|
92
|
+
|
|
93
|
+
Require each subagent response to include:
|
|
94
|
+
- `Outcome`: done / blocked / partial.
|
|
95
|
+
- `Changes`: files touched or "none".
|
|
96
|
+
- `Validation`: commands run and pass/fail results.
|
|
97
|
+
- `Findings`: prioritized defects/risks (or "none found").
|
|
98
|
+
- `Evidence`: artifact paths, manifests, or command outputs summary.
|
|
99
|
+
- `Open questions`: only unresolved items that block correctness.
|
|
100
|
+
|
|
101
|
+
Reject and rerun when responses are:
|
|
102
|
+
- Missing validation evidence for code changes.
|
|
103
|
+
- Missing ownership/scope boundaries.
|
|
104
|
+
- Excessively verbose with no actionable summary.
|
|
105
|
+
|
|
106
|
+
## Execution constraints for subagents
|
|
107
|
+
|
|
108
|
+
- Subagents are spawned with approval policy effectively set to `never`.
|
|
109
|
+
- Design subagent tasks so they can complete without approval/escalation prompts.
|
|
110
|
+
- Keep privileged/high-risk operations in the parent thread when interactive approval is required.
|
|
111
|
+
- Subagents inherit core execution context (for example cwd/sandbox constraints), so include environment assumptions explicitly in each brief.
|
|
112
|
+
|
|
113
|
+
## Review loop (standalone-review pairing)
|
|
114
|
+
|
|
115
|
+
Use a two-layer review loop:
|
|
116
|
+
|
|
117
|
+
1) Subagent self-review (when possible)
|
|
118
|
+
- If `codex review` is available in the working repo, have the subagent run the repo's standalone-review flow (including hardened fallback rules) for:
|
|
119
|
+
- `--uncommitted`, or
|
|
120
|
+
- `--base <branch>` when branch comparison is clearer.
|
|
121
|
+
- Capture top findings and fixes in the subagent summary.
|
|
122
|
+
- If self-review cannot run (tool/policy/trust constraints), require a manual checklist summary: correctness, regressions, missing tests.
|
|
123
|
+
|
|
124
|
+
2) Parent independent review (required)
|
|
125
|
+
- After integrating subagent work, run a standalone review from the parent.
|
|
126
|
+
- Prefer the global `standalone-review` skill workflow for consistent checks.
|
|
127
|
+
|
|
128
|
+
Do not treat wrapper handoff-only output as a completed review.
|
|
129
|
+
|
|
130
|
+
## Orchestrator + RLM path (optional, recommended for deep loops)
|
|
131
|
+
|
|
132
|
+
- Prefer orchestrator RLM/delegation loops for long-horizon, recursive, or high-risk tasks when available.
|
|
133
|
+
- Keep this additive: still perform final parent synthesis and standalone review.
|
|
134
|
+
- If orchestrator is unavailable, continue with local subagent orchestration and standalone review.
|
|
135
|
+
|
|
136
|
+
## Compatibility guardrail (JSONL/collab drift)
|
|
137
|
+
|
|
138
|
+
- Symptoms: missing collab/delegate tool-call evidence, framing/parsing errors, or unstable collab behavior after CLI upgrades.
|
|
139
|
+
- Check versions first: `codex --version` and `codex-orchestrator --version`.
|
|
140
|
+
- CO repo refresh path (safe default): `scripts/codex-cli-refresh.sh --repo <codex-repo> --no-push`.
|
|
141
|
+
- Rebuild managed CLI only: `codex-orchestrator codex setup --source <codex-repo> --yes --force`.
|
|
142
|
+
- If local codex is materially behind upstream, sync before diagnosing collab behavior differences.
|
|
143
|
+
- If compatibility remains unstable, continue with non-collab execution path and document the degraded mode.
|
|
144
|
+
|
|
145
|
+
## Depth-limit guardrail
|
|
146
|
+
|
|
147
|
+
- Collab spawn depth is bounded. At max depth, `spawn_agent` will fail and the branch must execute directly.
|
|
148
|
+
- Near max depth, collab may be disabled for newly spawned children; plan for leaf execution.
|
|
149
|
+
- When depth errors appear, stop recursive delegation and switch to parent-driven execution.
|
|
150
|
+
|
|
151
|
+
## Anti-patterns
|
|
152
|
+
|
|
153
|
+
- Do not delegate one giant stream with vague ownership.
|
|
154
|
+
- Do not spawn subagents before acceptance criteria are defined.
|
|
155
|
+
- Do not merge subagent output without independent validation.
|
|
156
|
+
- Do not copy raw multi-hundred-line logs into parent context.
|
|
157
|
+
- Do not keep long single-agent execution in parent when a focused subagent can own it.
|
|
158
|
+
- Do not skip delegation solely because there is only one implementation stream; single-stream delegation is valid for context offload.
|
|
159
|
+
- Do not rely on human-readable agent names in TUI labels for control flow; use stream ownership and evidence paths as source of truth.
|
|
160
|
+
- Do not end the parent work with unclosed collab agent ids.
|
|
161
|
+
|
|
162
|
+
## Completion checklist
|
|
163
|
+
|
|
164
|
+
- At least one subagent was used for non-trivial work (or explicit reason documented for skipping).
|
|
165
|
+
- Streams defined with clear ownership and acceptance criteria.
|
|
166
|
+
- Subagent briefs include complete context and constraints.
|
|
167
|
+
- All subagents completed or explicitly closed as blocked.
|
|
168
|
+
- Parent synthesis includes concise decisions and evidence paths.
|
|
169
|
+
- Parent-level review completed (standalone review or equivalent).
|
|
170
|
+
- Collab lifecycle closed (`spawn_agent` -> `wait` -> `close_agent` per id) or degraded mode explicitly recorded.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Subagent Brief Template
|
|
2
|
+
|
|
3
|
+
Use this template when spawning or re-scoping a subagent. Fill every field.
|
|
4
|
+
|
|
5
|
+
## Template
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
You are assigned a focused subtask. You are not alone in the codebase; other agents may edit unrelated files. Ignore unrelated edits and do not revert work you do not own.
|
|
9
|
+
|
|
10
|
+
Task label:
|
|
11
|
+
Objective:
|
|
12
|
+
Timebox:
|
|
13
|
+
Working repo/path:
|
|
14
|
+
Base branch / comparison scope:
|
|
15
|
+
|
|
16
|
+
Why this matters:
|
|
17
|
+
- <1-3 bullets of product/technical context>
|
|
18
|
+
|
|
19
|
+
Known context digest:
|
|
20
|
+
- <current branch / relevant files / recent decisions>
|
|
21
|
+
- <known runtime/tooling quirks in this repo>
|
|
22
|
+
- <links/paths to specs, tasks, notes, or manifests>
|
|
23
|
+
|
|
24
|
+
In scope:
|
|
25
|
+
- <exact responsibilities>
|
|
26
|
+
|
|
27
|
+
Out of scope:
|
|
28
|
+
- <explicit exclusions>
|
|
29
|
+
|
|
30
|
+
Ownership:
|
|
31
|
+
- Files/paths you may edit: <paths>
|
|
32
|
+
- Files/paths you must not edit: <paths>
|
|
33
|
+
|
|
34
|
+
Acceptance criteria:
|
|
35
|
+
- <bullet 1>
|
|
36
|
+
- <bullet 2>
|
|
37
|
+
- <bullet 3>
|
|
38
|
+
|
|
39
|
+
Validation required:
|
|
40
|
+
- Commands to run: <commands>
|
|
41
|
+
- Minimum checks: <tests/lint/build/review expectations>
|
|
42
|
+
|
|
43
|
+
Review:
|
|
44
|
+
- If available, run the repo's standalone-review flow on your changes before final response (`--uncommitted` or `--base <branch>` as appropriate).
|
|
45
|
+
- If review cannot run, provide a manual self-review for correctness, regressions, and missing tests.
|
|
46
|
+
|
|
47
|
+
Output format (required):
|
|
48
|
+
1. Outcome: done | partial | blocked
|
|
49
|
+
2. Changes: <file list + short summary>
|
|
50
|
+
3. Validation: <command> -> <pass/fail>
|
|
51
|
+
4. Findings: <prioritized issues/risks, or "none found">
|
|
52
|
+
5. Evidence: <paths/log references>
|
|
53
|
+
6. Open questions: <only blockers>
|
|
54
|
+
|
|
55
|
+
Keep the response concise. Put detailed notes in a file and return the path.
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Brief quality bar (required)
|
|
59
|
+
|
|
60
|
+
- Include enough context so the subagent can act without back-and-forth.
|
|
61
|
+
- Include explicit file ownership boundaries.
|
|
62
|
+
- Include a concrete output format and validation expectations.
|
|
63
|
+
- Include at least one "do not do" constraint to prevent drift.
|
|
64
|
+
- If task is review-only, explicitly prohibit implementation edits.
|
|
65
|
+
|
|
66
|
+
## Fast variants
|
|
67
|
+
|
|
68
|
+
### Research stream
|
|
69
|
+
|
|
70
|
+
```text
|
|
71
|
+
Objective: answer <question> with evidence.
|
|
72
|
+
Deliverable: 3-7 bullets + key risks + recommendation.
|
|
73
|
+
No code edits unless explicitly requested.
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Implementation stream
|
|
77
|
+
|
|
78
|
+
```text
|
|
79
|
+
Objective: implement <specific change>.
|
|
80
|
+
Deliverable: patch + validation output + self-review notes.
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Verification stream
|
|
84
|
+
|
|
85
|
+
```text
|
|
86
|
+
Objective: validate <existing change>.
|
|
87
|
+
Deliverable: failing/passing checks, defect list by severity, and minimal fix suggestions.
|
|
88
|
+
No broad refactors.
|
|
89
|
+
```
|
|
90
|
+
|
|
@@ -122,7 +122,11 @@ Delegation MCP expects JSONL. Keep `codex-orchestrator` aligned with the current
|
|
|
122
122
|
- Check: `codex-orchestrator --version`
|
|
123
123
|
- Update global: `npm i -g @kbediako/codex-orchestrator@latest`
|
|
124
124
|
- Or pin via npx: `npx -y @kbediako/codex-orchestrator@<version> delegate-server`
|
|
125
|
-
- If using a custom Codex fork, fast-forward from `upstream/main` regularly
|
|
125
|
+
- Stock `codex` is the default path. If using a custom Codex fork, fast-forward from `upstream/main` regularly.
|
|
126
|
+
- CO repo checkout only (helper is not shipped in npm): `scripts/codex-cli-refresh.sh --repo /path/to/codex --align-only`
|
|
127
|
+
- CO repo checkout only (managed rebuild helper): `scripts/codex-cli-refresh.sh --repo /path/to/codex --force-rebuild`
|
|
128
|
+
- Add `--no-push` only when you intentionally want local-only alignment without updating `origin/main`.
|
|
129
|
+
- npm-safe alternative (no repo helper): `codex-orchestrator codex setup --source /path/to/codex --yes --force`
|
|
126
130
|
|
|
127
131
|
## Common failures
|
|
128
132
|
|
|
@@ -81,7 +81,11 @@ For runner + delegation coordination (short `--task` flow), see `docs/delegation
|
|
|
81
81
|
- Check installed version: `codex-orchestrator --version`
|
|
82
82
|
- Preferred update path: `npm i -g @kbediako/codex-orchestrator@latest`
|
|
83
83
|
- Deterministic pin path (for reproducible environments): `npx -y @kbediako/codex-orchestrator@<version> delegate-server`
|
|
84
|
-
- If
|
|
84
|
+
- Stock `codex` is the default path. If you use a custom Codex fork, fast-forward it regularly from `upstream/main`.
|
|
85
|
+
- CO repo checkout only (helper is not shipped in npm): `scripts/codex-cli-refresh.sh --repo /path/to/codex --align-only`
|
|
86
|
+
- CO repo checkout only (managed rebuild helper): `scripts/codex-cli-refresh.sh --repo /path/to/codex --force-rebuild`
|
|
87
|
+
- Add `--no-push` only when you intentionally want local-only alignment without updating `origin/main`.
|
|
88
|
+
- npm-safe alternative (no repo helper): `codex-orchestrator codex setup --source /path/to/codex --yes --force`
|
|
85
89
|
|
|
86
90
|
### 0b) Background terminal bootstrap (required when MCP is disabled)
|
|
87
91
|
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: release
|
|
3
|
+
description: Ship a signed tag + GitHub Release + npm publish for @kbediako/codex-orchestrator with low-friction, agent-first steps (PR -> watch-merge -> tag -> watch publish -> downstream smoke).
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Release (CO Maintainer)
|
|
7
|
+
|
|
8
|
+
Use this skill when the user asks to ship a new CO version to npm/downstream users.
|
|
9
|
+
If a global `release` skill is installed, prefer that and fall back to this bundled skill.
|
|
10
|
+
|
|
11
|
+
## Guardrails (required)
|
|
12
|
+
|
|
13
|
+
- Never publish from an unmerged branch: release tags must point at `main`.
|
|
14
|
+
- Release tags must be **signed annotated tags** (`git tag -s vX.Y.Z -m "vX.Y.Z"`).
|
|
15
|
+
- Confirm `gh auth status` is OK before any PR/release steps.
|
|
16
|
+
- Prefer non-interactive commands; avoid anything that can hang on prompts.
|
|
17
|
+
- If any check fails (Core Lane, Cloud Canary, CodeRabbit, release workflow), stop and fix before proceeding.
|
|
18
|
+
|
|
19
|
+
## Workflow
|
|
20
|
+
|
|
21
|
+
### 1) Preflight
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
gh auth status -h github.com
|
|
25
|
+
git status -sb
|
|
26
|
+
git checkout main
|
|
27
|
+
git pull --ff-only
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### 2) Version bump PR
|
|
31
|
+
|
|
32
|
+
Pick a version (usually patch): `0.1.N+1`.
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
VERSION="0.1.20"
|
|
36
|
+
BRANCH="task/release-${VERSION}"
|
|
37
|
+
|
|
38
|
+
git checkout -b "$BRANCH"
|
|
39
|
+
npm version "$VERSION" --no-git-tag-version
|
|
40
|
+
git add package.json package-lock.json
|
|
41
|
+
git commit -m "chore(release): bump version to ${VERSION}"
|
|
42
|
+
git push -u origin "$BRANCH"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Open PR (use `--body-file` to avoid literal `\\n` rendering):
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
cat <<EOF > /tmp/pr-body.md
|
|
49
|
+
## What
|
|
50
|
+
- Bump version to ${VERSION}.
|
|
51
|
+
|
|
52
|
+
## Why
|
|
53
|
+
- Ship latest main to npm/downstream users.
|
|
54
|
+
|
|
55
|
+
## How Tested
|
|
56
|
+
- CI on this PR (Core Lane / Cloud Canary / CodeRabbit).
|
|
57
|
+
EOF
|
|
58
|
+
|
|
59
|
+
gh pr create --title "chore(release): bump version to ${VERSION}" --body-file /tmp/pr-body.md
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Monitor + auto-merge once green:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
PR_NUMBER="$(gh pr view --json number --jq .number)"
|
|
66
|
+
codex-orchestrator pr watch-merge --pr "$PR_NUMBER" --auto-merge --delete-branch --quiet-minutes 1 --interval-seconds 20
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 3) Create signed tag + push
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
git checkout main
|
|
73
|
+
git pull --ff-only
|
|
74
|
+
|
|
75
|
+
TAG="v${VERSION}"
|
|
76
|
+
git tag -s "$TAG" -m "$TAG"
|
|
77
|
+
git tag -v "$TAG"
|
|
78
|
+
git push origin "$TAG"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 4) Watch the release workflow + confirm npm publish
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
TAG_SHA="$(git rev-list -n 1 "$TAG")"
|
|
85
|
+
RUN_ID=""
|
|
86
|
+
for i in {1..30}; do
|
|
87
|
+
RUN_ID="$(
|
|
88
|
+
gh run list \
|
|
89
|
+
--workflow release.yml \
|
|
90
|
+
--limit 20 \
|
|
91
|
+
--json databaseId,headBranch,headSha \
|
|
92
|
+
--jq ".[] | select((.headBranch==\"${TAG}\") or (.headSha==\"${TAG_SHA}\")) | .databaseId" \
|
|
93
|
+
| head -n 1 \
|
|
94
|
+
|| true
|
|
95
|
+
)"
|
|
96
|
+
if [[ -n "$RUN_ID" && "$RUN_ID" != "null" ]]; then
|
|
97
|
+
break
|
|
98
|
+
fi
|
|
99
|
+
sleep 2
|
|
100
|
+
done
|
|
101
|
+
if [[ -z "$RUN_ID" || "$RUN_ID" == "null" ]]; then
|
|
102
|
+
echo "::error::No release workflow run found for ${TAG}."
|
|
103
|
+
exit 1
|
|
104
|
+
fi
|
|
105
|
+
gh run watch "$RUN_ID" --exit-status
|
|
106
|
+
|
|
107
|
+
npm view @kbediako/codex-orchestrator version
|
|
108
|
+
gh release view "v${VERSION}" --json url,assets --jq '{url: .url, assets: (.assets|map(.name))}'
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### 5) Update global + downstream smoke
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
npm i -g @kbediako/codex-orchestrator@"${VERSION}"
|
|
115
|
+
codex-orchestrator --version
|
|
116
|
+
|
|
117
|
+
TMPDIR="$(mktemp -d)"
|
|
118
|
+
cd "$TMPDIR"
|
|
119
|
+
npx -y @kbediako/codex-orchestrator@"${VERSION}" --version
|
|
120
|
+
npx -y @kbediako/codex-orchestrator@"${VERSION}" pr watch-merge --help | head -n 10
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
If the release included bundled skill changes, refresh local skills:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
codex-orchestrator skills install --force
|
|
127
|
+
```
|