@on-par/factory-core 0.9.3 → 0.9.5-next.1
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/dist/checkers/base-health.js +2 -2
- package/dist/checkers/base-health.js.map +1 -1
- package/dist/checkers/baseline-cache.d.ts +6 -4
- package/dist/checkers/baseline-cache.d.ts.map +1 -1
- package/dist/checkers/baseline-cache.js +24 -7
- package/dist/checkers/baseline-cache.js.map +1 -1
- package/dist/checkers/baseline.d.ts +2 -0
- package/dist/checkers/baseline.d.ts.map +1 -1
- package/dist/checkers/baseline.js +34 -3
- package/dist/checkers/baseline.js.map +1 -1
- package/dist/checkers/constitution-preflight.d.ts +8 -0
- package/dist/checkers/constitution-preflight.d.ts.map +1 -0
- package/dist/checkers/constitution-preflight.js +71 -0
- package/dist/checkers/constitution-preflight.js.map +1 -0
- package/dist/checkers/design-smells.d.ts.map +1 -1
- package/dist/checkers/design-smells.js +1 -0
- package/dist/checkers/design-smells.js.map +1 -1
- package/dist/checkers/index.d.ts +11 -0
- package/dist/checkers/index.d.ts.map +1 -1
- package/dist/checkers/index.js +87 -9
- package/dist/checkers/index.js.map +1 -1
- package/dist/config/config-lint.d.ts +28 -0
- package/dist/config/config-lint.d.ts.map +1 -0
- package/dist/config/config-lint.js +39 -0
- package/dist/config/config-lint.js.map +1 -0
- package/dist/config/config-schema-lint.d.ts +17 -0
- package/dist/config/config-schema-lint.d.ts.map +1 -0
- package/dist/config/config-schema-lint.js +104 -0
- package/dist/config/config-schema-lint.js.map +1 -0
- package/dist/config/example.d.ts.map +1 -1
- package/dist/config/example.js +23 -1
- package/dist/config/example.js.map +1 -1
- package/dist/config/index.d.ts +19 -3
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +38 -13
- package/dist/config/index.js.map +1 -1
- package/dist/config/repo.d.ts +3 -0
- package/dist/config/repo.d.ts.map +1 -1
- package/dist/config/repo.js +12 -1
- package/dist/config/repo.js.map +1 -1
- package/dist/config/template-tokens.d.ts +21 -0
- package/dist/config/template-tokens.d.ts.map +1 -0
- package/dist/config/template-tokens.js +62 -0
- package/dist/config/template-tokens.js.map +1 -0
- package/dist/constitutions/index.d.ts.map +1 -1
- package/dist/constitutions/index.js +2 -0
- package/dist/constitutions/index.js.map +1 -1
- package/dist/events/kinds.d.ts +1 -1
- package/dist/events/kinds.d.ts.map +1 -1
- package/dist/events/kinds.js +4 -0
- package/dist/events/kinds.js.map +1 -1
- package/dist/filing/index.d.ts.map +1 -1
- package/dist/filing/index.js +1 -0
- package/dist/filing/index.js.map +1 -1
- package/dist/harness/catalog.d.ts +4 -1
- package/dist/harness/catalog.d.ts.map +1 -1
- package/dist/harness/catalog.js +7 -1
- package/dist/harness/catalog.js.map +1 -1
- package/dist/harness/classify.d.ts.map +1 -1
- package/dist/harness/classify.js +5 -0
- package/dist/harness/classify.js.map +1 -1
- package/dist/harness/codex-cli.d.ts.map +1 -1
- package/dist/harness/codex-cli.js +7 -0
- package/dist/harness/codex-cli.js.map +1 -1
- package/dist/harness/contract.d.ts.map +1 -1
- package/dist/harness/contract.js +1 -0
- package/dist/harness/contract.js.map +1 -1
- package/dist/harness/opencode.d.ts +17 -2
- package/dist/harness/opencode.d.ts.map +1 -1
- package/dist/harness/opencode.js +150 -36
- package/dist/harness/opencode.js.map +1 -1
- package/dist/internal.d.ts +25 -6
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +17 -5
- package/dist/internal.js.map +1 -1
- package/dist/models/index.d.ts +5 -0
- package/dist/models/index.d.ts.map +1 -1
- package/dist/models/index.js +33 -3
- package/dist/models/index.js.map +1 -1
- package/dist/phases/build.d.ts +6 -0
- package/dist/phases/build.d.ts.map +1 -1
- package/dist/phases/build.js +19 -4
- package/dist/phases/build.js.map +1 -1
- package/dist/phases/check.d.ts +5 -1
- package/dist/phases/check.d.ts.map +1 -1
- package/dist/phases/check.js +33 -7
- package/dist/phases/check.js.map +1 -1
- package/dist/phases/plan.d.ts +16 -3
- package/dist/phases/plan.d.ts.map +1 -1
- package/dist/phases/plan.js +104 -30
- package/dist/phases/plan.js.map +1 -1
- package/dist/phases/ship-stack.d.ts +43 -0
- package/dist/phases/ship-stack.d.ts.map +1 -0
- package/dist/phases/ship-stack.js +91 -0
- package/dist/phases/ship-stack.js.map +1 -0
- package/dist/phases/ship.d.ts +5 -0
- package/dist/phases/ship.d.ts.map +1 -1
- package/dist/phases/ship.js +2 -1
- package/dist/phases/ship.js.map +1 -1
- package/dist/queue/index.d.ts.map +1 -1
- package/dist/queue/index.js +2 -11
- package/dist/queue/index.js.map +1 -1
- package/dist/readiness/decompose.d.ts +4 -2
- package/dist/readiness/decompose.d.ts.map +1 -1
- package/dist/readiness/decompose.js +6 -2
- package/dist/readiness/decompose.js.map +1 -1
- package/dist/readiness/enrich.d.ts +6 -0
- package/dist/readiness/enrich.d.ts.map +1 -1
- package/dist/readiness/enrich.js +20 -1
- package/dist/readiness/enrich.js.map +1 -1
- package/dist/readiness/slice-gate.d.ts +5 -0
- package/dist/readiness/slice-gate.d.ts.map +1 -1
- package/dist/readiness/slice-gate.js +10 -4
- package/dist/readiness/slice-gate.js.map +1 -1
- package/dist/readiness/slice-plan-github.d.ts +25 -0
- package/dist/readiness/slice-plan-github.d.ts.map +1 -1
- package/dist/readiness/slice-plan-github.js +23 -1
- package/dist/readiness/slice-plan-github.js.map +1 -1
- package/dist/readiness/slice-plan.d.ts +11 -2
- package/dist/readiness/slice-plan.d.ts.map +1 -1
- package/dist/readiness/slice-plan.js +19 -4
- package/dist/readiness/slice-plan.js.map +1 -1
- package/dist/reports/benchmark-artifacts.d.ts +1 -1
- package/dist/reports/benchmark-artifacts.d.ts.map +1 -1
- package/dist/reports/evidence-pack.d.ts +4 -0
- package/dist/reports/evidence-pack.d.ts.map +1 -1
- package/dist/reports/evidence-pack.js +5 -1
- package/dist/reports/evidence-pack.js.map +1 -1
- package/dist/router/index.d.ts +17 -0
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +84 -1
- package/dist/router/index.js.map +1 -1
- package/dist/run/outcome.d.ts +3 -1
- package/dist/run/outcome.d.ts.map +1 -1
- package/dist/run/outcome.js.map +1 -1
- package/dist/run/resume-context.d.ts +51 -0
- package/dist/run/resume-context.d.ts.map +1 -1
- package/dist/run/resume-context.js +97 -0
- package/dist/run/resume-context.js.map +1 -1
- package/dist/run/run-issue.d.ts +16 -1
- package/dist/run/run-issue.d.ts.map +1 -1
- package/dist/run/run-issue.js +290 -187
- package/dist/run/run-issue.js.map +1 -1
- package/dist/sandbox/egress-proxy.d.ts +16 -0
- package/dist/sandbox/egress-proxy.d.ts.map +1 -1
- package/dist/sandbox/egress-proxy.js +91 -7
- package/dist/sandbox/egress-proxy.js.map +1 -1
- package/dist/sandbox/index.d.ts +2 -0
- package/dist/sandbox/index.d.ts.map +1 -1
- package/dist/sandbox/index.js +3 -1
- package/dist/sandbox/index.js.map +1 -1
- package/dist/steward/diagnose.d.ts +5 -1
- package/dist/steward/diagnose.d.ts.map +1 -1
- package/dist/steward/diagnose.js +13 -5
- package/dist/steward/diagnose.js.map +1 -1
- package/dist/steward/run.d.ts +2 -0
- package/dist/steward/run.d.ts.map +1 -1
- package/dist/steward/run.js +1 -0
- package/dist/steward/run.js.map +1 -1
- package/dist/triage/index.d.ts +25 -0
- package/dist/triage/index.d.ts.map +1 -0
- package/dist/triage/index.js +95 -0
- package/dist/triage/index.js.map +1 -0
- package/dist/types/index.d.ts +10 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/usage/index.d.ts +19 -1
- package/dist/usage/index.d.ts.map +1 -1
- package/dist/usage/index.js +27 -1
- package/dist/usage/index.js.map +1 -1
- package/dist/utils/codex-config.d.ts +19 -1
- package/dist/utils/codex-config.d.ts.map +1 -1
- package/dist/utils/codex-config.js +51 -2
- package/dist/utils/codex-config.js.map +1 -1
- package/dist/utils/opencode-config.d.ts +12 -0
- package/dist/utils/opencode-config.d.ts.map +1 -0
- package/dist/utils/opencode-config.js +24 -0
- package/dist/utils/opencode-config.js.map +1 -0
- package/dist/work/github-issue.d.ts +2 -0
- package/dist/work/github-issue.d.ts.map +1 -1
- package/dist/work/github-issue.js +18 -0
- package/dist/work/github-issue.js.map +1 -1
- package/package.json +5 -5
package/dist/run/run-issue.js
CHANGED
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
// 3. the acquired Environment is released exactly once on every exit path.
|
|
7
7
|
// 4. a failed environment lease degrades (appPort undefined) rather than parking the run.
|
|
8
8
|
// 5. exactly one terminal event sequence is emitted per run, matching the returned RunOutcome
|
|
9
|
-
// (including the non-park `released` exit for an environment-caused CHECK failure, #1928).
|
|
9
|
+
// (including the non-park `released` exit for an environment-caused CHECK failure, #1928; a config cause parks, #2583).
|
|
10
10
|
// PLAN may be skipped by the planReuse port after an environment release (#2340).
|
|
11
|
+
// CHECK may be resumed on a kept worktree via request.resumeFrom after an environment release (#2564).
|
|
12
|
+
// A config preflight (#2586) rejects unfilled template tokens before base health and PLAN; any preflight failure parks with the `config` cause (#2587).
|
|
11
13
|
//
|
|
12
14
|
// Path-bound CLI concerns (Octokit, worktree/environment provisioning, the events sink,
|
|
13
15
|
// approval/steering, and reporting hooks) are injected as ports so this file stays free of
|
|
@@ -18,6 +20,7 @@ import { laneEnv } from '../environment/index.js';
|
|
|
18
20
|
import { buildPhase as buildPhaseDefault } from '../phases/build.js';
|
|
19
21
|
import { checkPhase as checkPhaseDefault, remainingShowstoppers, renderEnvironmentReleaseComment, } from '../phases/check.js';
|
|
20
22
|
import { describeConstitutionSource } from '../constitutions/index.js';
|
|
23
|
+
import { lintFactoryConfig } from '../config/config-lint.js';
|
|
21
24
|
import { planPhase as planPhaseDefault } from '../phases/plan.js';
|
|
22
25
|
import { shipPhase as shipPhaseDefault } from '../phases/ship.js';
|
|
23
26
|
import { upsertSlicePlanComment } from '../readiness/slice-plan-github.js';
|
|
@@ -35,7 +38,7 @@ import { gateBuildOnBreaker, parseResetCooldownMs } from '../router/breaker.js';
|
|
|
35
38
|
import { describeSteering } from '../steering/index.js';
|
|
36
39
|
import { touchedFilesFrom } from './lane-file-guard.js';
|
|
37
40
|
import { parkReasonFor } from './outcome.js';
|
|
38
|
-
import { issueBodyHash } from './resume-context.js';
|
|
41
|
+
import { configParkMessage, formatPreservedBuildMessage, issueBodyHash, preservedFixTarget, } from './resume-context.js';
|
|
39
42
|
function errorMessage(err) {
|
|
40
43
|
return err instanceof Error ? err.message : String(err);
|
|
41
44
|
}
|
|
@@ -278,6 +281,18 @@ export async function runIssue(request, policy, ports) {
|
|
|
278
281
|
// later would let the build worker author the standards it is graded by.
|
|
279
282
|
const constitution = ports.resolveConstitution();
|
|
280
283
|
log('constitution', describeConstitutionSource(constitution, request.product));
|
|
284
|
+
// Pre-PLAN config preflight (#2585/#2586), shared with `factory doctor` via lintFactoryConfig (#2612).
|
|
285
|
+
// An unknown `checkers:` entry would only fail closed in CHECK, after PLAN and BUILD spent model calls;
|
|
286
|
+
// unfilled scaffold tokens would ship a placeholder. Read-only, no subprocess, runs for localOnly too.
|
|
287
|
+
// Any failing verdict parks with the `config` cause (#2587); checker findings come first in the message.
|
|
288
|
+
const lint = lintFactoryConfig({ constitution, configFile: request.repoConfigFile });
|
|
289
|
+
if (lint.verdict !== 'ok') {
|
|
290
|
+
return terminalParked('config', [
|
|
291
|
+
'config preflight failed before PLAN:',
|
|
292
|
+
...lint.findings.map((f) => f.message),
|
|
293
|
+
`then run factory unpark ${request.issue}`,
|
|
294
|
+
].join('\n'));
|
|
295
|
+
}
|
|
281
296
|
// Pre-PLAN base health (#2339): a red origin/main releases the issue before any model spend;
|
|
282
297
|
// the CLI turns `released` into LaneEnvironmentPauseError, whose runLane branch writes the base hold.
|
|
283
298
|
if (ports.checkBaseHealth && !request.localOnly) {
|
|
@@ -312,193 +327,228 @@ export async function runIssue(request, policy, ports) {
|
|
|
312
327
|
// inside CHECK must diff against this same base rather than a moving target.
|
|
313
328
|
runStartDiffBase = request.localOnly ? await captureDiffBase(ports.workspace.path) : undefined;
|
|
314
329
|
// PLAN
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
330
|
+
const planStartedMs = Date.now();
|
|
331
|
+
let planRoute;
|
|
332
|
+
let planSlice;
|
|
333
|
+
let buildDiffBase;
|
|
334
|
+
let builtRoute;
|
|
335
|
+
let builtModel;
|
|
336
|
+
let pinNote;
|
|
337
|
+
let buildMinutes;
|
|
338
|
+
if (request.resumeFrom) {
|
|
339
|
+
const resumed = request.resumeFrom;
|
|
340
|
+
ports.events('check')('check', `resuming #${request.issue} from CHECK on kept branch ${request.branch} — PLAN and BUILD skipped`);
|
|
341
|
+
route = resumed.route;
|
|
342
|
+
planRoute = resumed.route;
|
|
343
|
+
planSlice = undefined;
|
|
344
|
+
buildDiffBase = resumed.diffBase;
|
|
345
|
+
builtRoute = resumed.route;
|
|
346
|
+
builtModel = resumed.model;
|
|
347
|
+
pinNote = undefined;
|
|
348
|
+
buildMinutes = resumed.buildMinutes;
|
|
333
349
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
350
|
+
else {
|
|
351
|
+
await setPhase('plan');
|
|
352
|
+
const planModel = await preferFallbackWhenProviderIsOpen(request.modelPins.plan, request.modelPins.planFallback, 'PLAN');
|
|
353
|
+
// A pinned build model determines the route (#1367): an explicit repo `route` still wins, but
|
|
354
|
+
// without one the pin's harness decides, so PLAN cannot send an Anthropic-pinned build to Codex.
|
|
355
|
+
// Two guards keep the pin from resurrecting a route the operator turned off: local-only mode
|
|
356
|
+
// forces codex in PLAN and a pin must not undo that; and a codex pin while codex is disabled
|
|
357
|
+
// cannot force codex — PLAN's codex→claude fallback applies and the compatibility check below
|
|
358
|
+
// drops the pin, exactly as on main.
|
|
359
|
+
let derivedRoute = request.preferredRoute || request.localOnly
|
|
360
|
+
? undefined
|
|
361
|
+
: routeForBuildModel(ports.router.registryRef, request.modelPins.build);
|
|
362
|
+
if (derivedRoute === 'codex' && request.codexDisabled) {
|
|
363
|
+
log('model-override', `pinned build model ${request.modelPins.build} needs the codex route but codex is disabled — the pin cannot force it`);
|
|
364
|
+
derivedRoute = undefined;
|
|
340
365
|
}
|
|
341
|
-
|
|
342
|
-
|
|
366
|
+
const pinnedRoute = request.preferredRoute ?? derivedRoute;
|
|
367
|
+
if (derivedRoute) {
|
|
368
|
+
log('model-override', `build route derived from pinned build model ${request.modelPins.build} → ${derivedRoute}`);
|
|
343
369
|
}
|
|
344
|
-
|
|
345
|
-
|
|
370
|
+
const reusablePlan = async () => {
|
|
371
|
+
if (!ports.planReuse)
|
|
372
|
+
return null;
|
|
373
|
+
let hit = null;
|
|
374
|
+
try {
|
|
375
|
+
hit = await ports.planReuse.load(issueBodyHash(request.work.brief));
|
|
376
|
+
}
|
|
377
|
+
catch (err) {
|
|
378
|
+
log('environment_warning', `could not read the plan-reuse record: ${errorMessage(err)} — planning`);
|
|
379
|
+
}
|
|
380
|
+
try {
|
|
381
|
+
await ports.planReuse.clear();
|
|
382
|
+
}
|
|
383
|
+
catch (err) {
|
|
384
|
+
log('environment_warning', `could not clear the plan-reuse record: ${errorMessage(err)}`);
|
|
385
|
+
}
|
|
386
|
+
if (hit) {
|
|
387
|
+
ports.events('plan')('plan_reused', `reusing frozen plan ${request.specPath} for #${request.issue} — body unchanged since the environment release, no PLAN model call`);
|
|
388
|
+
}
|
|
389
|
+
return hit;
|
|
390
|
+
};
|
|
391
|
+
const plan = (await reusablePlan()) ??
|
|
392
|
+
(await planPhase({
|
|
393
|
+
issue: request.issue,
|
|
394
|
+
repo: request.repo,
|
|
395
|
+
worktree: ports.workspace.path,
|
|
396
|
+
specPath: request.specPath,
|
|
397
|
+
constitution,
|
|
398
|
+
router: ports.router,
|
|
399
|
+
octokit: ports.octokit,
|
|
400
|
+
log: ports.events('plan'),
|
|
401
|
+
timeoutSeconds: request.timeouts.plan,
|
|
402
|
+
modelOverride: planModel,
|
|
403
|
+
modelFallbacks: planModel === request.modelPins.plan && request.modelPins.planFallback
|
|
404
|
+
? [request.modelPins.planFallback]
|
|
405
|
+
: undefined,
|
|
406
|
+
onProviderFailure: rememberProviderFailure,
|
|
407
|
+
branch: request.branch,
|
|
408
|
+
approvalGate: request.options.approvePlan ? ports.createApprovalGate?.() : undefined,
|
|
409
|
+
drainSteering: request.options.approvePlan ? ports.drainSteering : undefined,
|
|
410
|
+
codexDisabled: request.codexDisabled,
|
|
411
|
+
localOnly: request.localOnly,
|
|
412
|
+
laneId: request.lane,
|
|
413
|
+
workSource: request.workSource,
|
|
414
|
+
enforceReadiness: true,
|
|
415
|
+
fastPath: request.efficiency.fastPath,
|
|
416
|
+
enforceSizeGate: (request.sizeGateMode ?? 'file') !== 'off',
|
|
417
|
+
sizeGateMode: request.sizeGateMode,
|
|
418
|
+
maxSlices: request.maxSlices,
|
|
419
|
+
blockUnresolvedRegressions: request.blockUnresolvedRegressions,
|
|
420
|
+
updateIssueBodyOnEnrich: request.readinessUpdateIssueBody,
|
|
421
|
+
preferredRoute: pinnedRoute,
|
|
422
|
+
}));
|
|
423
|
+
route = plan.route;
|
|
424
|
+
if (!plan.ok) {
|
|
425
|
+
const decomposedChildren = plan.decomposed?.childIssues ?? [];
|
|
426
|
+
if (decomposedChildren.length > 0) {
|
|
427
|
+
await ports.onDecomposed?.(decomposedChildren);
|
|
428
|
+
const childList = decomposedChildren.map((n) => `#${n}`).join(', ');
|
|
429
|
+
return { state: 'escalated', reason: `issue #${request.issue} decomposed into ${childList}`, route };
|
|
430
|
+
}
|
|
431
|
+
if (plan.sliced) {
|
|
432
|
+
return terminalEscalated(`issue #${request.issue} sliced into ${plan.sliced.sliceCount} slice(s): ${plan.escalate ?? 'slice plan recorded'}`);
|
|
433
|
+
}
|
|
434
|
+
return terminalEscalated(`plan escalated: ${plan.escalate ?? 'unknown'}`);
|
|
346
435
|
}
|
|
347
|
-
|
|
348
|
-
|
|
436
|
+
const planBudget = await assertBudget('PLAN');
|
|
437
|
+
if (planBudget)
|
|
438
|
+
return planBudget;
|
|
439
|
+
// Same-file lane guard (#1515): before this run claims BUILD, make sure no other
|
|
440
|
+
// in-flight run in this repo already owns a file PLAN just said this run will touch.
|
|
441
|
+
if (ports.laneFileGuard && plan.designArtifact) {
|
|
442
|
+
const touchedFiles = touchedFilesFrom(plan.designArtifact);
|
|
443
|
+
const collision = await ports.laneFileGuard.findCollision(request.repo, request.issue, touchedFiles);
|
|
444
|
+
if (collision) {
|
|
445
|
+
return terminalParked('held', `issue held: #${request.issue} touches ${collision.file}, already claimed by in-flight issue #${collision.issue} — held until that lane finishes`);
|
|
446
|
+
}
|
|
447
|
+
if (touchedFiles.length > 0) {
|
|
448
|
+
await ports.laneFileGuard.register(request.repo, request.issue, touchedFiles);
|
|
449
|
+
laneFileClaimed = true;
|
|
450
|
+
}
|
|
349
451
|
}
|
|
350
|
-
|
|
351
|
-
|
|
452
|
+
// BUILD
|
|
453
|
+
await setPhase('build');
|
|
454
|
+
let buildSteering;
|
|
455
|
+
if (request.options.interactive) {
|
|
456
|
+
buildSteering = ports.drainSteering?.();
|
|
457
|
+
if (buildSteering && buildSteering.messages.length > 0) {
|
|
458
|
+
ports.events('build')('steering_applied', describeSteering(buildSteering));
|
|
459
|
+
}
|
|
352
460
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
461
|
+
let breakerBlocked = false;
|
|
462
|
+
if (request.failover.enabled) {
|
|
463
|
+
const providers = [
|
|
464
|
+
...new Set(ports.router
|
|
465
|
+
.resolveAll('build_codex')
|
|
466
|
+
.map((m) => ports.router.registryRef.get(m)?.provider)
|
|
467
|
+
.filter((p) => Boolean(p))),
|
|
468
|
+
];
|
|
469
|
+
const gate = await gateBuildOnBreaker({ breaker: ports.breaker, providers, log: ports.events('build') });
|
|
470
|
+
breakerBlocked = gate.codexBlocked;
|
|
471
|
+
}
|
|
472
|
+
// PLAN already honors pinnedRoute and rewrites the spec; re-apply here so a plan port that
|
|
473
|
+
// ignores it (a daemon or a test double) still cannot route a pinned worker elsewhere.
|
|
474
|
+
let buildRoute = pinnedRoute ?? plan.route;
|
|
475
|
+
let buildModel = request.modelPins.build;
|
|
476
|
+
if (request.failover.enabled && buildRoute === 'claude') {
|
|
477
|
+
const primaryClaude = buildModel ?? ports.router.resolveAll('build_claude')[0];
|
|
478
|
+
const fallbackCodex = request.modelPins.buildFallback ?? ports.router.resolveAll('build_codex')[0];
|
|
479
|
+
const selected = await preferFallbackWhenProviderIsOpen(primaryClaude, fallbackCodex, 'BUILD');
|
|
480
|
+
if (selected && selected !== primaryClaude) {
|
|
481
|
+
buildRoute = 'codex';
|
|
482
|
+
buildModel = selected;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
if (buildModel) {
|
|
486
|
+
const harnessId = ports.router.registryRef.getHarnessId(buildModel);
|
|
487
|
+
const compatible = buildRoute === 'codex'
|
|
488
|
+
? ports.router.registryRef.isCodexModel(buildModel)
|
|
489
|
+
: buildRoute === 'opencode'
|
|
490
|
+
? harnessId === 'opencode'
|
|
491
|
+
: harnessId === 'claude-cli';
|
|
492
|
+
if (!compatible) {
|
|
493
|
+
log('model_override_ignored', `build model ${buildModel} is incompatible with the ${buildRoute} route${buildRoute === request.preferredRoute ? ' pinned by .factory/config.json' : ''} — using that route's default worker`);
|
|
494
|
+
buildModel = undefined;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
const build = await buildPhase({
|
|
498
|
+
runId,
|
|
357
499
|
issue: request.issue,
|
|
358
500
|
repo: request.repo,
|
|
359
501
|
worktree: ports.workspace.path,
|
|
360
502
|
specPath: request.specPath,
|
|
503
|
+
branch: request.branch,
|
|
361
504
|
constitution,
|
|
505
|
+
route: buildRoute,
|
|
362
506
|
router: ports.router,
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
507
|
+
log: ports.events('build'),
|
|
508
|
+
timeoutSeconds: request.timeouts.build,
|
|
509
|
+
skipCI: request.skipCI,
|
|
510
|
+
disablePublish: Boolean(request.localOnly),
|
|
511
|
+
publishFromBuild: request.publishFromBuild,
|
|
512
|
+
modelOverride: buildModel,
|
|
513
|
+
pinnedModel: buildModel !== undefined && buildModel === request.modelPins.build,
|
|
514
|
+
codexFallbackModel: request.modelPins.buildFallback ?? ports.router.resolveAll('build_codex')[0],
|
|
370
515
|
onProviderFailure: rememberProviderFailure,
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
516
|
+
sandbox: request.sandboxPolicy,
|
|
517
|
+
steering: buildSteering,
|
|
518
|
+
appPort,
|
|
519
|
+
appBaseUrl,
|
|
520
|
+
codexDisabled: request.codexDisabled || breakerBlocked,
|
|
375
521
|
localOnly: request.localOnly,
|
|
522
|
+
autoFailover: { enabled: request.failover.enabled, fallbackModel: request.failover.fallbackModel },
|
|
523
|
+
onPgid,
|
|
376
524
|
laneId: request.lane,
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
route = plan.route;
|
|
388
|
-
if (!plan.ok) {
|
|
389
|
-
const decomposedChildren = plan.decomposed?.childIssues ?? [];
|
|
390
|
-
if (decomposedChildren.length > 0) {
|
|
391
|
-
await ports.onDecomposed?.(decomposedChildren);
|
|
392
|
-
const childList = decomposedChildren.map((n) => `#${n}`).join(', ');
|
|
393
|
-
return { state: 'escalated', reason: `issue #${request.issue} decomposed into ${childList}`, route };
|
|
394
|
-
}
|
|
395
|
-
if (plan.sliced) {
|
|
396
|
-
return terminalEscalated(`issue #${request.issue} sliced into ${plan.sliced.sliceCount} slice(s): ${plan.escalate ?? 'slice plan recorded'}`);
|
|
397
|
-
}
|
|
398
|
-
return terminalEscalated(`plan escalated: ${plan.escalate ?? 'unknown'}`);
|
|
399
|
-
}
|
|
400
|
-
const planBudget = await assertBudget('PLAN');
|
|
401
|
-
if (planBudget)
|
|
402
|
-
return planBudget;
|
|
403
|
-
// Same-file lane guard (#1515): before this run claims BUILD, make sure no other
|
|
404
|
-
// in-flight run in this repo already owns a file PLAN just said this run will touch.
|
|
405
|
-
if (ports.laneFileGuard && plan.designArtifact) {
|
|
406
|
-
const touchedFiles = touchedFilesFrom(plan.designArtifact);
|
|
407
|
-
const collision = await ports.laneFileGuard.findCollision(request.repo, request.issue, touchedFiles);
|
|
408
|
-
if (collision) {
|
|
409
|
-
return terminalParked('held', `issue held: #${request.issue} touches ${collision.file}, already claimed by in-flight issue #${collision.issue} — held until that lane finishes`);
|
|
410
|
-
}
|
|
411
|
-
if (touchedFiles.length > 0) {
|
|
412
|
-
await ports.laneFileGuard.register(request.repo, request.issue, touchedFiles);
|
|
413
|
-
laneFileClaimed = true;
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
// BUILD
|
|
417
|
-
await setPhase('build');
|
|
418
|
-
let buildSteering;
|
|
419
|
-
if (request.options.interactive) {
|
|
420
|
-
buildSteering = ports.drainSteering?.();
|
|
421
|
-
if (buildSteering && buildSteering.messages.length > 0) {
|
|
422
|
-
ports.events('build')('steering_applied', describeSteering(buildSteering));
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
let breakerBlocked = false;
|
|
426
|
-
if (request.failover.enabled) {
|
|
427
|
-
const providers = [
|
|
428
|
-
...new Set(ports.router
|
|
429
|
-
.resolveAll('build_codex')
|
|
430
|
-
.map((m) => ports.router.registryRef.get(m)?.provider)
|
|
431
|
-
.filter((p) => Boolean(p))),
|
|
432
|
-
];
|
|
433
|
-
const gate = await gateBuildOnBreaker({ breaker: ports.breaker, providers, log: ports.events('build') });
|
|
434
|
-
breakerBlocked = gate.codexBlocked;
|
|
435
|
-
}
|
|
436
|
-
// PLAN already honors pinnedRoute and rewrites the spec; re-apply here so a plan port that
|
|
437
|
-
// ignores it (a daemon or a test double) still cannot route a pinned worker elsewhere.
|
|
438
|
-
let buildRoute = pinnedRoute ?? plan.route;
|
|
439
|
-
let buildModel = request.modelPins.build;
|
|
440
|
-
if (request.failover.enabled && buildRoute === 'claude') {
|
|
441
|
-
const primaryClaude = buildModel ?? ports.router.resolveAll('build_claude')[0];
|
|
442
|
-
const fallbackCodex = request.modelPins.buildFallback ?? ports.router.resolveAll('build_codex')[0];
|
|
443
|
-
const selected = await preferFallbackWhenProviderIsOpen(primaryClaude, fallbackCodex, 'BUILD');
|
|
444
|
-
if (selected && selected !== primaryClaude) {
|
|
445
|
-
buildRoute = 'codex';
|
|
446
|
-
buildModel = selected;
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
if (buildModel) {
|
|
450
|
-
const harnessId = ports.router.registryRef.getHarnessId(buildModel);
|
|
451
|
-
const compatible = buildRoute === 'codex'
|
|
452
|
-
? ports.router.registryRef.isCodexModel(buildModel)
|
|
453
|
-
: buildRoute === 'opencode'
|
|
454
|
-
? harnessId === 'opencode'
|
|
455
|
-
: harnessId === 'claude-cli';
|
|
456
|
-
if (!compatible) {
|
|
457
|
-
log('model_override_ignored', `build model ${buildModel} is incompatible with the ${buildRoute} route${buildRoute === request.preferredRoute ? ' pinned by .factory/config.json' : ''} — using that route's default worker`);
|
|
458
|
-
buildModel = undefined;
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
const build = await buildPhase({
|
|
462
|
-
runId,
|
|
463
|
-
issue: request.issue,
|
|
464
|
-
repo: request.repo,
|
|
465
|
-
worktree: ports.workspace.path,
|
|
466
|
-
specPath: request.specPath,
|
|
467
|
-
branch: request.branch,
|
|
468
|
-
constitution,
|
|
469
|
-
route: buildRoute,
|
|
470
|
-
router: ports.router,
|
|
471
|
-
log: ports.events('build'),
|
|
472
|
-
timeoutSeconds: request.timeouts.build,
|
|
473
|
-
skipCI: request.skipCI,
|
|
474
|
-
disablePublish: Boolean(request.localOnly),
|
|
475
|
-
publishFromBuild: request.publishFromBuild,
|
|
476
|
-
modelOverride: buildModel,
|
|
477
|
-
codexFallbackModel: request.modelPins.buildFallback ?? ports.router.resolveAll('build_codex')[0],
|
|
478
|
-
onProviderFailure: rememberProviderFailure,
|
|
479
|
-
sandbox: request.sandboxPolicy,
|
|
480
|
-
steering: buildSteering,
|
|
481
|
-
appPort,
|
|
482
|
-
appBaseUrl,
|
|
483
|
-
codexDisabled: request.codexDisabled || breakerBlocked,
|
|
484
|
-
localOnly: request.localOnly,
|
|
485
|
-
autoFailover: { enabled: request.failover.enabled, fallbackModel: request.failover.fallbackModel },
|
|
486
|
-
onPgid,
|
|
487
|
-
laneId: request.lane,
|
|
488
|
-
});
|
|
489
|
-
route = build.route;
|
|
490
|
-
if (!build.ok) {
|
|
491
|
-
if (build.reason === 'no_diff') {
|
|
492
|
-
return terminalParked('fail', 'build produced no diff against the base ref — no implementation was produced');
|
|
493
|
-
}
|
|
494
|
-
if (build.reason === 'junk_only_diff') {
|
|
495
|
-
return terminalParked('fail', 'build changed only generated/cache files (e.g. __pycache__) — no implementation was produced');
|
|
525
|
+
});
|
|
526
|
+
route = build.route;
|
|
527
|
+
if (!build.ok) {
|
|
528
|
+
if (build.reason === 'no_diff') {
|
|
529
|
+
return terminalParked('fail', 'build produced no diff against the base ref — no implementation was produced');
|
|
530
|
+
}
|
|
531
|
+
if (build.reason === 'junk_only_diff') {
|
|
532
|
+
return terminalParked('fail', 'build changed only generated/cache files (e.g. __pycache__) — no implementation was produced');
|
|
533
|
+
}
|
|
534
|
+
return terminalEscalated(`build escalated: ${build.escalate ?? 'unknown'}`);
|
|
496
535
|
}
|
|
497
|
-
|
|
536
|
+
const buildBudget = await assertBudget('BUILD');
|
|
537
|
+
if (buildBudget)
|
|
538
|
+
return buildBudget;
|
|
539
|
+
// #2511: say so when a permitted failover built with something other than the user's pin.
|
|
540
|
+
const pinnedBuild = request.modelPins.build;
|
|
541
|
+
pinNote =
|
|
542
|
+
pinnedBuild !== undefined && build.model !== pinnedBuild
|
|
543
|
+
? `build pin ${pinnedBuild} not used (${build.failoverReason ?? 'overridden'}); built with ${build.model}`
|
|
544
|
+
: undefined;
|
|
545
|
+
planRoute = plan.route;
|
|
546
|
+
planSlice = plan.slice;
|
|
547
|
+
buildDiffBase = build.diffBase;
|
|
548
|
+
builtRoute = build.route;
|
|
549
|
+
builtModel = build.model;
|
|
550
|
+
buildMinutes = Math.max(1, Math.round((Date.now() - planStartedMs) / 60_000));
|
|
498
551
|
}
|
|
499
|
-
const buildBudget = await assertBudget('BUILD');
|
|
500
|
-
if (buildBudget)
|
|
501
|
-
return buildBudget;
|
|
502
552
|
// CHECK
|
|
503
553
|
await setPhase('check');
|
|
504
554
|
const priorFailureSignature = await ports.reworkHistory?.priorSignature(request.issue);
|
|
@@ -521,9 +571,9 @@ export async function runIssue(request, policy, ports) {
|
|
|
521
571
|
appBaseUrl,
|
|
522
572
|
onPgid,
|
|
523
573
|
priorFailureSignature,
|
|
524
|
-
diffBase: request.localOnly ? runStartDiffBase :
|
|
525
|
-
reworkRoute:
|
|
526
|
-
reworkModel:
|
|
574
|
+
diffBase: request.localOnly ? runStartDiffBase : buildDiffBase,
|
|
575
|
+
reworkRoute: builtRoute,
|
|
576
|
+
reworkModel: builtModel,
|
|
527
577
|
laneId: request.lane,
|
|
528
578
|
onActivity: ports.onActivity,
|
|
529
579
|
logsDir: request.logsDir,
|
|
@@ -555,12 +605,36 @@ export async function runIssue(request, policy, ports) {
|
|
|
555
605
|
});
|
|
556
606
|
}
|
|
557
607
|
}
|
|
608
|
+
const clearCheckResume = async () => {
|
|
609
|
+
if (!request.resumeFrom)
|
|
610
|
+
return;
|
|
611
|
+
try {
|
|
612
|
+
await ports.checkResume?.clear();
|
|
613
|
+
}
|
|
614
|
+
catch (err) {
|
|
615
|
+
log('environment_warning', `could not clear the check-resume point: ${errorMessage(err)}`);
|
|
616
|
+
}
|
|
617
|
+
};
|
|
558
618
|
const checkBudget = await assertBudget('CHECK');
|
|
559
619
|
if (checkBudget)
|
|
560
620
|
return checkBudget;
|
|
561
621
|
if (!check.passed) {
|
|
562
622
|
if (check.environment) {
|
|
563
623
|
const env = check.environment;
|
|
624
|
+
if (env.cause === 'config') {
|
|
625
|
+
// A config failure fails the same way on every requeue (#2583): park with file, line and fix.
|
|
626
|
+
await clearCheckResume();
|
|
627
|
+
const failure = check.failureSignature !== undefined
|
|
628
|
+
? { failureSignature: check.failureSignature, failingChecks: env.failingChecks }
|
|
629
|
+
: undefined;
|
|
630
|
+
return terminalParked('config', configParkMessage({
|
|
631
|
+
issue: request.issue,
|
|
632
|
+
failure: env,
|
|
633
|
+
summary: check.summary,
|
|
634
|
+
constitution,
|
|
635
|
+
configFile: request.repoConfigFile,
|
|
636
|
+
}), failure);
|
|
637
|
+
}
|
|
564
638
|
if (!request.localOnly) {
|
|
565
639
|
const [owner, repoName] = request.repo.split('/');
|
|
566
640
|
try {
|
|
@@ -575,13 +649,38 @@ export async function runIssue(request, policy, ports) {
|
|
|
575
649
|
log('environment_warning', `could not post the environment-failure comment: ${errorMessage(err)}`);
|
|
576
650
|
}
|
|
577
651
|
}
|
|
578
|
-
|
|
579
|
-
|
|
652
|
+
let preserve = ports.checkResume !== undefined && !request.localOnly && !planSlice;
|
|
653
|
+
let fixTarget = '';
|
|
654
|
+
if (preserve && ports.checkResume) {
|
|
655
|
+
fixTarget = preservedFixTarget(env, constitution, request.repoConfigFile);
|
|
656
|
+
try {
|
|
657
|
+
await ports.checkResume.record({
|
|
658
|
+
issue: request.issue,
|
|
659
|
+
branch: request.branch,
|
|
660
|
+
worktree: ports.workspace.path,
|
|
661
|
+
diffBase: buildDiffBase,
|
|
662
|
+
route: builtRoute,
|
|
663
|
+
model: builtModel,
|
|
664
|
+
buildMinutes,
|
|
665
|
+
fixTarget,
|
|
666
|
+
preservedAt: new Date().toISOString(),
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
catch (err) {
|
|
670
|
+
log('environment_warning', `could not record the check-resume point: ${errorMessage(err)}`);
|
|
671
|
+
preserve = false;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
log('environment-released', `every failing checker (${env.failingChecks.join(', ')}) also fails on base ${env.baseSha.slice(0, 8)} — no rework, releasing #${request.issue} to queued` +
|
|
675
|
+
(preserve
|
|
676
|
+
? ` — ${formatPreservedBuildMessage({ issue: request.issue, branch: request.branch, buildMinutes, fixTarget })}`
|
|
677
|
+
: ''));
|
|
678
|
+
if (ports.planReuse && !planSlice && !request.resumeFrom) {
|
|
580
679
|
try {
|
|
581
680
|
await ports.planReuse.record({
|
|
582
681
|
bodyHash: issueBodyHash(request.work.brief),
|
|
583
682
|
baseSha: env.baseSha,
|
|
584
|
-
route:
|
|
683
|
+
route: planRoute,
|
|
585
684
|
releasedAt: new Date().toISOString(),
|
|
586
685
|
});
|
|
587
686
|
}
|
|
@@ -598,6 +697,7 @@ export async function runIssue(request, policy, ports) {
|
|
|
598
697
|
baseSha: env.baseSha,
|
|
599
698
|
failingChecks: env.failingChecks,
|
|
600
699
|
...(check.failureSignature !== undefined ? { failureSignature: check.failureSignature } : {}),
|
|
700
|
+
...(preserve ? { buildPreserved: true } : {}),
|
|
601
701
|
};
|
|
602
702
|
}
|
|
603
703
|
const checkFailure = check.failureSignature !== undefined
|
|
@@ -619,8 +719,10 @@ export async function runIssue(request, policy, ports) {
|
|
|
619
719
|
const parkedMessage = showstoppers.length > 0
|
|
620
720
|
? `${message}\nremaining showstoppers:\n${showstoppers.map((s) => `- ${s}`).join('\n')}`
|
|
621
721
|
: message;
|
|
722
|
+
await clearCheckResume();
|
|
622
723
|
return terminalParked(reason, parkedMessage, checkFailure);
|
|
623
724
|
}
|
|
725
|
+
await clearCheckResume();
|
|
624
726
|
// Clean check — clear stale cross-run history so a future, genuinely different
|
|
625
727
|
// failure is not mistaken for a repeat of one already resolved.
|
|
626
728
|
await ports.reworkHistory?.clear(request.issue);
|
|
@@ -648,7 +750,7 @@ export async function runIssue(request, policy, ports) {
|
|
|
648
750
|
if (request.prClassifier) {
|
|
649
751
|
reviewRouting = await resolveReviewRouting({
|
|
650
752
|
worktree: ports.workspace.path,
|
|
651
|
-
fallbackBaseRef:
|
|
753
|
+
fallbackBaseRef: buildDiffBase,
|
|
652
754
|
rules: request.prClassifier.rules,
|
|
653
755
|
readChanges: ports.readReviewFloorChanges,
|
|
654
756
|
});
|
|
@@ -672,7 +774,7 @@ export async function runIssue(request, policy, ports) {
|
|
|
672
774
|
// decided, so it cannot change the label or merge routing.
|
|
673
775
|
const shadowInput = {
|
|
674
776
|
worktree: ports.workspace.path,
|
|
675
|
-
fallbackBaseRef:
|
|
777
|
+
fallbackBaseRef: buildDiffBase,
|
|
676
778
|
issueTitle: request.work.title,
|
|
677
779
|
issueBody: request.work.brief,
|
|
678
780
|
specPath: request.specPath,
|
|
@@ -728,7 +830,8 @@ export async function runIssue(request, policy, ports) {
|
|
|
728
830
|
work: request.work,
|
|
729
831
|
laneId: request.lane,
|
|
730
832
|
reviewRouting,
|
|
731
|
-
|
|
833
|
+
...(pinNote ? { pinNotes: [pinNote] } : {}),
|
|
834
|
+
slice: planSlice ? { index: planSlice.index, count: planSlice.plan.slices.length } : undefined,
|
|
732
835
|
});
|
|
733
836
|
if (!ship.ok) {
|
|
734
837
|
const reason = ship.denied ? 'escalate' : ship.ciOutcome === 'infra-failure' ? 'ci-infra' : 'fail';
|
|
@@ -739,8 +842,8 @@ export async function runIssue(request, policy, ports) {
|
|
|
739
842
|
: 'ship phase failed';
|
|
740
843
|
return terminalParked(reason, message);
|
|
741
844
|
}
|
|
742
|
-
if (
|
|
743
|
-
const { index, plan: slicePlan } =
|
|
845
|
+
if (planSlice) {
|
|
846
|
+
const { index, plan: slicePlan } = planSlice;
|
|
744
847
|
const count = slicePlan.slices.length;
|
|
745
848
|
if (ship.prNumber === undefined) {
|
|
746
849
|
log('ship', `slice ${index}/${count} shipped with no PR number — slice plan not updated`);
|
|
@@ -752,9 +855,9 @@ export async function runIssue(request, policy, ports) {
|
|
|
752
855
|
octokit: ports.octokit,
|
|
753
856
|
repo: request.repo,
|
|
754
857
|
issue: request.issue,
|
|
755
|
-
plan: withSliceState(slicePlan, index, state, ship.prNumber),
|
|
858
|
+
plan: withSliceState(slicePlan, index, state, ship.prNumber, request.branch),
|
|
756
859
|
});
|
|
757
|
-
log('ship', `recorded slice ${index}/${count} as ${state} (PR #${ship.prNumber}) in the slice plan comment`);
|
|
860
|
+
log('ship', `recorded slice ${index}/${count} as ${state} (PR #${ship.prNumber}, branch ${request.branch}) in the slice plan comment`);
|
|
758
861
|
}
|
|
759
862
|
catch (err) {
|
|
760
863
|
return terminalParked('fail', `slice ${index}/${count} PR #${ship.prNumber} opened but recording it in the slice plan comment failed: ${errorMessage(err)}`);
|
|
@@ -767,7 +870,7 @@ export async function runIssue(request, policy, ports) {
|
|
|
767
870
|
const readyMsg = ship.alreadyDelivered
|
|
768
871
|
? `already delivered${ship.prNumber !== undefined ? ` by merged PR #${ship.prNumber}` : ' — branch already landed on main'}`
|
|
769
872
|
: `PR #${ship.prNumber} ready for review`;
|
|
770
|
-
log('ready', readyMsg);
|
|
873
|
+
log('ready', pinNote ? `${readyMsg} (${pinNote})` : readyMsg);
|
|
771
874
|
await ports.writeLocalRunReport?.({ outcome: 'ready', route, branch: request.branch, reworkRounds });
|
|
772
875
|
return {
|
|
773
876
|
state: 'ready',
|
|
@@ -775,7 +878,7 @@ export async function runIssue(request, policy, ports) {
|
|
|
775
878
|
branch: request.branch,
|
|
776
879
|
reworkRounds,
|
|
777
880
|
prNumber: ship.prNumber,
|
|
778
|
-
...(
|
|
881
|
+
...(planSlice ? { slice: { index: planSlice.index, count: planSlice.plan.slices.length } } : {}),
|
|
779
882
|
};
|
|
780
883
|
}
|
|
781
884
|
catch (err) {
|