@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.
@@ -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
+ }
@@ -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
- '--order-id is required: the orchestrator issues the order label and the runner will not invent one. ' +
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
- '--scope is required for codex-build: comma-separated globs relative to the repo root, ' +
106
- 'e.g. --scope "packages/event-calendar/**,apps/orchestrator/src/maestro/calendar-refresh.ts". ' +
107
- 'Anything outside the list is off limits and fails the run.',
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;
@@ -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
@@ -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: ['summary', 'changes', 'verify_command', 'verify_passed', 'leftovers', 'report_markdown'],
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',