@lyupro/codex-bridge 0.1.1 → 0.1.3
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/CHANGELOG.md +21 -0
- package/README.md +2 -1
- package/cli/doctor.mjs +39 -36
- package/cli/install.mjs +57 -18
- package/cli/manifest.mjs +47 -13
- package/cli/rules-owners.mjs +93 -22
- package/cli/settings-merge.mjs +44 -22
- package/cli/uninstall.mjs +73 -26
- package/cli/update.mjs +35 -8
- package/package.json +1 -1
- package/src/agents/codex-build.md +10 -10
- package/src/agents/codex-review.md +10 -5
- package/src/agents/codex-scout.md +10 -6
- package/src/hook-definitions.mjs +25 -0
- package/src/hooks/order-gate.mjs +68 -0
- package/src/meta/outcome.mjs +65 -0
- package/src/meta/reply.mjs +23 -0
- package/src/meta/verdict.mjs +8 -0
- package/src/required-inputs.mjs +104 -0
- package/src/runner/args.mjs +13 -5
- package/src/runner/prompts.mjs +9 -0
- package/src/runner/schemas.mjs +23 -1
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Defines the inputs the orchestrator must give each Codex dispatcher.
|
|
3
|
+
*
|
|
4
|
+
* This contract belongs beside the gate because the dispatcher prompt was the only place that
|
|
5
|
+
* named order id and scope, so the caller launched codex-build without either value and the
|
|
6
|
+
* runner refused the call before Codex could do any work. Pass 2 can render the same entries
|
|
7
|
+
* into agent instructions without creating a second list that can drift.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const PLACEHOLDER_VALUES = new Set(['todo', 'tbd', 'label', 'none', 'order id', 'scope', 'xxx']);
|
|
11
|
+
|
|
12
|
+
const freezeEntries = (entries) => Object.freeze(entries.map((entry) => Object.freeze(entry)));
|
|
13
|
+
|
|
14
|
+
export const REQUIRED_INPUTS = Object.freeze({
|
|
15
|
+
'codex-scout': freezeEntries([
|
|
16
|
+
{
|
|
17
|
+
label: 'order id',
|
|
18
|
+
source: 'the orchestrator',
|
|
19
|
+
explanation: 'The label this order is known by. Repeating a call with the same label joins the run already in flight and costs no quota; a different piece of work needs a new label, never a reused one.',
|
|
20
|
+
example: 'plan-13-scout-20260804',
|
|
21
|
+
},
|
|
22
|
+
]),
|
|
23
|
+
'codex-build': freezeEntries([
|
|
24
|
+
{
|
|
25
|
+
label: 'order id',
|
|
26
|
+
source: 'the orchestrator',
|
|
27
|
+
explanation: 'The label this order is known by. Repeating a call with the same label joins the run already in flight and costs no quota; a different piece of work needs a new label, never a reused one.',
|
|
28
|
+
example: 'plan-13-build-20260804',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
label: 'scope',
|
|
32
|
+
source: 'the orchestrator',
|
|
33
|
+
explanation: 'Comma-separated globs relative to the repository root, listing every file the run may touch — including each caller of what changes, not only the file being edited. Anything outside the list fails the run.',
|
|
34
|
+
example: 'src/runner/**,tests/runner/**',
|
|
35
|
+
},
|
|
36
|
+
]),
|
|
37
|
+
'codex-review': freezeEntries([
|
|
38
|
+
{
|
|
39
|
+
label: 'order id',
|
|
40
|
+
source: 'the orchestrator',
|
|
41
|
+
explanation: 'The label this order is known by. Repeating a call with the same label joins the run already in flight and costs no quota; a different piece of work needs a new label, never a reused one.',
|
|
42
|
+
example: 'plan-13-review-20260804',
|
|
43
|
+
},
|
|
44
|
+
]),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
48
|
+
|
|
49
|
+
function cleanValue(value) {
|
|
50
|
+
if (typeof value !== 'string') return '';
|
|
51
|
+
return value.trim().replace(/^([`'\"])(.*)\1$/, '$2').trim();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isPlaceholder(value) {
|
|
55
|
+
const cleaned = cleanValue(value);
|
|
56
|
+
if (!cleaned) return true;
|
|
57
|
+
if (cleaned.startsWith('<') && cleaned.endsWith('>')) return true;
|
|
58
|
+
return PLACEHOLDER_VALUES.has(cleaned.replace(/[.,;:]+$/, '').trim().toLowerCase());
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function extractValue(promptText, label) {
|
|
62
|
+
const labelPattern = escapeRegExp(label).replaceAll('\\ ', '\\s+');
|
|
63
|
+
const flag = `--${label.replaceAll(/\s+/g, '-')}`;
|
|
64
|
+
const direct = new RegExp(
|
|
65
|
+
`(?:^|\\r?\\n)\\s*(?:[-*]\\s*)?(?:[*_` + '`' + `]?${labelPattern}[*_` + '`' + `]?)\\s*(?::|=|—|-)\\s*([^\\r\\n]*)`,
|
|
66
|
+
'im',
|
|
67
|
+
);
|
|
68
|
+
const directMatch = promptText.match(direct);
|
|
69
|
+
if (directMatch) return cleanValue(directMatch[1]);
|
|
70
|
+
|
|
71
|
+
const flagPattern = new RegExp(
|
|
72
|
+
`${escapeRegExp(flag)}(?:=|\\s+)(?:"([^"]*)"|'([^']*)'|([^\\s\\r\\n]+))`,
|
|
73
|
+
'i',
|
|
74
|
+
);
|
|
75
|
+
const flagMatch = promptText.match(flagPattern);
|
|
76
|
+
if (!flagMatch) return null;
|
|
77
|
+
return cleanValue(flagMatch[1] ?? flagMatch[2] ?? flagMatch[3]);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Returns the immutable input entries for one dispatcher type. */
|
|
81
|
+
export function requiredInputsFor(agentType) {
|
|
82
|
+
return REQUIRED_INPUTS[agentType] || [];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Returns required entries whose value is absent or is still an obvious template placeholder. */
|
|
86
|
+
export function missingInputs(agentType, promptText) {
|
|
87
|
+
const prompt = typeof promptText === 'string' ? promptText : '';
|
|
88
|
+
return requiredInputsFor(agentType).filter((entry) => isPlaceholder(extractValue(prompt, entry.label)));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Renders the compact contract the orchestrator sees while choosing a dispatcher. */
|
|
92
|
+
export function renderRequiredInputSummary(agentType) {
|
|
93
|
+
const entries = requiredInputsFor(agentType);
|
|
94
|
+
if (!entries.length) return '';
|
|
95
|
+
const labels = entries.map((entry) => `\`${entry.label}\``);
|
|
96
|
+
return `Requires ${entries[0].source}-provided ${labels.join(' and ')}.`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Renders the same contract for pass 2 agent instructions. */
|
|
100
|
+
export function renderRequiredInputs(agentType) {
|
|
101
|
+
return requiredInputsFor(agentType)
|
|
102
|
+
.map((entry) => `- ${entry.label}: ${entry.explanation} Example: \`${entry.example}\`.`)
|
|
103
|
+
.join('\n');
|
|
104
|
+
}
|
package/src/runner/args.mjs
CHANGED
|
@@ -8,12 +8,17 @@
|
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { AGENTS } from '../write-meta.mjs';
|
|
10
10
|
import { ALLOWED_EFFORTS } from '../run-config.mjs';
|
|
11
|
+
import { requiredInputsFor } from '../required-inputs.mjs';
|
|
11
12
|
|
|
12
13
|
export function die(message) {
|
|
13
14
|
console.error(`run-codex: ${message}`);
|
|
14
15
|
process.exit(2);
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
function requiredInput(agentType, label) {
|
|
19
|
+
return requiredInputsFor(agentType).find((entry) => entry.label === label);
|
|
20
|
+
}
|
|
21
|
+
|
|
17
22
|
// Flags that carry no value. A value is accepted only in its explicit yes/no spellings;
|
|
18
23
|
// anything else stops the run. The permissive reading this replaces — "not 0/false/no means
|
|
19
24
|
// yes" — turned the prompt's own placeholder text (`--continue "<only if the orchestrator provided
|
|
@@ -63,10 +68,12 @@ export function parseArgs(argv) {
|
|
|
63
68
|
if (!AGENTS[opts.agent]) die(`unknown --agent ${opts.agent}`);
|
|
64
69
|
opts.orderId = String(opts['order-id'] ?? '').trim();
|
|
65
70
|
if (!opts.orderId) {
|
|
71
|
+
const orderInput = requiredInput(opts.agent, 'order id');
|
|
66
72
|
die(
|
|
67
|
-
|
|
73
|
+
`--order-id is required: ${orderInput.source} supplies the ${orderInput.label}; the runner will not invent one. ` +
|
|
68
74
|
'A repeat of the same order should come back with --continue. If this appeared right after a package update, ' +
|
|
69
|
-
'the installed dispatcher prompts are stale and need npm run dev:install (local dev) or codex-bridge update.'
|
|
75
|
+
'the installed dispatcher prompts are stale and need npm run dev:install (local dev) or codex-bridge update. ' +
|
|
76
|
+
`Example: --order-id "${orderInput.example}". Action: get the value from ${orderInput.source}, pass it as --order-id, and retry.`,
|
|
70
77
|
);
|
|
71
78
|
}
|
|
72
79
|
if (opts.agent === 'codex-scout') {
|
|
@@ -101,10 +108,11 @@ export function parseArgs(argv) {
|
|
|
101
108
|
.map((p) => p.trim())
|
|
102
109
|
.filter(Boolean);
|
|
103
110
|
if (opts.agent === 'codex-build' && !opts.scopePatterns.length) {
|
|
111
|
+
const scopeInput = requiredInput(opts.agent, 'scope');
|
|
104
112
|
die(
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
113
|
+
`--scope is required for codex-build: ${scopeInput.source} supplies the ${scopeInput.label}. ` +
|
|
114
|
+
`${scopeInput.explanation} Example: --scope "${scopeInput.example}". ` +
|
|
115
|
+
`Action: get the value from ${scopeInput.source}, pass it as --scope, and retry.`,
|
|
108
116
|
);
|
|
109
117
|
}
|
|
110
118
|
return opts;
|
package/src/runner/prompts.mjs
CHANGED
|
@@ -52,6 +52,15 @@ Working rules:
|
|
|
52
52
|
cannot be done, do not pretend it is done; list it in leftovers.
|
|
53
53
|
- After making changes, run verification: ${opts.verify || 'determine the command from the project'}.
|
|
54
54
|
Put the command actually run in verify_command and whether it passed in verify_passed.
|
|
55
|
+
- Declare the outcome in outcome: "done" if the work the task asked for was carried out,
|
|
56
|
+
"fail" if it was not — for any reason at all, including an impossible order, a file that
|
|
57
|
+
does not exist, a scope that forbids the only fix, or running out of time. "done" with an
|
|
58
|
+
empty changes[] is correct only when the task asked you to check something and nothing
|
|
59
|
+
needed changing; if you could not do what was asked, "fail" is the answer even when the
|
|
60
|
+
worktree is clean and verification is green. This is not a self-grade: work that was done
|
|
61
|
+
and merely leaves follow-ups is "done" with the follow-ups in leftovers, and partial work
|
|
62
|
+
that satisfies the definition of done is "done" too. On "fail" the first sentence of summary
|
|
63
|
+
states the reason — it is what the orchestrator is shown.
|
|
55
64
|
- summary and changes describe only changes in this repository for this task. Do not include
|
|
56
65
|
anything unrelated in them.
|
|
57
66
|
- Each change is a separate changes[] entry, exactly one file per entry. The file field contains
|
package/src/runner/schemas.mjs
CHANGED
|
@@ -51,8 +51,30 @@ const BUILD_SCHEMA = {
|
|
|
51
51
|
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
52
52
|
type: 'object',
|
|
53
53
|
additionalProperties: false,
|
|
54
|
-
required: [
|
|
54
|
+
required: [
|
|
55
|
+
'outcome',
|
|
56
|
+
'summary',
|
|
57
|
+
'changes',
|
|
58
|
+
'verify_command',
|
|
59
|
+
'verify_passed',
|
|
60
|
+
'leftovers',
|
|
61
|
+
'report_markdown',
|
|
62
|
+
],
|
|
55
63
|
properties: {
|
|
64
|
+
// The outcome is declared, never inferred. On 2026-08-04 a build asked to fix a function
|
|
65
|
+
// in a module that does not exist answered `OK — no code change was made`: every artifact
|
|
66
|
+
// was well-formed, the tree was legitimately clean, and the runner has no way to know
|
|
67
|
+
// whether the order required an edit. A string rather than a boolean because the set of
|
|
68
|
+
// outcomes will grow (`blocked` is a candidate) and an enum extends without breaking the
|
|
69
|
+
// contract. Presence of this field in the run's own schema.json is also the marker that
|
|
70
|
+
// says the run was contracted to declare an outcome — see meta/outcome.mjs.
|
|
71
|
+
outcome: {
|
|
72
|
+
type: 'string',
|
|
73
|
+
enum: ['done', 'fail'],
|
|
74
|
+
description:
|
|
75
|
+
'done — the work the task asked for was carried out. fail — it was not, for any ' +
|
|
76
|
+
'reason at all (impossible order, missing file, blocked by scope, out of time).',
|
|
77
|
+
},
|
|
56
78
|
summary: { type: 'string', minLength: 1 },
|
|
57
79
|
changes: {
|
|
58
80
|
type: 'array',
|