agent-conveyor 0.1.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.
Files changed (85) hide show
  1. package/README.md +1123 -0
  2. package/dist/cli/main.d.ts +2 -0
  3. package/dist/cli/main.js +19 -0
  4. package/dist/cli/main.js.map +1 -0
  5. package/dist/cli/program-name.d.ts +2 -0
  6. package/dist/cli/program-name.js +12 -0
  7. package/dist/cli/program-name.js.map +1 -0
  8. package/dist/cli/typescript-runtime.d.ts +52 -0
  9. package/dist/cli/typescript-runtime.js +18009 -0
  10. package/dist/cli/typescript-runtime.js.map +1 -0
  11. package/dist/index.d.ts +37 -0
  12. package/dist/index.js +20 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/runtime/audit.d.ts +96 -0
  15. package/dist/runtime/audit.js +298 -0
  16. package/dist/runtime/audit.js.map +1 -0
  17. package/dist/runtime/classify.d.ts +8 -0
  18. package/dist/runtime/classify.js +128 -0
  19. package/dist/runtime/classify.js.map +1 -0
  20. package/dist/runtime/codex-session.d.ts +103 -0
  21. package/dist/runtime/codex-session.js +408 -0
  22. package/dist/runtime/codex-session.js.map +1 -0
  23. package/dist/runtime/commands.d.ts +92 -0
  24. package/dist/runtime/commands.js +408 -0
  25. package/dist/runtime/commands.js.map +1 -0
  26. package/dist/runtime/dispatch.d.ts +74 -0
  27. package/dist/runtime/dispatch.js +669 -0
  28. package/dist/runtime/dispatch.js.map +1 -0
  29. package/dist/runtime/export.d.ts +22 -0
  30. package/dist/runtime/export.js +77 -0
  31. package/dist/runtime/export.js.map +1 -0
  32. package/dist/runtime/ingest.d.ts +28 -0
  33. package/dist/runtime/ingest.js +177 -0
  34. package/dist/runtime/ingest.js.map +1 -0
  35. package/dist/runtime/loop-evidence.d.ts +87 -0
  36. package/dist/runtime/loop-evidence.js +448 -0
  37. package/dist/runtime/loop-evidence.js.map +1 -0
  38. package/dist/runtime/manager-config.d.ts +20 -0
  39. package/dist/runtime/manager-config.js +34 -0
  40. package/dist/runtime/manager-config.js.map +1 -0
  41. package/dist/runtime/manager-permissions.d.ts +7 -0
  42. package/dist/runtime/manager-permissions.js +85 -0
  43. package/dist/runtime/manager-permissions.js.map +1 -0
  44. package/dist/runtime/notifications.d.ts +89 -0
  45. package/dist/runtime/notifications.js +208 -0
  46. package/dist/runtime/notifications.js.map +1 -0
  47. package/dist/runtime/replay.d.ts +29 -0
  48. package/dist/runtime/replay.js +331 -0
  49. package/dist/runtime/replay.js.map +1 -0
  50. package/dist/runtime/tasks.d.ts +54 -0
  51. package/dist/runtime/tasks.js +195 -0
  52. package/dist/runtime/tasks.js.map +1 -0
  53. package/dist/runtime/tmux.d.ts +61 -0
  54. package/dist/runtime/tmux.js +189 -0
  55. package/dist/runtime/tmux.js.map +1 -0
  56. package/dist/runtime/visual-diff.d.ts +23 -0
  57. package/dist/runtime/visual-diff.js +234 -0
  58. package/dist/runtime/visual-diff.js.map +1 -0
  59. package/dist/state/database.d.ts +21 -0
  60. package/dist/state/database.js +142 -0
  61. package/dist/state/database.js.map +1 -0
  62. package/dist/state/files.d.ts +38 -0
  63. package/dist/state/files.js +73 -0
  64. package/dist/state/files.js.map +1 -0
  65. package/dist/state/schema-v22.d.ts +1 -0
  66. package/dist/state/schema-v22.js +566 -0
  67. package/dist/state/schema-v22.js.map +1 -0
  68. package/dist/state/sqlite-contract.d.ts +4 -0
  69. package/dist/state/sqlite-contract.js +78 -0
  70. package/dist/state/sqlite-contract.js.map +1 -0
  71. package/dist/state/status.d.ts +12 -0
  72. package/dist/state/status.js +40 -0
  73. package/dist/state/status.js.map +1 -0
  74. package/docs/typescript-migration/cli-contract.md +147 -0
  75. package/docs/typescript-migration/dashboard-contract.md +76 -0
  76. package/docs/typescript-migration/package-install-contract.md +98 -0
  77. package/docs/typescript-migration/qa-gate-matrix.md +103 -0
  78. package/docs/typescript-migration/sqlite-state-contract.md +92 -0
  79. package/docs/typescript-migration/t005-runtime-parity.md +47 -0
  80. package/package.json +88 -0
  81. package/scripts/capture-static-html-screenshot.mjs +88 -0
  82. package/skills/codex-review/SKILL.md +116 -0
  83. package/skills/codex-review/scripts/codex-review +344 -0
  84. package/skills/manage-codex-workers/SKILL.md +696 -0
  85. package/skills/manage-codex-workers/agents/openai.yaml +5 -0
package/package.json ADDED
@@ -0,0 +1,88 @@
1
+ {
2
+ "name": "agent-conveyor",
3
+ "version": "0.1.0",
4
+ "description": "Local agent manager/worker conveyor control plane for Codex sessions.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "conveyor": "dist/cli/main.js",
9
+ "workerctl": "dist/cli/main.js"
10
+ },
11
+ "main": "./dist/index.js",
12
+ "types": "./dist/index.d.ts",
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "default": "./dist/index.js"
17
+ },
18
+ "./cli": {
19
+ "types": "./dist/cli/program-name.d.ts",
20
+ "default": "./dist/cli/program-name.js"
21
+ }
22
+ },
23
+ "files": [
24
+ "dist/",
25
+ "scripts/capture-static-html-screenshot.mjs",
26
+ "skills/**/*",
27
+ "README.md",
28
+ "docs/typescript-migration/*.md"
29
+ ],
30
+ "scripts": {
31
+ "test": "node dashboard/scripts/run-tests.mjs",
32
+ "build": "npm run build:cli && npm run build:dashboard",
33
+ "build:cli": "tsc -p tsconfig.cli.json",
34
+ "build:dashboard": "tsc -p dashboard/tsconfig.json && vite build --config dashboard/vite.config.ts",
35
+ "lint": "eslint .",
36
+ "knip": "npm run build:cli && knip",
37
+ "migration:audit": "node scripts/ts-migration-audit.mjs",
38
+ "migration:audit:final": "node scripts/ts-migration-audit.mjs --require-zero-python",
39
+ "check": "npm run lint && npm run knip && npm test -- --runInBand && npm run build",
40
+ "dashboard": "tsx dashboard/server/index.ts",
41
+ "prepack": "rm -rf dist/cli && npm run build:cli",
42
+ "prepublishOnly": "node scripts/prepublish-guard.mjs"
43
+ },
44
+ "engines": {
45
+ "node": ">=20.19"
46
+ },
47
+ "keywords": [
48
+ "agent",
49
+ "codex",
50
+ "conveyor",
51
+ "manager-worker",
52
+ "workflow-automation"
53
+ ],
54
+ "repository": {
55
+ "type": "git",
56
+ "url": "git+https://github.com/neonwatty/agent-conveyor.git"
57
+ },
58
+ "bugs": {
59
+ "url": "https://github.com/neonwatty/agent-conveyor/issues"
60
+ },
61
+ "homepage": "https://github.com/neonwatty/agent-conveyor#readme",
62
+ "dependencies": {
63
+ "@homebridge/node-pty-prebuilt-multiarch": "^0.13.1",
64
+ "@playwright/test": "^1.60.0",
65
+ "@vitejs/plugin-react": "^5.1.0",
66
+ "@xterm/addon-fit": "^0.10.0",
67
+ "@xterm/xterm": "^5.5.0",
68
+ "express": "^5.1.0",
69
+ "react": "^19.2.0",
70
+ "react-dom": "^19.2.0",
71
+ "vite": "^7.2.4",
72
+ "ws": "^8.18.3"
73
+ },
74
+ "devDependencies": {
75
+ "@eslint/js": "^10.0.1",
76
+ "@types/express": "^5.0.5",
77
+ "@types/node": "^24.10.1",
78
+ "@types/react": "^19.2.6",
79
+ "@types/react-dom": "^19.2.3",
80
+ "@types/ws": "^8.18.1",
81
+ "eslint": "^10.4.1",
82
+ "globals": "^17.6.0",
83
+ "knip": "^6.15.0",
84
+ "tsx": "^4.21.0",
85
+ "typescript": "^5.9.3",
86
+ "typescript-eslint": "^8.60.1"
87
+ }
88
+ }
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
2
+
3
+ const ERROR_PREFIX = "browser-backed QA requires Playwright/Chromium or a configured browser capture helper";
4
+
5
+ function parseArgs(argv) {
6
+ const args = {};
7
+ for (let index = 2; index < argv.length; index += 2) {
8
+ const key = argv[index];
9
+ const value = argv[index + 1];
10
+ if (!key || !key.startsWith("--") || value === undefined) {
11
+ throw new Error("Usage: capture-static-html-screenshot.mjs --html HTML --output PNG --width WIDTH --height HEIGHT");
12
+ }
13
+ args[key.slice(2)] = value;
14
+ }
15
+ return args;
16
+ }
17
+
18
+ function parsePositiveInteger(value) {
19
+ if (!/^[1-9][0-9]*$/.test(value ?? "")) {
20
+ throw new Error("Usage: capture-static-html-screenshot.mjs --html HTML --output PNG --width WIDTH --height HEIGHT");
21
+ }
22
+ return Number(value);
23
+ }
24
+
25
+ async function main() {
26
+ const args = parseArgs(process.argv);
27
+ const htmlPath = args.html;
28
+ const outputPath = args.output;
29
+ const width = parsePositiveInteger(args.width);
30
+ const height = parsePositiveInteger(args.height);
31
+
32
+ if (!htmlPath || !outputPath) {
33
+ throw new Error("Usage: capture-static-html-screenshot.mjs --html HTML --output PNG --width WIDTH --height HEIGHT");
34
+ }
35
+
36
+ const { pathToFileURL } = await import("node:url");
37
+ const { chromium } = await import("@playwright/test");
38
+ const launchAttempts = [
39
+ { backend: "playwright-chromium", options: { headless: true } },
40
+ { backend: "playwright-chrome-channel", options: { channel: "chrome", headless: true } },
41
+ {
42
+ backend: "playwright-chrome-app",
43
+ options: {
44
+ executablePath: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
45
+ headless: true,
46
+ },
47
+ },
48
+ ];
49
+ let browser;
50
+ let backend;
51
+ let lastError;
52
+ for (const attempt of launchAttempts) {
53
+ try {
54
+ browser = await chromium.launch(attempt.options);
55
+ backend = attempt.backend;
56
+ break;
57
+ } catch (error) {
58
+ lastError = error;
59
+ }
60
+ }
61
+ if (!browser || !backend) {
62
+ throw lastError ?? new Error("No browser launch attempt was made.");
63
+ }
64
+ try {
65
+ const page = await browser.newPage({
66
+ deviceScaleFactor: 1,
67
+ viewport: { width, height },
68
+ });
69
+ await page.goto(pathToFileURL(htmlPath).href, { waitUntil: "load" });
70
+ await page.screenshot({ path: outputPath, fullPage: false });
71
+ console.log(JSON.stringify({
72
+ backend,
73
+ html_path: htmlPath,
74
+ screenshot_path: outputPath,
75
+ viewport: `${width}x${height}`,
76
+ }));
77
+ } finally {
78
+ await browser.close();
79
+ }
80
+ }
81
+
82
+ try {
83
+ await main();
84
+ } catch (error) {
85
+ const message = error instanceof Error ? error.message : String(error);
86
+ console.error(`${ERROR_PREFIX}: ${message}`);
87
+ process.exitCode = 2;
88
+ }
@@ -0,0 +1,116 @@
1
+ ---
2
+ name: codex-review
3
+ description: "Codex code review closeout: local dirty changes, PR branch vs main, parallel tests."
4
+ ---
5
+
6
+ # Codex Review
7
+
8
+ Run Codex's built-in code review as a closeout check. This is code review (`codex review`), not Guardian `auto_review` approval routing.
9
+
10
+ Use when:
11
+ - user asks for Codex review / autoreview / second-model review
12
+ - after non-trivial code edits, before final/commit/ship
13
+ - reviewing a local branch or PR branch after fixes
14
+
15
+ ## Contract
16
+
17
+ - Treat review output as advisory. Never blindly apply it.
18
+ - Verify every finding by reading the real code path and adjacent files.
19
+ - Read dependency docs/source/types when the finding depends on external behavior.
20
+ - Reject unrealistic edge cases, speculative risks, broad rewrites, and fixes that over-complicate the codebase.
21
+ - Prefer small fixes at the right ownership boundary; no refactor unless it clearly improves the bug class.
22
+ - Keep going until Codex review returns no accepted/actionable findings.
23
+ - If a review-triggered fix changes code, rerun focused tests and rerun Codex review.
24
+ - Never switch or override the review model. If the review hits model capacity, retry the same command a few times with the same model. If it hits sandbox/permission limits, use the helper's `--full-access` option instead of changing models.
25
+ - Stop as soon as the review command/helper exits 0 with no accepted/actionable findings. Do not run an extra direct `codex review` just to get a nicer "clean" line, a second opinion, or clearer closeout wording.
26
+ - Treat the helper's successful exit plus absence of actionable findings as the clean review result, even if the underlying Codex CLI output is terse.
27
+ - If rejecting a finding as intentional/not worth fixing, add a brief inline code comment only when it explains a real invariant or ownership decision that future reviewers should know.
28
+ - Do not push just to review. Push only when the user requested push/ship/PR update.
29
+
30
+ ## Pick Target
31
+
32
+ Dirty local work:
33
+
34
+ ```bash
35
+ codex review --uncommitted
36
+ ```
37
+
38
+ Use this only when the patch is actually unstaged/staged/untracked in the
39
+ current checkout. For committed, pushed, or PR work, review the branch against
40
+ its base instead; do not force `--mode local` / `--uncommitted` just because the
41
+ helper docs mention dirty work first. A clean `--uncommitted` review only proves
42
+ there is no local patch.
43
+
44
+ Branch/PR work:
45
+
46
+ ```bash
47
+ git fetch origin
48
+ codex review --base origin/main
49
+ ```
50
+
51
+ Do not pass an inline prompt with `--base`; current CLI rejects `--base` + `[PROMPT]` even though help text is ambiguous. If custom instructions are needed, run the plain base review first, then do a local/manual follow-up pass.
52
+
53
+ If an open PR exists, use its actual base:
54
+
55
+ ```bash
56
+ base=$(gh pr view --json baseRefName --jq .baseRefName)
57
+ codex review --base "origin/$base"
58
+ ```
59
+
60
+ Committed single change:
61
+
62
+ ```bash
63
+ codex review --commit HEAD
64
+ ```
65
+
66
+ ## Parallel Closeout
67
+
68
+ Format first if formatting can change line locations. Then it is OK to run tests and review in parallel:
69
+
70
+ ```bash
71
+ ~/.codex/skills/codex-review/scripts/codex-review --parallel-tests "<focused test command>"
72
+ ```
73
+
74
+ Tradeoff: tests may force code changes that stale the review. If tests or review lead to code edits, rerun the affected tests and rerun review until no accepted/actionable findings remain. Once that rerun exits cleanly, stop; do not spend another long review cycle on redundant confirmation.
75
+
76
+ ## Context Efficiency
77
+
78
+ Codex review is usually noisy. Default to a subagent filter when subagents are available. Ask it to run the review and return only:
79
+ - actionable findings it accepts
80
+ - findings it rejects, with one-line reason
81
+ - exact files/tests to rerun
82
+
83
+ Run inline only for tiny changes or when subagents are unavailable.
84
+
85
+ ## Helper
86
+
87
+ Bundled helper:
88
+
89
+ ```bash
90
+ ~/.codex/skills/codex-review/scripts/codex-review --help
91
+ ```
92
+
93
+ The helper:
94
+ - chooses dirty `--uncommitted` first
95
+ - otherwise uses current PR base if `gh pr view` works
96
+ - otherwise uses `origin/main` for non-main branches
97
+ - should be left in `--mode auto` or forced to `--mode branch` for committed/PR work; do not force `--mode local` after committing
98
+ - writes only to stdout unless `--output` or `CODEX_REVIEW_OUTPUT` is set
99
+ - supports `--dry-run` and `--parallel-tests`
100
+ - supports `--full-access` for nested review runs that need localhost bind/listen tests
101
+ - prints `codex-review clean: no accepted/actionable findings reported` when the selected review command exits 0
102
+
103
+ Recursion guard:
104
+ - The helper exports `CODEX_REVIEW_HELPER_LEVEL=1` to the nested `codex review` process.
105
+ - If a review session tries to invoke the helper again, the helper exits `78` with `nested codex-review invocation blocked`.
106
+ - Use `CODEX_REVIEW_ALLOW_NESTED=1` only for intentional debugging dry runs; do not use it in normal closeout, GoalBuddy conveyor, or PR review flows.
107
+
108
+ ## Final Report
109
+
110
+ Include:
111
+ - review command used
112
+ - tests/proof run
113
+ - findings accepted/rejected, briefly why
114
+ - the clean review result from the final helper/review run, or why a remaining finding was consciously rejected
115
+
116
+ Do not run another Codex review solely to improve the final report wording. If the final helper run exited 0 and produced no accepted/actionable findings, report that exact run as clean.
@@ -0,0 +1,344 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ usage() {
5
+ cat <<'EOF'
6
+ Usage: codex-review [options]
7
+
8
+ Options:
9
+ --mode auto|local|branch Target selection. Default: auto.
10
+ --base REF Base ref for branch review. Default: PR base or origin/main.
11
+ --codex-bin PATH Codex binary. Default: codex.
12
+ --full-access Run nested Codex review without sandbox/approval prompts.
13
+ --output FILE Also save output to file.
14
+ --parallel-tests CMD Run review and test command concurrently.
15
+ --dry-run Print selected commands, do not run.
16
+ -h, --help Show help.
17
+
18
+ Modes:
19
+ local codex review --uncommitted
20
+ branch codex review --base <base>
21
+ auto dirty tree -> local, else PR/current branch -> branch
22
+ EOF
23
+ }
24
+
25
+ require_option_value() {
26
+ local option=$1
27
+ local value=${2-}
28
+
29
+ if [[ -z "$value" || "$value" == --* || "$value" == "-h" ]]; then
30
+ echo "missing value for $option" >&2
31
+ usage >&2
32
+ exit 2
33
+ fi
34
+ }
35
+
36
+ mode=auto
37
+ base_ref=
38
+ codex_bin=${CODEX_BIN:-codex}
39
+ codex_args=()
40
+ output=${CODEX_REVIEW_OUTPUT:-}
41
+ parallel_tests=
42
+ dry_run=false
43
+
44
+ while [[ $# -gt 0 ]]; do
45
+ case "$1" in
46
+ --mode)
47
+ require_option_value "$1" "${2-}"
48
+ mode=$2
49
+ shift 2
50
+ ;;
51
+ --base)
52
+ require_option_value "$1" "${2-}"
53
+ base_ref=$2
54
+ shift 2
55
+ ;;
56
+ --codex-bin)
57
+ require_option_value "$1" "${2-}"
58
+ codex_bin=$2
59
+ shift 2
60
+ ;;
61
+ --full-access)
62
+ codex_args+=(--dangerously-bypass-approvals-and-sandbox)
63
+ shift
64
+ ;;
65
+ --output)
66
+ require_option_value "$1" "${2-}"
67
+ output=$2
68
+ shift 2
69
+ ;;
70
+ --parallel-tests)
71
+ require_option_value "$1" "${2-}"
72
+ parallel_tests=$2
73
+ shift 2
74
+ ;;
75
+ --dry-run)
76
+ dry_run=true
77
+ shift
78
+ ;;
79
+ -h|--help)
80
+ usage
81
+ exit 0
82
+ ;;
83
+ *)
84
+ usage >&2
85
+ exit 2
86
+ ;;
87
+ esac
88
+ done
89
+
90
+ case "$mode" in
91
+ auto|local|branch) ;;
92
+ *)
93
+ echo "invalid --mode: $mode" >&2
94
+ exit 2
95
+ ;;
96
+ esac
97
+
98
+ nested_level=${CODEX_REVIEW_HELPER_LEVEL:-0}
99
+ allow_nested=${CODEX_REVIEW_ALLOW_NESTED:-}
100
+ if ! [[ "$nested_level" =~ ^[0-9]+$ ]]; then
101
+ echo "codex-review: invalid CODEX_REVIEW_HELPER_LEVEL=$nested_level" >&2
102
+ exit 2
103
+ fi
104
+ nested_level_is_nested=false
105
+ if [[ "$nested_level" =~ [1-9] ]]; then
106
+ nested_level_is_nested=true
107
+ fi
108
+ if [[ "$nested_level_is_nested" == true && "$allow_nested" != "1" ]]; then
109
+ echo "codex-review refusal: nested codex-review invocation blocked; set CODEX_REVIEW_ALLOW_NESTED=1 only for intentional debugging" >&2
110
+ exit 78
111
+ fi
112
+ if [[ "$nested_level_is_nested" != true ]]; then
113
+ export CODEX_REVIEW_HELPER_LEVEL=1
114
+ elif (( ${#nested_level} <= 15 )); then
115
+ export CODEX_REVIEW_HELPER_LEVEL=$((10#$nested_level + 1))
116
+ else
117
+ export CODEX_REVIEW_HELPER_LEVEL=2
118
+ fi
119
+ export CODEX_REVIEW_HELPER_PARENT_PID=$$
120
+
121
+ git rev-parse --show-toplevel >/dev/null
122
+
123
+ current_branch=$(git branch --show-current 2>/dev/null || true)
124
+ dirty=false
125
+ if [[ -n "$(git status --porcelain)" ]]; then
126
+ dirty=true
127
+ fi
128
+
129
+ pr_url=
130
+ if [[ -z "$base_ref" && "$mode" != local ]] && command -v gh >/dev/null 2>&1; then
131
+ if pr_lines=$(gh pr view --json baseRefName,url --jq '[.baseRefName, .url] | @tsv' 2>/dev/null); then
132
+ base_name=${pr_lines%%$'\t'*}
133
+ pr_url=${pr_lines#*$'\t'}
134
+ if [[ -n "$base_name" ]]; then
135
+ base_ref="origin/$base_name"
136
+ fi
137
+ fi
138
+ fi
139
+
140
+ if [[ -z "$base_ref" ]]; then
141
+ base_ref=origin/main
142
+ fi
143
+
144
+ review_kind=
145
+ if [[ "$mode" == local || ( "$mode" == auto && "$dirty" == true ) ]]; then
146
+ review_kind=local
147
+ elif [[ "$mode" == branch || ( "$mode" == auto && -n "$current_branch" && "$current_branch" != "main" ) ]]; then
148
+ review_kind=branch
149
+ else
150
+ echo "no review target: clean main checkout and no forced mode" >&2
151
+ exit 1
152
+ fi
153
+
154
+ if [[ "$review_kind" == local ]]; then
155
+ review_cmd=("$codex_bin" ${codex_args[@]+"${codex_args[@]}"} review --uncommitted)
156
+ else
157
+ review_cmd=("$codex_bin" ${codex_args[@]+"${codex_args[@]}"} review --base "$base_ref")
158
+ fi
159
+
160
+ printf 'codex-review target: %s\n' "$review_kind"
161
+ printf 'branch: %s\n' "${current_branch:-detached}"
162
+ if [[ -n "$pr_url" ]]; then
163
+ printf 'pr: %s\n' "$pr_url"
164
+ fi
165
+ printf 'review:'
166
+ printf ' %q' "${review_cmd[@]}"
167
+ printf '\n'
168
+ if [[ -n "$parallel_tests" ]]; then
169
+ printf 'tests: %s\n' "$parallel_tests"
170
+ fi
171
+ if [[ "$review_kind" == branch ]]; then
172
+ printf 'fetch: git fetch origin --quiet\n'
173
+ fi
174
+ if [[ -n "$output" ]]; then
175
+ printf 'output: %s\n' "$output"
176
+ fi
177
+
178
+ if [[ "$dry_run" == true ]]; then
179
+ exit 0
180
+ fi
181
+
182
+ if [[ "$review_kind" == branch ]]; then
183
+ git fetch origin --quiet || {
184
+ echo "warning: git fetch origin failed; reviewing with existing refs" >&2
185
+ }
186
+ fi
187
+
188
+ review_output=$output
189
+ review_output_is_temp=false
190
+ if [[ -z "$review_output" ]]; then
191
+ review_output=$(mktemp)
192
+ review_output_is_temp=true
193
+ fi
194
+
195
+ cleanup() {
196
+ if [[ "${review_output_is_temp:-false}" == true && -n "${review_output:-}" ]]; then
197
+ rm -f "$review_output"
198
+ fi
199
+ }
200
+ trap cleanup EXIT
201
+
202
+ run_review() {
203
+ mkdir -p "$(dirname "$review_output")"
204
+ "${review_cmd[@]}" 2>&1 | tee "$review_output"
205
+ }
206
+
207
+ elapsed_since() {
208
+ local started_at=$1
209
+ local finished_at
210
+ finished_at=$(date +%s)
211
+ printf '%s\n' "$((finished_at - started_at))"
212
+ }
213
+
214
+ format_elapsed() {
215
+ local seconds=$1
216
+ if (( seconds < 60 )); then
217
+ printf '%ss\n' "$seconds"
218
+ else
219
+ printf '%sm%ss\n' "$((seconds / 60))" "$((seconds % 60))"
220
+ fi
221
+ }
222
+
223
+ review_output_empty() {
224
+ [[ ! -s "$review_output" ]] || ! grep -q '[^[:space:]]' "$review_output"
225
+ }
226
+
227
+ review_output_has_findings() {
228
+ # Codex CLI transcripts include tool output before the final `codex` section.
229
+ # After the CLI header, prefer the first final-answer marker after the last
230
+ # Codex diagnostic line; if there are no diagnostics, use the last marker.
231
+ # Direct/fake review commands are scanned as a whole.
232
+ awk '
233
+ {
234
+ lines[NR] = $0
235
+ if ($0 ~ /^OpenAI Codex v/) {
236
+ has_codex_header = 1
237
+ }
238
+ if ($0 ~ /^[0-9]{4}-[0-9]{2}-[0-9]{2}T.* (WARN|ERROR) /) {
239
+ last_diagnostic = NR
240
+ }
241
+ if ($0 ~ /\[P[0-3]\]/) {
242
+ any_finding = 1
243
+ }
244
+ }
245
+ END {
246
+ if (has_codex_header) {
247
+ if (last_diagnostic) {
248
+ for (i = last_diagnostic + 1; i <= NR; i++) {
249
+ if (lines[i] == "codex") {
250
+ final_start = i + 1
251
+ break
252
+ }
253
+ }
254
+ } else {
255
+ for (i = 1; i <= NR; i++) {
256
+ if (lines[i] == "codex") {
257
+ final_start = i + 1
258
+ }
259
+ }
260
+ }
261
+ if (final_start) {
262
+ for (i = final_start; i <= NR; i++) {
263
+ if (lines[i] ~ /\[P[0-3]\]/) {
264
+ exit 0
265
+ }
266
+ }
267
+ exit 1
268
+ }
269
+ }
270
+ exit any_finding ? 0 : 1
271
+ }
272
+ ' "$review_output"
273
+ }
274
+
275
+ report_clean_review_or_fail() {
276
+ local elapsed_text
277
+ elapsed_text=$(format_elapsed "${review_elapsed_seconds:-0}")
278
+
279
+ if review_output_has_findings; then
280
+ printf 'codex-review complete after %s\n' "$elapsed_text"
281
+ printf 'codex-review findings: accepted/actionable findings reported\n'
282
+ return 1
283
+ fi
284
+ if review_output_empty; then
285
+ printf 'codex-review complete after %s; no output\n' "$elapsed_text"
286
+ return 1
287
+ fi
288
+ printf 'codex-review complete after %s\n' "$elapsed_text"
289
+ printf 'codex-review clean: no accepted/actionable findings reported\n'
290
+ }
291
+
292
+ if [[ -z "$parallel_tests" ]]; then
293
+ review_started_at=$(date +%s)
294
+ set +e
295
+ run_review
296
+ review_status=$?
297
+ review_elapsed_seconds=$(elapsed_since "$review_started_at")
298
+ set -e
299
+ if [[ "$review_status" == 0 ]]; then
300
+ report_clean_review_or_fail
301
+ exit $?
302
+ fi
303
+ exit "$review_status"
304
+ fi
305
+
306
+ review_status_file=$(mktemp)
307
+ review_elapsed_file=$(mktemp)
308
+ tests_status_file=$(mktemp)
309
+
310
+ (
311
+ set +e
312
+ review_started_at=$(date +%s)
313
+ run_review
314
+ status=$?
315
+ elapsed=$(elapsed_since "$review_started_at")
316
+ printf '%s\n' "$status" > "$review_status_file"
317
+ printf '%s\n' "$elapsed" > "$review_elapsed_file"
318
+ ) &
319
+ review_pid=$!
320
+
321
+ (
322
+ set +e
323
+ bash -lc "$parallel_tests"
324
+ status=$?
325
+ printf '%s\n' "$status" > "$tests_status_file"
326
+ ) &
327
+ tests_pid=$!
328
+
329
+ wait "$review_pid" || true
330
+ wait "$tests_pid" || true
331
+
332
+ review_status=$(cat "$review_status_file")
333
+ review_elapsed_seconds=$(cat "$review_elapsed_file")
334
+ tests_status=$(cat "$tests_status_file")
335
+ rm -f "$review_status_file" "$review_elapsed_file" "$tests_status_file"
336
+
337
+ printf 'codex-review exit: %s\n' "$review_status"
338
+ printf 'tests exit: %s\n' "$tests_status"
339
+
340
+ if [[ "$review_status" != 0 || "$tests_status" != 0 ]]; then
341
+ exit 1
342
+ fi
343
+
344
+ report_clean_review_or_fail