@lyupro/codex-bridge 0.6.4 → 0.6.5
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 +45 -0
- package/README.md +3 -3
- package/package.json +2 -2
- package/src/agents/codex-advisor.md +187 -0
- package/src/agents/codex-build.md +4 -0
- package/src/commands/env.md +5 -4
- package/src/home/lib/agents.mjs +3 -0
- package/src/home/lib/budgets.mjs +64 -0
- package/src/home/lib/config-validate.mjs +3 -39
- package/src/home/lib/meta/advice-status.mjs +61 -0
- package/src/home/lib/meta/advice-verdict.mjs +226 -0
- package/src/home/lib/meta/advisor-task.mjs +69 -0
- package/src/home/lib/meta/reply.mjs +41 -2
- package/src/home/lib/meta/run-state.mjs +2 -0
- package/src/home/lib/meta/verdict.mjs +7 -0
- package/src/home/lib/required-inputs.mjs +25 -4
- package/src/home/lib/run-codex.mjs +1 -0
- package/src/home/lib/run-config.mjs +5 -1
- package/src/home/lib/runner/args.mjs +1 -0
- package/src/home/lib/runner/codex-args.mjs +4 -3
- package/src/home/lib/runner/codex-cmd.mjs +1 -1
- package/src/home/lib/runner/launcher.mjs +17 -4
- package/src/home/lib/runner/preflight.mjs +75 -1
- package/src/home/lib/runner/prompts.mjs +39 -0
- package/src/home/lib/runner/schemas.mjs +137 -1
- package/src/home/lib/runner/scope-check.mjs +9 -1
- package/src/home/lib/runner/worker-order.mjs +4 -2
- package/src/home/lib/runner/worker.mjs +2 -2
- package/src/home/lib/write-meta.mjs +13 -0
|
@@ -4,8 +4,82 @@
|
|
|
4
4
|
* only the project runs root and existing holders, never the prospective run's directory.
|
|
5
5
|
*/
|
|
6
6
|
import path from 'node:path';
|
|
7
|
-
import
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import { advisorTaskRefusal } from '../meta/advisor-task.mjs';
|
|
9
|
+
import { activeRunDetails, readJson } from '../write-meta.mjs';
|
|
8
10
|
import { codexUnavailableReason } from './codex-cmd.mjs';
|
|
11
|
+
import { agentRole } from '../agents.mjs';
|
|
12
|
+
import { die } from './args.mjs';
|
|
13
|
+
|
|
14
|
+
/** Plan_59 D5/D6: settle design authority before any probe can spend quota. */
|
|
15
|
+
export function taskPreflight({ agent, taskText }) {
|
|
16
|
+
const freeRefusal = (reason) => ({
|
|
17
|
+
refusal: `${reason}\nThe run folder was not created; quota was not spent.`,
|
|
18
|
+
});
|
|
19
|
+
if (agent === 'codex-advisor') {
|
|
20
|
+
const reason = advisorTaskRefusal(taskText);
|
|
21
|
+
return reason ? freeRefusal(reason) : { refusal: null };
|
|
22
|
+
}
|
|
23
|
+
if (agent !== 'codex-build') return { refusal: null };
|
|
24
|
+
const lines = taskText.split(/\r\n|\n|\r/)
|
|
25
|
+
.flatMap((line) => {
|
|
26
|
+
const match = line.match(/^\s*(?:-\s+)?advice\s*:\s*(.*?)\s*$/i);
|
|
27
|
+
return match ? [match[1]] : [];
|
|
28
|
+
});
|
|
29
|
+
const advice = lines[0];
|
|
30
|
+
if (lines.length === 1) {
|
|
31
|
+
if (['mechanical', 'revert', 'docs-only', 'test-only'].includes(advice)) {
|
|
32
|
+
return { refusal: null, advice };
|
|
33
|
+
}
|
|
34
|
+
if (path.isAbsolute(advice)) {
|
|
35
|
+
try {
|
|
36
|
+
if (fs.statSync(advice).isDirectory() &&
|
|
37
|
+
readJson(path.join(advice, 'meta.json'))?.agent === 'codex-advisor') {
|
|
38
|
+
return { refusal: null, advice };
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
// Plan_59 D6: missing or unreadable evidence cannot authorize a construction.
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return freeRefusal('codex-build requires exactly one advice: line: mechanical | revert | docs-only | test-only ' +
|
|
46
|
+
'or an absolute path to an existing advisor run directory with meta.json agent codex-advisor. ' +
|
|
47
|
+
'An order that invents a construction needs a second opinion first; only work with no design choice may skip it.');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Plan_59 D14: an advice pass may continue only a completed, successful advisor scope run. */
|
|
51
|
+
export function scopeRunRefusal({ agent, phase, runsRoot, grantRun }) {
|
|
52
|
+
if (agent !== 'codex-advisor' || phase !== 'advise') return null;
|
|
53
|
+
let meta;
|
|
54
|
+
try {
|
|
55
|
+
meta = readJson(path.join(runsRoot, grantRun, 'meta.json'));
|
|
56
|
+
} catch {
|
|
57
|
+
meta = null;
|
|
58
|
+
}
|
|
59
|
+
const failed = [
|
|
60
|
+
meta?.agent === 'codex-advisor' ? null : "agent must be 'codex-advisor'",
|
|
61
|
+
meta?.phase === 'scope' ? null : "phase must be 'scope'",
|
|
62
|
+
meta?.status === 'OK' ? null : "status must be 'OK'",
|
|
63
|
+
].filter(Boolean);
|
|
64
|
+
if (!failed.length) return null;
|
|
65
|
+
return `continued run meta.json failed ${failed.join(', ')}. The run folder was not created; quota was not spent.`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** D2: phase mistakes must refuse before even the sandbox probe can spend quota. */
|
|
69
|
+
export function resolveRunPhase({ agent, phase, continue: isContinue }, budgets) {
|
|
70
|
+
// Plan_59 D7: phase 2 must retain the scope run's predictions before quota is spent.
|
|
71
|
+
if (agent === 'codex-advisor' && phase === 'advise' && !isContinue) {
|
|
72
|
+
die('codex-advisor --phase advise requires --continue: phase 2 continues the scope run of the same order ' +
|
|
73
|
+
'so it can settle the risks phase 1 predicted. The run folder was not created; quota was not spent.');
|
|
74
|
+
}
|
|
75
|
+
const phases = budgets[agentRole(agent)];
|
|
76
|
+
const names = Object.keys(phases);
|
|
77
|
+
if (phase === undefined && names.length === 1 && names[0] === 'default') return 'default';
|
|
78
|
+
if (phase !== undefined && Object.hasOwn(phases, phase)) return phase;
|
|
79
|
+
die(`${phase === undefined ? '--phase is required' : `undeclared --phase ${JSON.stringify(phase)}`} ` +
|
|
80
|
+
`for ${agent}. Action: pass --phase <name>; allowed phases: ${names.join(', ')}. ` +
|
|
81
|
+
'The run folder was not created; quota was not spent.');
|
|
82
|
+
}
|
|
9
83
|
|
|
10
84
|
export function preflightRefusal({ agent, projectRunsRoot, repoRoot }) {
|
|
11
85
|
// Two writing runs share one tree: the second snapshot would include the first one's edits.
|
|
@@ -8,6 +8,45 @@
|
|
|
8
8
|
import { DEFAULTS } from '../run-config.mjs';
|
|
9
9
|
import { RUN_ENV } from './run-env.mjs';
|
|
10
10
|
const BODIES = {
|
|
11
|
+
// Plan_59 D3-D5/D7/D10: independent design advice needs explicit lenses and a risk ledger.
|
|
12
|
+
'codex-advisor': (opts) => {
|
|
13
|
+
if (opts.phase !== 'scope' && opts.phase !== 'advise') {
|
|
14
|
+
throw new RangeError('codex-advisor instructions require phase scope or advise.');
|
|
15
|
+
}
|
|
16
|
+
return `Judge a DESIGN before code exists.
|
|
17
|
+
|
|
18
|
+
Rules for both phases:
|
|
19
|
+
- Read ONLY the paths listed under \`## Paths\`, plus \`git log\` / \`git show\` on those paths.
|
|
20
|
+
- Write nothing. Do your own reading and checks; do not spawn or delegate to other agents.
|
|
21
|
+
- Every claim about code must carry a \`path:line\` address inside those paths. Addresses are
|
|
22
|
+
machine-checked for existence; do not invent addresses or treat an unchecked claim as evidence.
|
|
23
|
+
- Do not guess which option the caller prefers: the task carries no preference on purpose.
|
|
24
|
+
- Two failure modes are forbidden: agreeing without independent checks ("both options are good",
|
|
25
|
+
"you know better"), and disagreeing to look independent. A disagreement without an address is
|
|
26
|
+
an opinion, not advice. Agreeing is fine when your own checks support it.
|
|
27
|
+
${opts.phase === 'scope' ? `
|
|
28
|
+
Scope phase:
|
|
29
|
+
- BEFORE reading in depth, name 3-5 predicted_risks with ids r1..r5: where this design most likely
|
|
30
|
+
breaks. Record those predictions first, then read; do not replace them with hindsight.
|
|
31
|
+
- Answer sufficient, missing_paths (concrete repository paths you still need), and taken_on_trust
|
|
32
|
+
(facts accepted from the task without checking, never empty), together with predicted_risks.
|
|
33
|
+
` : `
|
|
34
|
+
Advise phase:
|
|
35
|
+
- Settle every predicted risk from the scope run in risk_outcomes: its risk_id, an outcome of
|
|
36
|
+
confirmed or refuted, and an address supporting that outcome.
|
|
37
|
+
- Give ONE recommendation by task option_id, or none-of-these with a described unlisted_option.
|
|
38
|
+
Put every rejected task option in rejected with its cost; for none-of-these this means EVERY
|
|
39
|
+
task option. Set unlisted_option to "none" when recommending a listed option.
|
|
40
|
+
- Each field is a distinct lens: strongest_counterargument comes from a skeptic;
|
|
41
|
+
pre_mortem comes from the executor who has only this advice and the repository. Give each
|
|
42
|
+
scenario an early_check of kind test | command | inspect with a target; inspect requires an address.
|
|
43
|
+
- question_defect comes from the requester who needs the problem solved, not an option picked.
|
|
44
|
+
- Rate assumptions VERIFIED (requires an address) | REASONABLE | FRAGILE. Anything the requester
|
|
45
|
+
could refute with knowledge you lack belongs in open_questions, not why.
|
|
46
|
+
- independent_checks lists what you verified yourself, with addresses. Ground why in those checks
|
|
47
|
+
and state confidence honestly.
|
|
48
|
+
`}`;
|
|
49
|
+
},
|
|
11
50
|
'codex-scout': (opts, scope, questions = []) => `Response rules:
|
|
12
51
|
- You are working in read-only mode: do not try to write or edit anything.
|
|
13
52
|
- Answer from your own reading: do not spawn or delegate to other agents.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The shape each agent's answer must have, one JSON schema per agent.
|
|
3
|
+
* Plan_59 D10 adds alternatives, rated assumptions, risk outcomes, pre-mortems and open questions.
|
|
3
4
|
*
|
|
4
5
|
* Handed to `codex exec --output-schema`, so a run that answers in the wrong shape is
|
|
5
6
|
* rejected by Codex itself rather than discovered later by write-meta.mjs. Keyed by agent
|
|
@@ -149,9 +150,144 @@ const REVIEW_SCHEMA = {
|
|
|
149
150
|
},
|
|
150
151
|
};
|
|
151
152
|
|
|
152
|
-
/**
|
|
153
|
+
/** Plan_59: execution schemas stay static; schemaFor selects the advisor contract by phase. */
|
|
153
154
|
export const SCHEMAS = {
|
|
154
155
|
'codex-scout': SCOUT_SCHEMA,
|
|
155
156
|
'codex-build': BUILD_SCHEMA,
|
|
156
157
|
'codex-review': REVIEW_SCHEMA,
|
|
157
158
|
};
|
|
159
|
+
|
|
160
|
+
// Plan_59 D3/D4/D5/D10 keeps the design contract separate from the existing execution roles.
|
|
161
|
+
export const PHASE_SCHEMAS = {
|
|
162
|
+
advisor: {
|
|
163
|
+
scope: {
|
|
164
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
165
|
+
type: 'object',
|
|
166
|
+
additionalProperties: false,
|
|
167
|
+
required: ['sufficient', 'missing_paths', 'taken_on_trust', 'predicted_risks'],
|
|
168
|
+
properties: {
|
|
169
|
+
sufficient: { type: 'boolean' },
|
|
170
|
+
missing_paths: { type: 'array', items: { type: 'string' } },
|
|
171
|
+
taken_on_trust: { type: 'array', minItems: 1, items: { type: 'string' } },
|
|
172
|
+
predicted_risks: {
|
|
173
|
+
type: 'array',
|
|
174
|
+
minItems: 3,
|
|
175
|
+
maxItems: 5,
|
|
176
|
+
items: {
|
|
177
|
+
type: 'object',
|
|
178
|
+
additionalProperties: false,
|
|
179
|
+
required: ['id', 'risk'],
|
|
180
|
+
properties: { id: { type: 'string', pattern: '^r[1-9]$' }, risk: { type: 'string' } },
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
advise: {
|
|
186
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
187
|
+
type: 'object',
|
|
188
|
+
additionalProperties: false,
|
|
189
|
+
required: [
|
|
190
|
+
'recommendation', 'why', 'rejected', 'strongest_counterargument', 'question_defect',
|
|
191
|
+
'unlisted_option', 'assumptions', 'risk_outcomes', 'pre_mortem', 'open_questions',
|
|
192
|
+
'confidence', 'independent_checks',
|
|
193
|
+
],
|
|
194
|
+
properties: {
|
|
195
|
+
recommendation: {
|
|
196
|
+
type: 'object',
|
|
197
|
+
additionalProperties: false,
|
|
198
|
+
required: ['option_id', 'text'],
|
|
199
|
+
properties: {
|
|
200
|
+
option_id: { type: 'string' },
|
|
201
|
+
text: { type: 'string', maxLength: 300, pattern: '^[^\\r\\n\\u2028\\u2029]*$' },
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
unlisted_option: { type: 'string' },
|
|
205
|
+
why: { type: 'array', minItems: 3, maxItems: 5, items: { type: 'string' } },
|
|
206
|
+
rejected: {
|
|
207
|
+
type: 'array',
|
|
208
|
+
minItems: 1,
|
|
209
|
+
items: {
|
|
210
|
+
type: 'object',
|
|
211
|
+
additionalProperties: false,
|
|
212
|
+
required: ['option_id', 'cost'],
|
|
213
|
+
properties: { option_id: { type: 'string' }, cost: { type: 'string' } },
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
strongest_counterargument: { type: 'string', minLength: 80 },
|
|
217
|
+
question_defect: { type: 'string' },
|
|
218
|
+
assumptions: {
|
|
219
|
+
type: 'array',
|
|
220
|
+
items: {
|
|
221
|
+
type: 'object',
|
|
222
|
+
additionalProperties: false,
|
|
223
|
+
required: ['claim', 'rating', 'address'],
|
|
224
|
+
properties: {
|
|
225
|
+
claim: { type: 'string' },
|
|
226
|
+
rating: { type: 'string', enum: ['VERIFIED', 'REASONABLE', 'FRAGILE'] },
|
|
227
|
+
address: { type: 'string' },
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
risk_outcomes: {
|
|
232
|
+
type: 'array',
|
|
233
|
+
items: {
|
|
234
|
+
type: 'object',
|
|
235
|
+
additionalProperties: false,
|
|
236
|
+
required: ['risk_id', 'outcome', 'address'],
|
|
237
|
+
properties: {
|
|
238
|
+
risk_id: { type: 'string' },
|
|
239
|
+
outcome: { type: 'string', enum: ['confirmed', 'refuted'] },
|
|
240
|
+
address: { type: 'string' },
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
pre_mortem: {
|
|
245
|
+
type: 'array',
|
|
246
|
+
minItems: 2,
|
|
247
|
+
maxItems: 3,
|
|
248
|
+
items: {
|
|
249
|
+
type: 'object',
|
|
250
|
+
additionalProperties: false,
|
|
251
|
+
required: ['scenario', 'early_check'],
|
|
252
|
+
properties: {
|
|
253
|
+
scenario: { type: 'string', minLength: 30 },
|
|
254
|
+
early_check: {
|
|
255
|
+
type: 'object',
|
|
256
|
+
additionalProperties: false,
|
|
257
|
+
required: ['kind', 'target'],
|
|
258
|
+
properties: {
|
|
259
|
+
kind: { type: 'string', enum: ['test', 'command', 'inspect'] },
|
|
260
|
+
target: { type: 'string', minLength: 3 },
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
open_questions: { type: 'array', items: { type: 'string' } },
|
|
267
|
+
confidence: { type: 'string', enum: ['high', 'medium', 'low'] },
|
|
268
|
+
independent_checks: {
|
|
269
|
+
type: 'array',
|
|
270
|
+
minItems: 1,
|
|
271
|
+
items: {
|
|
272
|
+
type: 'object',
|
|
273
|
+
additionalProperties: false,
|
|
274
|
+
required: ['check', 'address'],
|
|
275
|
+
properties: { check: { type: 'string' }, address: { type: 'string' } },
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
// Plan_59: an advisor's phase, not its agent name alone, defines the answer contract.
|
|
284
|
+
export function schemaFor(agent, phase) {
|
|
285
|
+
return agent === 'codex-advisor' ? advisorSchema(phase) : SCHEMAS[agent];
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export function advisorSchema(phase) {
|
|
289
|
+
if (!Object.hasOwn(PHASE_SCHEMAS.advisor, phase)) {
|
|
290
|
+
throw new RangeError(`Unknown advisor phase ${JSON.stringify(phase)}; use "scope" or "advise".`);
|
|
291
|
+
}
|
|
292
|
+
return PHASE_SCHEMAS.advisor[phase];
|
|
293
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import fs from 'node:fs';
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { spawnSync } from 'node:child_process';
|
|
10
|
-
import { globToRegExp, normalizePath } from '../meta/paths.mjs';
|
|
10
|
+
import { globToRegExp, normalizePath, SERVICE_RE } from '../meta/paths.mjs';
|
|
11
11
|
|
|
12
12
|
// Directories a self-walk must never descend into. Only reached when the repository has no git:
|
|
13
13
|
// with git present the file list comes from the repository itself, which already knows what is
|
|
@@ -43,6 +43,14 @@ function structuralRefusal(repoRoot, pattern) {
|
|
|
43
43
|
action: 'remove the .. segment and keep the path relative to the repository root',
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
|
+
// Plan_59 S1: 2026-09-22_235724_plan59-d1-advisor-docs-surface spent 22 minutes on a
|
|
47
|
+
// service-directory scope the verdict could never authorise. Refuse it before a run exists.
|
|
48
|
+
if (SERVICE_RE.test(normalizePath(pattern))) {
|
|
49
|
+
return {
|
|
50
|
+
reason: 'lies inside a service directory that no scope can authorise',
|
|
51
|
+
action: 'name the file outside the service directory, or make that edit yourself',
|
|
52
|
+
};
|
|
53
|
+
}
|
|
46
54
|
// Plan_58 D7: 2026-09-19_191402_ports-infra spent a run on a bare scope-new directory,
|
|
47
55
|
// which the verdict could only match as a file. Refuse that intent before spending a run.
|
|
48
56
|
if (!pattern || /[*?]/.test(pattern)) return null;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* After the launcher/worker split this file is the only connection between the two halves, and the
|
|
5
5
|
* worker never re-reads the CLI or the configuration — a run that consulted config.json twice could
|
|
6
6
|
* end up honouring two different limits. It carries `agent`, `slug`, `order_id`, `repo`,
|
|
7
|
-
* `is_git_repo`, `launcher_pid`, `budget_minutes`, `scope_new`, `profile` and `args`; worker.mjs
|
|
7
|
+
* `is_git_repo`, `launcher_pid`, `phase`, `budget_minutes`, `scope_new`, `profile` and `args`; worker.mjs
|
|
8
8
|
* reads `repo`, `agent`, `args`, `is_git_repo` and `budget_minutes`. The rest is deliberate: those
|
|
9
9
|
* fields are what a run folder read back months later needs in order to explain itself — which
|
|
10
10
|
* order it belonged to, which new paths it declared included, and which worker was actually
|
|
@@ -24,6 +24,7 @@ export function workerOrder({
|
|
|
24
24
|
repo,
|
|
25
25
|
isGitRepo,
|
|
26
26
|
launcherPid,
|
|
27
|
+
phase,
|
|
27
28
|
budgetMinutes,
|
|
28
29
|
scopeNew,
|
|
29
30
|
profile,
|
|
@@ -36,7 +37,8 @@ export function workerOrder({
|
|
|
36
37
|
repo,
|
|
37
38
|
is_git_repo: isGitRepo,
|
|
38
39
|
launcher_pid: launcherPid,
|
|
39
|
-
|
|
40
|
+
phase,
|
|
41
|
+
// D2: the phase's wall-clock budget is frozen before the worker, never re-read from config.
|
|
40
42
|
budget_minutes: budgetMinutes,
|
|
41
43
|
scope_new: scopeNew,
|
|
42
44
|
// Which worker was ordered, and where each half of that answer came from. Recorded separately
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* It takes its whole order from worker.json, written by launcher.mjs into the run folder —
|
|
5
5
|
* after the split that file is the only connection between the two halves. Read here:
|
|
6
6
|
* `repo`, `agent`, `args`, `is_git_repo`, `budget_minutes`. Written there: those five plus
|
|
7
|
-
* `slug`, `launcher_pid` and `scope_new`, which nothing here needs but a run folder read back
|
|
8
|
-
* months later does. The launcher must write the selected
|
|
7
|
+
* `phase`, `slug`, `launcher_pid` and `scope_new`, which nothing here needs but a run folder read back
|
|
8
|
+
* months later does. The launcher must write the selected phase's budget into worker.json before
|
|
9
9
|
* spawning this half, so the worker never re-reads config.json and one run cannot acquire two
|
|
10
10
|
* configurations.
|
|
11
11
|
* Neither side may change the shape alone.
|
|
@@ -82,6 +82,7 @@ export function collect(runDir, agent, exitCode) {
|
|
|
82
82
|
const exit = exitCode === undefined || exitCode === null ? null : Number(exitCode);
|
|
83
83
|
const events = readEvents(runDir);
|
|
84
84
|
const worker = readJson(path.join(runDir, 'worker.json'));
|
|
85
|
+
const launched = readJson(path.join(runDir, 'status.json')) || {};
|
|
85
86
|
|
|
86
87
|
const args = Array.isArray(worker?.args) ? worker.args : [];
|
|
87
88
|
const argValue = (flag) => {
|
|
@@ -99,6 +100,18 @@ export function collect(runDir, agent, exitCode) {
|
|
|
99
100
|
|
|
100
101
|
const meta = {
|
|
101
102
|
agent,
|
|
103
|
+
// D2: old runs have no phase fact; do not invent one from today's registry.
|
|
104
|
+
phase: worker?.phase ?? launched.phase ?? null,
|
|
105
|
+
// Plan_59 D4: the scope outcome is a field the orchestrator branches on, not a sentence in
|
|
106
|
+
// the reply. Null when the result never said, so a missing answer is not read as "sufficient".
|
|
107
|
+
...(agent === 'codex-advisor'
|
|
108
|
+
? {
|
|
109
|
+
sufficient: typeof result?.sufficient === 'boolean' ? result.sufficient : null,
|
|
110
|
+
missing_paths: Array.isArray(result?.missing_paths) ? result.missing_paths : null,
|
|
111
|
+
}
|
|
112
|
+
: {}),
|
|
113
|
+
// Plan_59 D6: which second opinion a build order named, so skipped advice is measurable.
|
|
114
|
+
...(agent === 'codex-build' ? { advice: launched.advice ?? null } : {}),
|
|
102
115
|
runner_version: RUNNER_VERSION,
|
|
103
116
|
project: path.basename(path.dirname(runDir)),
|
|
104
117
|
run: path.basename(runDir),
|