codex-orchestrator 2.0.2 → 2.0.4
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 +44 -426
- package/README.md +135 -34
- package/dist/src/index.d.ts +11 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +5 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/v2/acceptance-proof.d.ts +3 -0
- package/dist/src/v2/acceptance-proof.d.ts.map +1 -1
- package/dist/src/v2/acceptance-proof.js +2 -8
- package/dist/src/v2/acceptance-proof.js.map +1 -1
- package/dist/src/v2/adapters/gh-issue-adapter.d.ts +5 -3
- package/dist/src/v2/adapters/gh-issue-adapter.d.ts.map +1 -1
- package/dist/src/v2/adapters/gh-issue-adapter.js +67 -12
- package/dist/src/v2/adapters/gh-issue-adapter.js.map +1 -1
- package/dist/src/v2/adapters/issues.d.ts +16 -2
- package/dist/src/v2/adapters/issues.d.ts.map +1 -1
- package/dist/src/v2/adapters/issues.js +15 -5
- package/dist/src/v2/adapters/issues.js.map +1 -1
- package/dist/src/v2/adapters/mission-coordinator-lock.d.ts +1 -0
- package/dist/src/v2/adapters/mission-coordinator-lock.d.ts.map +1 -1
- package/dist/src/v2/adapters/mission-coordinator-lock.js +5 -1
- package/dist/src/v2/adapters/mission-coordinator-lock.js.map +1 -1
- package/dist/src/v2/cli-contract.d.ts +3 -3
- package/dist/src/v2/cli-contract.d.ts.map +1 -1
- package/dist/src/v2/cli-contract.js +9 -1
- package/dist/src/v2/cli-contract.js.map +1 -1
- package/dist/src/v2/cli.d.ts +24 -0
- package/dist/src/v2/cli.d.ts.map +1 -0
- package/dist/src/v2/{candidate-cli.js → cli.js} +30 -26
- package/dist/src/v2/cli.js.map +1 -0
- package/dist/src/v2/code-review-report.d.ts +66 -0
- package/dist/src/v2/code-review-report.d.ts.map +1 -0
- package/dist/src/v2/code-review-report.js +259 -0
- package/dist/src/v2/code-review-report.js.map +1 -0
- package/dist/src/v2/codex-process.d.ts +8 -1
- package/dist/src/v2/codex-process.d.ts.map +1 -1
- package/dist/src/v2/codex-process.js +22 -0
- package/dist/src/v2/codex-process.js.map +1 -1
- package/dist/src/v2/config.d.ts +4 -3
- package/dist/src/v2/config.d.ts.map +1 -1
- package/dist/src/v2/config.js +8 -3
- package/dist/src/v2/config.js.map +1 -1
- package/dist/src/v2/contained-report-operation.d.ts +100 -0
- package/dist/src/v2/contained-report-operation.d.ts.map +1 -0
- package/dist/src/v2/contained-report-operation.js +200 -0
- package/dist/src/v2/contained-report-operation.js.map +1 -0
- package/dist/src/v2/containment.d.ts +10 -0
- package/dist/src/v2/containment.d.ts.map +1 -1
- package/dist/src/v2/containment.js +49 -1
- package/dist/src/v2/containment.js.map +1 -1
- package/dist/src/v2/direct-delivery.d.ts +96 -0
- package/dist/src/v2/direct-delivery.d.ts.map +1 -0
- package/dist/src/v2/direct-delivery.js +482 -0
- package/dist/src/v2/direct-delivery.js.map +1 -0
- package/dist/src/v2/immutable-workflow-publisher.d.ts +40 -0
- package/dist/src/v2/immutable-workflow-publisher.d.ts.map +1 -0
- package/dist/src/v2/immutable-workflow-publisher.js +218 -0
- package/dist/src/v2/immutable-workflow-publisher.js.map +1 -0
- package/dist/src/v2/implementation-reviewer.d.ts +81 -0
- package/dist/src/v2/implementation-reviewer.d.ts.map +1 -0
- package/dist/src/v2/implementation-reviewer.js +157 -0
- package/dist/src/v2/implementation-reviewer.js.map +1 -0
- package/dist/src/v2/owner-control-lock.d.ts +41 -0
- package/dist/src/v2/owner-control-lock.d.ts.map +1 -0
- package/dist/src/v2/owner-control-lock.js +174 -0
- package/dist/src/v2/owner-control-lock.js.map +1 -0
- package/dist/src/v2/proof-report.d.ts.map +1 -1
- package/dist/src/v2/proof-report.js +55 -29
- package/dist/src/v2/proof-report.js.map +1 -1
- package/dist/src/v2/route-continuations.d.ts +32 -0
- package/dist/src/v2/route-continuations.d.ts.map +1 -0
- package/dist/src/v2/route-continuations.js +2 -0
- package/dist/src/v2/route-continuations.js.map +1 -0
- package/dist/src/v2/route-coordinator.d.ts +77 -0
- package/dist/src/v2/route-coordinator.d.ts.map +1 -0
- package/dist/src/v2/route-coordinator.js +370 -0
- package/dist/src/v2/route-coordinator.js.map +1 -0
- package/dist/src/v2/route-decision.d.ts +129 -0
- package/dist/src/v2/route-decision.d.ts.map +1 -0
- package/dist/src/v2/route-decision.js +400 -0
- package/dist/src/v2/route-decision.js.map +1 -0
- package/dist/src/v2/run-issue.d.ts +64 -6
- package/dist/src/v2/run-issue.d.ts.map +1 -1
- package/dist/src/v2/run-issue.js +962 -92
- package/dist/src/v2/run-issue.js.map +1 -1
- package/dist/src/v2/run-store.d.ts +25 -1
- package/dist/src/v2/run-store.d.ts.map +1 -1
- package/dist/src/v2/run-store.js +129 -4
- package/dist/src/v2/run-store.js.map +1 -1
- package/dist/src/v2/runtime-assets.d.ts +15 -13
- package/dist/src/v2/runtime-assets.d.ts.map +1 -1
- package/dist/src/v2/runtime-assets.js +263 -416
- package/dist/src/v2/runtime-assets.js.map +1 -1
- package/dist/src/v2/runtime.d.ts +17 -9
- package/dist/src/v2/runtime.d.ts.map +1 -1
- package/dist/src/v2/runtime.js +564 -64
- package/dist/src/v2/runtime.js.map +1 -1
- package/dist/src/v2/setup-cli.d.ts.map +1 -1
- package/dist/src/v2/setup-cli.js +4 -10
- package/dist/src/v2/setup-cli.js.map +1 -1
- package/dist/src/v2/setup-runtime.d.ts.map +1 -1
- package/dist/src/v2/setup-runtime.js +19 -131
- package/dist/src/v2/setup-runtime.js.map +1 -1
- package/dist/src/v2/setup-store.d.ts +0 -5
- package/dist/src/v2/setup-store.d.ts.map +1 -1
- package/dist/src/v2/setup-store.js +3 -106
- package/dist/src/v2/setup-store.js.map +1 -1
- package/dist/src/v2/setup.d.ts +6 -43
- package/dist/src/v2/setup.d.ts.map +1 -1
- package/dist/src/v2/setup.js +13 -192
- package/dist/src/v2/setup.js.map +1 -1
- package/dist/src/v2/spec-coordinator.d.ts +85 -0
- package/dist/src/v2/spec-coordinator.d.ts.map +1 -0
- package/dist/src/v2/spec-coordinator.js +88 -0
- package/dist/src/v2/spec-coordinator.js.map +1 -0
- package/dist/src/v2/spec-delivery.d.ts +143 -0
- package/dist/src/v2/spec-delivery.d.ts.map +1 -0
- package/dist/src/v2/spec-delivery.js +401 -0
- package/dist/src/v2/spec-delivery.js.map +1 -0
- package/dist/src/v2/triage-route.d.ts +68 -0
- package/dist/src/v2/triage-route.d.ts.map +1 -0
- package/dist/src/v2/triage-route.js +223 -0
- package/dist/src/v2/triage-route.js.map +1 -0
- package/dist/src/v2/waiting-human-coordinator.d.ts +49 -0
- package/dist/src/v2/waiting-human-coordinator.d.ts.map +1 -0
- package/dist/src/v2/waiting-human-coordinator.js +509 -0
- package/dist/src/v2/waiting-human-coordinator.js.map +1 -0
- package/dist/src/v2/waiting-human.d.ts +143 -0
- package/dist/src/v2/waiting-human.d.ts.map +1 -0
- package/dist/src/v2/waiting-human.js +408 -0
- package/dist/src/v2/waiting-human.js.map +1 -0
- package/dist/src/v2/workflow-assets.d.ts +98 -0
- package/dist/src/v2/workflow-assets.d.ts.map +1 -0
- package/dist/src/v2/workflow-assets.js +646 -0
- package/dist/src/v2/workflow-assets.js.map +1 -0
- package/docs/deep-dive.md +275 -52
- package/internal-workflow/docs/agents/bug-workflow-routing.md +24 -0
- package/internal-workflow/docs/agents/bugfix-quality-gate.md +11 -0
- package/internal-workflow/docs/agents/coding-skill-routing.md +123 -0
- package/internal-workflow/docs/agents/confidence-rubric.md +65 -0
- package/internal-workflow/docs/agents/contract-test-ledger.md +60 -0
- package/internal-workflow/docs/agents/review-gates.md +42 -0
- package/internal-workflow/docs/agents/review-protocol.md +98 -0
- package/internal-workflow/docs/agents/tool-usage.md +88 -0
- package/internal-workflow/evals/coding-skill-evals.json +66 -0
- package/internal-workflow/manifest.json +1 -0
- package/internal-workflow/operations/acceptance-proof/SKILL.md +9 -0
- package/internal-workflow/operations/ambiguity-review/SKILL.md +5 -0
- package/internal-workflow/operations/code-review/SKILL.md +23 -0
- package/internal-workflow/operations/implementation/SKILL.md +24 -0
- package/internal-workflow/operations/spec-author/SKILL.md +12 -0
- package/internal-workflow/operations/spec-review/SKILL.md +12 -0
- package/internal-workflow/operations/triage/SKILL.md +12 -0
- package/internal-workflow/profiles/analyst_deep.toml +9 -0
- package/internal-workflow/profiles/implementer_standard.toml +9 -0
- package/internal-workflow/profiles/proof_agent.toml +8 -0
- package/internal-workflow/profiles/reviewer_deep.toml +9 -0
- package/internal-workflow/profiles/reviewer_standard.toml +9 -0
- package/internal-workflow/schemas/ambiguity-review-v1.json +1 -0
- package/internal-workflow/schemas/code-review-v1.json +1 -0
- package/internal-workflow/schemas/implementation-report-v1.json +1 -0
- package/internal-workflow/schemas/proof-report-v1.json +1 -0
- package/internal-workflow/schemas/spec-author-v1.json +1 -0
- package/internal-workflow/schemas/spec-review-v1.json +30 -0
- package/internal-workflow/schemas/triage-route-v1.json +1 -0
- package/internal-workflow/skills/acceptance-proof/agents/openai.yaml +6 -0
- package/{internal-skills → internal-workflow/skills}/agent-auto/SKILL.md +6 -1
- package/internal-workflow/skills/agent-auto/agents/openai.yaml +6 -0
- package/internal-workflow/skills/code-debugger/SKILL.md +122 -0
- package/internal-workflow/skills/code-debugger/agents/openai.yaml +7 -0
- package/internal-workflow/skills/code-review/SKILL.md +279 -0
- package/internal-workflow/skills/code-review/agents/openai.yaml +4 -0
- package/internal-workflow/skills/code-review/references/bug-classes.md +56 -0
- package/internal-workflow/skills/code-review/references/cleanup-lens.md +52 -0
- package/internal-workflow/skills/code-review/references/framework-lenses.md +34 -0
- package/internal-workflow/skills/code-review/references/targeted-recipes.md +49 -0
- package/internal-workflow/skills/diagnosing-bugs/SKILL.md +138 -0
- package/internal-workflow/skills/diagnosing-bugs/agents/openai.yaml +6 -0
- package/internal-workflow/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +41 -0
- package/internal-workflow/skills/implementation-spec-maker/SKILL.md +102 -0
- package/internal-workflow/skills/implementation-spec-maker/agents/openai.yaml +6 -0
- package/internal-workflow/skills/implementation-spec-maker/references/source-modes.md +31 -0
- package/internal-workflow/skills/implementation-spec-maker/references/spec-template.md +146 -0
- package/internal-workflow/skills/implementation-spec-review/SKILL.md +115 -0
- package/internal-workflow/skills/implementation-spec-review/agents/openai.yaml +6 -0
- package/internal-workflow/skills/implementation-spec-review/evals/evals.json +24 -0
- package/internal-workflow/skills/implementation-spec-review/references/review-loop.md +93 -0
- package/internal-workflow/skills/small-task-implementer/SKILL.md +104 -0
- package/internal-workflow/skills/small-task-implementer/agents/openai.yaml +6 -0
- package/internal-workflow/skills/spec-implementer/SKILL.md +126 -0
- package/internal-workflow/skills/spec-implementer/agents/openai.yaml +6 -0
- package/internal-workflow/skills/spec-implementer/evals/evals.json +30 -0
- package/internal-workflow/skills/spec-implementer/references/review-loop.md +94 -0
- package/internal-workflow/skills/tdd/SKILL.md +72 -0
- package/internal-workflow/skills/tdd/agents/openai.yaml +6 -0
- package/internal-workflow/skills/tdd/interface-design.md +31 -0
- package/internal-workflow/skills/tdd/mocking.md +59 -0
- package/internal-workflow/skills/tdd/refactoring.md +10 -0
- package/internal-workflow/skills/tdd/tests.md +77 -0
- package/internal-workflow/skills/triage/AGENT-BRIEF.md +192 -0
- package/internal-workflow/skills/triage/OUT-OF-SCOPE.md +101 -0
- package/internal-workflow/skills/triage/SKILL.md +134 -0
- package/internal-workflow/skills/triage/agents/openai.yaml +6 -0
- package/package.json +14 -8
- package/dist/src/v2/adapters/target-activity-fence.d.ts +0 -23
- package/dist/src/v2/adapters/target-activity-fence.d.ts.map +0 -1
- package/dist/src/v2/adapters/target-activity-fence.js +0 -249
- package/dist/src/v2/adapters/target-activity-fence.js.map +0 -1
- package/dist/src/v2/candidate-cli.d.ts +0 -22
- package/dist/src/v2/candidate-cli.d.ts.map +0 -1
- package/dist/src/v2/candidate-cli.js.map +0 -1
- package/dist/src/v2/legacy-cutover.d.ts +0 -52
- package/dist/src/v2/legacy-cutover.d.ts.map +0 -1
- package/dist/src/v2/legacy-cutover.js +0 -87
- package/dist/src/v2/legacy-cutover.js.map +0 -1
- /package/{internal-skills → internal-workflow/skills}/acceptance-proof/SKILL.md +0 -0
- /package/{internal-skills → internal-workflow/skills}/acceptance-proof/references/android.md +0 -0
- /package/{internal-skills → internal-workflow/skills}/acceptance-proof/references/browser.md +0 -0
- /package/{internal-skills → internal-workflow/skills}/acceptance-proof/references/ios.md +0 -0
- /package/{internal-skills → internal-workflow/skills}/acceptance-proof/tools/android-lease.mjs +0 -0
- /package/{internal-skills → internal-workflow/skills}/acceptance-proof/tools/ios-lease.mjs +0 -0
package/dist/src/v2/run-issue.js
CHANGED
|
@@ -3,9 +3,15 @@ import { checkedChangePayloadSha256, } from './checked-change.js';
|
|
|
3
3
|
import { parseAgentAutoConfig } from './config.js';
|
|
4
4
|
import { canonicalJson, sha256 } from './containment.js';
|
|
5
5
|
import { validateImplementationReport } from './implementation-report.js';
|
|
6
|
+
import { acceptApprovedDirectReview, acceptNeedsWorkDirectReview, beginDirectReviewRepair, createInitialDirectReview, directReviewClosureRequestSha256, directReviewTargetFingerprint, launchDirectReviewInvocation, prepareDirectReviewClosure, prepareDirectReviewInvocation, projectTerminalDirectReview, } from './direct-delivery.js';
|
|
6
7
|
import { ProofQuiescenceError } from './acceptance-proof.js';
|
|
8
|
+
import { RouteInitializationUnrecoverableError, WorkflowGenerationUnrecoverableError, } from './run-store.js';
|
|
9
|
+
import { initialRouteExecution, } from './route-coordinator.js';
|
|
10
|
+
import { downstreamLifecycleForRoute, validateRouteTransition, validateTrustedAnswerResumeTransition, } from './route-decision.js';
|
|
7
11
|
export class OwnerLockSafetyError extends Error {
|
|
8
12
|
}
|
|
13
|
+
export class OwnerLockContentionError extends Error {
|
|
14
|
+
}
|
|
9
15
|
export class RunIssue {
|
|
10
16
|
dependencies;
|
|
11
17
|
signal;
|
|
@@ -245,7 +251,10 @@ export class RunIssue {
|
|
|
245
251
|
const evidence = await this.dependencies.writeEvidence({ runId, code: 'review-ready', summary: pullRequest.url });
|
|
246
252
|
const outcome = { status: 'review-ready', pullRequestUrl: pullRequest.url, evidencePath: evidence.path };
|
|
247
253
|
try {
|
|
248
|
-
await this.persist(active, {
|
|
254
|
+
await this.persist(active, {
|
|
255
|
+
lifecycle: 'review-ready', intent: undefined, outcomeEvidenceId: evidence.id, terminalOutcome: outcome,
|
|
256
|
+
...(active.record.waitingHuman ? { waitingHuman: terminalWaiting(active.record.waitingHuman, { status: 'review-ready' }) } : {}),
|
|
257
|
+
});
|
|
249
258
|
}
|
|
250
259
|
catch {
|
|
251
260
|
throw new PostEffectStateError(active);
|
|
@@ -267,6 +276,12 @@ export class RunIssue {
|
|
|
267
276
|
owner = await this.dependencies.ownerLock.acquire({ canonicalRepository, targetRoot });
|
|
268
277
|
}
|
|
269
278
|
catch (error) {
|
|
279
|
+
if (error instanceof OwnerLockContentionError) {
|
|
280
|
+
const evidence = await this.dependencies.writeEvidence({
|
|
281
|
+
runId: `issue-${input.issueNumber}`, code: 'owner-contention', summary: 'A known live owner is still running.',
|
|
282
|
+
});
|
|
283
|
+
return { status: 'requeued', reason: 'owner-contention', evidencePath: evidence.path };
|
|
284
|
+
}
|
|
270
285
|
if (error instanceof OwnerLockSafetyError) {
|
|
271
286
|
const evidence = await this.dependencies.writeEvidence({
|
|
272
287
|
runId: `issue-${input.issueNumber}`,
|
|
@@ -348,20 +363,54 @@ export class RunIssue {
|
|
|
348
363
|
return await this.terminal(active, { status: 'internal-error', code: 'local-git-effect-failed' });
|
|
349
364
|
}
|
|
350
365
|
}
|
|
351
|
-
active = await this.
|
|
366
|
+
active = await this.initializeClaimedRun(active, issue);
|
|
367
|
+
issueSnapshot = structuredClone(active.record.issueSnapshot);
|
|
352
368
|
}
|
|
353
369
|
else {
|
|
354
|
-
if (existing.lifecycle === 'safe-halt')
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
370
|
+
if (existing.lifecycle === 'safe-halt') {
|
|
371
|
+
const process = existing.process;
|
|
372
|
+
if (!process)
|
|
373
|
+
return await this.publicationDiverged(active, 'safe-halt-process-missing');
|
|
374
|
+
try {
|
|
375
|
+
await this.dependencies.waitForReviewProcessAbsence(process.processGroupId);
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
return await this.invokedFailure(active, 'safe-halt-process-absence-unconfirmed');
|
|
379
|
+
}
|
|
380
|
+
active = await this.persist(active, { lifecycle: process.resumeLifecycle, process: undefined });
|
|
358
381
|
}
|
|
359
|
-
if (existing.
|
|
360
|
-
|
|
382
|
+
if (existing.lifecycle === 'waiting-human') {
|
|
383
|
+
const waiting = await this.continueWaitingHuman(active);
|
|
384
|
+
if ('result' in waiting)
|
|
385
|
+
return waiting.result;
|
|
386
|
+
active = waiting.active;
|
|
387
|
+
issueSnapshot = structuredClone(active.record.issueSnapshot);
|
|
388
|
+
frozenCriteria = structuredClone(active.record.frozenCriteria);
|
|
389
|
+
}
|
|
390
|
+
if (existing.lifecycle === 'spec-authoring') {
|
|
391
|
+
if (!await this.authorized(input.issueNumber, runId, branchName, config))
|
|
392
|
+
return await this.revoked(active);
|
|
393
|
+
return await this.continueSpecRequired(active);
|
|
394
|
+
}
|
|
395
|
+
if (!['triaging', 'routed', 'implementing', 'reworking', 'checking', 'proving'].includes(active.record.lifecycle)) {
|
|
396
|
+
return await this.terminal(active, { status: 'internal-error', code: 'resume-phase-not-reconciled' });
|
|
361
397
|
}
|
|
362
398
|
if (!await this.authorized(input.issueNumber, runId, branchName, config))
|
|
363
399
|
return await this.revoked(active);
|
|
364
|
-
|
|
400
|
+
if (active.record.lifecycle !== 'triaging' && active.record.lifecycle !== 'routed') {
|
|
401
|
+
if (!active.record.routeExecution || !active.record.routeReceipt)
|
|
402
|
+
throw new RouteInitializationUnrecoverableError();
|
|
403
|
+
const reviewRecovery = active.record.lifecycle === 'implementing' && active.record.directReview?.status === 'active'
|
|
404
|
+
&& (active.record.directReview.stage === 'review-full' || active.record.directReview.stage === 'review-closure');
|
|
405
|
+
const checkRecovery = active.record.lifecycle === 'checking'
|
|
406
|
+
&& active.record.directReview?.status === 'clear';
|
|
407
|
+
if (!reviewRecovery && !checkRecovery) {
|
|
408
|
+
if (active.record.cycle >= config.runner.maxCycles) {
|
|
409
|
+
return await this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true });
|
|
410
|
+
}
|
|
411
|
+
active = await this.startNextCycle(active, [`Recovered interrupted ${active.record.lifecycle} phase.`]);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
365
414
|
}
|
|
366
415
|
}
|
|
367
416
|
else {
|
|
@@ -394,7 +443,20 @@ export class RunIssue {
|
|
|
394
443
|
catch {
|
|
395
444
|
return await this.terminal(active, { status: 'internal-error', code: 'local-git-effect-failed' });
|
|
396
445
|
}
|
|
397
|
-
active = await this.
|
|
446
|
+
active = await this.initializeClaimedRun(active, issue);
|
|
447
|
+
issueSnapshot = structuredClone(active.record.issueSnapshot);
|
|
448
|
+
}
|
|
449
|
+
if (active.record.lifecycle === 'triaging' || active.record.lifecycle === 'routed') {
|
|
450
|
+
const routed = await this.routeRun(active, issueSnapshot, frozenCriteria, worktreePath, config, input.issueNumber, branchName);
|
|
451
|
+
if ('result' in routed)
|
|
452
|
+
return routed.result;
|
|
453
|
+
active = routed.active;
|
|
454
|
+
}
|
|
455
|
+
if (active.record.lifecycle !== 'implementing' && active.record.lifecycle !== 'checking') {
|
|
456
|
+
return await this.terminal(active, { status: 'internal-error', code: 'route-dispatch-not-implementing' });
|
|
457
|
+
}
|
|
458
|
+
if (active.record.lifecycle === 'checking' && !active.record.directReview && active.record.routeReceipt?.route === 'direct') {
|
|
459
|
+
return await this.terminal(active, { status: 'internal-error', code: 'direct-review-state-missing' });
|
|
398
460
|
}
|
|
399
461
|
attemptLoop: while (true) {
|
|
400
462
|
if (!await this.authorized(input.issueNumber, runId, branchName, config)) {
|
|
@@ -402,67 +464,45 @@ export class RunIssue {
|
|
|
402
464
|
}
|
|
403
465
|
if (this.signal.aborted)
|
|
404
466
|
return await this.terminal(active, { status: 'cancelled' });
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
active =
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
break;
|
|
422
|
-
}
|
|
423
|
-
catch {
|
|
424
|
-
await new Promise((resolveWait) => setTimeout(resolveWait, 25));
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
if (await this.dependencies.git.fingerprintDeniedPaths(worktreePath, config.deny.readPaths) !== deniedPathsBaseline) {
|
|
428
|
-
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'denied-path-modified');
|
|
429
|
-
}
|
|
430
|
-
return await this.terminal(active, { status: 'transport-failed', resumable: false }, 'process-quiescence-delayed');
|
|
431
|
-
}
|
|
432
|
-
if (await this.dependencies.git.fingerprintDeniedPaths(worktreePath, config.deny.readPaths) !== deniedPathsBaseline) {
|
|
433
|
-
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'denied-path-modified');
|
|
434
|
-
}
|
|
435
|
-
if (implementation.kind === 'transport-failed' && implementation.resumable && active.record.transportRetries === 0) {
|
|
436
|
-
const afterTransport = await this.dependencies.git.snapshot(worktreePath);
|
|
437
|
-
if (!sameFreshness(implementationBaseline, afterTransport)) {
|
|
438
|
-
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'transport-baseline-changed');
|
|
439
|
-
}
|
|
440
|
-
active = await this.persist(active, { transportRetries: 1 });
|
|
441
|
-
continue attemptLoop;
|
|
442
|
-
}
|
|
443
|
-
if (implementation.kind !== 'completed')
|
|
444
|
-
return await this.mapImplementationFailure(active, implementation);
|
|
445
|
-
let report;
|
|
446
|
-
try {
|
|
447
|
-
report = validateImplementationReport(implementation.report);
|
|
467
|
+
let resumeAtChecks = active.record.lifecycle === 'checking'
|
|
468
|
+
&& active.record.directReview?.status === 'clear';
|
|
469
|
+
if (active.record.lifecycle === 'implementing'
|
|
470
|
+
&& active.record.directReview?.status === 'active'
|
|
471
|
+
&& (active.record.directReview.stage === 'review-full' || active.record.directReview.stage === 'review-closure')) {
|
|
472
|
+
const recovered = await this.recoverDirectReviewInvocation(active);
|
|
473
|
+
if ('status' in recovered)
|
|
474
|
+
return recovered;
|
|
475
|
+
active = recovered;
|
|
476
|
+
const reviewed = await this.runDirectReviewFull(active, publicIssueSnapshot(issueSnapshot), frozenCriteria, `recovered-implementation-cycle-${active.record.cycle}`, config.runner.maxCycles);
|
|
477
|
+
if ('status' in reviewed)
|
|
478
|
+
return reviewed;
|
|
479
|
+
active = reviewed;
|
|
480
|
+
if (active.record.lifecycle === 'implementing')
|
|
481
|
+
continue attemptLoop;
|
|
482
|
+
resumeAtChecks = true;
|
|
448
483
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
}
|
|
453
|
-
const repairBaseline = await this.dependencies.git.snapshot(worktreePath);
|
|
454
|
-
active = await this.persist(active, { reportRepairs: 1 });
|
|
455
|
-
implementation = await this.runImplementation({
|
|
484
|
+
if (!resumeAtChecks) {
|
|
485
|
+
const deniedPathsBaseline = await this.dependencies.git.fingerprintDeniedPaths(worktreePath, config.deny.readPaths);
|
|
486
|
+
let implementation = await this.runImplementation({
|
|
456
487
|
runId,
|
|
457
488
|
worktreePath,
|
|
458
|
-
issue: issueSnapshot,
|
|
489
|
+
issue: publicIssueSnapshot(issueSnapshot),
|
|
459
490
|
frozenCriteria,
|
|
460
491
|
cycle: active.record.cycle,
|
|
461
|
-
reworkFindings:
|
|
462
|
-
repairOnly:
|
|
492
|
+
reworkFindings: active.record.reworkFindings,
|
|
493
|
+
repairOnly: false,
|
|
494
|
+
workflowGeneration: active.record.workflowGeneration,
|
|
463
495
|
});
|
|
464
496
|
if (implementation.kind === 'safe-halt') {
|
|
465
|
-
active = await this.persist(active, {
|
|
497
|
+
active = await this.persist(active, {
|
|
498
|
+
lifecycle: 'safe-halt',
|
|
499
|
+
process: {
|
|
500
|
+
...implementation.process,
|
|
501
|
+
purpose: 'implementation',
|
|
502
|
+
resumeLifecycle: 'implementing',
|
|
503
|
+
resumeReviewStage: null,
|
|
504
|
+
},
|
|
505
|
+
});
|
|
466
506
|
while (true) {
|
|
467
507
|
try {
|
|
468
508
|
await implementation.waitForAbsence();
|
|
@@ -472,33 +512,175 @@ export class RunIssue {
|
|
|
472
512
|
await new Promise((resolveWait) => setTimeout(resolveWait, 25));
|
|
473
513
|
}
|
|
474
514
|
}
|
|
515
|
+
if (await this.dependencies.git.fingerprintDeniedPaths(worktreePath, config.deny.readPaths) !== deniedPathsBaseline) {
|
|
516
|
+
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'denied-path-modified');
|
|
517
|
+
}
|
|
475
518
|
return await this.terminal(active, { status: 'transport-failed', resumable: false }, 'process-quiescence-delayed');
|
|
476
519
|
}
|
|
520
|
+
if (await this.dependencies.git.fingerprintDeniedPaths(worktreePath, config.deny.readPaths) !== deniedPathsBaseline) {
|
|
521
|
+
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'denied-path-modified');
|
|
522
|
+
}
|
|
523
|
+
if (implementation.kind === 'transport-failed' && implementation.resumable && active.record.transportRetries === 0) {
|
|
524
|
+
active = await this.persist(active, { transportRetries: 1 });
|
|
525
|
+
continue attemptLoop;
|
|
526
|
+
}
|
|
477
527
|
if (implementation.kind !== 'completed')
|
|
478
528
|
return await this.mapImplementationFailure(active, implementation);
|
|
479
|
-
|
|
480
|
-
if (!sameFreshness(repairBaseline, afterRepair)) {
|
|
481
|
-
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'report-repair-modified-worktree');
|
|
482
|
-
}
|
|
529
|
+
let report;
|
|
483
530
|
try {
|
|
484
531
|
report = validateImplementationReport(implementation.report);
|
|
485
532
|
}
|
|
486
533
|
catch {
|
|
487
|
-
|
|
534
|
+
if (active.record.reportRepairs >= 1) {
|
|
535
|
+
return await this.terminal(active, { status: 'internal-error', code: 'implementation-report-malformed' });
|
|
536
|
+
}
|
|
537
|
+
const repairBaseline = await this.dependencies.git.snapshot(worktreePath);
|
|
538
|
+
active = await this.persist(active, { reportRepairs: 1 });
|
|
539
|
+
implementation = await this.runImplementation({
|
|
540
|
+
runId,
|
|
541
|
+
worktreePath,
|
|
542
|
+
issue: publicIssueSnapshot(issueSnapshot),
|
|
543
|
+
frozenCriteria,
|
|
544
|
+
cycle: active.record.cycle,
|
|
545
|
+
reworkFindings: ['The previous implementation report did not match the generated schema.'],
|
|
546
|
+
repairOnly: true,
|
|
547
|
+
workflowGeneration: active.record.workflowGeneration,
|
|
548
|
+
});
|
|
549
|
+
if (implementation.kind === 'safe-halt') {
|
|
550
|
+
active = await this.persist(active, {
|
|
551
|
+
lifecycle: 'safe-halt',
|
|
552
|
+
process: {
|
|
553
|
+
...implementation.process,
|
|
554
|
+
purpose: 'implementation',
|
|
555
|
+
resumeLifecycle: 'implementing',
|
|
556
|
+
resumeReviewStage: null,
|
|
557
|
+
},
|
|
558
|
+
});
|
|
559
|
+
while (true) {
|
|
560
|
+
try {
|
|
561
|
+
await implementation.waitForAbsence();
|
|
562
|
+
break;
|
|
563
|
+
}
|
|
564
|
+
catch {
|
|
565
|
+
await new Promise((resolveWait) => setTimeout(resolveWait, 25));
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return await this.terminal(active, { status: 'transport-failed', resumable: false }, 'process-quiescence-delayed');
|
|
569
|
+
}
|
|
570
|
+
if (implementation.kind !== 'completed')
|
|
571
|
+
return await this.mapImplementationFailure(active, implementation);
|
|
572
|
+
const afterRepair = await this.dependencies.git.snapshot(worktreePath);
|
|
573
|
+
if (!sameFreshness(repairBaseline, afterRepair)) {
|
|
574
|
+
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'report-repair-modified-worktree');
|
|
575
|
+
}
|
|
576
|
+
try {
|
|
577
|
+
report = validateImplementationReport(implementation.report);
|
|
578
|
+
}
|
|
579
|
+
catch {
|
|
580
|
+
return await this.terminal(active, { status: 'internal-error', code: 'implementation-report-malformed' });
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
if (report.status === 'external-block') {
|
|
584
|
+
return await this.terminal(active, { status: 'blocked', kind: 'external', resumable: true });
|
|
585
|
+
}
|
|
586
|
+
if (await this.dependencies.git.getHead(worktreePath) !== baseSha) {
|
|
587
|
+
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true });
|
|
588
|
+
}
|
|
589
|
+
const changedFiles = await this.dependencies.git.listChangedFiles(worktreePath);
|
|
590
|
+
if (changedFiles.length === 0 || !sameStrings(changedFiles, report.changedFiles)) {
|
|
591
|
+
if (changedFiles.length === 0 || active.record.reportRepairs >= 1) {
|
|
592
|
+
return await this.terminal(active, { status: 'internal-error', code: 'implementation-change-set-invalid' });
|
|
593
|
+
}
|
|
594
|
+
const repairBaseline = await this.dependencies.git.snapshot(worktreePath);
|
|
595
|
+
active = await this.persist(active, { reportRepairs: 1 });
|
|
596
|
+
implementation = await this.runImplementation({
|
|
597
|
+
runId,
|
|
598
|
+
worktreePath,
|
|
599
|
+
issue: publicIssueSnapshot(issueSnapshot),
|
|
600
|
+
frozenCriteria,
|
|
601
|
+
cycle: active.record.cycle,
|
|
602
|
+
reworkFindings: [`The report changedFiles must equal the complete current product change set: ${canonicalJson(changedFiles)}.`],
|
|
603
|
+
repairOnly: true,
|
|
604
|
+
workflowGeneration: active.record.workflowGeneration,
|
|
605
|
+
});
|
|
606
|
+
if (implementation.kind === 'safe-halt') {
|
|
607
|
+
active = await this.persist(active, {
|
|
608
|
+
lifecycle: 'safe-halt',
|
|
609
|
+
process: {
|
|
610
|
+
...implementation.process,
|
|
611
|
+
purpose: 'implementation',
|
|
612
|
+
resumeLifecycle: 'implementing',
|
|
613
|
+
resumeReviewStage: null,
|
|
614
|
+
},
|
|
615
|
+
});
|
|
616
|
+
while (true) {
|
|
617
|
+
try {
|
|
618
|
+
await implementation.waitForAbsence();
|
|
619
|
+
break;
|
|
620
|
+
}
|
|
621
|
+
catch {
|
|
622
|
+
await new Promise((resolveWait) => setTimeout(resolveWait, 25));
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
return await this.terminal(active, { status: 'transport-failed', resumable: false }, 'process-quiescence-delayed');
|
|
626
|
+
}
|
|
627
|
+
if (implementation.kind !== 'completed')
|
|
628
|
+
return await this.mapImplementationFailure(active, implementation);
|
|
629
|
+
const afterRepair = await this.dependencies.git.snapshot(worktreePath);
|
|
630
|
+
if (!sameFreshness(repairBaseline, afterRepair)) {
|
|
631
|
+
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'report-repair-modified-worktree');
|
|
632
|
+
}
|
|
633
|
+
try {
|
|
634
|
+
report = validateImplementationReport(implementation.report);
|
|
635
|
+
}
|
|
636
|
+
catch {
|
|
637
|
+
return await this.terminal(active, { status: 'internal-error', code: 'implementation-report-malformed' });
|
|
638
|
+
}
|
|
639
|
+
if (report.status === 'external-block') {
|
|
640
|
+
return await this.terminal(active, { status: 'blocked', kind: 'external', resumable: true });
|
|
641
|
+
}
|
|
642
|
+
if (!sameStrings(changedFiles, report.changedFiles)) {
|
|
643
|
+
return await this.terminal(active, { status: 'internal-error', code: 'implementation-change-set-invalid' });
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
if (active.record.routeReceipt?.route === 'direct') {
|
|
647
|
+
const targetFingerprint = directReviewTargetFingerprint({
|
|
648
|
+
snapshot: await this.dependencies.git.snapshot(worktreePath),
|
|
649
|
+
changedFiles,
|
|
650
|
+
routeDecisionSha256: active.record.routeReceipt.decisionSha256,
|
|
651
|
+
workflowGenerationHash: active.record.workflowGeneration.generationHash,
|
|
652
|
+
cycle: active.record.cycle,
|
|
653
|
+
frozenCriteria,
|
|
654
|
+
});
|
|
655
|
+
if (active.record.directReview?.stage === 'review-repair') {
|
|
656
|
+
active = await this.persist(active, {
|
|
657
|
+
directReview: prepareDirectReviewClosure(active.record.directReview, targetFingerprint).state,
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
else {
|
|
661
|
+
const reviewerSessionId = this.dependencies.createReviewSessionId();
|
|
662
|
+
assertNonEmptyString(reviewerSessionId, 'reviewerSessionId');
|
|
663
|
+
active = await this.persist(active, {
|
|
664
|
+
directReview: createInitialDirectReview({
|
|
665
|
+
targetFingerprint,
|
|
666
|
+
codeReviewerSessionId: reviewerSessionId,
|
|
667
|
+
}),
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
const reviewed = await this.runDirectReviewFull(active, publicIssueSnapshot(issueSnapshot), frozenCriteria, implementation.attemptId ?? `implementation-cycle-${active.record.cycle}`, config.runner.maxCycles);
|
|
671
|
+
if ('status' in reviewed)
|
|
672
|
+
return reviewed;
|
|
673
|
+
active = reviewed;
|
|
674
|
+
if (active.record.lifecycle === 'implementing')
|
|
675
|
+
continue attemptLoop;
|
|
676
|
+
}
|
|
677
|
+
else {
|
|
678
|
+
active = await this.persist(active, { lifecycle: 'checking' });
|
|
488
679
|
}
|
|
489
680
|
}
|
|
490
|
-
if (report.status === 'external-block') {
|
|
491
|
-
return await this.terminal(active, { status: 'blocked', kind: 'external', resumable: true });
|
|
492
|
-
}
|
|
493
|
-
if (await this.dependencies.git.getHead(worktreePath) !== baseSha) {
|
|
494
|
-
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true });
|
|
495
|
-
}
|
|
496
|
-
const changedFiles = await this.dependencies.git.listChangedFiles(worktreePath);
|
|
497
|
-
if (changedFiles.length === 0 || !sameStrings(changedFiles, report.changedFiles)) {
|
|
498
|
-
return await this.terminal(active, { status: 'internal-error', code: 'implementation-change-set-invalid' });
|
|
499
|
-
}
|
|
500
|
-
active = await this.persist(active, { lifecycle: 'checking' });
|
|
501
681
|
for (const [id, command] of Object.entries(config.checks)) {
|
|
682
|
+
if (active.record.checks.some((check) => check.id === id && check.status === 'passed'))
|
|
683
|
+
continue;
|
|
502
684
|
if (this.signal.aborted)
|
|
503
685
|
return await this.terminal(active, { status: 'cancelled' });
|
|
504
686
|
let check;
|
|
@@ -516,12 +698,38 @@ export class RunIssue {
|
|
|
516
698
|
if (active.record.cycle >= config.runner.maxCycles) {
|
|
517
699
|
return await this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true });
|
|
518
700
|
}
|
|
519
|
-
|
|
701
|
+
const summary = `Check ${id} failed:\n${check.output.toString('utf8').slice(0, 8 * 1024)}`;
|
|
702
|
+
active = await this.startNextCycle(active, [summary], [{
|
|
703
|
+
provenance: 'check', sourceId: `check:${id}:${row.outputSha256}`, summary,
|
|
704
|
+
affectedContracts: ['configured-checks'],
|
|
705
|
+
}]);
|
|
520
706
|
continue attemptLoop;
|
|
521
707
|
}
|
|
522
708
|
}
|
|
709
|
+
let reviewedStageBinding;
|
|
523
710
|
try {
|
|
711
|
+
const reviewedFiles = await this.dependencies.git.listChangedFiles(worktreePath);
|
|
712
|
+
const reviewedContentSha256 = await this.dependencies.git.fingerprintChangedFiles(worktreePath, reviewedFiles);
|
|
713
|
+
reviewedStageBinding = { files: reviewedFiles, contentSha256: reviewedContentSha256 };
|
|
714
|
+
if (active.record.directReview?.status === 'clear') {
|
|
715
|
+
const currentFingerprint = directReviewTargetFingerprint({
|
|
716
|
+
snapshot: await this.dependencies.git.snapshot(worktreePath),
|
|
717
|
+
changedFiles: await this.dependencies.git.listChangedFiles(worktreePath),
|
|
718
|
+
routeDecisionSha256: active.record.routeReceipt.decisionSha256,
|
|
719
|
+
workflowGenerationHash: active.record.workflowGeneration.generationHash,
|
|
720
|
+
cycle: active.record.cycle,
|
|
721
|
+
frozenCriteria,
|
|
722
|
+
});
|
|
723
|
+
if (currentFingerprint !== active.record.directReview.targetFingerprint) {
|
|
724
|
+
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'direct-review-target-drift');
|
|
725
|
+
}
|
|
726
|
+
}
|
|
524
727
|
await this.dependencies.git.stageAll(worktreePath);
|
|
728
|
+
const stagedFiles = await this.dependencies.git.listChangedFiles(worktreePath);
|
|
729
|
+
const stagedContentSha256 = await this.dependencies.git.fingerprintChangedFiles(worktreePath, stagedFiles);
|
|
730
|
+
if (!sameStrings(reviewedFiles, stagedFiles) || reviewedContentSha256 !== stagedContentSha256) {
|
|
731
|
+
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'post-stage-review-binding-drift');
|
|
732
|
+
}
|
|
525
733
|
}
|
|
526
734
|
catch {
|
|
527
735
|
return await this.terminal(active, { status: 'internal-error', code: 'local-git-effect-failed' });
|
|
@@ -531,6 +739,11 @@ export class RunIssue {
|
|
|
531
739
|
}
|
|
532
740
|
const proofChangedFiles = await this.dependencies.git.listChangedFiles(worktreePath);
|
|
533
741
|
const freshness = await this.dependencies.git.snapshot(worktreePath);
|
|
742
|
+
const proofContentSha256 = await this.dependencies.git.fingerprintChangedFiles(worktreePath, proofChangedFiles);
|
|
743
|
+
if (!reviewedStageBinding || !sameStrings(reviewedStageBinding.files, proofChangedFiles)
|
|
744
|
+
|| reviewedStageBinding.contentSha256 !== proofContentSha256) {
|
|
745
|
+
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'checked-change-review-binding-drift');
|
|
746
|
+
}
|
|
534
747
|
const payload = {
|
|
535
748
|
version: 1,
|
|
536
749
|
canonicalRepository,
|
|
@@ -546,7 +759,7 @@ export class RunIssue {
|
|
|
546
759
|
changedFiles: proofChangedFiles,
|
|
547
760
|
checks: active.record.checks.map((check) => ({ ...check, status: 'passed' })),
|
|
548
761
|
checkPolicySha256: sha256(canonicalJson(config.checks)),
|
|
549
|
-
packageVersion:
|
|
762
|
+
packageVersion: active.record.packageVersion,
|
|
550
763
|
proofSchemaVersion: 1,
|
|
551
764
|
};
|
|
552
765
|
const checkedChange = this.dependencies.checkedChangeMint.mint(payload);
|
|
@@ -556,7 +769,13 @@ export class RunIssue {
|
|
|
556
769
|
active = await this.persist(active, { lifecycle: 'proving', checkedChangeSha256, proofId });
|
|
557
770
|
let proof;
|
|
558
771
|
try {
|
|
559
|
-
proof = await this.dependencies.proof.proveChange({
|
|
772
|
+
proof = await this.dependencies.proof.proveChange({
|
|
773
|
+
proofId,
|
|
774
|
+
issue: publicIssueSnapshot(issueSnapshot),
|
|
775
|
+
frozenCriteria,
|
|
776
|
+
checkedChange,
|
|
777
|
+
workflowGeneration: structuredClone(active.record.workflowGeneration),
|
|
778
|
+
});
|
|
560
779
|
}
|
|
561
780
|
catch (error) {
|
|
562
781
|
if (error instanceof ProofQuiescenceError) {
|
|
@@ -573,6 +792,9 @@ export class RunIssue {
|
|
|
573
792
|
untrackedContentSha256: freshness.untrackedContentSha256,
|
|
574
793
|
worktreeIdentity: freshness.worktreeIdentity,
|
|
575
794
|
},
|
|
795
|
+
purpose: 'proof',
|
|
796
|
+
resumeLifecycle: 'proving',
|
|
797
|
+
resumeReviewStage: null,
|
|
576
798
|
},
|
|
577
799
|
});
|
|
578
800
|
while (true) {
|
|
@@ -594,7 +816,12 @@ export class RunIssue {
|
|
|
594
816
|
if (active.record.cycle >= config.runner.maxCycles) {
|
|
595
817
|
return await this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true });
|
|
596
818
|
}
|
|
597
|
-
active = await this.startNextCycle(active, proof.findings)
|
|
819
|
+
active = await this.startNextCycle(active, proof.findings, proof.findings.map((summary) => ({
|
|
820
|
+
provenance: 'proof',
|
|
821
|
+
sourceId: `proof:${proofId}:${sha256(summary)}`,
|
|
822
|
+
summary,
|
|
823
|
+
affectedContracts: ['acceptance-proof'],
|
|
824
|
+
})));
|
|
598
825
|
continue attemptLoop;
|
|
599
826
|
}
|
|
600
827
|
if (proof.status !== 'passed')
|
|
@@ -608,12 +835,23 @@ export class RunIssue {
|
|
|
608
835
|
if (!active && error instanceof TransportReadError) {
|
|
609
836
|
return await this.preClaimTransport(input.issueNumber);
|
|
610
837
|
}
|
|
838
|
+
if (!active && error instanceof WorkflowGenerationUnrecoverableError) {
|
|
839
|
+
return await this.preClaimInternal('workflow-generation-unrecoverable', input.issueNumber);
|
|
840
|
+
}
|
|
611
841
|
if (active && error instanceof PostEffectStateError) {
|
|
612
842
|
return await this.invokedFailure(error.active, 'post-effect-state-write-failed');
|
|
613
843
|
}
|
|
614
844
|
if (active && error instanceof TransportReadError) {
|
|
615
845
|
return await this.invokedFailure(active, 'authorization-read-failed');
|
|
616
846
|
}
|
|
847
|
+
if (active && error instanceof RouteInitializationUnrecoverableError) {
|
|
848
|
+
const evidence = await this.dependencies.writeEvidence({
|
|
849
|
+
runId: active.record.runId,
|
|
850
|
+
code: 'route-initialization-unrecoverable',
|
|
851
|
+
summary: 'The claimed run cannot be safely initialized without product-state ambiguity.',
|
|
852
|
+
});
|
|
853
|
+
return { status: 'blocked', kind: 'safety', resumable: false, evidencePath: evidence.path };
|
|
854
|
+
}
|
|
617
855
|
if (active) {
|
|
618
856
|
try {
|
|
619
857
|
return await this.terminal(active, { status: 'internal-error', code: 'state-write-failed' });
|
|
@@ -641,6 +879,320 @@ export class RunIssue {
|
|
|
641
879
|
}
|
|
642
880
|
}
|
|
643
881
|
}
|
|
882
|
+
async initializeClaimedRun(active, issue) {
|
|
883
|
+
if (active.record.lifecycle !== 'claimed'
|
|
884
|
+
|| active.record.process
|
|
885
|
+
|| active.record.intent
|
|
886
|
+
|| active.record.routeExecution
|
|
887
|
+
|| active.record.routeReceipt) {
|
|
888
|
+
throw new RouteInitializationUnrecoverableError();
|
|
889
|
+
}
|
|
890
|
+
const [snapshot, changedFiles] = await Promise.all([
|
|
891
|
+
this.dependencies.git.snapshot(active.record.worktreePath),
|
|
892
|
+
this.dependencies.git.listChangedFiles(active.record.worktreePath),
|
|
893
|
+
]);
|
|
894
|
+
if (snapshot.headSha !== active.record.baseSha || changedFiles.length !== 0) {
|
|
895
|
+
throw new RouteInitializationUnrecoverableError();
|
|
896
|
+
}
|
|
897
|
+
try {
|
|
898
|
+
await this.dependencies.verifyWorkflowGeneration(active.record.workflowGeneration);
|
|
899
|
+
}
|
|
900
|
+
catch {
|
|
901
|
+
throw new RouteInitializationUnrecoverableError();
|
|
902
|
+
}
|
|
903
|
+
const issueSnapshot = issue?.state === 'OPEN' ? snapshotIssue(issue) : active.record.issueSnapshot;
|
|
904
|
+
return this.persist(active, { lifecycle: 'triaging', routeExecution: initialRouteExecution(), issueSnapshot });
|
|
905
|
+
}
|
|
906
|
+
async routeRun(starting, issue, frozenCriteria, worktreePath, config, issueNumber, branchName) {
|
|
907
|
+
let active = starting;
|
|
908
|
+
while (active.record.lifecycle === 'triaging') {
|
|
909
|
+
const state = {
|
|
910
|
+
read: async () => requireRouteExecution(active.record.routeExecution),
|
|
911
|
+
compareAndSwap: async (expected, next) => {
|
|
912
|
+
if (!sameRouteExecution(active.record.routeExecution, expected))
|
|
913
|
+
return false;
|
|
914
|
+
active = await this.persist(active, { routeExecution: structuredClone(next) });
|
|
915
|
+
return true;
|
|
916
|
+
},
|
|
917
|
+
complete: async (expected, next, receipt) => {
|
|
918
|
+
if (!sameRouteExecution(active.record.routeExecution, expected))
|
|
919
|
+
return false;
|
|
920
|
+
active = await this.persist(active, {
|
|
921
|
+
lifecycle: 'routed',
|
|
922
|
+
routeExecution: structuredClone(next),
|
|
923
|
+
routeReceipt: structuredClone(receipt),
|
|
924
|
+
});
|
|
925
|
+
return true;
|
|
926
|
+
},
|
|
927
|
+
cancel: async (expected) => {
|
|
928
|
+
if (!sameRouteExecution(active.record.routeExecution, expected))
|
|
929
|
+
return false;
|
|
930
|
+
const evidence = await this.dependencies.writeEvidence({
|
|
931
|
+
runId: active.record.runId,
|
|
932
|
+
code: 'cancelled',
|
|
933
|
+
summary: 'Routing was cancelled.',
|
|
934
|
+
});
|
|
935
|
+
active = await this.persist(active, {
|
|
936
|
+
lifecycle: 'cancelled',
|
|
937
|
+
routeExecution: undefined,
|
|
938
|
+
routeReceipt: undefined,
|
|
939
|
+
outcomeEvidenceId: evidence.id,
|
|
940
|
+
terminalOutcome: { status: 'cancelled', evidencePath: evidence.path },
|
|
941
|
+
...(active.record.waitingHuman ? { waitingHuman: terminalWaiting(active.record.waitingHuman, { status: 'cancelled' }) } : {}),
|
|
942
|
+
});
|
|
943
|
+
return true;
|
|
944
|
+
},
|
|
945
|
+
};
|
|
946
|
+
const result = await this.dependencies.routeCoordinator.run({
|
|
947
|
+
state,
|
|
948
|
+
runId: active.record.runId,
|
|
949
|
+
worktreePath,
|
|
950
|
+
workflowGeneration: structuredClone(active.record.workflowGeneration),
|
|
951
|
+
promptFacts: [
|
|
952
|
+
`issue=${canonicalJson(issue)}`,
|
|
953
|
+
`frozenCriteria=${canonicalJson(frozenCriteria)}`,
|
|
954
|
+
`canonicalRepository=${active.record.canonicalRepository}`,
|
|
955
|
+
`baseSha=${active.record.baseSha}`,
|
|
956
|
+
...(active.record.waitingHuman?.phase === 'resumed' ? [
|
|
957
|
+
`trustedAnswer=${canonicalJson(active.record.waitingHuman.trustedAnswer)}`,
|
|
958
|
+
`priorWaitingRoute=${active.record.waitingHuman.history.at(-1)?.routeReceipt.decisionSha256 ?? ''}`,
|
|
959
|
+
] : []),
|
|
960
|
+
],
|
|
961
|
+
signal: this.signal,
|
|
962
|
+
});
|
|
963
|
+
if (result.status === 'repairable' || result.status === 'retryable')
|
|
964
|
+
continue;
|
|
965
|
+
if (result.status === 'safe-halt') {
|
|
966
|
+
active = await this.persist(active, {
|
|
967
|
+
lifecycle: 'safe-halt',
|
|
968
|
+
process: {
|
|
969
|
+
...result.process,
|
|
970
|
+
purpose: 'route',
|
|
971
|
+
resumeLifecycle: 'triaging',
|
|
972
|
+
resumeReviewStage: null,
|
|
973
|
+
},
|
|
974
|
+
});
|
|
975
|
+
while (true) {
|
|
976
|
+
try {
|
|
977
|
+
await result.waitForAbsence();
|
|
978
|
+
break;
|
|
979
|
+
}
|
|
980
|
+
catch {
|
|
981
|
+
await new Promise((resolveWait) => setTimeout(resolveWait, 25));
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
const after = await this.dependencies.git.snapshot(worktreePath);
|
|
985
|
+
if (!sameFreshness(result.process.baseline, after)) {
|
|
986
|
+
return { result: await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: false }, 'report-operation-worktree-mutated') };
|
|
987
|
+
}
|
|
988
|
+
return { result: await this.terminal(active, { status: 'transport-failed', resumable: false }, 'process-quiescence-delayed') };
|
|
989
|
+
}
|
|
990
|
+
if (result.status === 'cancelled') {
|
|
991
|
+
if (!active.record.terminalOutcome)
|
|
992
|
+
throw new Error('route cancellation was not persisted');
|
|
993
|
+
return { result: publicOutcome(active.record.terminalOutcome) };
|
|
994
|
+
}
|
|
995
|
+
if (result.status === 'blocked') {
|
|
996
|
+
return { result: await this.terminal(active, {
|
|
997
|
+
status: 'blocked',
|
|
998
|
+
kind: result.kind,
|
|
999
|
+
resumable: result.kind !== 'exhausted',
|
|
1000
|
+
}, result.code) };
|
|
1001
|
+
}
|
|
1002
|
+
if (active.record.lifecycle !== 'routed' || !active.record.routeReceipt) {
|
|
1003
|
+
return { result: await this.terminal(active, { status: 'internal-error', code: 'route-completion-not-persisted' }) };
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
if (active.record.lifecycle !== 'routed' || !active.record.routeReceipt) {
|
|
1007
|
+
return { result: await this.terminal(active, { status: 'internal-error', code: 'route-state-not-dispatchable' }) };
|
|
1008
|
+
}
|
|
1009
|
+
try {
|
|
1010
|
+
if (!await this.authorized(issueNumber, active.record.runId, branchName, config)) {
|
|
1011
|
+
return { result: await this.revoked(active) };
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
catch (error) {
|
|
1015
|
+
if (error instanceof TransportReadError)
|
|
1016
|
+
return { result: await this.invokedFailure(active, 'authorization-read-failed') };
|
|
1017
|
+
throw error;
|
|
1018
|
+
}
|
|
1019
|
+
const receipt = structuredClone(active.record.routeReceipt);
|
|
1020
|
+
const lifecycle = downstreamLifecycleForRoute(receipt, active.record.workflowGeneration.generationHash);
|
|
1021
|
+
validateRouteTransition({
|
|
1022
|
+
lifecycle: 'routed',
|
|
1023
|
+
routeExecution: active.record.routeExecution,
|
|
1024
|
+
routeReceipt: receipt,
|
|
1025
|
+
generationHash: active.record.workflowGeneration.generationHash,
|
|
1026
|
+
}, {
|
|
1027
|
+
lifecycle,
|
|
1028
|
+
routeExecution: active.record.routeExecution,
|
|
1029
|
+
routeReceipt: receipt,
|
|
1030
|
+
generationHash: active.record.workflowGeneration.generationHash,
|
|
1031
|
+
});
|
|
1032
|
+
if (receipt.route !== 'awaiting-user')
|
|
1033
|
+
active = await this.persist(active, { lifecycle });
|
|
1034
|
+
try {
|
|
1035
|
+
if (!await this.authorized(issueNumber, active.record.runId, branchName, config)) {
|
|
1036
|
+
return { result: await this.revoked(active) };
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
catch (error) {
|
|
1040
|
+
if (error instanceof TransportReadError)
|
|
1041
|
+
return { result: await this.invokedFailure(active, 'authorization-read-failed') };
|
|
1042
|
+
throw error;
|
|
1043
|
+
}
|
|
1044
|
+
const context = {
|
|
1045
|
+
runId: active.record.runId,
|
|
1046
|
+
issue: structuredClone(active.record.issueSnapshot),
|
|
1047
|
+
frozenCriteria: structuredClone(active.record.frozenCriteria),
|
|
1048
|
+
worktreePath,
|
|
1049
|
+
workflowGeneration: structuredClone(active.record.workflowGeneration),
|
|
1050
|
+
receipt,
|
|
1051
|
+
};
|
|
1052
|
+
if (receipt.route === 'awaiting-user') {
|
|
1053
|
+
const waiting = await this.dependencies.routeContinuations.awaitingUser(context, this.waitingState(() => active, (next) => { active = next; }), this.signal);
|
|
1054
|
+
const mapped = await this.mapWaitingResult(active, waiting);
|
|
1055
|
+
if ('result' in mapped)
|
|
1056
|
+
return mapped;
|
|
1057
|
+
return this.routeRun(mapped.active, mapped.active.record.issueSnapshot, mapped.active.record.frozenCriteria, worktreePath, config, issueNumber, branchName);
|
|
1058
|
+
}
|
|
1059
|
+
const continuation = receipt.route === 'direct'
|
|
1060
|
+
? await this.dependencies.routeContinuations.direct(context)
|
|
1061
|
+
: await this.dependencies.routeContinuations.specRequired(context, this.specState(() => active, (next) => { active = next; }), this.signal);
|
|
1062
|
+
if (continuation.status === 'cancelled')
|
|
1063
|
+
return { result: await this.terminal(active, { status: 'cancelled' }) };
|
|
1064
|
+
if (continuation.status === 'blocked') {
|
|
1065
|
+
return { result: await this.terminal(active, {
|
|
1066
|
+
status: 'blocked', kind: continuation.kind, resumable: continuation.kind !== 'exhausted',
|
|
1067
|
+
}, continuation.code) };
|
|
1068
|
+
}
|
|
1069
|
+
if (continuation.status === 'retryable') {
|
|
1070
|
+
return { result: await this.terminal(active, { status: 'transport-failed', resumable: true }, continuation.code) };
|
|
1071
|
+
}
|
|
1072
|
+
if (receipt.route !== 'direct') {
|
|
1073
|
+
const specContinuation = continuation;
|
|
1074
|
+
if (specContinuation.status !== 'completed')
|
|
1075
|
+
return { result: await this.terminal(active, { status: 'internal-error', code: 'spec-freeze-receipt-missing' }) };
|
|
1076
|
+
const evidence = await this.dependencies.writeEvidence({ runId: active.record.runId, code: 'spec-frozen', summary: specContinuation.receipt.receiptSha256 });
|
|
1077
|
+
return { result: { status: 'spec-frozen', receipt: specContinuation.receipt, evidencePath: evidence.path } };
|
|
1078
|
+
}
|
|
1079
|
+
return { active };
|
|
1080
|
+
}
|
|
1081
|
+
waitingState(readActive, writeActive) {
|
|
1082
|
+
return {
|
|
1083
|
+
read: async () => structuredClone(readActive().record.waitingHuman),
|
|
1084
|
+
compareAndSwap: async (expected, next) => {
|
|
1085
|
+
const active = readActive();
|
|
1086
|
+
const observed = active.record.waitingHuman;
|
|
1087
|
+
if (observed === undefined || expected === undefined) {
|
|
1088
|
+
if (observed !== expected)
|
|
1089
|
+
return false;
|
|
1090
|
+
}
|
|
1091
|
+
else if (canonicalJson(observed) !== canonicalJson(expected))
|
|
1092
|
+
return false;
|
|
1093
|
+
const saved = await this.persist(active, {
|
|
1094
|
+
...(active.record.lifecycle === 'routed'
|
|
1095
|
+
&& (expected === undefined || (expected.phase === 'resumed' && next.phase !== 'resumed' && next.phase !== 'history-only'))
|
|
1096
|
+
? { lifecycle: 'waiting-human' }
|
|
1097
|
+
: {}),
|
|
1098
|
+
waitingHuman: structuredClone(next),
|
|
1099
|
+
});
|
|
1100
|
+
writeActive(saved);
|
|
1101
|
+
return true;
|
|
1102
|
+
},
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
specState(readActive, writeActive) {
|
|
1106
|
+
return {
|
|
1107
|
+
read: async () => structuredClone(readActive().record.specDelivery),
|
|
1108
|
+
compareAndSwap: async (expected, next) => {
|
|
1109
|
+
const active = readActive();
|
|
1110
|
+
const observed = active.record.specDelivery;
|
|
1111
|
+
if (observed === undefined || expected === undefined) {
|
|
1112
|
+
if (observed !== expected)
|
|
1113
|
+
return false;
|
|
1114
|
+
}
|
|
1115
|
+
else if (canonicalJson(observed) !== canonicalJson(expected))
|
|
1116
|
+
return false;
|
|
1117
|
+
const saved = await this.persist(active, { specDelivery: structuredClone(next) });
|
|
1118
|
+
writeActive(saved);
|
|
1119
|
+
return true;
|
|
1120
|
+
},
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
async continueSpecRequired(active) {
|
|
1124
|
+
if (!active.record.routeReceipt || active.record.routeReceipt.route !== 'spec-required') {
|
|
1125
|
+
return await this.terminal(active, { status: 'internal-error', code: 'spec-route-missing' });
|
|
1126
|
+
}
|
|
1127
|
+
let current = active;
|
|
1128
|
+
const context = {
|
|
1129
|
+
runId: current.record.runId, issue: structuredClone(current.record.issueSnapshot),
|
|
1130
|
+
frozenCriteria: structuredClone(current.record.frozenCriteria), worktreePath: current.record.worktreePath,
|
|
1131
|
+
workflowGeneration: structuredClone(current.record.workflowGeneration), receipt: structuredClone(current.record.routeReceipt),
|
|
1132
|
+
};
|
|
1133
|
+
const result = await this.dependencies.routeContinuations.specRequired(context, this.specState(() => current, (next) => { current = next; }), this.signal);
|
|
1134
|
+
if (result.status === 'completed') {
|
|
1135
|
+
const evidence = await this.dependencies.writeEvidence({ runId: current.record.runId, code: 'spec-frozen', summary: result.receipt.receiptSha256 });
|
|
1136
|
+
return { status: 'spec-frozen', receipt: result.receipt, evidencePath: evidence.path };
|
|
1137
|
+
}
|
|
1138
|
+
if (result.status === 'cancelled')
|
|
1139
|
+
return await this.terminal(current, { status: 'cancelled' });
|
|
1140
|
+
if (result.status === 'retryable')
|
|
1141
|
+
return await this.terminal(current, { status: 'transport-failed', resumable: true }, result.code);
|
|
1142
|
+
return await this.terminal(current, { status: 'blocked', kind: result.kind, resumable: result.kind !== 'exhausted' }, result.code);
|
|
1143
|
+
}
|
|
1144
|
+
async continueWaitingHuman(active) {
|
|
1145
|
+
if (!active.record.routeReceipt || !active.record.workflowGeneration) {
|
|
1146
|
+
return { result: await this.terminal(active, { status: 'internal-error', code: 'waiting-route-missing' }) };
|
|
1147
|
+
}
|
|
1148
|
+
const context = {
|
|
1149
|
+
runId: active.record.runId,
|
|
1150
|
+
issue: structuredClone(active.record.issueSnapshot),
|
|
1151
|
+
frozenCriteria: structuredClone(active.record.frozenCriteria),
|
|
1152
|
+
worktreePath: active.record.worktreePath,
|
|
1153
|
+
workflowGeneration: structuredClone(active.record.workflowGeneration),
|
|
1154
|
+
receipt: structuredClone(active.record.routeReceipt),
|
|
1155
|
+
};
|
|
1156
|
+
let current = active;
|
|
1157
|
+
const result = await this.dependencies.routeContinuations.awaitingUser(context, this.waitingState(() => current, (next) => { current = next; }), this.signal);
|
|
1158
|
+
return this.mapWaitingResult(current, result);
|
|
1159
|
+
}
|
|
1160
|
+
async mapWaitingResult(active, result) {
|
|
1161
|
+
if (result.status === 'awaiting-answer') {
|
|
1162
|
+
const evidence = await this.dependencies.writeEvidence({ runId: active.record.runId, code: 'awaiting-user', summary: result.questionId });
|
|
1163
|
+
return { result: { status: 'awaiting-user', questionId: result.questionId, answerPrefix: result.answerPrefix, evidencePath: evidence.path } };
|
|
1164
|
+
}
|
|
1165
|
+
if (result.status === 'retryable') {
|
|
1166
|
+
const evidence = await this.dependencies.writeEvidence({ runId: active.record.runId, code: result.code, summary: result.owner });
|
|
1167
|
+
return { result: { status: 'transport-failed', resumable: true, evidencePath: evidence.path } };
|
|
1168
|
+
}
|
|
1169
|
+
if (result.status === 'cancelled')
|
|
1170
|
+
return { result: await this.terminal(active, { status: 'cancelled' }) };
|
|
1171
|
+
if (result.status === 'blocked') {
|
|
1172
|
+
return { result: await this.terminal(active, { status: 'blocked', kind: result.kind, resumable: result.resumable }, result.code) };
|
|
1173
|
+
}
|
|
1174
|
+
const waitingHuman = archiveWaiting(active.record, result.answer, {
|
|
1175
|
+
phase: 'resumed', trustedAnswer: structuredClone(result.answer),
|
|
1176
|
+
});
|
|
1177
|
+
const routeExecution = initialRouteExecution();
|
|
1178
|
+
validateTrustedAnswerResumeTransition({
|
|
1179
|
+
lifecycle: active.record.lifecycle,
|
|
1180
|
+
routeExecution: active.record.routeExecution,
|
|
1181
|
+
routeReceipt: active.record.routeReceipt,
|
|
1182
|
+
generationHash: active.record.workflowGeneration.generationHash,
|
|
1183
|
+
}, {
|
|
1184
|
+
lifecycle: 'triaging', routeExecution, routeReceipt: undefined,
|
|
1185
|
+
generationHash: active.record.workflowGeneration.generationHash,
|
|
1186
|
+
}, active.record.waitingHuman);
|
|
1187
|
+
const resumed = await this.persist(active, {
|
|
1188
|
+
lifecycle: 'triaging',
|
|
1189
|
+
issueSnapshot: structuredClone(active.record.issueSnapshot),
|
|
1190
|
+
routeExecution,
|
|
1191
|
+
routeReceipt: undefined,
|
|
1192
|
+
waitingHuman,
|
|
1193
|
+
});
|
|
1194
|
+
return { active: resumed };
|
|
1195
|
+
}
|
|
644
1196
|
async readStrictConfig(targetRoot) {
|
|
645
1197
|
const value = await this.dependencies.readConfig(targetRoot);
|
|
646
1198
|
return { bytes: Buffer.from(value.bytes), config: parseAgentAutoConfig(structuredClone(value.config)) };
|
|
@@ -653,7 +1205,8 @@ export class RunIssue {
|
|
|
653
1205
|
return 'Issue is not open.';
|
|
654
1206
|
if (!labels.has(config.github.labels.auto.name))
|
|
655
1207
|
return 'Issue lacks the auto label.';
|
|
656
|
-
if ([config.github.labels.running.name, config.github.labels.blocked.name, config.github.labels.review.name
|
|
1208
|
+
if ([config.github.labels.running.name, config.github.labels.blocked.name, config.github.labels.review.name, config.github.labels.waitingHuman.name]
|
|
1209
|
+
.some((label) => labels.has(label))) {
|
|
657
1210
|
return 'Issue already has a terminal or running label.';
|
|
658
1211
|
}
|
|
659
1212
|
const branchName = `codex/issue-${issue.number}`;
|
|
@@ -669,12 +1222,14 @@ export class RunIssue {
|
|
|
669
1222
|
async createRun(input) {
|
|
670
1223
|
const state = await this.dependencies.runRecords.read();
|
|
671
1224
|
const now = this.timestamp();
|
|
1225
|
+
const workflow = await this.dependencies.createWorkflowGeneration();
|
|
672
1226
|
const record = {
|
|
673
1227
|
...input,
|
|
674
1228
|
lifecycle: 'claimed', cycle: 1, reportRepairs: 0, transportRetries: 0,
|
|
675
1229
|
reworkFindings: [],
|
|
676
|
-
packageVersion:
|
|
677
|
-
|
|
1230
|
+
packageVersion: workflow.receipt.packageVersion,
|
|
1231
|
+
workflowGeneration: structuredClone(workflow.receipt),
|
|
1232
|
+
skillHashes: structuredClone(workflow.skillHashes),
|
|
678
1233
|
checks: [], createdAt: now, updatedAt: now,
|
|
679
1234
|
};
|
|
680
1235
|
const saved = await this.dependencies.runRecords.compareAndSwap(state.generation, {
|
|
@@ -688,7 +1243,7 @@ export class RunIssue {
|
|
|
688
1243
|
delete record.intent;
|
|
689
1244
|
if (Object.hasOwn(changes, 'process') && changes.process === undefined)
|
|
690
1245
|
delete record.process;
|
|
691
|
-
for (const key of ['checkedChangeSha256', 'proofId', 'proofReceipt', 'terminalOutcome', 'outcomeEvidenceId']) {
|
|
1246
|
+
for (const key of ['checkedChangeSha256', 'proofId', 'proofReceipt', 'terminalOutcome', 'outcomeEvidenceId', 'routeExecution', 'routeReceipt']) {
|
|
692
1247
|
if (Object.hasOwn(changes, key) && changes[key] === undefined)
|
|
693
1248
|
delete record[key];
|
|
694
1249
|
}
|
|
@@ -739,19 +1294,230 @@ export class RunIssue {
|
|
|
739
1294
|
}
|
|
740
1295
|
async runImplementation(input) {
|
|
741
1296
|
try {
|
|
742
|
-
return await this.dependencies.implementationAgent.run({
|
|
1297
|
+
return await this.dependencies.implementationAgent.run({
|
|
1298
|
+
...input,
|
|
1299
|
+
workflowGeneration: structuredClone(input.workflowGeneration),
|
|
1300
|
+
signal: this.signal,
|
|
1301
|
+
});
|
|
743
1302
|
}
|
|
744
1303
|
catch {
|
|
745
1304
|
return { kind: 'internal-error' };
|
|
746
1305
|
}
|
|
747
1306
|
}
|
|
748
|
-
async
|
|
1307
|
+
async runDirectReviewFull(starting, issue, frozenCriteria, implementationAttemptId, maxCycles) {
|
|
1308
|
+
let active = starting;
|
|
1309
|
+
let reportRepair;
|
|
1310
|
+
while (true) {
|
|
1311
|
+
const directReview = active.record.directReview;
|
|
1312
|
+
const routeReceipt = active.record.routeReceipt;
|
|
1313
|
+
if (!directReview || !routeReceipt || routeReceipt.route !== 'direct'
|
|
1314
|
+
|| (directReview.stage !== 'review-full' && directReview.stage !== 'review-closure')) {
|
|
1315
|
+
return this.terminal(active, { status: 'internal-error', code: 'direct-review-state-invalid' });
|
|
1316
|
+
}
|
|
1317
|
+
const reviewerSessionId = directReview.review.reviewerSessionId;
|
|
1318
|
+
if (!reviewerSessionId)
|
|
1319
|
+
return this.terminal(active, { status: 'internal-error', code: 'direct-review-session-missing' });
|
|
1320
|
+
const closureRequestSha256 = directReview.stage === 'review-closure'
|
|
1321
|
+
? directReviewClosureRequestSha256(directReview)
|
|
1322
|
+
: null;
|
|
1323
|
+
const mode = directReview.stage === 'review-closure' ? 'closure' : 'full';
|
|
1324
|
+
const result = await this.dependencies.implementationReviewer.run({
|
|
1325
|
+
runId: active.record.runId,
|
|
1326
|
+
worktreePath: active.record.worktreePath,
|
|
1327
|
+
operation: 'code-review',
|
|
1328
|
+
mode,
|
|
1329
|
+
reviewerSessionId,
|
|
1330
|
+
implementationAttemptId,
|
|
1331
|
+
targetRevision: directReview.targetRevision,
|
|
1332
|
+
targetFingerprint: directReview.targetFingerprint,
|
|
1333
|
+
closureRequestSha256,
|
|
1334
|
+
issue,
|
|
1335
|
+
frozenCriteria,
|
|
1336
|
+
routeReceipt: structuredClone(routeReceipt),
|
|
1337
|
+
defects: structuredClone(directReview.review.defects),
|
|
1338
|
+
affectedDefectIds: [...directReview.review.affectedDefectIds],
|
|
1339
|
+
fixedRepairFindings: directReview.repairFindings.filter((finding) => finding.status === 'fixed')
|
|
1340
|
+
.map((finding) => ({ id: finding.id, affectedContracts: [...finding.affectedContracts] })),
|
|
1341
|
+
mandatoryCoverage: directReview.stage === 'review-closure'
|
|
1342
|
+
? [...directReview.review.coverage]
|
|
1343
|
+
: ['acceptance-criteria', 'correctness', 'test-quality'],
|
|
1344
|
+
workflowGeneration: structuredClone(active.record.workflowGeneration),
|
|
1345
|
+
repairOnly: reportRepair !== undefined,
|
|
1346
|
+
originalReportSha256: reportRepair?.originalReportSha256 ?? null,
|
|
1347
|
+
validationDiagnostic: reportRepair?.diagnostic ?? null,
|
|
1348
|
+
originalReportBytes: reportRepair?.originalReportBytes ?? null,
|
|
1349
|
+
signal: this.signal,
|
|
1350
|
+
onPrepared: async (invocation) => {
|
|
1351
|
+
const current = active.record.directReview;
|
|
1352
|
+
if (!current)
|
|
1353
|
+
throw new Error('direct review disappeared before prepare');
|
|
1354
|
+
active = await this.persist(active, {
|
|
1355
|
+
directReview: prepareDirectReviewInvocation(current, invocation),
|
|
1356
|
+
});
|
|
1357
|
+
},
|
|
1358
|
+
onLaunched: async (invocation) => {
|
|
1359
|
+
const current = active.record.directReview;
|
|
1360
|
+
if (!current)
|
|
1361
|
+
throw new Error('direct review disappeared before launch');
|
|
1362
|
+
active = await this.persist(active, {
|
|
1363
|
+
directReview: launchDirectReviewInvocation(current, invocation),
|
|
1364
|
+
});
|
|
1365
|
+
},
|
|
1366
|
+
});
|
|
1367
|
+
if (result.kind === 'completed') {
|
|
1368
|
+
const current = active.record.directReview;
|
|
1369
|
+
if (!current)
|
|
1370
|
+
return this.terminal(active, { status: 'internal-error', code: 'direct-review-result-orphaned' });
|
|
1371
|
+
if (result.report.verdict === 'needs-work') {
|
|
1372
|
+
if (active.record.cycle >= maxCycles) {
|
|
1373
|
+
return this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true }, 'direct-review-repair-exhausted');
|
|
1374
|
+
}
|
|
1375
|
+
const repaired = acceptNeedsWorkDirectReview(current, result.report, result.artifactSha256);
|
|
1376
|
+
const findings = [
|
|
1377
|
+
...result.report.defects
|
|
1378
|
+
.filter((defect) => defect.status === 'open' || defect.status === 'reopened')
|
|
1379
|
+
.map((defect) => `${defect.id}: ${defect.failure}\nRepair: ${defect.repair}`),
|
|
1380
|
+
...repaired.repairFindings
|
|
1381
|
+
.filter((finding) => finding.status === 'reopened')
|
|
1382
|
+
.map((finding) => `${finding.id}: ${finding.summary}`),
|
|
1383
|
+
];
|
|
1384
|
+
return this.persist(active, {
|
|
1385
|
+
lifecycle: 'implementing',
|
|
1386
|
+
cycle: (active.record.cycle + 1),
|
|
1387
|
+
directReview: repaired,
|
|
1388
|
+
reworkFindings: findings,
|
|
1389
|
+
checks: [],
|
|
1390
|
+
checkedChangeSha256: undefined,
|
|
1391
|
+
proofId: undefined,
|
|
1392
|
+
proofReceipt: undefined,
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
if (result.report.verdict !== 'approved')
|
|
1396
|
+
return this.terminal(active, { status: 'blocked', kind: 'safety', resumable: false }, 'direct-review-rejected');
|
|
1397
|
+
return this.persist(active, {
|
|
1398
|
+
lifecycle: 'checking',
|
|
1399
|
+
directReview: acceptApprovedDirectReview(current, result.report, result.artifactSha256),
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
if (result.kind === 'report-invalid') {
|
|
1403
|
+
const current = active.record.directReview;
|
|
1404
|
+
if (!current)
|
|
1405
|
+
return this.terminal(active, { status: 'internal-error', code: 'direct-review-result-orphaned' });
|
|
1406
|
+
if (current.review.reportRepairs >= 1 || reportRepair) {
|
|
1407
|
+
return this.terminal(active, { status: 'internal-error', code: 'direct-review-report-malformed' });
|
|
1408
|
+
}
|
|
1409
|
+
const { invocation: _invocation, ...withoutInvocation } = structuredClone(current);
|
|
1410
|
+
active = await this.persist(active, {
|
|
1411
|
+
directReview: {
|
|
1412
|
+
...withoutInvocation,
|
|
1413
|
+
review: { ...withoutInvocation.review, reportRepairs: 1 },
|
|
1414
|
+
},
|
|
1415
|
+
});
|
|
1416
|
+
reportRepair = {
|
|
1417
|
+
originalReportSha256: result.originalReportSha256,
|
|
1418
|
+
originalReportBytes: Buffer.from(result.originalReportBytes),
|
|
1419
|
+
diagnostic: result.diagnostic,
|
|
1420
|
+
};
|
|
1421
|
+
continue;
|
|
1422
|
+
}
|
|
1423
|
+
if (result.kind === 'transport-failed') {
|
|
1424
|
+
const current = active.record.directReview;
|
|
1425
|
+
if (!current || current.review.transportRetries >= 1) {
|
|
1426
|
+
return this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true }, 'direct-review-transport-exhausted');
|
|
1427
|
+
}
|
|
1428
|
+
const { invocation: _invocation, ...withoutInvocation } = structuredClone(current);
|
|
1429
|
+
active = await this.persist(active, {
|
|
1430
|
+
directReview: {
|
|
1431
|
+
...withoutInvocation,
|
|
1432
|
+
review: { ...withoutInvocation.review, transportRetries: 1 },
|
|
1433
|
+
},
|
|
1434
|
+
});
|
|
1435
|
+
continue;
|
|
1436
|
+
}
|
|
1437
|
+
if (result.kind === 'safe-halt') {
|
|
1438
|
+
active = await this.persist(active, {
|
|
1439
|
+
lifecycle: 'safe-halt',
|
|
1440
|
+
process: {
|
|
1441
|
+
...result.process,
|
|
1442
|
+
purpose: 'code-review',
|
|
1443
|
+
resumeLifecycle: 'implementing',
|
|
1444
|
+
resumeReviewStage: directReview.stage,
|
|
1445
|
+
},
|
|
1446
|
+
});
|
|
1447
|
+
try {
|
|
1448
|
+
await result.waitForAbsence();
|
|
1449
|
+
}
|
|
1450
|
+
catch {
|
|
1451
|
+
return this.terminal(active, { status: 'transport-failed', resumable: false }, 'direct-review-quiescence-unconfirmed');
|
|
1452
|
+
}
|
|
1453
|
+
return this.terminal(active, { status: 'transport-failed', resumable: false }, 'direct-review-quiescence-delayed');
|
|
1454
|
+
}
|
|
1455
|
+
if (result.kind === 'cancelled')
|
|
1456
|
+
return this.terminal(active, { status: 'cancelled' });
|
|
1457
|
+
return this.terminal(active, { status: 'internal-error', code: result.code });
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
async recoverDirectReviewInvocation(starting) {
|
|
1461
|
+
const directReview = starting.record.directReview;
|
|
1462
|
+
const invocation = directReview?.invocation;
|
|
1463
|
+
if (!directReview || !invocation)
|
|
1464
|
+
return starting;
|
|
1465
|
+
const { invocation: _invocation, ...withoutInvocation } = structuredClone(directReview);
|
|
1466
|
+
if (invocation.status === 'prepared') {
|
|
1467
|
+
return this.persist(starting, { directReview: withoutInvocation });
|
|
1468
|
+
}
|
|
1469
|
+
if (invocation.status === 'launched') {
|
|
1470
|
+
try {
|
|
1471
|
+
await this.dependencies.waitForReviewProcessAbsence(invocation.processGroupId);
|
|
1472
|
+
}
|
|
1473
|
+
catch {
|
|
1474
|
+
return this.terminal(starting, { status: 'blocked', kind: 'safety', resumable: false }, 'direct-review-process-absence-unconfirmed');
|
|
1475
|
+
}
|
|
1476
|
+
if (directReview.review.transportRetries >= 1) {
|
|
1477
|
+
return this.terminal(starting, { status: 'blocked', kind: 'exhausted', resumable: true }, 'direct-review-transport-exhausted');
|
|
1478
|
+
}
|
|
1479
|
+
return this.persist(starting, {
|
|
1480
|
+
directReview: {
|
|
1481
|
+
...withoutInvocation,
|
|
1482
|
+
review: { ...withoutInvocation.review, transportRetries: 1 },
|
|
1483
|
+
},
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
return this.persist(starting, { directReview: withoutInvocation });
|
|
1487
|
+
}
|
|
1488
|
+
async startNextCycle(active, findings, sources) {
|
|
1489
|
+
if (active.record.directReview?.status === 'clear') {
|
|
1490
|
+
const fallbackProvenance = active.record.lifecycle === 'proving' ? 'proof' : 'check';
|
|
1491
|
+
const normalizedSources = sources ?? findings.map((summary) => ({
|
|
1492
|
+
provenance: fallbackProvenance,
|
|
1493
|
+
sourceId: `${fallbackProvenance}:recovery:${sha256(summary)}`,
|
|
1494
|
+
summary,
|
|
1495
|
+
affectedContracts: [fallbackProvenance === 'proof' ? 'acceptance-proof' : 'configured-checks'],
|
|
1496
|
+
}));
|
|
1497
|
+
const repairFindings = normalizedSources.map((source) => ({
|
|
1498
|
+
id: source.sourceId,
|
|
1499
|
+
provenance: source.provenance,
|
|
1500
|
+
sourceId: source.sourceId,
|
|
1501
|
+
targetRevision: active.record.directReview.targetRevision,
|
|
1502
|
+
summary: source.summary,
|
|
1503
|
+
affectedContracts: source.affectedContracts,
|
|
1504
|
+
status: 'open',
|
|
1505
|
+
}));
|
|
1506
|
+
return this.persist(active, {
|
|
1507
|
+
lifecycle: 'implementing',
|
|
1508
|
+
cycle: (active.record.cycle + 1),
|
|
1509
|
+
reworkFindings: [...findings],
|
|
1510
|
+
directReview: beginDirectReviewRepair(active.record.directReview, repairFindings),
|
|
1511
|
+
checks: [],
|
|
1512
|
+
checkedChangeSha256: undefined,
|
|
1513
|
+
proofId: undefined,
|
|
1514
|
+
proofReceipt: undefined,
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
749
1517
|
active = await this.persist(active, { lifecycle: 'reworking', reworkFindings: [...findings] });
|
|
750
1518
|
return this.persist(active, {
|
|
751
1519
|
lifecycle: 'implementing',
|
|
752
1520
|
cycle: (active.record.cycle + 1),
|
|
753
|
-
packageVersion: this.dependencies.packageVersion,
|
|
754
|
-
skillHashes: structuredClone(this.dependencies.skillHashes),
|
|
755
1521
|
checks: [],
|
|
756
1522
|
checkedChangeSha256: undefined,
|
|
757
1523
|
proofId: undefined,
|
|
@@ -792,6 +1558,38 @@ export class RunIssue {
|
|
|
792
1558
|
outcomeEvidenceId: evidence.id,
|
|
793
1559
|
process: undefined,
|
|
794
1560
|
};
|
|
1561
|
+
if (outcome.status !== 'review-ready' && active.record.directReview && active.record.directReview.status !== 'terminal') {
|
|
1562
|
+
changes.directReview = projectTerminalDirectReview(active.record.directReview, outcome.status === 'blocked'
|
|
1563
|
+
? { status: 'blocked', kind: outcome.kind }
|
|
1564
|
+
: { status: outcome.status });
|
|
1565
|
+
}
|
|
1566
|
+
if (active.record.waitingHuman && (active.record.lifecycle === 'waiting-human' || active.record.waitingHuman.phase === 'resumed')) {
|
|
1567
|
+
if (active.record.waitingHuman.phase === 'resumed') {
|
|
1568
|
+
changes.waitingHuman = {
|
|
1569
|
+
version: 1,
|
|
1570
|
+
clarificationAttempts: active.record.waitingHuman.clarificationAttempts,
|
|
1571
|
+
permissionRetries: active.record.waitingHuman.permissionRetries,
|
|
1572
|
+
effectRetries: structuredClone(active.record.waitingHuman.effectRetries),
|
|
1573
|
+
history: structuredClone(active.record.waitingHuman.history),
|
|
1574
|
+
phase: 'history-only',
|
|
1575
|
+
terminalOutcome: outcome.status === 'blocked'
|
|
1576
|
+
? { status: 'blocked', kind: outcome.kind }
|
|
1577
|
+
: { status: outcome.status },
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
else if (outcome.status === 'blocked' || outcome.status === 'cancelled') {
|
|
1581
|
+
changes.waitingHuman = archiveWaiting(active.record, waitingAnswer(active.record.waitingHuman), {
|
|
1582
|
+
phase: 'history-only',
|
|
1583
|
+
terminalOutcome: outcome.status === 'cancelled'
|
|
1584
|
+
? { status: 'cancelled' }
|
|
1585
|
+
: { status: 'blocked', kind: outcome.kind },
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
if (active.record.lifecycle === 'triaging' || (active.record.lifecycle === 'safe-halt' && !active.record.routeReceipt)) {
|
|
1590
|
+
changes.routeExecution = undefined;
|
|
1591
|
+
changes.routeReceipt = undefined;
|
|
1592
|
+
}
|
|
795
1593
|
if (!retainIntent)
|
|
796
1594
|
changes.intent = undefined;
|
|
797
1595
|
await this.persist(active, changes);
|
|
@@ -835,6 +1633,17 @@ function snapshotIssue(issue) {
|
|
|
835
1633
|
url: issue.url,
|
|
836
1634
|
state: 'OPEN',
|
|
837
1635
|
labels: sortedUnique(issue.labels),
|
|
1636
|
+
comments: structuredClone(issue.comments),
|
|
1637
|
+
};
|
|
1638
|
+
}
|
|
1639
|
+
function publicIssueSnapshot(issue) {
|
|
1640
|
+
return {
|
|
1641
|
+
number: issue.number,
|
|
1642
|
+
title: issue.title,
|
|
1643
|
+
body: issue.body,
|
|
1644
|
+
url: issue.url,
|
|
1645
|
+
state: issue.state,
|
|
1646
|
+
labels: [...issue.labels],
|
|
838
1647
|
};
|
|
839
1648
|
}
|
|
840
1649
|
function freezeCriteria(issue) {
|
|
@@ -864,11 +1673,72 @@ function findRun(state, runId) {
|
|
|
864
1673
|
throw new Error('persisted run is missing');
|
|
865
1674
|
return record;
|
|
866
1675
|
}
|
|
1676
|
+
function waitingAnswer(waiting) {
|
|
1677
|
+
if ('answerReceipt' in waiting)
|
|
1678
|
+
return structuredClone(waiting.answerReceipt);
|
|
1679
|
+
if (waiting.phase === 'resumed')
|
|
1680
|
+
return structuredClone(waiting.trustedAnswer);
|
|
1681
|
+
return null;
|
|
1682
|
+
}
|
|
1683
|
+
function archiveWaiting(record, answer, terminal) {
|
|
1684
|
+
const waiting = record.waitingHuman;
|
|
1685
|
+
const routeReceipt = record.routeReceipt;
|
|
1686
|
+
if (!waiting || !routeReceipt)
|
|
1687
|
+
throw new Error('waiting archive requires active route evidence');
|
|
1688
|
+
const question = 'question' in waiting ? waiting.question : 'questionReceipt' in waiting ? waiting.questionReceipt.question : undefined;
|
|
1689
|
+
if (!question)
|
|
1690
|
+
throw new Error('waiting archive requires current question');
|
|
1691
|
+
const questionReceipt = 'questionReceipt' in waiting ? waiting.questionReceipt : null;
|
|
1692
|
+
const entry = {
|
|
1693
|
+
routeReceipt: structuredClone(routeReceipt),
|
|
1694
|
+
question: structuredClone(question),
|
|
1695
|
+
questionReceipt: questionReceipt ? structuredClone(questionReceipt) : null,
|
|
1696
|
+
answerReceipt: answer ? structuredClone(answer) : null,
|
|
1697
|
+
conflictHashes: [...question.conflictHashes],
|
|
1698
|
+
};
|
|
1699
|
+
const history = [...waiting.history];
|
|
1700
|
+
if (history.at(-1)?.question.questionSha256 === question.questionSha256) {
|
|
1701
|
+
if (canonicalJson(history.at(-1)) !== canonicalJson(entry))
|
|
1702
|
+
throw new Error('waiting archive evidence mismatch');
|
|
1703
|
+
}
|
|
1704
|
+
else {
|
|
1705
|
+
history.push(entry);
|
|
1706
|
+
}
|
|
1707
|
+
return {
|
|
1708
|
+
version: 1,
|
|
1709
|
+
clarificationAttempts: waiting.clarificationAttempts,
|
|
1710
|
+
permissionRetries: waiting.permissionRetries,
|
|
1711
|
+
effectRetries: structuredClone(waiting.effectRetries),
|
|
1712
|
+
history,
|
|
1713
|
+
...terminal,
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1716
|
+
function terminalWaiting(waiting, terminalOutcome) {
|
|
1717
|
+
if (waiting.phase !== 'resumed' && waiting.phase !== 'history-only')
|
|
1718
|
+
throw new Error('terminal waiting projection requires archived history');
|
|
1719
|
+
return {
|
|
1720
|
+
version: 1,
|
|
1721
|
+
clarificationAttempts: waiting.clarificationAttempts,
|
|
1722
|
+
permissionRetries: waiting.permissionRetries,
|
|
1723
|
+
effectRetries: structuredClone(waiting.effectRetries),
|
|
1724
|
+
history: structuredClone(waiting.history),
|
|
1725
|
+
phase: 'history-only',
|
|
1726
|
+
terminalOutcome,
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
867
1729
|
function publicOutcome(outcome) {
|
|
868
1730
|
if (outcome.status === 'internal-error')
|
|
869
1731
|
return { status: 'internal-error', evidencePath: outcome.evidencePath };
|
|
870
1732
|
return structuredClone(outcome);
|
|
871
1733
|
}
|
|
1734
|
+
function requireRouteExecution(value) {
|
|
1735
|
+
if (!value)
|
|
1736
|
+
throw new Error('route execution is missing');
|
|
1737
|
+
return structuredClone(value);
|
|
1738
|
+
}
|
|
1739
|
+
function sameRouteExecution(left, right) {
|
|
1740
|
+
return left !== undefined && canonicalJson(left) === canonicalJson(right);
|
|
1741
|
+
}
|
|
872
1742
|
function sameStrings(left, right) {
|
|
873
1743
|
const a = [...left].sort();
|
|
874
1744
|
const b = [...right].sort();
|