codex-orchestrator 2.0.1 → 2.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +12 -9
- package/dist/src/index.d.ts +10 -0
- 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 +63 -7
- 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/candidate-cli.d.ts +4 -0
- package/dist/src/v2/candidate-cli.d.ts.map +1 -1
- package/dist/src/v2/candidate-cli.js +26 -11
- package/dist/src/v2/candidate-cli.js.map +1 -1
- package/dist/src/v2/cli-contract.d.ts +1 -1
- package/dist/src/v2/cli-contract.d.ts.map +1 -1
- package/dist/src/v2/cli-contract.js +10 -0
- package/dist/src/v2/cli-contract.js.map +1 -1
- 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 +11 -0
- package/dist/src/v2/codex-process.js.map +1 -1
- package/dist/src/v2/config.d.ts +2 -1
- 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 +6 -0
- package/dist/src/v2/containment.d.ts.map +1 -1
- package/dist/src/v2/containment.js +40 -1
- package/dist/src/v2/containment.js.map +1 -1
- package/dist/src/v2/direct-delivery.d.ts +101 -0
- package/dist/src/v2/direct-delivery.d.ts.map +1 -0
- package/dist/src/v2/direct-delivery.js +547 -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/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 +63 -2
- package/dist/src/v2/run-issue.d.ts.map +1 -1
- package/dist/src/v2/run-issue.js +906 -91
- 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 +143 -3
- 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 +14 -6
- package/dist/src/v2/runtime.d.ts.map +1 -1
- package/dist/src/v2/runtime.js +478 -56
- 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 +1 -0
- 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 +20 -72
- package/dist/src/v2/setup-runtime.js.map +1 -1
- package/dist/src/v2/setup.d.ts +4 -1
- package/dist/src/v2/setup.d.ts.map +1 -1
- package/dist/src/v2/setup.js +104 -1
- 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 +90 -0
- package/dist/src/v2/workflow-assets.d.ts.map +1 -0
- package/dist/src/v2/workflow-assets.js +554 -0
- package/dist/src/v2/workflow-assets.js.map +1 -0
- package/docs/deep-dive.md +15 -8
- package/internal-workflow/docs/agents/artifact-review-loop.md +267 -0
- package/internal-workflow/docs/agents/bug-workflow-routing.md +24 -0
- package/internal-workflow/docs/agents/coding-skill-routing.md +203 -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/implementation-review-loop.md +302 -0
- package/internal-workflow/docs/agents/review-gates.md +49 -0
- package/internal-workflow/docs/agents/review-protocol.md +170 -0
- package/internal-workflow/docs/agents/tool-usage.md +88 -0
- package/internal-workflow/manifest.json +1 -0
- package/internal-workflow/operations/acceptance-proof/SKILL.md +3 -0
- package/internal-workflow/operations/ambiguity-review/SKILL.md +3 -0
- package/internal-workflow/operations/cleanup-review/SKILL.md +3 -0
- package/internal-workflow/operations/code-review/SKILL.md +3 -0
- package/internal-workflow/operations/implementation/SKILL.md +3 -0
- package/internal-workflow/operations/spec-author/SKILL.md +3 -0
- package/internal-workflow/operations/spec-implementation/SKILL.md +3 -0
- package/internal-workflow/operations/spec-review/SKILL.md +3 -0
- package/internal-workflow/operations/triage/SKILL.md +3 -0
- package/internal-workflow/profiles/analyst_deep.toml +9 -0
- package/internal-workflow/profiles/implementer_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/researcher_standard.toml +9 -0
- package/internal-workflow/profiles/reviewer_deep.toml +9 -0
- package/internal-workflow/profiles/reviewer_fast.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-workflow/skills/agent-auto/agents/openai.yaml +6 -0
- package/internal-workflow/skills/cleanup-review/SKILL.md +84 -0
- package/internal-workflow/skills/cleanup-review/agents/openai.yaml +6 -0
- package/internal-workflow/skills/code-review/SKILL.md +257 -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/framework-lenses.md +34 -0
- package/internal-workflow/skills/code-review/references/targeted-recipes.md +49 -0
- package/internal-workflow/skills/codebase-design/DEEPENING.md +35 -0
- package/internal-workflow/skills/codebase-design/DESIGN-IT-TWICE.md +50 -0
- package/internal-workflow/skills/codebase-design/SKILL.md +82 -0
- package/internal-workflow/skills/codebase-design/agents/openai.yaml +6 -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 +93 -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 +211 -0
- package/internal-workflow/skills/implementation-spec-review/agents/openai.yaml +6 -0
- package/internal-workflow/skills/research/SKILL.md +107 -0
- package/internal-workflow/skills/research/agents/openai.yaml +6 -0
- package/internal-workflow/skills/small-task-implementer/SKILL.md +97 -0
- package/internal-workflow/skills/small-task-implementer/agents/openai.yaml +6 -0
- package/internal-workflow/skills/spec-implementer/SKILL.md +197 -0
- package/internal-workflow/skills/spec-implementer/agents/openai.yaml +6 -0
- package/internal-workflow/skills/tdd/SKILL.md +59 -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/internal-workflow/skills/ui-evidence-proof/SKILL.md +123 -0
- package/internal-workflow/skills/ui-evidence-proof/agents/openai.yaml +6 -0
- package/package.json +6 -3
- /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/{internal-skills → internal-workflow/skills}/agent-auto/SKILL.md +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, createLegacyBypassDirectReview, createInitialDirectReview, directReviewClosureRequestSha256, directReviewTargetFingerprint, launchDirectReviewInvocation, prepareDirectReviewClosure, prepareDirectReviewInvocation, projectTerminalDirectReview, } from './direct-delivery.js';
|
|
6
7
|
import { ProofQuiescenceError } from './acceptance-proof.js';
|
|
8
|
+
import { RouteMigrationUnrecoverableError, 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,57 @@ 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.migratePreRouteRun(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
|
+
if (!process.purpose || !process.resumeLifecycle || process.resumeReviewStage === undefined) {
|
|
381
|
+
return await this.terminal(active, { status: 'internal-error', code: 'direct-review-migration-unrecoverable' });
|
|
382
|
+
}
|
|
383
|
+
active = await this.persist(active, { lifecycle: process.resumeLifecycle, process: undefined });
|
|
358
384
|
}
|
|
359
|
-
if (existing.
|
|
360
|
-
|
|
385
|
+
if (existing.lifecycle === 'waiting-human') {
|
|
386
|
+
const waiting = await this.continueWaitingHuman(active);
|
|
387
|
+
if ('result' in waiting)
|
|
388
|
+
return waiting.result;
|
|
389
|
+
active = waiting.active;
|
|
390
|
+
issueSnapshot = structuredClone(active.record.issueSnapshot);
|
|
391
|
+
frozenCriteria = structuredClone(active.record.frozenCriteria);
|
|
392
|
+
}
|
|
393
|
+
if (existing.lifecycle === 'spec-authoring') {
|
|
394
|
+
if (!await this.authorized(input.issueNumber, runId, branchName, config))
|
|
395
|
+
return await this.revoked(active);
|
|
396
|
+
return await this.continueSpecRequired(active);
|
|
397
|
+
}
|
|
398
|
+
if (!['triaging', 'routed', 'implementing', 'reworking', 'checking', 'proving'].includes(active.record.lifecycle)) {
|
|
399
|
+
return await this.terminal(active, { status: 'internal-error', code: 'resume-phase-not-reconciled' });
|
|
361
400
|
}
|
|
362
401
|
if (!await this.authorized(input.issueNumber, runId, branchName, config))
|
|
363
402
|
return await this.revoked(active);
|
|
364
|
-
|
|
403
|
+
if (active.record.lifecycle !== 'triaging' && active.record.lifecycle !== 'routed') {
|
|
404
|
+
if (!active.record.routeExecution || !active.record.routeReceipt)
|
|
405
|
+
throw new RouteMigrationUnrecoverableError();
|
|
406
|
+
const reviewRecovery = active.record.lifecycle === 'implementing' && active.record.directReview?.status === 'active'
|
|
407
|
+
&& (active.record.directReview.stage === 'review-full' || active.record.directReview.stage === 'review-closure');
|
|
408
|
+
const checkRecovery = active.record.lifecycle === 'checking'
|
|
409
|
+
&& (active.record.directReview?.status === 'clear' || active.record.directReview?.status === 'legacy-bypass');
|
|
410
|
+
if (!reviewRecovery && !checkRecovery) {
|
|
411
|
+
if (active.record.cycle >= config.runner.maxCycles) {
|
|
412
|
+
return await this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true });
|
|
413
|
+
}
|
|
414
|
+
active = await this.startNextCycle(active, [`Recovered interrupted ${active.record.lifecycle} phase.`]);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
365
417
|
}
|
|
366
418
|
}
|
|
367
419
|
else {
|
|
@@ -394,7 +446,29 @@ export class RunIssue {
|
|
|
394
446
|
catch {
|
|
395
447
|
return await this.terminal(active, { status: 'internal-error', code: 'local-git-effect-failed' });
|
|
396
448
|
}
|
|
397
|
-
active = await this.
|
|
449
|
+
active = await this.migratePreRouteRun(active, issue);
|
|
450
|
+
issueSnapshot = structuredClone(active.record.issueSnapshot);
|
|
451
|
+
}
|
|
452
|
+
if (active.record.lifecycle === 'triaging' || active.record.lifecycle === 'routed') {
|
|
453
|
+
const routed = await this.routeRun(active, issueSnapshot, frozenCriteria, worktreePath, config, input.issueNumber, branchName);
|
|
454
|
+
if ('result' in routed)
|
|
455
|
+
return routed.result;
|
|
456
|
+
active = routed.active;
|
|
457
|
+
}
|
|
458
|
+
if (active.record.lifecycle !== 'implementing' && active.record.lifecycle !== 'checking') {
|
|
459
|
+
return await this.terminal(active, { status: 'internal-error', code: 'route-dispatch-not-implementing' });
|
|
460
|
+
}
|
|
461
|
+
if (active.record.lifecycle === 'checking' && !active.record.directReview && active.record.routeReceipt?.route === 'direct') {
|
|
462
|
+
const changedFiles = await this.dependencies.git.listChangedFiles(worktreePath);
|
|
463
|
+
const targetFingerprint = directReviewTargetFingerprint({
|
|
464
|
+
snapshot: await this.dependencies.git.snapshot(worktreePath),
|
|
465
|
+
changedFiles,
|
|
466
|
+
routeDecisionSha256: active.record.routeReceipt.decisionSha256,
|
|
467
|
+
workflowGenerationHash: active.record.workflowGeneration.generationHash,
|
|
468
|
+
cycle: active.record.cycle,
|
|
469
|
+
frozenCriteria,
|
|
470
|
+
});
|
|
471
|
+
active = await this.persist(active, { directReview: createLegacyBypassDirectReview(targetFingerprint) });
|
|
398
472
|
}
|
|
399
473
|
attemptLoop: while (true) {
|
|
400
474
|
if (!await this.authorized(input.issueNumber, runId, branchName, config)) {
|
|
@@ -402,64 +476,35 @@ export class RunIssue {
|
|
|
402
476
|
}
|
|
403
477
|
if (this.signal.aborted)
|
|
404
478
|
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);
|
|
479
|
+
let resumeAtChecks = active.record.lifecycle === 'checking'
|
|
480
|
+
&& (active.record.directReview?.status === 'clear' || active.record.directReview?.status === 'legacy-bypass');
|
|
481
|
+
if (active.record.lifecycle === 'implementing'
|
|
482
|
+
&& active.record.directReview?.status === 'active'
|
|
483
|
+
&& (active.record.directReview.stage === 'review-full' || active.record.directReview.stage === 'review-closure')) {
|
|
484
|
+
const recovered = await this.recoverDirectReviewInvocation(active);
|
|
485
|
+
if ('status' in recovered)
|
|
486
|
+
return recovered;
|
|
487
|
+
active = recovered;
|
|
488
|
+
const reviewed = await this.runDirectReviewFull(active, publicIssueSnapshot(issueSnapshot), frozenCriteria, `recovered-implementation-cycle-${active.record.cycle}`, config.runner.maxCycles);
|
|
489
|
+
if ('status' in reviewed)
|
|
490
|
+
return reviewed;
|
|
491
|
+
active = reviewed;
|
|
492
|
+
if (active.record.lifecycle === 'implementing')
|
|
493
|
+
continue attemptLoop;
|
|
494
|
+
resumeAtChecks = true;
|
|
448
495
|
}
|
|
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({
|
|
496
|
+
if (!resumeAtChecks) {
|
|
497
|
+
const implementationBaseline = await this.dependencies.git.snapshot(worktreePath);
|
|
498
|
+
const deniedPathsBaseline = await this.dependencies.git.fingerprintDeniedPaths(worktreePath, config.deny.readPaths);
|
|
499
|
+
let implementation = await this.runImplementation({
|
|
456
500
|
runId,
|
|
457
501
|
worktreePath,
|
|
458
|
-
issue: issueSnapshot,
|
|
502
|
+
issue: publicIssueSnapshot(issueSnapshot),
|
|
459
503
|
frozenCriteria,
|
|
460
504
|
cycle: active.record.cycle,
|
|
461
|
-
reworkFindings:
|
|
462
|
-
repairOnly:
|
|
505
|
+
reworkFindings: active.record.reworkFindings,
|
|
506
|
+
repairOnly: false,
|
|
507
|
+
workflowGeneration: active.record.workflowGeneration,
|
|
463
508
|
});
|
|
464
509
|
if (implementation.kind === 'safe-halt') {
|
|
465
510
|
active = await this.persist(active, { lifecycle: 'safe-halt', process: implementation.process });
|
|
@@ -472,33 +517,121 @@ export class RunIssue {
|
|
|
472
517
|
await new Promise((resolveWait) => setTimeout(resolveWait, 25));
|
|
473
518
|
}
|
|
474
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
|
+
}
|
|
475
523
|
return await this.terminal(active, { status: 'transport-failed', resumable: false }, 'process-quiescence-delayed');
|
|
476
524
|
}
|
|
525
|
+
if (await this.dependencies.git.fingerprintDeniedPaths(worktreePath, config.deny.readPaths) !== deniedPathsBaseline) {
|
|
526
|
+
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'denied-path-modified');
|
|
527
|
+
}
|
|
528
|
+
if (implementation.kind === 'transport-failed' && implementation.resumable && active.record.transportRetries === 0) {
|
|
529
|
+
const afterTransport = await this.dependencies.git.snapshot(worktreePath);
|
|
530
|
+
if (!sameFreshness(implementationBaseline, afterTransport)) {
|
|
531
|
+
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'transport-baseline-changed');
|
|
532
|
+
}
|
|
533
|
+
active = await this.persist(active, { transportRetries: 1 });
|
|
534
|
+
continue attemptLoop;
|
|
535
|
+
}
|
|
477
536
|
if (implementation.kind !== 'completed')
|
|
478
537
|
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
|
-
}
|
|
538
|
+
let report;
|
|
483
539
|
try {
|
|
484
540
|
report = validateImplementationReport(implementation.report);
|
|
485
541
|
}
|
|
486
542
|
catch {
|
|
487
|
-
|
|
543
|
+
if (active.record.reportRepairs >= 1) {
|
|
544
|
+
return await this.terminal(active, { status: 'internal-error', code: 'implementation-report-malformed' });
|
|
545
|
+
}
|
|
546
|
+
const repairBaseline = await this.dependencies.git.snapshot(worktreePath);
|
|
547
|
+
active = await this.persist(active, { reportRepairs: 1 });
|
|
548
|
+
implementation = await this.runImplementation({
|
|
549
|
+
runId,
|
|
550
|
+
worktreePath,
|
|
551
|
+
issue: publicIssueSnapshot(issueSnapshot),
|
|
552
|
+
frozenCriteria,
|
|
553
|
+
cycle: active.record.cycle,
|
|
554
|
+
reworkFindings: ['The previous implementation report did not match the generated schema.'],
|
|
555
|
+
repairOnly: true,
|
|
556
|
+
workflowGeneration: active.record.workflowGeneration,
|
|
557
|
+
});
|
|
558
|
+
if (implementation.kind === 'safe-halt') {
|
|
559
|
+
active = await this.persist(active, { lifecycle: 'safe-halt', process: implementation.process });
|
|
560
|
+
while (true) {
|
|
561
|
+
try {
|
|
562
|
+
await implementation.waitForAbsence();
|
|
563
|
+
break;
|
|
564
|
+
}
|
|
565
|
+
catch {
|
|
566
|
+
await new Promise((resolveWait) => setTimeout(resolveWait, 25));
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return await this.terminal(active, { status: 'transport-failed', resumable: false }, 'process-quiescence-delayed');
|
|
570
|
+
}
|
|
571
|
+
if (implementation.kind !== 'completed')
|
|
572
|
+
return await this.mapImplementationFailure(active, implementation);
|
|
573
|
+
const afterRepair = await this.dependencies.git.snapshot(worktreePath);
|
|
574
|
+
if (!sameFreshness(repairBaseline, afterRepair)) {
|
|
575
|
+
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'report-repair-modified-worktree');
|
|
576
|
+
}
|
|
577
|
+
try {
|
|
578
|
+
report = validateImplementationReport(implementation.report);
|
|
579
|
+
}
|
|
580
|
+
catch {
|
|
581
|
+
return await this.terminal(active, { status: 'internal-error', code: 'implementation-report-malformed' });
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
if (report.status === 'external-block') {
|
|
585
|
+
return await this.terminal(active, { status: 'blocked', kind: 'external', resumable: true });
|
|
586
|
+
}
|
|
587
|
+
if (await this.dependencies.git.getHead(worktreePath) !== baseSha) {
|
|
588
|
+
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true });
|
|
589
|
+
}
|
|
590
|
+
const changedFiles = await this.dependencies.git.listChangedFiles(worktreePath);
|
|
591
|
+
if (changedFiles.length === 0 || !sameStrings(changedFiles, report.changedFiles)) {
|
|
592
|
+
return await this.terminal(active, { status: 'internal-error', code: 'implementation-change-set-invalid' });
|
|
593
|
+
}
|
|
594
|
+
if (active.record.routeReceipt?.route === 'direct') {
|
|
595
|
+
const targetFingerprint = directReviewTargetFingerprint({
|
|
596
|
+
snapshot: await this.dependencies.git.snapshot(worktreePath),
|
|
597
|
+
changedFiles,
|
|
598
|
+
routeDecisionSha256: active.record.routeReceipt.decisionSha256,
|
|
599
|
+
workflowGenerationHash: active.record.workflowGeneration.generationHash,
|
|
600
|
+
cycle: active.record.cycle,
|
|
601
|
+
frozenCriteria,
|
|
602
|
+
});
|
|
603
|
+
if (active.record.directReview?.stage === 'review-repair') {
|
|
604
|
+
active = await this.persist(active, {
|
|
605
|
+
directReview: prepareDirectReviewClosure(active.record.directReview, targetFingerprint).state,
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
else {
|
|
609
|
+
const reviewerSessionId = this.dependencies.createReviewSessionId();
|
|
610
|
+
assertNonEmptyString(reviewerSessionId, 'reviewerSessionId');
|
|
611
|
+
active = await this.persist(active, {
|
|
612
|
+
directReview: createInitialDirectReview({
|
|
613
|
+
targetFingerprint,
|
|
614
|
+
cleanupRequired: false,
|
|
615
|
+
cleanupReason: 'bounded final review owns cleanup',
|
|
616
|
+
cleanupReviewerSessionId: null,
|
|
617
|
+
codeReviewerSessionId: reviewerSessionId,
|
|
618
|
+
}),
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
const reviewed = await this.runDirectReviewFull(active, publicIssueSnapshot(issueSnapshot), frozenCriteria, implementation.attemptId ?? `implementation-cycle-${active.record.cycle}`, config.runner.maxCycles);
|
|
622
|
+
if ('status' in reviewed)
|
|
623
|
+
return reviewed;
|
|
624
|
+
active = reviewed;
|
|
625
|
+
if (active.record.lifecycle === 'implementing')
|
|
626
|
+
continue attemptLoop;
|
|
627
|
+
}
|
|
628
|
+
else {
|
|
629
|
+
active = await this.persist(active, { lifecycle: 'checking' });
|
|
488
630
|
}
|
|
489
631
|
}
|
|
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
632
|
for (const [id, command] of Object.entries(config.checks)) {
|
|
633
|
+
if (active.record.checks.some((check) => check.id === id && check.status === 'passed'))
|
|
634
|
+
continue;
|
|
502
635
|
if (this.signal.aborted)
|
|
503
636
|
return await this.terminal(active, { status: 'cancelled' });
|
|
504
637
|
let check;
|
|
@@ -516,12 +649,38 @@ export class RunIssue {
|
|
|
516
649
|
if (active.record.cycle >= config.runner.maxCycles) {
|
|
517
650
|
return await this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true });
|
|
518
651
|
}
|
|
519
|
-
|
|
652
|
+
const summary = `Check ${id} failed:\n${check.output.toString('utf8').slice(0, 8 * 1024)}`;
|
|
653
|
+
active = await this.startNextCycle(active, [summary], [{
|
|
654
|
+
provenance: 'check', sourceId: `check:${id}:${row.outputSha256}`, summary,
|
|
655
|
+
affectedContracts: ['configured-checks'],
|
|
656
|
+
}]);
|
|
520
657
|
continue attemptLoop;
|
|
521
658
|
}
|
|
522
659
|
}
|
|
660
|
+
let reviewedStageBinding;
|
|
523
661
|
try {
|
|
662
|
+
const reviewedFiles = await this.dependencies.git.listChangedFiles(worktreePath);
|
|
663
|
+
const reviewedContentSha256 = await this.dependencies.git.fingerprintChangedFiles(worktreePath, reviewedFiles);
|
|
664
|
+
reviewedStageBinding = { files: reviewedFiles, contentSha256: reviewedContentSha256 };
|
|
665
|
+
if (active.record.directReview?.status === 'clear') {
|
|
666
|
+
const currentFingerprint = directReviewTargetFingerprint({
|
|
667
|
+
snapshot: await this.dependencies.git.snapshot(worktreePath),
|
|
668
|
+
changedFiles: await this.dependencies.git.listChangedFiles(worktreePath),
|
|
669
|
+
routeDecisionSha256: active.record.routeReceipt.decisionSha256,
|
|
670
|
+
workflowGenerationHash: active.record.workflowGeneration.generationHash,
|
|
671
|
+
cycle: active.record.cycle,
|
|
672
|
+
frozenCriteria,
|
|
673
|
+
});
|
|
674
|
+
if (currentFingerprint !== active.record.directReview.targetFingerprint) {
|
|
675
|
+
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'direct-review-target-drift');
|
|
676
|
+
}
|
|
677
|
+
}
|
|
524
678
|
await this.dependencies.git.stageAll(worktreePath);
|
|
679
|
+
const stagedFiles = await this.dependencies.git.listChangedFiles(worktreePath);
|
|
680
|
+
const stagedContentSha256 = await this.dependencies.git.fingerprintChangedFiles(worktreePath, stagedFiles);
|
|
681
|
+
if (!sameStrings(reviewedFiles, stagedFiles) || reviewedContentSha256 !== stagedContentSha256) {
|
|
682
|
+
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'post-stage-review-binding-drift');
|
|
683
|
+
}
|
|
525
684
|
}
|
|
526
685
|
catch {
|
|
527
686
|
return await this.terminal(active, { status: 'internal-error', code: 'local-git-effect-failed' });
|
|
@@ -531,6 +690,11 @@ export class RunIssue {
|
|
|
531
690
|
}
|
|
532
691
|
const proofChangedFiles = await this.dependencies.git.listChangedFiles(worktreePath);
|
|
533
692
|
const freshness = await this.dependencies.git.snapshot(worktreePath);
|
|
693
|
+
const proofContentSha256 = await this.dependencies.git.fingerprintChangedFiles(worktreePath, proofChangedFiles);
|
|
694
|
+
if (!reviewedStageBinding || !sameStrings(reviewedStageBinding.files, proofChangedFiles)
|
|
695
|
+
|| reviewedStageBinding.contentSha256 !== proofContentSha256) {
|
|
696
|
+
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'checked-change-review-binding-drift');
|
|
697
|
+
}
|
|
534
698
|
const payload = {
|
|
535
699
|
version: 1,
|
|
536
700
|
canonicalRepository,
|
|
@@ -546,7 +710,7 @@ export class RunIssue {
|
|
|
546
710
|
changedFiles: proofChangedFiles,
|
|
547
711
|
checks: active.record.checks.map((check) => ({ ...check, status: 'passed' })),
|
|
548
712
|
checkPolicySha256: sha256(canonicalJson(config.checks)),
|
|
549
|
-
packageVersion:
|
|
713
|
+
packageVersion: active.record.packageVersion,
|
|
550
714
|
proofSchemaVersion: 1,
|
|
551
715
|
};
|
|
552
716
|
const checkedChange = this.dependencies.checkedChangeMint.mint(payload);
|
|
@@ -556,7 +720,13 @@ export class RunIssue {
|
|
|
556
720
|
active = await this.persist(active, { lifecycle: 'proving', checkedChangeSha256, proofId });
|
|
557
721
|
let proof;
|
|
558
722
|
try {
|
|
559
|
-
proof = await this.dependencies.proof.proveChange({
|
|
723
|
+
proof = await this.dependencies.proof.proveChange({
|
|
724
|
+
proofId,
|
|
725
|
+
issue: publicIssueSnapshot(issueSnapshot),
|
|
726
|
+
frozenCriteria,
|
|
727
|
+
checkedChange,
|
|
728
|
+
workflowGeneration: structuredClone(active.record.workflowGeneration),
|
|
729
|
+
});
|
|
560
730
|
}
|
|
561
731
|
catch (error) {
|
|
562
732
|
if (error instanceof ProofQuiescenceError) {
|
|
@@ -573,6 +743,9 @@ export class RunIssue {
|
|
|
573
743
|
untrackedContentSha256: freshness.untrackedContentSha256,
|
|
574
744
|
worktreeIdentity: freshness.worktreeIdentity,
|
|
575
745
|
},
|
|
746
|
+
purpose: 'proof',
|
|
747
|
+
resumeLifecycle: 'proving',
|
|
748
|
+
resumeReviewStage: null,
|
|
576
749
|
},
|
|
577
750
|
});
|
|
578
751
|
while (true) {
|
|
@@ -594,7 +767,12 @@ export class RunIssue {
|
|
|
594
767
|
if (active.record.cycle >= config.runner.maxCycles) {
|
|
595
768
|
return await this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true });
|
|
596
769
|
}
|
|
597
|
-
active = await this.startNextCycle(active, proof.findings)
|
|
770
|
+
active = await this.startNextCycle(active, proof.findings, proof.findings.map((summary) => ({
|
|
771
|
+
provenance: 'proof',
|
|
772
|
+
sourceId: `proof:${proofId}:${sha256(summary)}`,
|
|
773
|
+
summary,
|
|
774
|
+
affectedContracts: ['acceptance-proof'],
|
|
775
|
+
})));
|
|
598
776
|
continue attemptLoop;
|
|
599
777
|
}
|
|
600
778
|
if (proof.status !== 'passed')
|
|
@@ -608,12 +786,23 @@ export class RunIssue {
|
|
|
608
786
|
if (!active && error instanceof TransportReadError) {
|
|
609
787
|
return await this.preClaimTransport(input.issueNumber);
|
|
610
788
|
}
|
|
789
|
+
if (!active && error instanceof WorkflowGenerationUnrecoverableError) {
|
|
790
|
+
return await this.preClaimInternal('workflow-generation-unrecoverable', input.issueNumber);
|
|
791
|
+
}
|
|
611
792
|
if (active && error instanceof PostEffectStateError) {
|
|
612
793
|
return await this.invokedFailure(error.active, 'post-effect-state-write-failed');
|
|
613
794
|
}
|
|
614
795
|
if (active && error instanceof TransportReadError) {
|
|
615
796
|
return await this.invokedFailure(active, 'authorization-read-failed');
|
|
616
797
|
}
|
|
798
|
+
if (active && error instanceof RouteMigrationUnrecoverableError) {
|
|
799
|
+
const evidence = await this.dependencies.writeEvidence({
|
|
800
|
+
runId: active.record.runId,
|
|
801
|
+
code: 'route-migration-unrecoverable',
|
|
802
|
+
summary: 'The pre-route run cannot be safely migrated without product-state ambiguity.',
|
|
803
|
+
});
|
|
804
|
+
return { status: 'blocked', kind: 'safety', resumable: false, evidencePath: evidence.path };
|
|
805
|
+
}
|
|
617
806
|
if (active) {
|
|
618
807
|
try {
|
|
619
808
|
return await this.terminal(active, { status: 'internal-error', code: 'state-write-failed' });
|
|
@@ -641,6 +830,312 @@ export class RunIssue {
|
|
|
641
830
|
}
|
|
642
831
|
}
|
|
643
832
|
}
|
|
833
|
+
async migratePreRouteRun(active, issue) {
|
|
834
|
+
if (active.record.lifecycle !== 'claimed'
|
|
835
|
+
|| active.record.process
|
|
836
|
+
|| active.record.intent
|
|
837
|
+
|| active.record.routeExecution
|
|
838
|
+
|| active.record.routeReceipt) {
|
|
839
|
+
throw new RouteMigrationUnrecoverableError();
|
|
840
|
+
}
|
|
841
|
+
const [snapshot, changedFiles] = await Promise.all([
|
|
842
|
+
this.dependencies.git.snapshot(active.record.worktreePath),
|
|
843
|
+
this.dependencies.git.listChangedFiles(active.record.worktreePath),
|
|
844
|
+
]);
|
|
845
|
+
if (snapshot.headSha !== active.record.baseSha || changedFiles.length !== 0) {
|
|
846
|
+
throw new RouteMigrationUnrecoverableError();
|
|
847
|
+
}
|
|
848
|
+
try {
|
|
849
|
+
await this.dependencies.verifyWorkflowGeneration(active.record.workflowGeneration);
|
|
850
|
+
}
|
|
851
|
+
catch {
|
|
852
|
+
throw new RouteMigrationUnrecoverableError();
|
|
853
|
+
}
|
|
854
|
+
const issueSnapshot = issue?.state === 'OPEN' ? snapshotIssue(issue) : active.record.issueSnapshot;
|
|
855
|
+
return this.persist(active, { lifecycle: 'triaging', routeExecution: initialRouteExecution(), issueSnapshot });
|
|
856
|
+
}
|
|
857
|
+
async routeRun(starting, issue, frozenCriteria, worktreePath, config, issueNumber, branchName) {
|
|
858
|
+
let active = starting;
|
|
859
|
+
while (active.record.lifecycle === 'triaging') {
|
|
860
|
+
const state = {
|
|
861
|
+
read: async () => requireRouteExecution(active.record.routeExecution),
|
|
862
|
+
compareAndSwap: async (expected, next) => {
|
|
863
|
+
if (!sameRouteExecution(active.record.routeExecution, expected))
|
|
864
|
+
return false;
|
|
865
|
+
active = await this.persist(active, { routeExecution: structuredClone(next) });
|
|
866
|
+
return true;
|
|
867
|
+
},
|
|
868
|
+
complete: async (expected, next, receipt) => {
|
|
869
|
+
if (!sameRouteExecution(active.record.routeExecution, expected))
|
|
870
|
+
return false;
|
|
871
|
+
active = await this.persist(active, {
|
|
872
|
+
lifecycle: 'routed',
|
|
873
|
+
routeExecution: structuredClone(next),
|
|
874
|
+
routeReceipt: structuredClone(receipt),
|
|
875
|
+
});
|
|
876
|
+
return true;
|
|
877
|
+
},
|
|
878
|
+
cancel: async (expected) => {
|
|
879
|
+
if (!sameRouteExecution(active.record.routeExecution, expected))
|
|
880
|
+
return false;
|
|
881
|
+
const evidence = await this.dependencies.writeEvidence({
|
|
882
|
+
runId: active.record.runId,
|
|
883
|
+
code: 'cancelled',
|
|
884
|
+
summary: 'Routing was cancelled.',
|
|
885
|
+
});
|
|
886
|
+
active = await this.persist(active, {
|
|
887
|
+
lifecycle: 'cancelled',
|
|
888
|
+
routeExecution: undefined,
|
|
889
|
+
routeReceipt: undefined,
|
|
890
|
+
outcomeEvidenceId: evidence.id,
|
|
891
|
+
terminalOutcome: { status: 'cancelled', evidencePath: evidence.path },
|
|
892
|
+
...(active.record.waitingHuman ? { waitingHuman: terminalWaiting(active.record.waitingHuman, { status: 'cancelled' }) } : {}),
|
|
893
|
+
});
|
|
894
|
+
return true;
|
|
895
|
+
},
|
|
896
|
+
};
|
|
897
|
+
const result = await this.dependencies.routeCoordinator.run({
|
|
898
|
+
state,
|
|
899
|
+
runId: active.record.runId,
|
|
900
|
+
worktreePath,
|
|
901
|
+
workflowGeneration: structuredClone(active.record.workflowGeneration),
|
|
902
|
+
promptFacts: [
|
|
903
|
+
`issue=${canonicalJson(issue)}`,
|
|
904
|
+
`frozenCriteria=${canonicalJson(frozenCriteria)}`,
|
|
905
|
+
`canonicalRepository=${active.record.canonicalRepository}`,
|
|
906
|
+
`baseSha=${active.record.baseSha}`,
|
|
907
|
+
...(active.record.waitingHuman?.phase === 'resumed' ? [
|
|
908
|
+
`trustedAnswer=${canonicalJson(active.record.waitingHuman.trustedAnswer)}`,
|
|
909
|
+
`priorWaitingRoute=${active.record.waitingHuman.history.at(-1)?.routeReceipt.decisionSha256 ?? ''}`,
|
|
910
|
+
] : []),
|
|
911
|
+
],
|
|
912
|
+
signal: this.signal,
|
|
913
|
+
});
|
|
914
|
+
if (result.status === 'repairable' || result.status === 'retryable')
|
|
915
|
+
continue;
|
|
916
|
+
if (result.status === 'safe-halt') {
|
|
917
|
+
active = await this.persist(active, { lifecycle: 'safe-halt', process: result.process });
|
|
918
|
+
while (true) {
|
|
919
|
+
try {
|
|
920
|
+
await result.waitForAbsence();
|
|
921
|
+
break;
|
|
922
|
+
}
|
|
923
|
+
catch {
|
|
924
|
+
await new Promise((resolveWait) => setTimeout(resolveWait, 25));
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
const after = await this.dependencies.git.snapshot(worktreePath);
|
|
928
|
+
if (!sameFreshness(result.process.baseline, after)) {
|
|
929
|
+
return { result: await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: false }, 'report-operation-worktree-mutated') };
|
|
930
|
+
}
|
|
931
|
+
return { result: await this.terminal(active, { status: 'transport-failed', resumable: false }, 'process-quiescence-delayed') };
|
|
932
|
+
}
|
|
933
|
+
if (result.status === 'cancelled') {
|
|
934
|
+
if (!active.record.terminalOutcome)
|
|
935
|
+
throw new Error('route cancellation was not persisted');
|
|
936
|
+
return { result: publicOutcome(active.record.terminalOutcome) };
|
|
937
|
+
}
|
|
938
|
+
if (result.status === 'blocked') {
|
|
939
|
+
return { result: await this.terminal(active, {
|
|
940
|
+
status: 'blocked',
|
|
941
|
+
kind: result.kind,
|
|
942
|
+
resumable: result.kind !== 'exhausted',
|
|
943
|
+
}, result.code) };
|
|
944
|
+
}
|
|
945
|
+
if (active.record.lifecycle !== 'routed' || !active.record.routeReceipt) {
|
|
946
|
+
return { result: await this.terminal(active, { status: 'internal-error', code: 'route-completion-not-persisted' }) };
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
if (active.record.lifecycle !== 'routed' || !active.record.routeReceipt) {
|
|
950
|
+
return { result: await this.terminal(active, { status: 'internal-error', code: 'route-state-not-dispatchable' }) };
|
|
951
|
+
}
|
|
952
|
+
try {
|
|
953
|
+
if (!await this.authorized(issueNumber, active.record.runId, branchName, config)) {
|
|
954
|
+
return { result: await this.revoked(active) };
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
catch (error) {
|
|
958
|
+
if (error instanceof TransportReadError)
|
|
959
|
+
return { result: await this.invokedFailure(active, 'authorization-read-failed') };
|
|
960
|
+
throw error;
|
|
961
|
+
}
|
|
962
|
+
const receipt = structuredClone(active.record.routeReceipt);
|
|
963
|
+
const lifecycle = downstreamLifecycleForRoute(receipt, active.record.workflowGeneration.generationHash);
|
|
964
|
+
validateRouteTransition({
|
|
965
|
+
lifecycle: 'routed',
|
|
966
|
+
routeExecution: active.record.routeExecution,
|
|
967
|
+
routeReceipt: receipt,
|
|
968
|
+
generationHash: active.record.workflowGeneration.generationHash,
|
|
969
|
+
}, {
|
|
970
|
+
lifecycle,
|
|
971
|
+
routeExecution: active.record.routeExecution,
|
|
972
|
+
routeReceipt: receipt,
|
|
973
|
+
generationHash: active.record.workflowGeneration.generationHash,
|
|
974
|
+
});
|
|
975
|
+
if (receipt.route !== 'awaiting-user')
|
|
976
|
+
active = await this.persist(active, { lifecycle });
|
|
977
|
+
try {
|
|
978
|
+
if (!await this.authorized(issueNumber, active.record.runId, branchName, config)) {
|
|
979
|
+
return { result: await this.revoked(active) };
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
catch (error) {
|
|
983
|
+
if (error instanceof TransportReadError)
|
|
984
|
+
return { result: await this.invokedFailure(active, 'authorization-read-failed') };
|
|
985
|
+
throw error;
|
|
986
|
+
}
|
|
987
|
+
const context = {
|
|
988
|
+
runId: active.record.runId,
|
|
989
|
+
issue: structuredClone(active.record.issueSnapshot),
|
|
990
|
+
frozenCriteria: structuredClone(active.record.frozenCriteria),
|
|
991
|
+
worktreePath,
|
|
992
|
+
workflowGeneration: structuredClone(active.record.workflowGeneration),
|
|
993
|
+
receipt,
|
|
994
|
+
};
|
|
995
|
+
if (receipt.route === 'awaiting-user') {
|
|
996
|
+
const waiting = await this.dependencies.routeContinuations.awaitingUser(context, this.waitingState(() => active, (next) => { active = next; }), this.signal);
|
|
997
|
+
const mapped = await this.mapWaitingResult(active, waiting);
|
|
998
|
+
if ('result' in mapped)
|
|
999
|
+
return mapped;
|
|
1000
|
+
return this.routeRun(mapped.active, mapped.active.record.issueSnapshot, mapped.active.record.frozenCriteria, worktreePath, config, issueNumber, branchName);
|
|
1001
|
+
}
|
|
1002
|
+
const continuation = receipt.route === 'direct'
|
|
1003
|
+
? await this.dependencies.routeContinuations.direct(context)
|
|
1004
|
+
: await this.dependencies.routeContinuations.specRequired(context, this.specState(() => active, (next) => { active = next; }), this.signal);
|
|
1005
|
+
if (continuation.status === 'cancelled')
|
|
1006
|
+
return { result: await this.terminal(active, { status: 'cancelled' }) };
|
|
1007
|
+
if (continuation.status === 'blocked') {
|
|
1008
|
+
return { result: await this.terminal(active, {
|
|
1009
|
+
status: 'blocked', kind: continuation.kind, resumable: continuation.kind !== 'exhausted',
|
|
1010
|
+
}, continuation.code) };
|
|
1011
|
+
}
|
|
1012
|
+
if (continuation.status === 'retryable') {
|
|
1013
|
+
return { result: await this.terminal(active, { status: 'transport-failed', resumable: true }, continuation.code) };
|
|
1014
|
+
}
|
|
1015
|
+
if (receipt.route !== 'direct') {
|
|
1016
|
+
const specContinuation = continuation;
|
|
1017
|
+
if (specContinuation.status !== 'completed')
|
|
1018
|
+
return { result: await this.terminal(active, { status: 'internal-error', code: 'spec-freeze-receipt-missing' }) };
|
|
1019
|
+
const evidence = await this.dependencies.writeEvidence({ runId: active.record.runId, code: 'spec-frozen', summary: specContinuation.receipt.receiptSha256 });
|
|
1020
|
+
return { result: { status: 'spec-frozen', receipt: specContinuation.receipt, evidencePath: evidence.path } };
|
|
1021
|
+
}
|
|
1022
|
+
return { active };
|
|
1023
|
+
}
|
|
1024
|
+
waitingState(readActive, writeActive) {
|
|
1025
|
+
return {
|
|
1026
|
+
read: async () => structuredClone(readActive().record.waitingHuman),
|
|
1027
|
+
compareAndSwap: async (expected, next) => {
|
|
1028
|
+
const active = readActive();
|
|
1029
|
+
const observed = active.record.waitingHuman;
|
|
1030
|
+
if (observed === undefined || expected === undefined) {
|
|
1031
|
+
if (observed !== expected)
|
|
1032
|
+
return false;
|
|
1033
|
+
}
|
|
1034
|
+
else if (canonicalJson(observed) !== canonicalJson(expected))
|
|
1035
|
+
return false;
|
|
1036
|
+
const saved = await this.persist(active, {
|
|
1037
|
+
...(active.record.lifecycle === 'routed'
|
|
1038
|
+
&& (expected === undefined || (expected.phase === 'resumed' && next.phase !== 'resumed' && next.phase !== 'history-only'))
|
|
1039
|
+
? { lifecycle: 'waiting-human' }
|
|
1040
|
+
: {}),
|
|
1041
|
+
waitingHuman: structuredClone(next),
|
|
1042
|
+
});
|
|
1043
|
+
writeActive(saved);
|
|
1044
|
+
return true;
|
|
1045
|
+
},
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
specState(readActive, writeActive) {
|
|
1049
|
+
return {
|
|
1050
|
+
read: async () => structuredClone(readActive().record.specDelivery),
|
|
1051
|
+
compareAndSwap: async (expected, next) => {
|
|
1052
|
+
const active = readActive();
|
|
1053
|
+
const observed = active.record.specDelivery;
|
|
1054
|
+
if (observed === undefined || expected === undefined) {
|
|
1055
|
+
if (observed !== expected)
|
|
1056
|
+
return false;
|
|
1057
|
+
}
|
|
1058
|
+
else if (canonicalJson(observed) !== canonicalJson(expected))
|
|
1059
|
+
return false;
|
|
1060
|
+
const saved = await this.persist(active, { specDelivery: structuredClone(next) });
|
|
1061
|
+
writeActive(saved);
|
|
1062
|
+
return true;
|
|
1063
|
+
},
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
async continueSpecRequired(active) {
|
|
1067
|
+
if (!active.record.routeReceipt || active.record.routeReceipt.route !== 'spec-required') {
|
|
1068
|
+
return await this.terminal(active, { status: 'internal-error', code: 'spec-route-missing' });
|
|
1069
|
+
}
|
|
1070
|
+
let current = active;
|
|
1071
|
+
const context = {
|
|
1072
|
+
runId: current.record.runId, issue: structuredClone(current.record.issueSnapshot),
|
|
1073
|
+
frozenCriteria: structuredClone(current.record.frozenCriteria), worktreePath: current.record.worktreePath,
|
|
1074
|
+
workflowGeneration: structuredClone(current.record.workflowGeneration), receipt: structuredClone(current.record.routeReceipt),
|
|
1075
|
+
};
|
|
1076
|
+
const result = await this.dependencies.routeContinuations.specRequired(context, this.specState(() => current, (next) => { current = next; }), this.signal);
|
|
1077
|
+
if (result.status === 'completed') {
|
|
1078
|
+
const evidence = await this.dependencies.writeEvidence({ runId: current.record.runId, code: 'spec-frozen', summary: result.receipt.receiptSha256 });
|
|
1079
|
+
return { status: 'spec-frozen', receipt: result.receipt, evidencePath: evidence.path };
|
|
1080
|
+
}
|
|
1081
|
+
if (result.status === 'cancelled')
|
|
1082
|
+
return await this.terminal(current, { status: 'cancelled' });
|
|
1083
|
+
if (result.status === 'retryable')
|
|
1084
|
+
return await this.terminal(current, { status: 'transport-failed', resumable: true }, result.code);
|
|
1085
|
+
return await this.terminal(current, { status: 'blocked', kind: result.kind, resumable: result.kind !== 'exhausted' }, result.code);
|
|
1086
|
+
}
|
|
1087
|
+
async continueWaitingHuman(active) {
|
|
1088
|
+
if (!active.record.routeReceipt || !active.record.workflowGeneration) {
|
|
1089
|
+
return { result: await this.terminal(active, { status: 'internal-error', code: 'waiting-route-missing' }) };
|
|
1090
|
+
}
|
|
1091
|
+
const context = {
|
|
1092
|
+
runId: active.record.runId,
|
|
1093
|
+
issue: structuredClone(active.record.issueSnapshot),
|
|
1094
|
+
frozenCriteria: structuredClone(active.record.frozenCriteria),
|
|
1095
|
+
worktreePath: active.record.worktreePath,
|
|
1096
|
+
workflowGeneration: structuredClone(active.record.workflowGeneration),
|
|
1097
|
+
receipt: structuredClone(active.record.routeReceipt),
|
|
1098
|
+
};
|
|
1099
|
+
let current = active;
|
|
1100
|
+
const result = await this.dependencies.routeContinuations.awaitingUser(context, this.waitingState(() => current, (next) => { current = next; }), this.signal);
|
|
1101
|
+
return this.mapWaitingResult(current, result);
|
|
1102
|
+
}
|
|
1103
|
+
async mapWaitingResult(active, result) {
|
|
1104
|
+
if (result.status === 'awaiting-answer') {
|
|
1105
|
+
const evidence = await this.dependencies.writeEvidence({ runId: active.record.runId, code: 'awaiting-user', summary: result.questionId });
|
|
1106
|
+
return { result: { status: 'awaiting-user', questionId: result.questionId, answerPrefix: result.answerPrefix, evidencePath: evidence.path } };
|
|
1107
|
+
}
|
|
1108
|
+
if (result.status === 'retryable') {
|
|
1109
|
+
const evidence = await this.dependencies.writeEvidence({ runId: active.record.runId, code: result.code, summary: result.owner });
|
|
1110
|
+
return { result: { status: 'transport-failed', resumable: true, evidencePath: evidence.path } };
|
|
1111
|
+
}
|
|
1112
|
+
if (result.status === 'cancelled')
|
|
1113
|
+
return { result: await this.terminal(active, { status: 'cancelled' }) };
|
|
1114
|
+
if (result.status === 'blocked') {
|
|
1115
|
+
return { result: await this.terminal(active, { status: 'blocked', kind: result.kind, resumable: result.resumable }, result.code) };
|
|
1116
|
+
}
|
|
1117
|
+
const waitingHuman = archiveWaiting(active.record, result.answer, {
|
|
1118
|
+
phase: 'resumed', trustedAnswer: structuredClone(result.answer),
|
|
1119
|
+
});
|
|
1120
|
+
const routeExecution = initialRouteExecution();
|
|
1121
|
+
validateTrustedAnswerResumeTransition({
|
|
1122
|
+
lifecycle: active.record.lifecycle,
|
|
1123
|
+
routeExecution: active.record.routeExecution,
|
|
1124
|
+
routeReceipt: active.record.routeReceipt,
|
|
1125
|
+
generationHash: active.record.workflowGeneration.generationHash,
|
|
1126
|
+
}, {
|
|
1127
|
+
lifecycle: 'triaging', routeExecution, routeReceipt: undefined,
|
|
1128
|
+
generationHash: active.record.workflowGeneration.generationHash,
|
|
1129
|
+
}, active.record.waitingHuman);
|
|
1130
|
+
const resumed = await this.persist(active, {
|
|
1131
|
+
lifecycle: 'triaging',
|
|
1132
|
+
issueSnapshot: structuredClone(active.record.issueSnapshot),
|
|
1133
|
+
routeExecution,
|
|
1134
|
+
routeReceipt: undefined,
|
|
1135
|
+
waitingHuman,
|
|
1136
|
+
});
|
|
1137
|
+
return { active: resumed };
|
|
1138
|
+
}
|
|
644
1139
|
async readStrictConfig(targetRoot) {
|
|
645
1140
|
const value = await this.dependencies.readConfig(targetRoot);
|
|
646
1141
|
return { bytes: Buffer.from(value.bytes), config: parseAgentAutoConfig(structuredClone(value.config)) };
|
|
@@ -653,7 +1148,8 @@ export class RunIssue {
|
|
|
653
1148
|
return 'Issue is not open.';
|
|
654
1149
|
if (!labels.has(config.github.labels.auto.name))
|
|
655
1150
|
return 'Issue lacks the auto label.';
|
|
656
|
-
if ([config.github.labels.running.name, config.github.labels.blocked.name, config.github.labels.review.name
|
|
1151
|
+
if ([config.github.labels.running.name, config.github.labels.blocked.name, config.github.labels.review.name, config.github.labels.waitingHuman.name]
|
|
1152
|
+
.some((label) => labels.has(label))) {
|
|
657
1153
|
return 'Issue already has a terminal or running label.';
|
|
658
1154
|
}
|
|
659
1155
|
const branchName = `codex/issue-${issue.number}`;
|
|
@@ -669,12 +1165,14 @@ export class RunIssue {
|
|
|
669
1165
|
async createRun(input) {
|
|
670
1166
|
const state = await this.dependencies.runRecords.read();
|
|
671
1167
|
const now = this.timestamp();
|
|
1168
|
+
const workflow = await this.dependencies.createWorkflowGeneration();
|
|
672
1169
|
const record = {
|
|
673
1170
|
...input,
|
|
674
1171
|
lifecycle: 'claimed', cycle: 1, reportRepairs: 0, transportRetries: 0,
|
|
675
1172
|
reworkFindings: [],
|
|
676
|
-
packageVersion:
|
|
677
|
-
|
|
1173
|
+
packageVersion: workflow.receipt.packageVersion,
|
|
1174
|
+
workflowGeneration: structuredClone(workflow.receipt),
|
|
1175
|
+
skillHashes: structuredClone(workflow.skillHashes),
|
|
678
1176
|
checks: [], createdAt: now, updatedAt: now,
|
|
679
1177
|
};
|
|
680
1178
|
const saved = await this.dependencies.runRecords.compareAndSwap(state.generation, {
|
|
@@ -688,7 +1186,7 @@ export class RunIssue {
|
|
|
688
1186
|
delete record.intent;
|
|
689
1187
|
if (Object.hasOwn(changes, 'process') && changes.process === undefined)
|
|
690
1188
|
delete record.process;
|
|
691
|
-
for (const key of ['checkedChangeSha256', 'proofId', 'proofReceipt', 'terminalOutcome', 'outcomeEvidenceId']) {
|
|
1189
|
+
for (const key of ['checkedChangeSha256', 'proofId', 'proofReceipt', 'terminalOutcome', 'outcomeEvidenceId', 'routeExecution', 'routeReceipt']) {
|
|
692
1190
|
if (Object.hasOwn(changes, key) && changes[key] === undefined)
|
|
693
1191
|
delete record[key];
|
|
694
1192
|
}
|
|
@@ -739,19 +1237,232 @@ export class RunIssue {
|
|
|
739
1237
|
}
|
|
740
1238
|
async runImplementation(input) {
|
|
741
1239
|
try {
|
|
742
|
-
return await this.dependencies.implementationAgent.run({
|
|
1240
|
+
return await this.dependencies.implementationAgent.run({
|
|
1241
|
+
...input,
|
|
1242
|
+
workflowGeneration: structuredClone(input.workflowGeneration),
|
|
1243
|
+
signal: this.signal,
|
|
1244
|
+
});
|
|
743
1245
|
}
|
|
744
1246
|
catch {
|
|
745
1247
|
return { kind: 'internal-error' };
|
|
746
1248
|
}
|
|
747
1249
|
}
|
|
748
|
-
async
|
|
1250
|
+
async runDirectReviewFull(starting, issue, frozenCriteria, implementationAttemptId, maxCycles) {
|
|
1251
|
+
let active = starting;
|
|
1252
|
+
let reportRepair;
|
|
1253
|
+
while (true) {
|
|
1254
|
+
const directReview = active.record.directReview;
|
|
1255
|
+
const routeReceipt = active.record.routeReceipt;
|
|
1256
|
+
if (!directReview || !routeReceipt || routeReceipt.route !== 'direct'
|
|
1257
|
+
|| (directReview.stage !== 'review-full' && directReview.stage !== 'review-closure')) {
|
|
1258
|
+
return this.terminal(active, { status: 'internal-error', code: 'direct-review-state-invalid' });
|
|
1259
|
+
}
|
|
1260
|
+
const reviewerSessionId = directReview.review.reviewerSessionId;
|
|
1261
|
+
if (!reviewerSessionId)
|
|
1262
|
+
return this.terminal(active, { status: 'internal-error', code: 'direct-review-session-missing' });
|
|
1263
|
+
const closureRequestSha256 = directReview.stage === 'review-closure'
|
|
1264
|
+
? directReviewClosureRequestSha256(directReview)
|
|
1265
|
+
: null;
|
|
1266
|
+
const mode = directReview.stage === 'review-closure' ? 'closure' : 'full';
|
|
1267
|
+
const result = await this.dependencies.implementationReviewer.run({
|
|
1268
|
+
runId: active.record.runId,
|
|
1269
|
+
worktreePath: active.record.worktreePath,
|
|
1270
|
+
operation: 'code-review',
|
|
1271
|
+
mode,
|
|
1272
|
+
reviewerSessionId,
|
|
1273
|
+
implementationAttemptId,
|
|
1274
|
+
targetRevision: directReview.targetRevision,
|
|
1275
|
+
targetFingerprint: directReview.targetFingerprint,
|
|
1276
|
+
closureRequestSha256,
|
|
1277
|
+
issue,
|
|
1278
|
+
frozenCriteria,
|
|
1279
|
+
routeReceipt: structuredClone(routeReceipt),
|
|
1280
|
+
defects: structuredClone(directReview.review.defects),
|
|
1281
|
+
affectedDefectIds: [...directReview.review.affectedDefectIds],
|
|
1282
|
+
fixedRepairFindings: directReview.repairFindings.filter((finding) => finding.status === 'fixed')
|
|
1283
|
+
.map((finding) => ({ id: finding.id, affectedContracts: [...finding.affectedContracts] })),
|
|
1284
|
+
mandatoryCoverage: directReview.stage === 'review-closure'
|
|
1285
|
+
? [...directReview.review.coverage]
|
|
1286
|
+
: ['acceptance-criteria', 'correctness', 'test-quality'],
|
|
1287
|
+
workflowGeneration: structuredClone(active.record.workflowGeneration),
|
|
1288
|
+
repairOnly: reportRepair !== undefined,
|
|
1289
|
+
originalReportSha256: reportRepair?.originalReportSha256 ?? null,
|
|
1290
|
+
validationDiagnostic: reportRepair?.diagnostic ?? null,
|
|
1291
|
+
originalReportBytes: reportRepair?.originalReportBytes ?? null,
|
|
1292
|
+
signal: this.signal,
|
|
1293
|
+
onPrepared: async (invocation) => {
|
|
1294
|
+
const current = active.record.directReview;
|
|
1295
|
+
if (!current)
|
|
1296
|
+
throw new Error('direct review disappeared before prepare');
|
|
1297
|
+
active = await this.persist(active, {
|
|
1298
|
+
directReview: prepareDirectReviewInvocation(current, invocation),
|
|
1299
|
+
});
|
|
1300
|
+
},
|
|
1301
|
+
onLaunched: async (invocation) => {
|
|
1302
|
+
const current = active.record.directReview;
|
|
1303
|
+
if (!current)
|
|
1304
|
+
throw new Error('direct review disappeared before launch');
|
|
1305
|
+
active = await this.persist(active, {
|
|
1306
|
+
directReview: launchDirectReviewInvocation(current, invocation),
|
|
1307
|
+
});
|
|
1308
|
+
},
|
|
1309
|
+
});
|
|
1310
|
+
if (result.kind === 'completed') {
|
|
1311
|
+
const current = active.record.directReview;
|
|
1312
|
+
if (!current)
|
|
1313
|
+
return this.terminal(active, { status: 'internal-error', code: 'direct-review-result-orphaned' });
|
|
1314
|
+
if (result.report.verdict === 'needs-work') {
|
|
1315
|
+
if (active.record.cycle >= maxCycles) {
|
|
1316
|
+
return this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true }, 'direct-review-repair-exhausted');
|
|
1317
|
+
}
|
|
1318
|
+
const repaired = acceptNeedsWorkDirectReview(current, result.report, result.artifactSha256);
|
|
1319
|
+
const findings = [
|
|
1320
|
+
...result.report.defects
|
|
1321
|
+
.filter((defect) => defect.status === 'open' || defect.status === 'reopened')
|
|
1322
|
+
.map((defect) => `${defect.id}: ${defect.failure}\nRepair: ${defect.repair}`),
|
|
1323
|
+
...repaired.repairFindings
|
|
1324
|
+
.filter((finding) => finding.status === 'reopened')
|
|
1325
|
+
.map((finding) => `${finding.id}: ${finding.summary}`),
|
|
1326
|
+
];
|
|
1327
|
+
return this.persist(active, {
|
|
1328
|
+
lifecycle: 'implementing',
|
|
1329
|
+
cycle: (active.record.cycle + 1),
|
|
1330
|
+
directReview: repaired,
|
|
1331
|
+
reworkFindings: findings,
|
|
1332
|
+
checks: [],
|
|
1333
|
+
checkedChangeSha256: undefined,
|
|
1334
|
+
proofId: undefined,
|
|
1335
|
+
proofReceipt: undefined,
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1338
|
+
if (result.report.verdict !== 'approved')
|
|
1339
|
+
return this.terminal(active, { status: 'blocked', kind: 'safety', resumable: false }, 'direct-review-rejected');
|
|
1340
|
+
return this.persist(active, {
|
|
1341
|
+
lifecycle: 'checking',
|
|
1342
|
+
directReview: acceptApprovedDirectReview(current, result.report, result.artifactSha256),
|
|
1343
|
+
});
|
|
1344
|
+
}
|
|
1345
|
+
if (result.kind === 'report-invalid') {
|
|
1346
|
+
const current = active.record.directReview;
|
|
1347
|
+
if (!current)
|
|
1348
|
+
return this.terminal(active, { status: 'internal-error', code: 'direct-review-result-orphaned' });
|
|
1349
|
+
const selected = current.stage?.startsWith('cleanup-') ? current.cleanup : current.review;
|
|
1350
|
+
if (selected.reportRepairs >= 1 || reportRepair) {
|
|
1351
|
+
return this.terminal(active, { status: 'internal-error', code: 'direct-review-report-malformed' });
|
|
1352
|
+
}
|
|
1353
|
+
const { invocation: _invocation, ...withoutInvocation } = structuredClone(current);
|
|
1354
|
+
const trackKey = current.stage?.startsWith('cleanup-') ? 'cleanup' : 'review';
|
|
1355
|
+
active = await this.persist(active, {
|
|
1356
|
+
directReview: {
|
|
1357
|
+
...withoutInvocation,
|
|
1358
|
+
[trackKey]: { ...withoutInvocation[trackKey], reportRepairs: 1 },
|
|
1359
|
+
},
|
|
1360
|
+
});
|
|
1361
|
+
reportRepair = {
|
|
1362
|
+
originalReportSha256: result.originalReportSha256,
|
|
1363
|
+
originalReportBytes: Buffer.from(result.originalReportBytes),
|
|
1364
|
+
diagnostic: result.diagnostic,
|
|
1365
|
+
};
|
|
1366
|
+
continue;
|
|
1367
|
+
}
|
|
1368
|
+
if (result.kind === 'transport-failed') {
|
|
1369
|
+
const current = active.record.directReview;
|
|
1370
|
+
if (!current || current.review.transportRetries >= 1) {
|
|
1371
|
+
return this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true }, 'direct-review-transport-exhausted');
|
|
1372
|
+
}
|
|
1373
|
+
const { invocation: _invocation, ...withoutInvocation } = structuredClone(current);
|
|
1374
|
+
active = await this.persist(active, {
|
|
1375
|
+
directReview: {
|
|
1376
|
+
...withoutInvocation,
|
|
1377
|
+
review: { ...withoutInvocation.review, transportRetries: 1 },
|
|
1378
|
+
},
|
|
1379
|
+
});
|
|
1380
|
+
continue;
|
|
1381
|
+
}
|
|
1382
|
+
if (result.kind === 'safe-halt') {
|
|
1383
|
+
active = await this.persist(active, {
|
|
1384
|
+
lifecycle: 'safe-halt',
|
|
1385
|
+
process: {
|
|
1386
|
+
...result.process,
|
|
1387
|
+
purpose: 'code-review',
|
|
1388
|
+
resumeLifecycle: 'implementing',
|
|
1389
|
+
resumeReviewStage: directReview.stage,
|
|
1390
|
+
},
|
|
1391
|
+
});
|
|
1392
|
+
try {
|
|
1393
|
+
await result.waitForAbsence();
|
|
1394
|
+
}
|
|
1395
|
+
catch {
|
|
1396
|
+
return this.terminal(active, { status: 'transport-failed', resumable: false }, 'direct-review-quiescence-unconfirmed');
|
|
1397
|
+
}
|
|
1398
|
+
return this.terminal(active, { status: 'transport-failed', resumable: false }, 'direct-review-quiescence-delayed');
|
|
1399
|
+
}
|
|
1400
|
+
if (result.kind === 'cancelled')
|
|
1401
|
+
return this.terminal(active, { status: 'cancelled' });
|
|
1402
|
+
return this.terminal(active, { status: 'internal-error', code: result.code });
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
async recoverDirectReviewInvocation(starting) {
|
|
1406
|
+
const directReview = starting.record.directReview;
|
|
1407
|
+
const invocation = directReview?.invocation;
|
|
1408
|
+
if (!directReview || !invocation)
|
|
1409
|
+
return starting;
|
|
1410
|
+
const { invocation: _invocation, ...withoutInvocation } = structuredClone(directReview);
|
|
1411
|
+
if (invocation.status === 'prepared') {
|
|
1412
|
+
return this.persist(starting, { directReview: withoutInvocation });
|
|
1413
|
+
}
|
|
1414
|
+
if (invocation.status === 'launched') {
|
|
1415
|
+
try {
|
|
1416
|
+
await this.dependencies.waitForReviewProcessAbsence(invocation.processGroupId);
|
|
1417
|
+
}
|
|
1418
|
+
catch {
|
|
1419
|
+
return this.terminal(starting, { status: 'blocked', kind: 'safety', resumable: false }, 'direct-review-process-absence-unconfirmed');
|
|
1420
|
+
}
|
|
1421
|
+
if (directReview.review.transportRetries >= 1) {
|
|
1422
|
+
return this.terminal(starting, { status: 'blocked', kind: 'exhausted', resumable: true }, 'direct-review-transport-exhausted');
|
|
1423
|
+
}
|
|
1424
|
+
return this.persist(starting, {
|
|
1425
|
+
directReview: {
|
|
1426
|
+
...withoutInvocation,
|
|
1427
|
+
review: { ...withoutInvocation.review, transportRetries: 1 },
|
|
1428
|
+
},
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1431
|
+
return this.persist(starting, { directReview: withoutInvocation });
|
|
1432
|
+
}
|
|
1433
|
+
async startNextCycle(active, findings, sources) {
|
|
1434
|
+
if (active.record.directReview?.status === 'clear') {
|
|
1435
|
+
const fallbackProvenance = active.record.lifecycle === 'proving' ? 'proof' : 'check';
|
|
1436
|
+
const normalizedSources = sources ?? findings.map((summary) => ({
|
|
1437
|
+
provenance: fallbackProvenance,
|
|
1438
|
+
sourceId: `${fallbackProvenance}:recovery:${sha256(summary)}`,
|
|
1439
|
+
summary,
|
|
1440
|
+
affectedContracts: [fallbackProvenance === 'proof' ? 'acceptance-proof' : 'configured-checks'],
|
|
1441
|
+
}));
|
|
1442
|
+
const repairFindings = normalizedSources.map((source) => ({
|
|
1443
|
+
id: source.sourceId,
|
|
1444
|
+
provenance: source.provenance,
|
|
1445
|
+
sourceId: source.sourceId,
|
|
1446
|
+
targetRevision: active.record.directReview.targetRevision,
|
|
1447
|
+
summary: source.summary,
|
|
1448
|
+
affectedContracts: source.affectedContracts,
|
|
1449
|
+
status: 'open',
|
|
1450
|
+
}));
|
|
1451
|
+
return this.persist(active, {
|
|
1452
|
+
lifecycle: 'implementing',
|
|
1453
|
+
cycle: (active.record.cycle + 1),
|
|
1454
|
+
reworkFindings: [...findings],
|
|
1455
|
+
directReview: beginDirectReviewRepair(active.record.directReview, repairFindings),
|
|
1456
|
+
checks: [],
|
|
1457
|
+
checkedChangeSha256: undefined,
|
|
1458
|
+
proofId: undefined,
|
|
1459
|
+
proofReceipt: undefined,
|
|
1460
|
+
});
|
|
1461
|
+
}
|
|
749
1462
|
active = await this.persist(active, { lifecycle: 'reworking', reworkFindings: [...findings] });
|
|
750
1463
|
return this.persist(active, {
|
|
751
1464
|
lifecycle: 'implementing',
|
|
752
1465
|
cycle: (active.record.cycle + 1),
|
|
753
|
-
packageVersion: this.dependencies.packageVersion,
|
|
754
|
-
skillHashes: structuredClone(this.dependencies.skillHashes),
|
|
755
1466
|
checks: [],
|
|
756
1467
|
checkedChangeSha256: undefined,
|
|
757
1468
|
proofId: undefined,
|
|
@@ -792,6 +1503,38 @@ export class RunIssue {
|
|
|
792
1503
|
outcomeEvidenceId: evidence.id,
|
|
793
1504
|
process: undefined,
|
|
794
1505
|
};
|
|
1506
|
+
if (outcome.status !== 'review-ready' && active.record.directReview && active.record.directReview.status !== 'terminal') {
|
|
1507
|
+
changes.directReview = projectTerminalDirectReview(active.record.directReview, outcome.status === 'blocked'
|
|
1508
|
+
? { status: 'blocked', kind: outcome.kind }
|
|
1509
|
+
: { status: outcome.status });
|
|
1510
|
+
}
|
|
1511
|
+
if (active.record.waitingHuman && (active.record.lifecycle === 'waiting-human' || active.record.waitingHuman.phase === 'resumed')) {
|
|
1512
|
+
if (active.record.waitingHuman.phase === 'resumed') {
|
|
1513
|
+
changes.waitingHuman = {
|
|
1514
|
+
version: 1,
|
|
1515
|
+
clarificationAttempts: active.record.waitingHuman.clarificationAttempts,
|
|
1516
|
+
permissionRetries: active.record.waitingHuman.permissionRetries,
|
|
1517
|
+
effectRetries: structuredClone(active.record.waitingHuman.effectRetries),
|
|
1518
|
+
history: structuredClone(active.record.waitingHuman.history),
|
|
1519
|
+
phase: 'history-only',
|
|
1520
|
+
terminalOutcome: outcome.status === 'blocked'
|
|
1521
|
+
? { status: 'blocked', kind: outcome.kind }
|
|
1522
|
+
: { status: outcome.status },
|
|
1523
|
+
};
|
|
1524
|
+
}
|
|
1525
|
+
else if (outcome.status === 'blocked' || outcome.status === 'cancelled') {
|
|
1526
|
+
changes.waitingHuman = archiveWaiting(active.record, waitingAnswer(active.record.waitingHuman), {
|
|
1527
|
+
phase: 'history-only',
|
|
1528
|
+
terminalOutcome: outcome.status === 'cancelled'
|
|
1529
|
+
? { status: 'cancelled' }
|
|
1530
|
+
: { status: 'blocked', kind: outcome.kind },
|
|
1531
|
+
});
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
if (active.record.lifecycle === 'triaging' || (active.record.lifecycle === 'safe-halt' && !active.record.routeReceipt)) {
|
|
1535
|
+
changes.routeExecution = undefined;
|
|
1536
|
+
changes.routeReceipt = undefined;
|
|
1537
|
+
}
|
|
795
1538
|
if (!retainIntent)
|
|
796
1539
|
changes.intent = undefined;
|
|
797
1540
|
await this.persist(active, changes);
|
|
@@ -835,6 +1578,17 @@ function snapshotIssue(issue) {
|
|
|
835
1578
|
url: issue.url,
|
|
836
1579
|
state: 'OPEN',
|
|
837
1580
|
labels: sortedUnique(issue.labels),
|
|
1581
|
+
comments: structuredClone(issue.comments),
|
|
1582
|
+
};
|
|
1583
|
+
}
|
|
1584
|
+
function publicIssueSnapshot(issue) {
|
|
1585
|
+
return {
|
|
1586
|
+
number: issue.number,
|
|
1587
|
+
title: issue.title,
|
|
1588
|
+
body: issue.body,
|
|
1589
|
+
url: issue.url,
|
|
1590
|
+
state: issue.state,
|
|
1591
|
+
labels: [...issue.labels],
|
|
838
1592
|
};
|
|
839
1593
|
}
|
|
840
1594
|
function freezeCriteria(issue) {
|
|
@@ -864,11 +1618,72 @@ function findRun(state, runId) {
|
|
|
864
1618
|
throw new Error('persisted run is missing');
|
|
865
1619
|
return record;
|
|
866
1620
|
}
|
|
1621
|
+
function waitingAnswer(waiting) {
|
|
1622
|
+
if ('answerReceipt' in waiting)
|
|
1623
|
+
return structuredClone(waiting.answerReceipt);
|
|
1624
|
+
if (waiting.phase === 'resumed')
|
|
1625
|
+
return structuredClone(waiting.trustedAnswer);
|
|
1626
|
+
return null;
|
|
1627
|
+
}
|
|
1628
|
+
function archiveWaiting(record, answer, terminal) {
|
|
1629
|
+
const waiting = record.waitingHuman;
|
|
1630
|
+
const routeReceipt = record.routeReceipt;
|
|
1631
|
+
if (!waiting || !routeReceipt)
|
|
1632
|
+
throw new Error('waiting archive requires active route evidence');
|
|
1633
|
+
const question = 'question' in waiting ? waiting.question : 'questionReceipt' in waiting ? waiting.questionReceipt.question : undefined;
|
|
1634
|
+
if (!question)
|
|
1635
|
+
throw new Error('waiting archive requires current question');
|
|
1636
|
+
const questionReceipt = 'questionReceipt' in waiting ? waiting.questionReceipt : null;
|
|
1637
|
+
const entry = {
|
|
1638
|
+
routeReceipt: structuredClone(routeReceipt),
|
|
1639
|
+
question: structuredClone(question),
|
|
1640
|
+
questionReceipt: questionReceipt ? structuredClone(questionReceipt) : null,
|
|
1641
|
+
answerReceipt: answer ? structuredClone(answer) : null,
|
|
1642
|
+
conflictHashes: [...question.conflictHashes],
|
|
1643
|
+
};
|
|
1644
|
+
const history = [...waiting.history];
|
|
1645
|
+
if (history.at(-1)?.question.questionSha256 === question.questionSha256) {
|
|
1646
|
+
if (canonicalJson(history.at(-1)) !== canonicalJson(entry))
|
|
1647
|
+
throw new Error('waiting archive evidence mismatch');
|
|
1648
|
+
}
|
|
1649
|
+
else {
|
|
1650
|
+
history.push(entry);
|
|
1651
|
+
}
|
|
1652
|
+
return {
|
|
1653
|
+
version: 1,
|
|
1654
|
+
clarificationAttempts: waiting.clarificationAttempts,
|
|
1655
|
+
permissionRetries: waiting.permissionRetries,
|
|
1656
|
+
effectRetries: structuredClone(waiting.effectRetries),
|
|
1657
|
+
history,
|
|
1658
|
+
...terminal,
|
|
1659
|
+
};
|
|
1660
|
+
}
|
|
1661
|
+
function terminalWaiting(waiting, terminalOutcome) {
|
|
1662
|
+
if (waiting.phase !== 'resumed' && waiting.phase !== 'history-only')
|
|
1663
|
+
throw new Error('terminal waiting projection requires archived history');
|
|
1664
|
+
return {
|
|
1665
|
+
version: 1,
|
|
1666
|
+
clarificationAttempts: waiting.clarificationAttempts,
|
|
1667
|
+
permissionRetries: waiting.permissionRetries,
|
|
1668
|
+
effectRetries: structuredClone(waiting.effectRetries),
|
|
1669
|
+
history: structuredClone(waiting.history),
|
|
1670
|
+
phase: 'history-only',
|
|
1671
|
+
terminalOutcome,
|
|
1672
|
+
};
|
|
1673
|
+
}
|
|
867
1674
|
function publicOutcome(outcome) {
|
|
868
1675
|
if (outcome.status === 'internal-error')
|
|
869
1676
|
return { status: 'internal-error', evidencePath: outcome.evidencePath };
|
|
870
1677
|
return structuredClone(outcome);
|
|
871
1678
|
}
|
|
1679
|
+
function requireRouteExecution(value) {
|
|
1680
|
+
if (!value)
|
|
1681
|
+
throw new Error('route execution is missing');
|
|
1682
|
+
return structuredClone(value);
|
|
1683
|
+
}
|
|
1684
|
+
function sameRouteExecution(left, right) {
|
|
1685
|
+
return left !== undefined && canonicalJson(left) === canonicalJson(right);
|
|
1686
|
+
}
|
|
872
1687
|
function sameStrings(left, right) {
|
|
873
1688
|
const a = [...left].sort();
|
|
874
1689
|
const b = [...right].sort();
|