onto-mcp 0.4.12 → 0.4.13
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/.onto/authority/core-lexicon.yaml +1 -1
- package/.onto/authority/supported-models.yaml +137 -0
- package/.onto/domains/accounting-kr/competency_qs.md +153 -153
- package/.onto/processes/reconstruct/obligation-coverage-ledger.yaml +842 -0
- package/.onto/processes/reconstruct/obligation-coverage-recorded.yaml +225 -0
- package/.onto/processes/reconstruct/ontology-seeding-and-maturation-design.md +93 -54
- package/.onto/processes/reconstruct/reconstruct-contract-registry.yaml +169 -8
- package/.onto/processes/reconstruct/reconstruct-execution-ux-contract.md +72 -1
- package/.onto/processes/reconstruct/source-profiles/spreadsheet.md +35 -0
- package/.onto/processes/review/productized-live-path.md +9 -5
- package/.onto/processes/review/prompt-execution-runner-contract.md +32 -8
- package/.onto/processes/review/review-target-profile-contract.md +47 -4
- package/.onto/processes/shared/pipeline-execution-ledger-contract.md +11 -0
- package/.onto/processes/shared/target-material-kind-contract.md +1 -1
- package/README.md +64 -1
- package/dist/cli.js +11 -1
- package/dist/core-api/reconstruct-api.js +575 -24
- package/dist/core-api/review-api.js +70 -5
- package/dist/core-api/review-progress.js +7 -0
- package/dist/core-api/runtime-observation.js +7 -0
- package/dist/core-runtime/artifact-io.js +75 -3
- package/dist/core-runtime/cli/claude-code-review-unit-executor.js +19 -8
- package/dist/core-runtime/cli/materialize-review-prompt-packets.js +68 -14
- package/dist/core-runtime/cli/prepare-review-session.js +34 -1
- package/dist/core-runtime/cli/render-review-final-output.js +2 -1
- package/dist/core-runtime/cli/run-review-prompt-execution.js +758 -48
- package/dist/core-runtime/cli/run-seats.js +18 -0
- package/dist/core-runtime/cli/runtime-submit-context.js +10 -2
- package/dist/core-runtime/cli/unit-resubmit.js +269 -0
- package/dist/core-runtime/cli/worker-structured-output.js +80 -2
- package/dist/core-runtime/discovery/host-detection.js +71 -22
- package/dist/core-runtime/discovery/review-cert-assemble.js +208 -0
- package/dist/core-runtime/discovery/review-cert-record.js +442 -0
- package/dist/core-runtime/discovery/review-execution-units.js +22 -0
- package/dist/core-runtime/discovery/seat-inventory.js +167 -0
- package/dist/core-runtime/discovery/settings-chain.js +311 -52
- package/dist/core-runtime/discovery/supported-models.js +282 -29
- package/dist/core-runtime/discovery/synthesize-cert-assemble.js +197 -0
- package/dist/core-runtime/discovery/synthesize-cert-capsule.js +399 -0
- package/dist/core-runtime/discovery/synthesize-cert-judge.js +340 -0
- package/dist/core-runtime/discovery/synthesize-cert-loop.js +234 -0
- package/dist/core-runtime/discovery/synthesize-cert-mutation.js +117 -0
- package/dist/core-runtime/discovery/synthesize-cert-packet.js +182 -0
- package/dist/core-runtime/discovery/synthesize-cert-record.js +831 -0
- package/dist/core-runtime/discovery/synthesize-cert-sampler.js +336 -0
- package/dist/core-runtime/effort-calibration-ingest.js +135 -0
- package/dist/core-runtime/llm/dispatch-breaker.js +412 -0
- package/dist/core-runtime/llm/dispatch-fallback-adapter-capabilities.js +33 -0
- package/dist/core-runtime/llm/llm-caller.js +188 -47
- package/dist/core-runtime/llm/mock-llm-realization.js +35 -4
- package/dist/core-runtime/llm/model-switcher.js +1 -1
- package/dist/core-runtime/llm/openai-responses-incomplete-error.js +46 -0
- package/dist/core-runtime/llm/sealed-dispatch-capability.js +359 -0
- package/dist/core-runtime/llm/structured-dispatch-error.js +15 -0
- package/dist/core-runtime/onboard/claude-profile-scan.js +65 -0
- package/dist/core-runtime/onboard/cli-host.js +5 -2
- package/dist/core-runtime/onboard/configure-provider.js +33 -1
- package/dist/core-runtime/onboard/host-target.js +28 -3
- package/dist/core-runtime/onboard/register.js +54 -12
- package/dist/core-runtime/pipeline-execution-ledger.js +10 -0
- package/dist/core-runtime/reconstruct/artifact-types.js +35 -0
- package/dist/core-runtime/reconstruct/claim-projection-validation.js +17 -0
- package/dist/core-runtime/reconstruct/competency-projection-contract.js +63 -0
- package/dist/core-runtime/reconstruct/comprehension-artifact.js +321 -0
- package/dist/core-runtime/reconstruct/comprehension-reduce.js +367 -0
- package/dist/core-runtime/reconstruct/comprehension-semantic-map.js +771 -0
- package/dist/core-runtime/reconstruct/contract-registry.js +20 -0
- package/dist/core-runtime/reconstruct/dispatch-fallback-artifacts.js +640 -0
- package/dist/core-runtime/reconstruct/execution-telemetry.js +83 -9
- package/dist/core-runtime/reconstruct/final-output-sections.js +89 -0
- package/dist/core-runtime/reconstruct/governing-snapshot.js +36 -1
- package/dist/core-runtime/reconstruct/leaf-reader.js +305 -0
- package/dist/core-runtime/reconstruct/llm-dispatch-failure.js +270 -0
- package/dist/core-runtime/reconstruct/llm-touch-fingerprint.js +94 -0
- package/dist/core-runtime/reconstruct/material-admission-validation.js +18 -0
- package/dist/core-runtime/reconstruct/material-profile-validation.js +14 -0
- package/dist/core-runtime/reconstruct/materialize-preparation.js +304 -18
- package/dist/core-runtime/reconstruct/maturation-validation.js +1231 -53
- package/dist/core-runtime/reconstruct/mock-llm-realization.js +107 -3
- package/dist/core-runtime/reconstruct/obligation-assertion.js +18 -0
- package/dist/core-runtime/reconstruct/ontology-seed-validation.js +27 -0
- package/dist/core-runtime/reconstruct/output-budget.js +6 -0
- package/dist/core-runtime/reconstruct/pipeline-execution-ledger.js +87 -3
- package/dist/core-runtime/reconstruct/post-seed-validation.js +118 -5
- package/dist/core-runtime/reconstruct/purpose-authority-validation.js +117 -2
- package/dist/core-runtime/reconstruct/record.js +42 -0
- package/dist/core-runtime/reconstruct/registry-verification-validation.js +16 -0
- package/dist/core-runtime/reconstruct/run-control-validation.js +1011 -34
- package/dist/core-runtime/reconstruct/run.js +5198 -536
- package/dist/core-runtime/reconstruct/seed-authoring-readiness-validation.js +54 -3
- package/dist/core-runtime/reconstruct/semantic-quality-gate.js +105 -2
- package/dist/core-runtime/reconstruct/source-observation-delta-validation.js +41 -0
- package/dist/core-runtime/reconstruct/source-observations.js +166 -0
- package/dist/core-runtime/reconstruct/source-safety-validation.js +71 -183
- package/dist/core-runtime/reconstruct/source-scout-pack-validation.js +37 -1
- package/dist/core-runtime/reconstruct/terminal-validation.js +132 -2
- package/dist/core-runtime/review/continuation-plan.js +12 -5
- package/dist/core-runtime/review/issue-artifact-runtime.js +4 -1
- package/dist/core-runtime/review/materializers.js +100 -11
- package/dist/core-runtime/review/nesting-batch.js +6 -2
- package/dist/core-runtime/review/obligation-shardability.js +138 -0
- package/dist/core-runtime/review/pipeline-execution-ledger.js +42 -2
- package/dist/core-runtime/review/review-artifact-utils.js +217 -5
- package/dist/core-runtime/review/review-execution-steps.js +32 -8
- package/dist/core-runtime/review/review-progress-contract.js +8 -0
- package/dist/core-runtime/review/review-prompt-budget.js +97 -0
- package/dist/core-runtime/review/semantic-quality-gate.js +33 -7
- package/dist/core-runtime/review/spreadsheet-review-disposition.js +94 -0
- package/dist/core-runtime/route-identity.js +182 -0
- package/dist/core-runtime/spreadsheet-header-escalation.js +204 -0
- package/dist/core-runtime/spreadsheet-structure-observer.js +2689 -0
- package/dist/core-runtime/target-material-kind.js +58 -0
- package/dist/mcp/server.js +12 -5
- package/dist/tui/app.js +167 -0
- package/dist/tui/data/event-follower.js +116 -0
- package/dist/tui/data/node-detail.js +42 -0
- package/dist/tui/data/projection-poll.js +26 -0
- package/dist/tui/data/session-discovery.js +65 -0
- package/dist/tui/index.js +108 -0
- package/dist/tui/screens/log.js +25 -0
- package/dist/tui/screens/node-detail.js +17 -0
- package/dist/tui/screens/session-selector.js +26 -0
- package/dist/tui/screens/workflow-tree.js +90 -0
- package/dist/tui/view-model/reconstruct-adapter.js +162 -0
- package/dist/tui/view-model/review-adapter.js +270 -0
- package/dist/tui/view-model/tree-view-model.js +19 -0
- package/package.json +18 -3
- package/settings.example.json +77 -16
|
@@ -13,7 +13,7 @@ import { lensIdFromRound1ArtifactPath, readValidatedLensSidecarArtifact, writeLe
|
|
|
13
13
|
import { allLensOutputsAreSidecars, renderRuntimeFindingLedgerPacket, writeFindingLedgerFromLensSidecars, } from "../review/lens-sidecar-ledger.js";
|
|
14
14
|
import { effectiveReviewUnitLlmRef } from "../review/review-execution-profile.js";
|
|
15
15
|
import { buildInitialExecutionResultScaffold, computeReviewFrontier, mergeUnitResultIntoExecutionResult, validateUnitSeatToResult, } from "../review/review-execution-steps.js";
|
|
16
|
-
import {
|
|
16
|
+
import { isResolvedLedgerUnit, } from "../pipeline-execution-ledger.js";
|
|
17
17
|
import { buildReviewExecutionRoute, buildReviewRuntimeRouteArtifactProjection, } from "../review/review-execution-route.js";
|
|
18
18
|
import { buildIssueScopedDeliberationWorklist, buildIssueScopedLensDeliberationPrompt, buildNoPlannedDeliberationResolution, buildRuntimeIssueDeliberationUnavailableResponse, buildRuntimeUnavailableDeliberationResolution, buildTeamleadIssueResolutionPrompt, deliberationResolutionPath, renderDeliberationMarkdownProjection, validateDeliberationResolutionObject, validateIssueDeliberationResponseObject, } from "../review/controlled-lens-deliberation.js";
|
|
19
19
|
import { computeLensCompletionBarrier, resolveRequiredParticipatingLensCount, } from "../review/lens-completion-policy.js";
|
|
@@ -29,6 +29,11 @@ import { renderReviewUnitBoundaryDetailsSection, } from "../review/unit-boundary
|
|
|
29
29
|
import { renderIssueSynthesisPrompt, buildRuntimeIssueSynthesisUnavailableResponse, synthesisLedgerPath, synthesisWorkItemsPath, validateIssueSynthesisResponseOnDisk, writeReviewSynthesisLedger, writeReviewSynthesisWorkItems, writeSynthesisMarkdownFromLedger, } from "../review/synthesis-map-reduce.js";
|
|
30
30
|
import { isDirectModelCallSelection, normalizeLlmModelSwitcher, } from "../llm/model-switcher.js";
|
|
31
31
|
import { parseRuntimeIssueDeliberationSchemaContext } from "./runtime-submit-context.js";
|
|
32
|
+
import { parseRuntimeIssueStanceSchemaContext } from "./runtime-submit-context.js";
|
|
33
|
+
import { parseRuntimeIssueSynthesisSchemaContext } from "./runtime-submit-context.js";
|
|
34
|
+
import { salvageInputPathFor } from "./submit-salvage.js";
|
|
35
|
+
import { DispatchBreakerState, TRANSIENT_TRANSPORT_MESSAGE_PATTERNS, buildDispatchIncompleteArtifact, classifySystemicDispatchFailure, dispatchIncompleteArtifactPath, } from "../llm/dispatch-breaker.js";
|
|
36
|
+
import { CORRELATED_VALIDATION_HALT_REASON, applyResubmitErrorSpecToPacket, buildResubmitErrorSpec, classifyUnsupportedEvidenceRefFailure, classifyDeliberationUnsupportedEvidenceRefFailure, classifySynthesisUnsupportedSourceRefFailure, isUnsupportedEvidenceRefFailureMessage, correlatedValidationExceeded, } from "./unit-resubmit.js";
|
|
32
37
|
import { isReviewArtifactGenerationRealization, semanticQualityEvidenceForArtifactGeneration, } from "../review/artifact-generation-realization.js";
|
|
33
38
|
function errorMessage(error) {
|
|
34
39
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -204,11 +209,21 @@ class ReviewUnitOutputContractError extends Error {
|
|
|
204
209
|
class ReviewIssueArtifactDispatchError extends Error {
|
|
205
210
|
outcome;
|
|
206
211
|
originalError;
|
|
207
|
-
|
|
212
|
+
/** Explicit halt_reason override (e.g. `correlated_validation: …`);
|
|
213
|
+
* null keeps the default `Issue artifact generation failed: …` phrasing. */
|
|
214
|
+
haltReason;
|
|
215
|
+
/** 설계 B 트립 전용: halt 시점까지 기록된 배치 outcome 전체(완료+최종
|
|
216
|
+
* 실패). 완료 유닛의 행이 execution-result에 남아야 continuation ledger가
|
|
217
|
+
* 그 유닛을 재디스패치하지 않는다 — 회복 집합 == 미완료 집합 (규칙 5).
|
|
218
|
+
* 트립 외 halt 경로는 빈 배열(현행 동작 보존). */
|
|
219
|
+
batchOutcomes;
|
|
220
|
+
constructor(message, outcome, originalError, haltReason = null, batchOutcomes = []) {
|
|
208
221
|
super(message);
|
|
209
222
|
this.name = "ReviewIssueArtifactDispatchError";
|
|
210
223
|
this.outcome = outcome;
|
|
211
224
|
this.originalError = originalError;
|
|
225
|
+
this.haltReason = haltReason;
|
|
226
|
+
this.batchOutcomes = batchOutcomes;
|
|
212
227
|
}
|
|
213
228
|
}
|
|
214
229
|
function issueArtifactOutcomeFromError(error) {
|
|
@@ -944,18 +959,15 @@ function failureKindFromError(error) {
|
|
|
944
959
|
return failureKindFromMessage(error.message);
|
|
945
960
|
return "unknown";
|
|
946
961
|
}
|
|
962
|
+
// Shared transient-transport vocabulary is single-sourced in the dispatch
|
|
963
|
+
// breaker module; this consumer adds only its executor-specific extra.
|
|
964
|
+
const TRANSIENT_EXECUTOR_FAILURE_PATTERNS = [
|
|
965
|
+
...TRANSIENT_TRANSPORT_MESSAGE_PATTERNS,
|
|
966
|
+
"responses_retry",
|
|
967
|
+
];
|
|
947
968
|
function isTransientExecutorFailureMessage(message) {
|
|
948
969
|
const normalized = message.toLowerCase();
|
|
949
|
-
return
|
|
950
|
-
"stream disconnected before completion",
|
|
951
|
-
"connection reset by peer",
|
|
952
|
-
"error sending request",
|
|
953
|
-
"failed to connect to websocket",
|
|
954
|
-
"transport channel closed",
|
|
955
|
-
"http/request failed",
|
|
956
|
-
"request failed after",
|
|
957
|
-
"responses_retry",
|
|
958
|
-
].some((pattern) => normalized.includes(pattern));
|
|
970
|
+
return TRANSIENT_EXECUTOR_FAILURE_PATTERNS.some((pattern) => normalized.includes(pattern));
|
|
959
971
|
}
|
|
960
972
|
function failureKindFromMessage(message) {
|
|
961
973
|
const normalized = message.toLowerCase();
|
|
@@ -992,16 +1004,48 @@ function failureKindFromMessage(message) {
|
|
|
992
1004
|
}
|
|
993
1005
|
return "executor_exit";
|
|
994
1006
|
}
|
|
995
|
-
function shouldRetryUnitFailure(args) {
|
|
1007
|
+
export function shouldRetryUnitFailure(args) {
|
|
996
1008
|
if (args.attempt >= args.maxRetries)
|
|
997
1009
|
return false;
|
|
998
1010
|
const failureKind = failureKindFromError(args.error);
|
|
999
|
-
if (failureKind === "empty_output"
|
|
1000
|
-
failureKind === "output_contract") {
|
|
1011
|
+
if (failureKind === "empty_output")
|
|
1001
1012
|
return false;
|
|
1013
|
+
if (failureKind === "output_contract") {
|
|
1014
|
+
// §4-2c structural retry gate: an output_contract failure is normally
|
|
1015
|
+
// terminal, but a resubmit-correctable whitelist rejection is
|
|
1016
|
+
// substring-misclassified as output_contract when its message contains an
|
|
1017
|
+
// envelope field name (synthesis: always; deliberation: rare hallucinated
|
|
1018
|
+
// ref). Route it back to a corrective retry only when resubmit will actually
|
|
1019
|
+
// fire on it — otherwise keep the terminal, byte-identical behavior.
|
|
1020
|
+
return isResubmitCorrectableRetry(args);
|
|
1002
1021
|
}
|
|
1003
1022
|
return true;
|
|
1004
1023
|
}
|
|
1024
|
+
/**
|
|
1025
|
+
* Allow an output_contract retry iff resubmit is enabled AND the unit is
|
|
1026
|
+
* gate-eligible AND the precise structural classifier matches — making the
|
|
1027
|
+
* gate's activation a strict subset of the resubmit strategy's activation
|
|
1028
|
+
* (design §10: F-1 retry ⟺ strategy fires). Reads the shared
|
|
1029
|
+
* RESUBMIT_UNIT_ROUTING table so the gate and dispatcher cannot diverge (M-1).
|
|
1030
|
+
* OFF (resubmit disabled) → false → byte-identical to output_contract being
|
|
1031
|
+
* terminal. Issue-stance is gate-eligible as of the rare-poison hardening cut:
|
|
1032
|
+
* final stance demote/correlated decisions still read the terminal outcome, so
|
|
1033
|
+
* an infra final failure remains a whole-run halt rather than being
|
|
1034
|
+
* reinterpreted as validation.
|
|
1035
|
+
*/
|
|
1036
|
+
function isResubmitCorrectableRetry(args) {
|
|
1037
|
+
if (args.reviewExecutionProfile?.retry?.resubmit?.enabled !== true) {
|
|
1038
|
+
return false;
|
|
1039
|
+
}
|
|
1040
|
+
const outputFormat = args.dispatch.output_format;
|
|
1041
|
+
const routing = outputFormat
|
|
1042
|
+
? RESUBMIT_UNIT_ROUTING[outputFormat]
|
|
1043
|
+
: undefined;
|
|
1044
|
+
if (!routing || !routing.gateEligible)
|
|
1045
|
+
return false;
|
|
1046
|
+
const message = args.error instanceof Error ? args.error.message : String(args.error);
|
|
1047
|
+
return routing.classify(message) !== null;
|
|
1048
|
+
}
|
|
1005
1049
|
function retryTimeoutMs(baseTimeoutMs, attempt) {
|
|
1006
1050
|
const multiplier = attempt + 1;
|
|
1007
1051
|
const expanded = baseTimeoutMs * multiplier;
|
|
@@ -1009,6 +1053,369 @@ function retryTimeoutMs(baseTimeoutMs, attempt) {
|
|
|
1009
1053
|
? Math.floor(expanded)
|
|
1010
1054
|
: baseTimeoutMs;
|
|
1011
1055
|
}
|
|
1056
|
+
/**
|
|
1057
|
+
* 설계 B: 리뷰 fan-out 풀(lens/stance)의 dispatch breaker (opt-in,
|
|
1058
|
+
* `review.execution.retry.dispatch_breaker`). 리뷰는 per-unit bounded retry가
|
|
1059
|
+
* 이미 있으므로 backoff 재시도는 얹지 않는다(규칙 1은 기존 유닛 재시도
|
|
1060
|
+
* 예산으로 충족; policy의 backoff_* 필드는 리뷰 배선에서 미소비) — 최종
|
|
1061
|
+
* outcome 기록 + 계통 임계 감지 + 배치 halt + 미완료 아티팩트만 추가한다.
|
|
1062
|
+
* OFF(기본) = 현행 halt/배리어 동작 보존.
|
|
1063
|
+
*/
|
|
1064
|
+
function reviewDispatchBreakerFromProfile(profile, args) {
|
|
1065
|
+
const policy = profile?.retry?.dispatch_breaker;
|
|
1066
|
+
// Review lens/stance pools can be Promise.all fan-outs, so completion order
|
|
1067
|
+
// must not decide poison-vs-outage recovery classification. This is
|
|
1068
|
+
// runtime-owned; it is deliberately not a user-facing settings key.
|
|
1069
|
+
return policy?.enabled === true
|
|
1070
|
+
? new DispatchBreakerState({ ...policy, concurrent: args.concurrent })
|
|
1071
|
+
: null;
|
|
1072
|
+
}
|
|
1073
|
+
/** 리뷰 경로는 `invokeExecutor` 직행이라 dispatch 마커가 없다 — 최종
|
|
1074
|
+
* failure.message 기반 분류를 쓴다. stderr 기반이라 content-derived 오분류
|
|
1075
|
+
* 리스크는 낮으나 잔여 리스크로 기록 (handoff §3.2). */
|
|
1076
|
+
function reviewSystemicFailureClassFromOutcome(outcome) {
|
|
1077
|
+
return classifySystemicDispatchFailure(outcome.failure?.message);
|
|
1078
|
+
}
|
|
1079
|
+
/** §4-1: record a nested-pool unit's FINAL outcome to the dispatch breaker.
|
|
1080
|
+
*
|
|
1081
|
+
* A batch-window SUCCESS (`nestedBatchWindow` + success) is recorded as skipped:
|
|
1082
|
+
* completed for the recovery set, but no proof the provider lane is alive THIS
|
|
1083
|
+
* run, so it must not reset a systemic streak. A FAILURE — whether from the
|
|
1084
|
+
* batch window or a real flat dispatch — is classified like the flat path
|
|
1085
|
+
* (item-local → dead-letter, systemic → recovery victim); it is never skipped
|
|
1086
|
+
* (교차검증 F2/Finding A: skipping a failure mis-labels it completed and drops
|
|
1087
|
+
* it from the recovery set, diverging from the flat path). A real flat success
|
|
1088
|
+
* drives `recordItemSuccess`. Returns the trip state on the threshold crossing so
|
|
1089
|
+
* the caller can freeze halt attribution to it. */
|
|
1090
|
+
export function recordNestedUnitOutcomeToBreaker(breaker, outcome) {
|
|
1091
|
+
if (outcome.nestedBatchWindow && outcome.success) {
|
|
1092
|
+
breaker.recordItemSkipped(outcome.dispatch.unit_id);
|
|
1093
|
+
return null;
|
|
1094
|
+
}
|
|
1095
|
+
if (outcome.success) {
|
|
1096
|
+
breaker.recordItemSuccess(outcome.dispatch.unit_id);
|
|
1097
|
+
return null;
|
|
1098
|
+
}
|
|
1099
|
+
return breaker.recordItemFailure({
|
|
1100
|
+
item_id: outcome.dispatch.unit_id,
|
|
1101
|
+
failure_class: reviewSystemicFailureClassFromOutcome(outcome),
|
|
1102
|
+
failure_message: outcome.failure?.message ?? "unknown error",
|
|
1103
|
+
attempt_count: outcome.attemptCount ?? 1,
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
/** halt_reason vocabulary for a review-side breaker trip — the prefix is the
|
|
1107
|
+
* grep/consumer key (reconstruct의 DispatchBreakerTrippedError 문구와 동형). */
|
|
1108
|
+
export const REVIEW_DISPATCH_BREAKER_HALT_REASON_PREFIX = "dispatch_breaker";
|
|
1109
|
+
function reviewDispatchBreakerHaltReason(trip, incompleteArtifactPath) {
|
|
1110
|
+
return `${REVIEW_DISPATCH_BREAKER_HALT_REASON_PREFIX}: ${trip.failure_class} failed ${trip.consecutive_item_count} consecutive units (threshold ${trip.threshold}) — batch halted, incomplete units persisted for exact re-dispatch (${incompleteArtifactPath})`;
|
|
1111
|
+
}
|
|
1112
|
+
/** 규칙 6 관측 상시화 + 규칙 5 정확 재디스패치 집합: breaker-ON 배치는
|
|
1113
|
+
* 트립이든 완주든 end-state를 리뷰 세션 루트에 영속한다 (reconstruct의
|
|
1114
|
+
* persistDispatchIncompleteArtifact 동형; 경로는 breaker 모듈이 단일소스). */
|
|
1115
|
+
async function persistReviewDispatchIncompleteArtifact(args) {
|
|
1116
|
+
const artifactPath = dispatchIncompleteArtifactPath(args.sessionRoot);
|
|
1117
|
+
await writeYamlDocument(artifactPath, buildDispatchIncompleteArtifact({
|
|
1118
|
+
pipeline: "review",
|
|
1119
|
+
batchLabel: args.batchLabel,
|
|
1120
|
+
createdAt: isoFromTimestamp(Date.now()),
|
|
1121
|
+
plannedItemIds: args.plannedItemIds,
|
|
1122
|
+
state: args.state,
|
|
1123
|
+
}));
|
|
1124
|
+
return artifactPath;
|
|
1125
|
+
}
|
|
1126
|
+
/**
|
|
1127
|
+
* 설계 A (bounded resubmit): before the next retry of an issue-stance unit
|
|
1128
|
+
* whose submit was rejected by the `issue_evidence_refs` whitelist, inject
|
|
1129
|
+
* the error spec into the unit's packet so the retry is a corrective
|
|
1130
|
+
* resubmit instead of a blind re-run. The retry budget itself is unchanged
|
|
1131
|
+
* (`issue_artifact_max_retries`); only the packet content differs. Opt-in
|
|
1132
|
+
* via `review.execution.retry.resubmit.enabled`; returns true when a spec
|
|
1133
|
+
* was applied.
|
|
1134
|
+
*/
|
|
1135
|
+
async function applyStanceResubmitErrorSpec(args) {
|
|
1136
|
+
if (args.reviewExecutionProfile?.retry?.resubmit?.enabled !== true) {
|
|
1137
|
+
return false;
|
|
1138
|
+
}
|
|
1139
|
+
if (args.dispatch.output_format !== "issue-stance-response")
|
|
1140
|
+
return false;
|
|
1141
|
+
const violation = classifyUnsupportedEvidenceRefFailure(args.error instanceof Error ? args.error.message : String(args.error)) ?? (await readFrozenUnsupportedRefViolation(args.dispatch.output_path));
|
|
1142
|
+
if (!violation)
|
|
1143
|
+
return false;
|
|
1144
|
+
let packetText;
|
|
1145
|
+
try {
|
|
1146
|
+
packetText = await fs.readFile(args.dispatch.packet_path, "utf8");
|
|
1147
|
+
}
|
|
1148
|
+
catch {
|
|
1149
|
+
return false;
|
|
1150
|
+
}
|
|
1151
|
+
let allowedRefs = [];
|
|
1152
|
+
try {
|
|
1153
|
+
const context = parseRuntimeIssueStanceSchemaContext(packetText);
|
|
1154
|
+
allowedRefs = context.issue_evidence_refs[violation.issueId] ?? [];
|
|
1155
|
+
}
|
|
1156
|
+
catch {
|
|
1157
|
+
allowedRefs = [];
|
|
1158
|
+
}
|
|
1159
|
+
const resubmitAttempt = args.attempt + 1;
|
|
1160
|
+
await fs.writeFile(args.dispatch.packet_path, applyResubmitErrorSpecToPacket(packetText, buildResubmitErrorSpec({
|
|
1161
|
+
violation,
|
|
1162
|
+
allowedEvidenceRefs: allowedRefs,
|
|
1163
|
+
resubmitAttempt,
|
|
1164
|
+
})), "utf8");
|
|
1165
|
+
await appendExecutionProgress(args.errorLogPath, `runner stance resubmit: ${args.dispatch.unit_id}`, [
|
|
1166
|
+
`resubmit_attempt: ${resubmitAttempt}`,
|
|
1167
|
+
`issue_id: ${violation.issueId}`,
|
|
1168
|
+
`unsupported_ref: ${violation.evidenceRef}`,
|
|
1169
|
+
]);
|
|
1170
|
+
return true;
|
|
1171
|
+
}
|
|
1172
|
+
/** Worker adapters exit before stderr reliably carries the validation text;
|
|
1173
|
+
* the per-attempt frozen salvage input is the structural evidence source. */
|
|
1174
|
+
async function readFrozenUnsupportedRefViolation(outputPath) {
|
|
1175
|
+
try {
|
|
1176
|
+
const raw = await fs.readFile(salvageInputPathFor(outputPath), "utf8");
|
|
1177
|
+
const frozen = JSON.parse(raw);
|
|
1178
|
+
return typeof frozen.error === "string"
|
|
1179
|
+
? classifyUnsupportedEvidenceRefFailure(frozen.error)
|
|
1180
|
+
: null;
|
|
1181
|
+
}
|
|
1182
|
+
catch {
|
|
1183
|
+
return null;
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
export const RESUBMIT_UNIT_ROUTING = Object.freeze({
|
|
1187
|
+
"issue-stance-response": {
|
|
1188
|
+
classify: classifyUnsupportedEvidenceRefFailure,
|
|
1189
|
+
apply: applyStanceResubmitErrorSpec,
|
|
1190
|
+
gateEligible: true,
|
|
1191
|
+
},
|
|
1192
|
+
"issue-deliberation-response": {
|
|
1193
|
+
classify: classifyDeliberationUnsupportedEvidenceRefFailure,
|
|
1194
|
+
apply: applyDeliberationResubmitErrorSpec,
|
|
1195
|
+
gateEligible: true,
|
|
1196
|
+
},
|
|
1197
|
+
"issue-synthesis-response": {
|
|
1198
|
+
classify: classifySynthesisUnsupportedSourceRefFailure,
|
|
1199
|
+
apply: applySynthesisResubmitErrorSpec,
|
|
1200
|
+
gateEligible: true,
|
|
1201
|
+
},
|
|
1202
|
+
});
|
|
1203
|
+
/**
|
|
1204
|
+
* 설계 A / §4-6a / §4-2c: unit-agnostic entry for bounded resubmit error-spec
|
|
1205
|
+
* injection. Routes by `output_format` through the shared routing table to the
|
|
1206
|
+
* per-unit strategy; unrouted formats (ledgers, lenses, …) are a no-op, so the
|
|
1207
|
+
* retry stays blind for units without a classifiable, spec-correctable
|
|
1208
|
+
* validation rejection. The opt-in gate is re-checked here so OFF returns before
|
|
1209
|
+
* any per-unit work.
|
|
1210
|
+
*/
|
|
1211
|
+
export async function applyResubmitErrorSpec(args) {
|
|
1212
|
+
if (args.reviewExecutionProfile?.retry?.resubmit?.enabled !== true) {
|
|
1213
|
+
return false;
|
|
1214
|
+
}
|
|
1215
|
+
const outputFormat = args.dispatch.output_format;
|
|
1216
|
+
const routing = outputFormat
|
|
1217
|
+
? RESUBMIT_UNIT_ROUTING[outputFormat]
|
|
1218
|
+
: undefined;
|
|
1219
|
+
const applied = routing ? await routing.apply(args) : false;
|
|
1220
|
+
if (applied && args.executionPlan !== undefined) {
|
|
1221
|
+
await refreshManifestPacketHash({
|
|
1222
|
+
executionPlan: args.executionPlan,
|
|
1223
|
+
packetPath: args.dispatch.packet_path,
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
1226
|
+
return applied;
|
|
1227
|
+
}
|
|
1228
|
+
/** K2 (20260712-stance-ref-vocabulary-unification-design.md §5): re-pin the
|
|
1229
|
+
* manifest packet_sha256 after a legitimate runtime-owned packet mutation
|
|
1230
|
+
* (resubmit error spec). No-op when the packet has no manifest ref yet. */
|
|
1231
|
+
async function refreshManifestPacketHash(args) {
|
|
1232
|
+
let manifestPath;
|
|
1233
|
+
let manifest;
|
|
1234
|
+
try {
|
|
1235
|
+
({ manifestPath, manifest } = await readReviewContextManifest(args.executionPlan));
|
|
1236
|
+
}
|
|
1237
|
+
catch {
|
|
1238
|
+
// No materialized manifest (e.g. unit-scoped test harnesses) → nothing is
|
|
1239
|
+
// pinned, so there is nothing to re-pin.
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
const resolvedPacketPath = path.resolve(args.packetPath);
|
|
1243
|
+
const entry = manifest.packet_refs.find((ref) => path.resolve(ref.packet_ref) === resolvedPacketPath);
|
|
1244
|
+
if (!entry)
|
|
1245
|
+
return;
|
|
1246
|
+
const packetSha256 = await optionalFileDigest(args.packetPath);
|
|
1247
|
+
if (!packetSha256 || packetSha256 === entry.packet_sha256)
|
|
1248
|
+
return;
|
|
1249
|
+
const updatedManifest = {
|
|
1250
|
+
...manifest,
|
|
1251
|
+
packet_refs: manifest.packet_refs.map((ref) => path.resolve(ref.packet_ref) === resolvedPacketPath
|
|
1252
|
+
? { ...ref, packet_sha256: packetSha256 }
|
|
1253
|
+
: ref),
|
|
1254
|
+
};
|
|
1255
|
+
await writeYamlDocument(manifestPath, updatedManifest);
|
|
1256
|
+
}
|
|
1257
|
+
/** deliberation unit_id is `deliberation:<issueId>:<lensId>` (the live colon
|
|
1258
|
+
* form built by buildIssueScopedLensDeliberationPrompt); mirrors the split
|
|
1259
|
+
* convention in haltLensIdFromOutcome. */
|
|
1260
|
+
function deliberationUnitIdParts(unitId) {
|
|
1261
|
+
if (!unitId.startsWith("deliberation:"))
|
|
1262
|
+
return null;
|
|
1263
|
+
const [, issueId, lensId] = unitId.split(":");
|
|
1264
|
+
if (!issueId || !lensId)
|
|
1265
|
+
return null;
|
|
1266
|
+
return { issueId, lensId };
|
|
1267
|
+
}
|
|
1268
|
+
/**
|
|
1269
|
+
* §4-6a deliberation strategy: inject the evidence_refs error spec before the
|
|
1270
|
+
* next retry of a deliberation-response unit whose submit was rejected by the
|
|
1271
|
+
* `allowed_evidence_refs` whitelist. The deliberation throw carries only the
|
|
1272
|
+
* ref, so issue_id/lens_id come from the dispatch unit_id and the allowed set
|
|
1273
|
+
* from the packet's runtime projection (flat, single-(issue,lens) scope).
|
|
1274
|
+
* Cap exhaustion keeps deliberation's existing non-halting degrade
|
|
1275
|
+
* (completeUnavailableDeliberationResponseUnit) — no demotion machinery.
|
|
1276
|
+
*/
|
|
1277
|
+
async function applyDeliberationResubmitErrorSpec(args) {
|
|
1278
|
+
if (args.reviewExecutionProfile?.retry?.resubmit?.enabled !== true) {
|
|
1279
|
+
return false;
|
|
1280
|
+
}
|
|
1281
|
+
if (args.dispatch.output_format !== "issue-deliberation-response") {
|
|
1282
|
+
return false;
|
|
1283
|
+
}
|
|
1284
|
+
const violation = classifyDeliberationUnsupportedEvidenceRefFailure(args.error instanceof Error ? args.error.message : String(args.error)) ??
|
|
1285
|
+
(await readFrozenDeliberationUnsupportedRefViolation(args.dispatch.output_path));
|
|
1286
|
+
if (!violation)
|
|
1287
|
+
return false;
|
|
1288
|
+
const parts = deliberationUnitIdParts(args.dispatch.unit_id);
|
|
1289
|
+
if (!parts)
|
|
1290
|
+
return false;
|
|
1291
|
+
let packetText;
|
|
1292
|
+
try {
|
|
1293
|
+
packetText = await fs.readFile(args.dispatch.packet_path, "utf8");
|
|
1294
|
+
}
|
|
1295
|
+
catch {
|
|
1296
|
+
return false;
|
|
1297
|
+
}
|
|
1298
|
+
let allowedRefs = [];
|
|
1299
|
+
try {
|
|
1300
|
+
allowedRefs =
|
|
1301
|
+
parseRuntimeIssueDeliberationSchemaContext(packetText).allowed_evidence_refs;
|
|
1302
|
+
}
|
|
1303
|
+
catch {
|
|
1304
|
+
allowedRefs = [];
|
|
1305
|
+
}
|
|
1306
|
+
const resubmitAttempt = args.attempt + 1;
|
|
1307
|
+
await fs.writeFile(args.dispatch.packet_path, applyResubmitErrorSpecToPacket(packetText, buildResubmitErrorSpec({
|
|
1308
|
+
violation: {
|
|
1309
|
+
stanceIndex: null,
|
|
1310
|
+
issueId: parts.issueId,
|
|
1311
|
+
evidenceRef: violation.evidenceRef,
|
|
1312
|
+
},
|
|
1313
|
+
allowedEvidenceRefs: allowedRefs,
|
|
1314
|
+
resubmitAttempt,
|
|
1315
|
+
unit: { kind: "deliberation", lensId: parts.lensId },
|
|
1316
|
+
})), "utf8");
|
|
1317
|
+
await appendExecutionProgress(args.errorLogPath, `runner deliberation resubmit: ${args.dispatch.unit_id}`, [
|
|
1318
|
+
`resubmit_attempt: ${resubmitAttempt}`,
|
|
1319
|
+
`issue_id: ${parts.issueId}`,
|
|
1320
|
+
`lens_id: ${parts.lensId}`,
|
|
1321
|
+
`unsupported_ref: ${violation.evidenceRef}`,
|
|
1322
|
+
]);
|
|
1323
|
+
return true;
|
|
1324
|
+
}
|
|
1325
|
+
/** Deliberation counterpart of readFrozenUnsupportedRefViolation: the salvage
|
|
1326
|
+
* freeze is output_format-agnostic, so a deliberation submit rejection is
|
|
1327
|
+
* recoverable from the frozen error even when the adapter swallowed stderr. */
|
|
1328
|
+
async function readFrozenDeliberationUnsupportedRefViolation(outputPath) {
|
|
1329
|
+
try {
|
|
1330
|
+
const raw = await fs.readFile(salvageInputPathFor(outputPath), "utf8");
|
|
1331
|
+
const frozen = JSON.parse(raw);
|
|
1332
|
+
return typeof frozen.error === "string"
|
|
1333
|
+
? classifyDeliberationUnsupportedEvidenceRefFailure(frozen.error)
|
|
1334
|
+
: null;
|
|
1335
|
+
}
|
|
1336
|
+
catch {
|
|
1337
|
+
return null;
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
/** synthesis unit_id is `synthesis:<issueId>` (workItem.work_item_id, built in
|
|
1341
|
+
* synthesis-map-reduce as `synthesis:${issue_id}`); the source_refs_used
|
|
1342
|
+
* rejection text carries no issue_id, so it is recovered here. */
|
|
1343
|
+
function synthesisIssueIdFromUnitId(unitId) {
|
|
1344
|
+
if (!unitId.startsWith("synthesis:"))
|
|
1345
|
+
return null;
|
|
1346
|
+
const issueId = unitId.slice("synthesis:".length);
|
|
1347
|
+
return issueId.length > 0 ? issueId : null;
|
|
1348
|
+
}
|
|
1349
|
+
/**
|
|
1350
|
+
* §4-2c/2-A synthesis strategy: inject the source_refs_used error spec before
|
|
1351
|
+
* the next retry of a synthesis-response unit whose submit was rejected by the
|
|
1352
|
+
* `allowed_source_refs` whitelist (bad ref) or the "must include at least one"
|
|
1353
|
+
* guard. issue_id comes from the dispatch unit_id, the allowed set from the
|
|
1354
|
+
* packet's runtime projection. Cap exhaustion keeps synthesis's existing
|
|
1355
|
+
* non-halting degrade (completeUnavailableSynthesisResponseUnit) — no demotion
|
|
1356
|
+
* machinery. In-loop retry reachability depends on the structural retry gate
|
|
1357
|
+
* (shouldRetryUnitFailure), because the synthesis rejection message always
|
|
1358
|
+
* substring-classifies as output_contract.
|
|
1359
|
+
*/
|
|
1360
|
+
async function applySynthesisResubmitErrorSpec(args) {
|
|
1361
|
+
if (args.reviewExecutionProfile?.retry?.resubmit?.enabled !== true) {
|
|
1362
|
+
return false;
|
|
1363
|
+
}
|
|
1364
|
+
if (args.dispatch.output_format !== "issue-synthesis-response")
|
|
1365
|
+
return false;
|
|
1366
|
+
const violation = classifySynthesisUnsupportedSourceRefFailure(args.error instanceof Error ? args.error.message : String(args.error)) ??
|
|
1367
|
+
(await readFrozenSynthesisUnsupportedRefViolation(args.dispatch.output_path));
|
|
1368
|
+
if (!violation)
|
|
1369
|
+
return false;
|
|
1370
|
+
const issueId = synthesisIssueIdFromUnitId(args.dispatch.unit_id);
|
|
1371
|
+
if (!issueId)
|
|
1372
|
+
return false;
|
|
1373
|
+
let packetText;
|
|
1374
|
+
try {
|
|
1375
|
+
packetText = await fs.readFile(args.dispatch.packet_path, "utf8");
|
|
1376
|
+
}
|
|
1377
|
+
catch {
|
|
1378
|
+
return false;
|
|
1379
|
+
}
|
|
1380
|
+
let allowedRefs = [];
|
|
1381
|
+
try {
|
|
1382
|
+
allowedRefs =
|
|
1383
|
+
parseRuntimeIssueSynthesisSchemaContext(packetText).allowed_source_refs;
|
|
1384
|
+
}
|
|
1385
|
+
catch {
|
|
1386
|
+
allowedRefs = [];
|
|
1387
|
+
}
|
|
1388
|
+
const resubmitAttempt = args.attempt + 1;
|
|
1389
|
+
await fs.writeFile(args.dispatch.packet_path, applyResubmitErrorSpecToPacket(packetText, buildResubmitErrorSpec({
|
|
1390
|
+
violation: {
|
|
1391
|
+
stanceIndex: null,
|
|
1392
|
+
issueId,
|
|
1393
|
+
evidenceRef: violation.sourceRef ?? "",
|
|
1394
|
+
},
|
|
1395
|
+
allowedEvidenceRefs: allowedRefs,
|
|
1396
|
+
resubmitAttempt,
|
|
1397
|
+
unit: { kind: "synthesis", issueId },
|
|
1398
|
+
})), "utf8");
|
|
1399
|
+
await appendExecutionProgress(args.errorLogPath, `runner synthesis resubmit: ${args.dispatch.unit_id}`, [
|
|
1400
|
+
`resubmit_attempt: ${resubmitAttempt}`,
|
|
1401
|
+
`issue_id: ${issueId}`,
|
|
1402
|
+
`unsupported_ref: ${violation.sourceRef ?? "(none — must cite >=1 allowed source ref)"}`,
|
|
1403
|
+
]);
|
|
1404
|
+
return true;
|
|
1405
|
+
}
|
|
1406
|
+
/** Synthesis counterpart of readFrozenUnsupportedRefViolation. */
|
|
1407
|
+
async function readFrozenSynthesisUnsupportedRefViolation(outputPath) {
|
|
1408
|
+
try {
|
|
1409
|
+
const raw = await fs.readFile(salvageInputPathFor(outputPath), "utf8");
|
|
1410
|
+
const frozen = JSON.parse(raw);
|
|
1411
|
+
return typeof frozen.error === "string"
|
|
1412
|
+
? classifySynthesisUnsupportedSourceRefFailure(frozen.error)
|
|
1413
|
+
: null;
|
|
1414
|
+
}
|
|
1415
|
+
catch {
|
|
1416
|
+
return null;
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1012
1419
|
function parseExecutorRunMetadata(stdout) {
|
|
1013
1420
|
const trimmed = stdout.trim();
|
|
1014
1421
|
if (!trimmed.startsWith("{") || !trimmed.endsWith("}"))
|
|
@@ -1358,7 +1765,10 @@ extraArgs = []) {
|
|
|
1358
1765
|
});
|
|
1359
1766
|
return parseExecutorRunMetadata(stdout);
|
|
1360
1767
|
}
|
|
1361
|
-
|
|
1768
|
+
// Exported for the resubmit-marker falsifiability tests (review-cert/v2
|
|
1769
|
+
// §5.3): the outcome→unit-result projection is where the marker would be
|
|
1770
|
+
// silently dropped if the wiring regressed.
|
|
1771
|
+
export function toUnitExecutionResult(outcome) {
|
|
1362
1772
|
if (outcome.preservedResult) {
|
|
1363
1773
|
return normalizePreservedUnitExecutionResult(outcome.preservedResult);
|
|
1364
1774
|
}
|
|
@@ -1379,6 +1789,7 @@ function toUnitExecutionResult(outcome) {
|
|
|
1379
1789
|
...(outcome.attemptCount !== undefined
|
|
1380
1790
|
? { attempt_count: outcome.attemptCount }
|
|
1381
1791
|
: {}),
|
|
1792
|
+
...(outcome.resubmitApplied ? { resubmit_applied: true } : {}),
|
|
1382
1793
|
...(outcome.packetBytes !== undefined
|
|
1383
1794
|
? { packet_bytes: outcome.packetBytes }
|
|
1384
1795
|
: {}),
|
|
@@ -1656,6 +2067,7 @@ function inferFailureLensId(artifact, result) {
|
|
|
1656
2067
|
return null;
|
|
1657
2068
|
}
|
|
1658
2069
|
function collectFailedUnits(artifact) {
|
|
2070
|
+
const includeAttemptCount = artifact.retry_policy?.resubmit?.enabled === true;
|
|
1659
2071
|
return allUnitExecutionResults(artifact)
|
|
1660
2072
|
.filter((result) => result.status === "failed")
|
|
1661
2073
|
.map((result) => ({
|
|
@@ -1666,6 +2078,9 @@ function collectFailedUnits(artifact) {
|
|
|
1666
2078
|
output_path: result.output_path,
|
|
1667
2079
|
failure_kind: result.failure_kind ?? null,
|
|
1668
2080
|
failure_message: result.failure_message ?? "unknown failure",
|
|
2081
|
+
...(includeAttemptCount && result.attempt_count !== undefined
|
|
2082
|
+
? { attempt_count: result.attempt_count }
|
|
2083
|
+
: {}),
|
|
1669
2084
|
}));
|
|
1670
2085
|
}
|
|
1671
2086
|
function degradationKindsFor(artifact, failedUnits) {
|
|
@@ -2427,6 +2842,9 @@ async function resetExecutionOutputs(executionPlan) {
|
|
|
2427
2842
|
executionPlan.final_output_path,
|
|
2428
2843
|
executionPlan.lens_completion_barrier_path ??
|
|
2429
2844
|
path.join(executionPlan.session_root, "lens-completion-barrier.yaml"),
|
|
2845
|
+
// 설계 B: 이전 run의 breaker end-state는 폐기되는 run의 기록이다 —
|
|
2846
|
+
// 남기면 fresh 재실행(특히 breaker OFF)이 낡은 트립/회복 집합을 주장한다.
|
|
2847
|
+
dispatchIncompleteArtifactPath(executionPlan.session_root),
|
|
2430
2848
|
executionPlan.teamlead_deliberation_prompt_packet_path,
|
|
2431
2849
|
...executionPlan.lens_execution_seats.map((seat) => seat.output_path),
|
|
2432
2850
|
...executionPlan.lens_execution_seats
|
|
@@ -2511,8 +2929,23 @@ async function runSingleDispatchWithRetries(args) {
|
|
|
2511
2929
|
dispatch,
|
|
2512
2930
|
fallback: unitTimeoutMs,
|
|
2513
2931
|
});
|
|
2932
|
+
let resubmitApplied = false;
|
|
2514
2933
|
for (let attempt = 0; attempt <= effectiveMaxRetries; attempt += 1) {
|
|
2515
2934
|
attemptsUsed = attempt + 1;
|
|
2935
|
+
if (attempt === 0) {
|
|
2936
|
+
// 설계 A: nested-batch 1차 시도 실패의 flat 폴백과 halted 세션 resume은
|
|
2937
|
+
// 이 루프 밖에서 실패해 frozen salvage input만 남는다 — 그 구조적
|
|
2938
|
+
// 근거가 있으면 첫 flat 시도 전에 오류 명세를 주입해 blind 재실행을
|
|
2939
|
+
// 막는다. (스위치 OFF·미지원 output_format·freeze 부재 시 no-op)
|
|
2940
|
+
resubmitApplied = (await applyResubmitErrorSpec({
|
|
2941
|
+
dispatch,
|
|
2942
|
+
error: null,
|
|
2943
|
+
attempt: 0,
|
|
2944
|
+
reviewExecutionProfile,
|
|
2945
|
+
errorLogPath: executionPlan.error_log_path,
|
|
2946
|
+
executionPlan,
|
|
2947
|
+
})) || resubmitApplied;
|
|
2948
|
+
}
|
|
2516
2949
|
try {
|
|
2517
2950
|
const executorMetadata = await invokeExecutor(executorConfig, projectRoot, sessionRoot, executionPlan, dispatch, retryTimeoutMs(effectiveUnitTimeoutMs, attempt), reviewExecutionProfile);
|
|
2518
2951
|
const completedAtMs = Date.now();
|
|
@@ -2528,6 +2961,7 @@ async function runSingleDispatchWithRetries(args) {
|
|
|
2528
2961
|
startedAtMs,
|
|
2529
2962
|
completedAtMs,
|
|
2530
2963
|
attemptCount: attempt + 1,
|
|
2964
|
+
...(resubmitApplied ? { resubmitApplied: true } : {}),
|
|
2531
2965
|
...(executorMetadata !== undefined ? { executorMetadata } : {}),
|
|
2532
2966
|
artifactGenerationRealization: executorMetadata?.artifact_generation_realization ??
|
|
2533
2967
|
artifactGenerationRealization,
|
|
@@ -2538,7 +2972,7 @@ async function runSingleDispatchWithRetries(args) {
|
|
|
2538
2972
|
}
|
|
2539
2973
|
catch (error) {
|
|
2540
2974
|
lastError = error;
|
|
2541
|
-
if (shouldRetryUnitFailure({ error, attempt, maxRetries: effectiveMaxRetries })) {
|
|
2975
|
+
if (shouldRetryUnitFailure({ error, attempt, maxRetries: effectiveMaxRetries, dispatch, reviewExecutionProfile })) {
|
|
2542
2976
|
const retryDelay = effectiveRetryInitialDelayMs * (attempt + 1);
|
|
2543
2977
|
console.log(`[review runner] ${dispatch.unit_id} attempt ${attempt + 1} failed, retrying in ${retryDelay}ms...`);
|
|
2544
2978
|
await appendExecutionProgress(executionPlan.error_log_path, `runner dispatch retry: ${dispatch.unit_id}`, [
|
|
@@ -2546,6 +2980,14 @@ async function runSingleDispatchWithRetries(args) {
|
|
|
2546
2980
|
`retry_delay_ms: ${retryDelay}`,
|
|
2547
2981
|
`error: ${error instanceof Error ? error.message.slice(0, 200) : String(error).slice(0, 200)}`,
|
|
2548
2982
|
]);
|
|
2983
|
+
resubmitApplied = (await applyResubmitErrorSpec({
|
|
2984
|
+
dispatch,
|
|
2985
|
+
error,
|
|
2986
|
+
attempt,
|
|
2987
|
+
reviewExecutionProfile,
|
|
2988
|
+
errorLogPath: executionPlan.error_log_path,
|
|
2989
|
+
executionPlan,
|
|
2990
|
+
})) || resubmitApplied;
|
|
2549
2991
|
if (dispatch.unit_kind === "synthesize") {
|
|
2550
2992
|
await appendExecutionProgress(executionPlan.error_log_path, `runner synthesize retry: ${dispatch.unit_id}`, [
|
|
2551
2993
|
`attempt: ${attempt + 1}/${effectiveMaxRetries}`,
|
|
@@ -2555,7 +2997,7 @@ async function runSingleDispatchWithRetries(args) {
|
|
|
2555
2997
|
}
|
|
2556
2998
|
await sleep(retryDelay);
|
|
2557
2999
|
}
|
|
2558
|
-
if (!shouldRetryUnitFailure({ error, attempt, maxRetries: effectiveMaxRetries }))
|
|
3000
|
+
if (!shouldRetryUnitFailure({ error, attempt, maxRetries: effectiveMaxRetries, dispatch, reviewExecutionProfile }))
|
|
2559
3001
|
break;
|
|
2560
3002
|
}
|
|
2561
3003
|
}
|
|
@@ -2624,6 +3066,7 @@ async function runSingleDispatchWithRetries(args) {
|
|
|
2624
3066
|
completedAtMs: Date.now(),
|
|
2625
3067
|
attemptCount: attemptsUsed + 1,
|
|
2626
3068
|
recovery: "salvaged_submit",
|
|
3069
|
+
...(resubmitApplied ? { resubmitApplied: true } : {}),
|
|
2627
3070
|
childOutcomes: [failedOutcome],
|
|
2628
3071
|
...(executorMetadata !== undefined ? { executorMetadata } : {}),
|
|
2629
3072
|
artifactGenerationRealization: executorMetadata?.artifact_generation_realization ??
|
|
@@ -2651,6 +3094,7 @@ async function runSingleDispatchWithRetries(args) {
|
|
|
2651
3094
|
startedAtMs,
|
|
2652
3095
|
completedAtMs,
|
|
2653
3096
|
attemptCount: attemptsUsed,
|
|
3097
|
+
...(resubmitApplied ? { resubmitApplied: true } : {}),
|
|
2654
3098
|
packetBytes,
|
|
2655
3099
|
outputBytes,
|
|
2656
3100
|
failure,
|
|
@@ -2767,6 +3211,12 @@ export async function runNestedStageFirstAttempt(args) {
|
|
|
2767
3211
|
* - batch fail + remaining budget → flat retries (effective - 1);
|
|
2768
3212
|
* - batch fail + zero budget → finalize the failure (no extra attempt —
|
|
2769
3213
|
* an explicit zero-retry policy means exactly one attempt).
|
|
3214
|
+
*
|
|
3215
|
+
* The two batch-window branches (batch ok, zero-budget fail) carry
|
|
3216
|
+
* `nestedBatchWindow: true` marking them as not directly observed. A breaker
|
|
3217
|
+
* skips a batch-window SUCCESS (completed, no streak reset) but still records a
|
|
3218
|
+
* batch-window FAILURE as a failure (see recordNestedUnitOutcomeToBreaker) —
|
|
3219
|
+
* the flat-retry branch is a real dispatch and stays untagged (§4-1).
|
|
2770
3220
|
*/
|
|
2771
3221
|
export async function unitOutcomeWithNestedFirstAttempt(args) {
|
|
2772
3222
|
const runFlat = args.runFlat ?? runSingleDispatchWithRetries;
|
|
@@ -2791,6 +3241,7 @@ export async function unitOutcomeWithNestedFirstAttempt(args) {
|
|
|
2791
3241
|
startedAtMs: args.batch.startedAtMs,
|
|
2792
3242
|
completedAtMs: args.batch.completedAtMs,
|
|
2793
3243
|
attemptCount: 1,
|
|
3244
|
+
nestedBatchWindow: true,
|
|
2794
3245
|
artifactGenerationRealization,
|
|
2795
3246
|
semanticQualityEvidence,
|
|
2796
3247
|
packetBytes: await fileSizeIfPresent(dispatch.packet_path),
|
|
@@ -2825,6 +3276,7 @@ export async function unitOutcomeWithNestedFirstAttempt(args) {
|
|
|
2825
3276
|
startedAtMs: args.batch.startedAtMs,
|
|
2826
3277
|
completedAtMs: args.batch.completedAtMs,
|
|
2827
3278
|
attemptCount: 1,
|
|
3279
|
+
nestedBatchWindow: true,
|
|
2828
3280
|
packetBytes,
|
|
2829
3281
|
outputBytes,
|
|
2830
3282
|
failure,
|
|
@@ -2880,8 +3332,14 @@ export async function executeIssueStanceUnit(args) {
|
|
|
2880
3332
|
};
|
|
2881
3333
|
await removeFileIfExists(dispatch.output_path);
|
|
2882
3334
|
await appendExecutionFailure(ctx.executionPlan.error_log_path, failure, ctx.executionPlan.effective_boundary_state);
|
|
3335
|
+
// §4-1: an on-disk validation failure is a directly-observed unit failure,
|
|
3336
|
+
// not a batch-window outcome — drop the nested-batch tag so the breaker
|
|
3337
|
+
// records it as a failure (dead-letter), consistent with the flat path.
|
|
3338
|
+
// (A batch-ok→validation-fail unit would otherwise spread
|
|
3339
|
+
// `nestedBatchWindow: true` and be mis-recorded as skipped/completed.)
|
|
3340
|
+
const { nestedBatchWindow: _batchWindow, ...observed } = outcome;
|
|
2883
3341
|
return {
|
|
2884
|
-
...
|
|
3342
|
+
...observed,
|
|
2885
3343
|
success: false,
|
|
2886
3344
|
completedAtMs: Date.now(),
|
|
2887
3345
|
outputBytes: await fileSizeIfPresent(dispatch.output_path),
|
|
@@ -3275,22 +3733,38 @@ async function runIssueStanceMatrixCollectionDispatch(args) {
|
|
|
3275
3733
|
packetPath: dispatch.packet_path,
|
|
3276
3734
|
});
|
|
3277
3735
|
}));
|
|
3736
|
+
// Continuation per-unit gate: run-owing 유닛만 디스패치 대상이다.
|
|
3737
|
+
// preserved 유닛은 완료된 prior result에서 outcome을 복원한다 (아래 워커).
|
|
3738
|
+
const owesStanceRun = (unitId) => args.runUnitIds === undefined || args.runUnitIds.has(unitId);
|
|
3739
|
+
const runOwingDispatches = dispatches.filter((dispatch) => owesStanceRun(dispatch.unit_id));
|
|
3278
3740
|
// nested-workers: attempt #1 for the whole stage goes through ONE outer
|
|
3279
3741
|
// nesting batch worker (waves capped at the flat pool width); failed
|
|
3280
|
-
// units fall back to the flat per-unit retry loop below.
|
|
3742
|
+
// units fall back to the flat per-unit retry loop below. preserved 유닛은
|
|
3743
|
+
// 배치에 넣지 않는다 — 디스패치를 빚지지 않은 유닛이다.
|
|
3281
3744
|
const stanceNestedBatch = await runNestedStageFirstAttempt({
|
|
3282
3745
|
stageLabel: "issue-stance",
|
|
3283
3746
|
projectRoot: args.projectRoot,
|
|
3284
3747
|
sessionRoot: args.sessionRoot,
|
|
3285
3748
|
executionPlan: args.executionPlan,
|
|
3286
3749
|
executorConfig: args.executorConfig,
|
|
3287
|
-
dispatches,
|
|
3750
|
+
dispatches: runOwingDispatches,
|
|
3288
3751
|
dispatchWidth: maxConcurrentIssueStanceResponses,
|
|
3289
3752
|
unitTimeoutMs: args.unitTimeoutMs,
|
|
3290
3753
|
reviewExecutionProfile: args.reviewExecutionProfile,
|
|
3291
3754
|
});
|
|
3292
3755
|
const outcomes = new Array(dispatches.length);
|
|
3293
3756
|
let nextDispatchIndex = 0;
|
|
3757
|
+
// 설계 B + §4-1: stance 풀 breaker — 유닛의 최종 outcome(per-unit bounded
|
|
3758
|
+
// retry 소진 후)을 관찰 단위로 기록한다. nested 1차 배치가 실행돼도 생성한다:
|
|
3759
|
+
// 배치-창 SUCCESS는 실 디스패치가 아니므로 recordItemSkipped로 완료만 집계해
|
|
3760
|
+
// 과거 배치 창의 생존 증거가 현재 계통 실패 streak을 오리셋하지 못하게 하고
|
|
3761
|
+
// (#166 결함 클래스 재유입 차단), 배치-창 FAILURE는 flat 경로처럼 실패로 기록
|
|
3762
|
+
// 한다(item-local→dead-letter, 계통→회복 victim). 배치-실패 유닛이 flat 재시도
|
|
3763
|
+
// 예산을 쓰면 그 실 관측이 streak을 구동한다.
|
|
3764
|
+
const breakerState = reviewDispatchBreakerFromProfile(args.reviewExecutionProfile, {
|
|
3765
|
+
concurrent: Math.min(maxConcurrentIssueStanceResponses, runOwingDispatches.length) > 1,
|
|
3766
|
+
});
|
|
3767
|
+
let breakerTripOutcome = null;
|
|
3294
3768
|
async function runIssueStanceWorker() {
|
|
3295
3769
|
while (true) {
|
|
3296
3770
|
const dispatchIndex = nextDispatchIndex;
|
|
@@ -3298,7 +3772,39 @@ async function runIssueStanceMatrixCollectionDispatch(args) {
|
|
|
3298
3772
|
if (dispatchIndex >= dispatches.length)
|
|
3299
3773
|
return;
|
|
3300
3774
|
const dispatch = dispatches[dispatchIndex];
|
|
3301
|
-
|
|
3775
|
+
if (!owesStanceRun(dispatch.unit_id)) {
|
|
3776
|
+
// preserved/continuation 유닛: 디스패치 없이 완료 증거를 복원한다.
|
|
3777
|
+
// breaker에는 기록하지 않는다 — planned 집합 자체가 run-owing
|
|
3778
|
+
// 유닛으로 계산된다 (lens 풀과 동일 규약). 트립 여부와 무관하게
|
|
3779
|
+
// 기록해야 완료 증거가 결과 아티팩트에서 유실되지 않는다.
|
|
3780
|
+
const prior = args.preservedResultsByUnitId?.get(dispatch.unit_id);
|
|
3781
|
+
if (!prior || prior.status !== "completed") {
|
|
3782
|
+
throw new Error(`Cannot preserve continuation unit without a completed prior result: ${dispatch.unit_id}`);
|
|
3783
|
+
}
|
|
3784
|
+
outcomes[dispatchIndex] = outcomeFromPreviousResult(prior);
|
|
3785
|
+
continue;
|
|
3786
|
+
}
|
|
3787
|
+
// 트립 이후엔 새 flat 디스패치를 빚지는 유닛만 건너뛴다 — 그런 유닛은
|
|
3788
|
+
// 미디스패치로 incomplete 집합에 남아 회복 재디스패치 대상이 된다(규칙 5).
|
|
3789
|
+
// 배치-성공(디스패치 안 빚음)과 zero-retry 배치-실패(예산 소진 → 새 flat
|
|
3790
|
+
// 디스패치 없음)는 트립 후에도 처리해 recordNested…로 기록·분류한다:
|
|
3791
|
+
// 미기록 시 incomplete로 오집계되고, item-local 배치-실패는 dead-letter
|
|
3792
|
+
// 여야 하는데 스킵하면 incomplete로 오분류된다(교차검증 — lens 풀과 대칭,
|
|
3793
|
+
// 계약의 nested 균일 규칙 준수). return이 아닌 continue: 뒤 인덱스
|
|
3794
|
+
// preserved 복원도 마저 소진.
|
|
3795
|
+
if (breakerState?.tripped()) {
|
|
3796
|
+
const batchOutcome = stanceNestedBatch?.byUnitId.get(dispatch.unit_id);
|
|
3797
|
+
const owesNewDispatch = batchOutcome?.ok !== true &&
|
|
3798
|
+
(batchOutcome === undefined ||
|
|
3799
|
+
maxRetriesForDispatch({
|
|
3800
|
+
profile: args.reviewExecutionProfile,
|
|
3801
|
+
dispatch,
|
|
3802
|
+
fallback: args.retryPolicy.issueArtifactMaxRetries,
|
|
3803
|
+
}) >= 1);
|
|
3804
|
+
if (owesNewDispatch)
|
|
3805
|
+
continue;
|
|
3806
|
+
}
|
|
3807
|
+
const outcome = await executeIssueStanceUnit({
|
|
3302
3808
|
ctx: {
|
|
3303
3809
|
projectRoot: args.projectRoot,
|
|
3304
3810
|
sessionRoot: args.sessionRoot,
|
|
@@ -3312,22 +3818,105 @@ async function runIssueStanceMatrixCollectionDispatch(args) {
|
|
|
3312
3818
|
participatingLensIds,
|
|
3313
3819
|
nestedBatch: stanceNestedBatch,
|
|
3314
3820
|
});
|
|
3821
|
+
outcomes[dispatchIndex] = outcome;
|
|
3822
|
+
if (breakerState) {
|
|
3823
|
+
// §4-1: 배치-창 결과는 skipped(완료만, 계통 streak 불변), 실 flat
|
|
3824
|
+
// 디스패치는 성공/실패로 반영한다. 첫 임계 도달이 트립 권위 — halt
|
|
3825
|
+
// 귀속을 그 유닛의 outcome으로 고정한다.
|
|
3826
|
+
const trip = recordNestedUnitOutcomeToBreaker(breakerState, outcome);
|
|
3827
|
+
if (trip !== null)
|
|
3828
|
+
breakerTripOutcome = outcome;
|
|
3829
|
+
}
|
|
3315
3830
|
}
|
|
3316
3831
|
}
|
|
3317
3832
|
await Promise.all(Array.from({
|
|
3318
3833
|
length: Math.min(maxConcurrentIssueStanceResponses, dispatches.length),
|
|
3319
3834
|
}, async () => runIssueStanceWorker()));
|
|
3320
3835
|
const completedOutcomes = outcomes.filter((outcome) => outcome !== undefined);
|
|
3321
|
-
const
|
|
3322
|
-
|
|
3323
|
-
|
|
3836
|
+
const failedOutcomes = completedOutcomes.filter((outcome) => !outcome.success);
|
|
3837
|
+
const resubmitEnabled = args.reviewExecutionProfile?.retry?.resubmit?.enabled === true;
|
|
3838
|
+
const stanceDispatchError = (outcome, haltReason = null, batchOutcomes = []) => {
|
|
3839
|
+
const message = outcome.failure?.message ?? "unknown error";
|
|
3840
|
+
return new ReviewIssueArtifactDispatchError(`Issue stance response failed: ${message}`, outcome, message, haltReason, batchOutcomes);
|
|
3841
|
+
};
|
|
3842
|
+
if (breakerState) {
|
|
3843
|
+
// 규칙 6: 트립이든 완주든 배치 end-state를 영속 — 회복 절차가 항상
|
|
3844
|
+
// 정확한 재디스패치 집합을 갖는다. 트립이 아니어도 아래의 기존
|
|
3845
|
+
// 강등/halt 규칙은 그대로 진행된다 (breaker는 구제하지 않는다).
|
|
3846
|
+
const incompleteArtifactPath = await persistReviewDispatchIncompleteArtifact({
|
|
3847
|
+
sessionRoot: args.sessionRoot,
|
|
3848
|
+
batchLabel: "issue-stance",
|
|
3849
|
+
// planned = 이번 run이 실제 디스패치를 빚진 유닛 집합 (preserved 제외).
|
|
3850
|
+
plannedItemIds: runOwingDispatches.map((dispatch) => dispatch.unit_id),
|
|
3851
|
+
state: breakerState,
|
|
3852
|
+
});
|
|
3853
|
+
const trip = breakerState.tripped();
|
|
3854
|
+
if (trip) {
|
|
3855
|
+
// 규칙 4: 계통 실패 임계 도달 — 배치 halt + 사용자 공지(halt_reason에
|
|
3856
|
+
// 미완료 목록 경로 포함). 설계 A의 halt 배관(4번째 인자 haltReason →
|
|
3857
|
+
// haltAfterIssueArtifactFailure → halted_partial)을 재사용한다.
|
|
3858
|
+
const haltReason = reviewDispatchBreakerHaltReason(trip, incompleteArtifactPath);
|
|
3859
|
+
await appendExecutionProgress(args.executionPlan.error_log_path, "runner issue stance dispatch breaker tripped", [
|
|
3860
|
+
`failure_class: ${trip.failure_class}`,
|
|
3861
|
+
`consecutive_unit_count: ${trip.consecutive_item_count}`,
|
|
3862
|
+
`threshold: ${trip.threshold}`,
|
|
3863
|
+
`dispatch_incomplete_path: ${incompleteArtifactPath}`,
|
|
3864
|
+
]);
|
|
3865
|
+
throw stanceDispatchError(breakerTripOutcome ?? failedOutcomes[0], haltReason,
|
|
3866
|
+
// halt 시점까지의 배치 진실 전체 — 완료 유닛의 행이 남아야 회복
|
|
3867
|
+
// 재디스패치 집합이 dispatch-incomplete의 미완료 집합과 일치한다.
|
|
3868
|
+
completedOutcomes);
|
|
3869
|
+
}
|
|
3324
3870
|
}
|
|
3871
|
+
let demotedLensIds = [];
|
|
3872
|
+
if (failedOutcomes.length > 0) {
|
|
3873
|
+
// 검증-거부 분류는 두 근거를 모두 본다: in-process 경로는 실패 메시지
|
|
3874
|
+
// (submit-시점·on-disk 검증기 양쪽 문구), worker 경로는 stderr가 검증
|
|
3875
|
+
// 문구를 보장하지 않으므로 frozen salvage input을 구조적 근거로 읽는다.
|
|
3876
|
+
const validationFailures = [];
|
|
3877
|
+
if (resubmitEnabled) {
|
|
3878
|
+
for (const outcome of failedOutcomes) {
|
|
3879
|
+
const classified = isUnsupportedEvidenceRefFailureMessage(outcome.failure?.message) ||
|
|
3880
|
+
(await readFrozenUnsupportedRefViolation(outcome.dispatch.output_path)) !== null;
|
|
3881
|
+
if (classified)
|
|
3882
|
+
validationFailures.push(outcome);
|
|
3883
|
+
}
|
|
3884
|
+
}
|
|
3885
|
+
if (resubmitEnabled &&
|
|
3886
|
+
correlatedValidationExceeded({
|
|
3887
|
+
validationFailedUnitCount: validationFailures.length,
|
|
3888
|
+
totalUnitCount: dispatches.length,
|
|
3889
|
+
})) {
|
|
3890
|
+
// 설계 A 상관 에스컬레이션: 같은 검증 클래스가 stance 유닛 과반에서
|
|
3891
|
+
// 실패하면 구조 결함(프롬프트/스키마/컨텍스트 조립)이므로 whole-run
|
|
3892
|
+
// halt를 보존한다.
|
|
3893
|
+
throw stanceDispatchError(validationFailures[0], `${CORRELATED_VALIDATION_HALT_REASON}: evidence_refs validation rejected ${validationFailures.length}/${dispatches.length} stance units`);
|
|
3894
|
+
}
|
|
3895
|
+
const demotable = resubmitEnabled && validationFailures.length === failedOutcomes.length;
|
|
3896
|
+
if (!demotable) {
|
|
3897
|
+
// 현행 승격 규칙 보존: 인프라 실패(timeout/transport/…)와 OFF 경로는
|
|
3898
|
+
// 지금처럼 whole-run halt.
|
|
3899
|
+
throw stanceDispatchError(failedOutcomes[0]);
|
|
3900
|
+
}
|
|
3901
|
+
// 설계 A 유닛 강등: resubmit cap을 소진한 검증-거부 유닛만
|
|
3902
|
+
// complete-with-failure로 남긴다. 실패 outcome은 집계 outcome의 failed
|
|
3903
|
+
// child로 유지되어 degradation-summary와 상태 강등이 자동 전파되고,
|
|
3904
|
+
// 리뷰는 생존 렌즈의 stance로 계속한다.
|
|
3905
|
+
demotedLensIds = validationFailures.map((outcome) => outcome.dispatch.unit_id.slice("issue-stance:".length));
|
|
3906
|
+
await appendExecutionProgress(args.executionPlan.error_log_path, "runner stance units demoted (bounded resubmit exhausted)", demotedLensIds.map((lensId) => `lens_id: ${lensId}`));
|
|
3907
|
+
}
|
|
3908
|
+
const survivorLensIds = participatingLensIds.filter((lensId) => !demotedLensIds.includes(lensId));
|
|
3909
|
+
const survivorResponsePaths = [
|
|
3910
|
+
...new Set(survivorLensIds
|
|
3911
|
+
.map((lensId) => responsePathsByLensId.get(lensId))
|
|
3912
|
+
.filter((value) => value !== undefined)),
|
|
3913
|
+
];
|
|
3325
3914
|
await fs.mkdir(path.dirname(seat.packet_path), { recursive: true });
|
|
3326
3915
|
await fs.writeFile(seat.packet_path, `${renderRuntimeIssueStanceMatrixPacket({
|
|
3327
3916
|
projectRoot: args.projectRoot,
|
|
3328
3917
|
sessionId: args.executionPlan.session_id,
|
|
3329
3918
|
outputPath: seat.output_path,
|
|
3330
|
-
responsePaths:
|
|
3919
|
+
responsePaths: survivorResponsePaths,
|
|
3331
3920
|
}).trimEnd()}\n`, "utf8");
|
|
3332
3921
|
await registerGeneratedPromptPacketRefForDispatch({
|
|
3333
3922
|
executionPlan: args.executionPlan,
|
|
@@ -3338,7 +3927,8 @@ async function runIssueStanceMatrixCollectionDispatch(args) {
|
|
|
3338
3927
|
executionPlan: args.executionPlan,
|
|
3339
3928
|
projectRoot: args.projectRoot,
|
|
3340
3929
|
responsePathsByLensId,
|
|
3341
|
-
participatingLensIds,
|
|
3930
|
+
participatingLensIds: survivorLensIds,
|
|
3931
|
+
demotedLensIds,
|
|
3342
3932
|
outputPath: seat.output_path,
|
|
3343
3933
|
});
|
|
3344
3934
|
return {
|
|
@@ -3371,6 +3961,8 @@ async function runIssueArtifactDispatch(args) {
|
|
|
3371
3961
|
unitTimeoutMs: args.unitTimeoutMs,
|
|
3372
3962
|
retryPolicy: args.retryPolicy,
|
|
3373
3963
|
reviewExecutionProfile: args.reviewExecutionProfile,
|
|
3964
|
+
runUnitIds: args.runUnitIds,
|
|
3965
|
+
preservedResultsByUnitId: args.preservedResultsByUnitId,
|
|
3374
3966
|
});
|
|
3375
3967
|
}
|
|
3376
3968
|
const seat = await writeIssueArtifactPromptPacket({
|
|
@@ -4533,6 +5125,23 @@ export async function executeReviewPromptExecution(params) {
|
|
|
4533
5125
|
}
|
|
4534
5126
|
const executionOutcomes = new Array(lensDispatches.length);
|
|
4535
5127
|
let nextLensIndex = 0;
|
|
5128
|
+
// A-path 스코프 판별을 호이스트: 초기 lens 페이즈가 nested 배치로 가는
|
|
5129
|
+
// 조건 (continuation/repair 패스는 항상 flat per-unit 루프).
|
|
5130
|
+
const nestedLensWorkerExecutor = !continuationMode &&
|
|
5131
|
+
params.reviewExecutionProfile?.mode === "nested-workers" &&
|
|
5132
|
+
(params.reviewExecutionProfile.worker_executor === "codex" ||
|
|
5133
|
+
params.reviewExecutionProfile.worker_executor === "claude_code")
|
|
5134
|
+
? params.reviewExecutionProfile.worker_executor
|
|
5135
|
+
: null;
|
|
5136
|
+
// 설계 B + §4-1: lens 풀 breaker — flat per-unit 루프(runLensWorker)와 nested
|
|
5137
|
+
// 1차 배치의 flat-fallback이 최종 outcome을 관찰 단위로 기록한다. nested
|
|
5138
|
+
// 배치-성공 유닛은 실 디스패치가 아니므로 recordItemSkipped로 완료만
|
|
5139
|
+
// 집계하고(미기록 시 incomplete로 오집계, 배치-창 생존이 계통 streak을
|
|
5140
|
+
// 오리셋하는 것 차단), 배치-실패 유닛의 flat 재시도만 streak을 구동한다.
|
|
5141
|
+
const lensBreakerState = reviewDispatchBreakerFromProfile(params.reviewExecutionProfile, {
|
|
5142
|
+
concurrent: Math.min(maxConcurrentLenses, lensDispatches.filter((dispatch) => shouldRunUnit(dispatch.unit_id))
|
|
5143
|
+
.length) > 1,
|
|
5144
|
+
});
|
|
4536
5145
|
async function haltForCancellation(args) {
|
|
4537
5146
|
const completedLensOutcomes = executionOutcomes.filter(isSuccessfulOutcome);
|
|
4538
5147
|
const successfulLensDispatches = completedLensOutcomes.map((outcome) => outcome.dispatch);
|
|
@@ -4626,9 +5235,21 @@ export async function executeReviewPromptExecution(params) {
|
|
|
4626
5235
|
return;
|
|
4627
5236
|
}
|
|
4628
5237
|
if (!shouldRunUnit(dispatch.unit_id)) {
|
|
5238
|
+
// preserved/continuation 유닛은 breaker에 기록하지 않는다 — planned
|
|
5239
|
+
// 집합 자체가 "이번 run이 실제 디스패치하는 유닛"으로 계산된다.
|
|
5240
|
+
// 트립 여부와 무관하게 기록한다: 디스패치가 필요 없는 완료 증거를
|
|
5241
|
+
// 빼먹으면 barrier/execution-result가 완료 lens를 missing으로
|
|
5242
|
+
// 기록해 다음 continuation이 이미 완료된 lens를 재디스패치한다.
|
|
4629
5243
|
executionOutcomes[currentIndex] = preservedOutcomeForDispatch(dispatch);
|
|
4630
5244
|
continue;
|
|
4631
5245
|
}
|
|
5246
|
+
// 설계 B: 트립 이후 새 lens를 디스패치하지 않는다 — 남은 실행 유닛은
|
|
5247
|
+
// 미디스패치로 incomplete 집합에 남아 회복 재디스패치 대상이 된다
|
|
5248
|
+
// (규칙 5). return이 아닌 continue: 뒤 인덱스의 preserved 유닛 기록을
|
|
5249
|
+
// 마저 소진해야 한다.
|
|
5250
|
+
if (lensBreakerState?.tripped()) {
|
|
5251
|
+
continue;
|
|
5252
|
+
}
|
|
4632
5253
|
console.log(`[review runner] starting ${dispatch.unit_kind}: ${dispatch.unit_id}`);
|
|
4633
5254
|
await appendExecutionProgress(executionPlan.error_log_path, `runner dispatch started: ${dispatch.unit_id}`, [
|
|
4634
5255
|
`unit_id: ${dispatch.unit_id}`,
|
|
@@ -4669,6 +5290,8 @@ export async function executeReviewPromptExecution(params) {
|
|
|
4669
5290
|
error,
|
|
4670
5291
|
attempt,
|
|
4671
5292
|
maxRetries: effectiveMaxRetries,
|
|
5293
|
+
dispatch,
|
|
5294
|
+
reviewExecutionProfile: params.reviewExecutionProfile,
|
|
4672
5295
|
})) {
|
|
4673
5296
|
const retryDelay = effectiveRetryInitialDelayMs * (attempt + 1);
|
|
4674
5297
|
console.log(`[review runner] ${dispatch.unit_id} attempt ${attempt + 1} failed, retrying in ${retryDelay}ms...`);
|
|
@@ -4683,6 +5306,8 @@ export async function executeReviewPromptExecution(params) {
|
|
|
4683
5306
|
error,
|
|
4684
5307
|
attempt,
|
|
4685
5308
|
maxRetries: effectiveMaxRetries,
|
|
5309
|
+
dispatch,
|
|
5310
|
+
reviewExecutionProfile: params.reviewExecutionProfile,
|
|
4686
5311
|
}))
|
|
4687
5312
|
break;
|
|
4688
5313
|
}
|
|
@@ -4705,6 +5330,8 @@ export async function executeReviewPromptExecution(params) {
|
|
|
4705
5330
|
packetBytes: await fileSizeIfPresent(dispatch.packet_path),
|
|
4706
5331
|
outputBytes: await fileSizeIfPresent(dispatch.output_path),
|
|
4707
5332
|
};
|
|
5333
|
+
// 실 디스패치 성공만 프로바이더 생존 증거다 (breaker 규칙 2).
|
|
5334
|
+
lensBreakerState?.recordItemSuccess(dispatch.unit_id);
|
|
4708
5335
|
}
|
|
4709
5336
|
else {
|
|
4710
5337
|
const completedAtMs = Date.now();
|
|
@@ -4720,7 +5347,7 @@ export async function executeReviewPromptExecution(params) {
|
|
|
4720
5347
|
const outputBytes = await fileSizeIfPresent(dispatch.output_path);
|
|
4721
5348
|
await removeFileIfExists(dispatch.output_path);
|
|
4722
5349
|
await appendExecutionFailure(executionPlan.error_log_path, failure, executionPlan.effective_boundary_state);
|
|
4723
|
-
|
|
5350
|
+
const outcome = {
|
|
4724
5351
|
dispatch,
|
|
4725
5352
|
success: false,
|
|
4726
5353
|
startedAtMs,
|
|
@@ -4730,6 +5357,16 @@ export async function executeReviewPromptExecution(params) {
|
|
|
4730
5357
|
outputBytes,
|
|
4731
5358
|
failure,
|
|
4732
5359
|
};
|
|
5360
|
+
executionOutcomes[currentIndex] = outcome;
|
|
5361
|
+
// 유닛의 최종 실패(per-unit retry 소진)를 관찰 단위로 기록. 계통
|
|
5362
|
+
// 클래스가 아니면(dead-letter) streak에 닿지 않고, 트립 여부는 루프
|
|
5363
|
+
// 상단 체크가 소비한다 — halt는 배리어 뒤 epilogue가 수행.
|
|
5364
|
+
lensBreakerState?.recordItemFailure({
|
|
5365
|
+
item_id: dispatch.unit_id,
|
|
5366
|
+
failure_class: reviewSystemicFailureClassFromOutcome(outcome),
|
|
5367
|
+
failure_message: failure.message,
|
|
5368
|
+
attempt_count: attemptsUsed,
|
|
5369
|
+
});
|
|
4733
5370
|
}
|
|
4734
5371
|
}
|
|
4735
5372
|
}
|
|
@@ -4737,18 +5374,15 @@ export async function executeReviewPromptExecution(params) {
|
|
|
4737
5374
|
// scope). Continuation/repair passes re-dispatch remaining units through
|
|
4738
5375
|
// the flat per-unit loop — same unit-executor invocation, same artifact
|
|
4739
5376
|
// contract, so the seat truth is identical either way.
|
|
4740
|
-
if (
|
|
4741
|
-
|
|
4742
|
-
(params.reviewExecutionProfile.worker_executor === "codex" ||
|
|
4743
|
-
params.reviewExecutionProfile.worker_executor === "claude_code")) {
|
|
4744
|
-
const nestedBrand = params.reviewExecutionProfile.worker_executor === "codex"
|
|
5377
|
+
if (nestedLensWorkerExecutor !== null) {
|
|
5378
|
+
const nestedBrand = nestedLensWorkerExecutor === "codex"
|
|
4745
5379
|
? "codex"
|
|
4746
5380
|
: "claude";
|
|
4747
|
-
console.log(`[review runner] mode=nested-workers worker_executor=${
|
|
5381
|
+
console.log(`[review runner] mode=nested-workers worker_executor=${nestedLensWorkerExecutor}`);
|
|
4748
5382
|
await appendExecutionProgress(executionPlan.error_log_path, "runner profile dispatch: nested-workers", [
|
|
4749
|
-
`teamlead_seat: ${params.reviewExecutionProfile
|
|
4750
|
-
`lens_seat: ${params.reviewExecutionProfile
|
|
4751
|
-
`worker_executor: ${
|
|
5383
|
+
`teamlead_seat: ${params.reviewExecutionProfile?.teamlead.seat}`,
|
|
5384
|
+
`lens_seat: ${params.reviewExecutionProfile?.lens.seat}`,
|
|
5385
|
+
`worker_executor: ${nestedLensWorkerExecutor}`,
|
|
4752
5386
|
`planned_lens_count: ${lensDispatches.length}`,
|
|
4753
5387
|
]);
|
|
4754
5388
|
const nestedStartedAtMs = Date.now();
|
|
@@ -4834,15 +5468,22 @@ export async function executeReviewPromptExecution(params) {
|
|
|
4834
5468
|
`unit_kind: ${dispatch.unit_kind}`,
|
|
4835
5469
|
`output_path: ${dispatch.output_path}`,
|
|
4836
5470
|
]);
|
|
4837
|
-
|
|
5471
|
+
const okOutcome = {
|
|
4838
5472
|
dispatch,
|
|
4839
5473
|
success: true,
|
|
4840
5474
|
startedAtMs: nestedStartedAtMs,
|
|
4841
5475
|
completedAtMs: nestedCompletedAtMs,
|
|
4842
5476
|
attemptCount: 1,
|
|
5477
|
+
nestedBatchWindow: true,
|
|
4843
5478
|
packetBytes: await fileSizeIfPresent(dispatch.packet_path),
|
|
4844
5479
|
outputBytes: await fileSizeIfPresent(dispatch.output_path),
|
|
4845
5480
|
};
|
|
5481
|
+
executionOutcomes[i] = okOutcome;
|
|
5482
|
+
// §4-1: 배치-성공은 실 디스패치가 아니다 — 헬퍼가 skipped로 완료만
|
|
5483
|
+
// 집계(계통 streak 불변). 미기록 시 incomplete로 오집계된다.
|
|
5484
|
+
if (lensBreakerState) {
|
|
5485
|
+
recordNestedUnitOutcomeToBreaker(lensBreakerState, okOutcome);
|
|
5486
|
+
}
|
|
4846
5487
|
continue;
|
|
4847
5488
|
}
|
|
4848
5489
|
catch (error) {
|
|
@@ -4863,6 +5504,11 @@ export async function executeReviewPromptExecution(params) {
|
|
|
4863
5504
|
fallback: maxRetries,
|
|
4864
5505
|
});
|
|
4865
5506
|
if (effectiveLensMaxRetries >= 1) {
|
|
5507
|
+
// §4-1: 트립 이후에는 새 flat 재시도를 디스패치하지 않는다 — 이 유닛은
|
|
5508
|
+
// 미디스패치로 incomplete 집합에 남아 회복 재디스패치 대상이 된다
|
|
5509
|
+
// (규칙 5). 앞선 배치-성공 유닛은 이미 skipped로 기록됐다.
|
|
5510
|
+
if (lensBreakerState?.tripped())
|
|
5511
|
+
continue;
|
|
4866
5512
|
// Batch consumed attempt #1 — clear the dead seat and spend the
|
|
4867
5513
|
// remaining budget through the flat loop (same executor config
|
|
4868
5514
|
// derivation as the flat lens path: invokeExecutor applies the
|
|
@@ -4872,7 +5518,7 @@ export async function executeReviewPromptExecution(params) {
|
|
|
4872
5518
|
`batch_failure: ${batchFailureMessage}`,
|
|
4873
5519
|
`remaining_max_retries: ${effectiveLensMaxRetries - 1}`,
|
|
4874
5520
|
]);
|
|
4875
|
-
|
|
5521
|
+
const flatOutcome = await runSingleDispatchWithRetries({
|
|
4876
5522
|
projectRoot,
|
|
4877
5523
|
sessionRoot,
|
|
4878
5524
|
executionPlan,
|
|
@@ -4884,6 +5530,14 @@ export async function executeReviewPromptExecution(params) {
|
|
|
4884
5530
|
reviewExecutionProfile: params.reviewExecutionProfile,
|
|
4885
5531
|
maxRetriesOverride: effectiveLensMaxRetries - 1,
|
|
4886
5532
|
});
|
|
5533
|
+
executionOutcomes[i] = flatOutcome;
|
|
5534
|
+
// flat 재시도는 실 디스패치 관측이다 — 성공/실패를 breaker에 반영
|
|
5535
|
+
// (§4-1; flatOutcome은 배치-창 태그가 없어 skipped로 빠지지 않는다).
|
|
5536
|
+
// lens 트립 halt는 배리어 뒤 epilogue가 tripped()로 수행하므로 반환
|
|
5537
|
+
// trip은 소비하지 않는다.
|
|
5538
|
+
if (lensBreakerState) {
|
|
5539
|
+
recordNestedUnitOutcomeToBreaker(lensBreakerState, flatOutcome);
|
|
5540
|
+
}
|
|
4887
5541
|
continue;
|
|
4888
5542
|
}
|
|
4889
5543
|
const failure = {
|
|
@@ -4898,7 +5552,7 @@ export async function executeReviewPromptExecution(params) {
|
|
|
4898
5552
|
const outputBytes = await fileSizeIfPresent(dispatch.output_path);
|
|
4899
5553
|
await removeFileIfExists(dispatch.output_path);
|
|
4900
5554
|
await appendExecutionFailure(executionPlan.error_log_path, failure, executionPlan.effective_boundary_state);
|
|
4901
|
-
|
|
5555
|
+
const failureOutcome = {
|
|
4902
5556
|
dispatch,
|
|
4903
5557
|
success: false,
|
|
4904
5558
|
startedAtMs: nestedStartedAtMs,
|
|
@@ -4908,6 +5562,13 @@ export async function executeReviewPromptExecution(params) {
|
|
|
4908
5562
|
outputBytes,
|
|
4909
5563
|
failure,
|
|
4910
5564
|
};
|
|
5565
|
+
executionOutcomes[i] = failureOutcome;
|
|
5566
|
+
// §4-1 (교차검증 F2/Finding A): zero-retry 배치 실패는 flat 경로처럼
|
|
5567
|
+
// 실패로 기록한다 — 검증 실패(item-local)는 dead-letter, 계통 실패는
|
|
5568
|
+
// 회복 victim(incomplete). skipped(완료 오집계)가 아니다.
|
|
5569
|
+
if (lensBreakerState) {
|
|
5570
|
+
recordNestedUnitOutcomeToBreaker(lensBreakerState, failureOutcome);
|
|
5571
|
+
}
|
|
4911
5572
|
}
|
|
4912
5573
|
// Capture outer teamlead halt_reason for the post-dispatch halt check
|
|
4913
5574
|
// (synthesize may still run if enough lenses participated, matching the
|
|
@@ -4919,6 +5580,19 @@ export async function executeReviewPromptExecution(params) {
|
|
|
4919
5580
|
else {
|
|
4920
5581
|
await Promise.all(Array.from({ length: Math.min(maxConcurrentLenses, lensDispatches.length) }, async (_, workerIndex) => runLensWorker(workerIndex)));
|
|
4921
5582
|
}
|
|
5583
|
+
// 설계 B 규칙 6: breaker-ON lens 배치는 트립이든 완주든 end-state를
|
|
5584
|
+
// 영속한다 — 회복 절차가 항상 정확한 재디스패치 집합을 갖는다.
|
|
5585
|
+
let lensDispatchIncompletePath = null;
|
|
5586
|
+
if (lensBreakerState) {
|
|
5587
|
+
lensDispatchIncompletePath = await persistReviewDispatchIncompleteArtifact({
|
|
5588
|
+
sessionRoot,
|
|
5589
|
+
batchLabel: "lens",
|
|
5590
|
+
plannedItemIds: lensDispatches
|
|
5591
|
+
.filter((dispatch) => shouldRunUnit(dispatch.unit_id))
|
|
5592
|
+
.map((dispatch) => dispatch.unit_id),
|
|
5593
|
+
state: lensBreakerState,
|
|
5594
|
+
});
|
|
5595
|
+
}
|
|
4922
5596
|
const postLensCancelRequest = await readReviewCancelRequest(sessionRoot);
|
|
4923
5597
|
if (postLensCancelRequest) {
|
|
4924
5598
|
return haltForCancellation({
|
|
@@ -4941,10 +5615,21 @@ export async function executeReviewPromptExecution(params) {
|
|
|
4941
5615
|
successfulLensDispatches,
|
|
4942
5616
|
executionFailures,
|
|
4943
5617
|
});
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
5618
|
+
// 설계 B 규칙 4: lens 트립은 배리어 판정과 무관하게 무조건 halt한다
|
|
5619
|
+
// (트립 시점 이후 유닛은 미디스패치라 배리어가 우연히 downstream을 허용해도
|
|
5620
|
+
// 배치는 이미 불완전하다). 배리어 아티팩트는 위에서 정상 기록되어
|
|
5621
|
+
// continuation frontier가 미완료 lens를 재제안할 수 있다. lens 풀은
|
|
5622
|
+
// 실패를 throw하지 않고 outcome으로만 기록하므로(전파 캐치 없음), 트립
|
|
5623
|
+
// halt도 기존 배리어 halt와 같은 구조화 블록으로 수행한다.
|
|
5624
|
+
const lensBreakerTrip = lensBreakerState?.tripped() ?? null;
|
|
5625
|
+
if (lensBreakerTrip !== null || !lensCompletionBarrier.downstream_allowed) {
|
|
5626
|
+
const haltReason = lensBreakerTrip !== null
|
|
5627
|
+
? reviewDispatchBreakerHaltReason(lensBreakerTrip, lensDispatchIncompletePath ??
|
|
5628
|
+
dispatchIncompleteArtifactPath(sessionRoot))
|
|
5629
|
+
: successfulLensDispatches.length === 0
|
|
5630
|
+
? "No participating lens outputs were produced."
|
|
5631
|
+
: `Selected lens completion barrier failed: ${lensCompletionBarrier.completed_lens_ids.length}/${lensCompletionBarrier.planned_lens_ids.length} planned lenses completed.`;
|
|
5632
|
+
const haltPhase = lensBreakerTrip !== null ? "lens_dispatch_breaker" : "lens_completion_barrier";
|
|
4948
5633
|
await appendMarkdownLogEntry(executionPlan.error_log_path, "runner halted before synthesize", `${haltReason}\n\n[effective_boundary_state]\n${renderEffectiveBoundaryStateLog(executionPlan.effective_boundary_state)}`);
|
|
4949
5634
|
const degradedLensIds = executionFailures
|
|
4950
5635
|
.filter((failure) => failure.unit_kind === "lens")
|
|
@@ -4976,7 +5661,7 @@ export async function executeReviewPromptExecution(params) {
|
|
|
4976
5661
|
synthesis_executed: false,
|
|
4977
5662
|
deliberation_status: "not_performed",
|
|
4978
5663
|
halt_reason: haltReason,
|
|
4979
|
-
...haltArtifactFields(
|
|
5664
|
+
...haltArtifactFields(haltPhase, null),
|
|
4980
5665
|
error_log_path: executionPlan.error_log_path,
|
|
4981
5666
|
lens_completion_barrier_ref: executionPlan.lens_completion_barrier_path ??
|
|
4982
5667
|
path.join(sessionRoot, "lens-completion-barrier.yaml"),
|
|
@@ -4996,12 +5681,23 @@ export async function executeReviewPromptExecution(params) {
|
|
|
4996
5681
|
synthesis_executed: false,
|
|
4997
5682
|
error_log_path: executionPlan.error_log_path,
|
|
4998
5683
|
halt_reason: haltReason,
|
|
4999
|
-
...haltArtifactFields(
|
|
5684
|
+
...haltArtifactFields(haltPhase, null),
|
|
5000
5685
|
};
|
|
5001
5686
|
}
|
|
5002
5687
|
const issueArtifactOutcomes = [];
|
|
5003
5688
|
const lensOutputPaths = successfulLensDispatches.map((dispatch) => dispatch.output_path);
|
|
5004
5689
|
const haltAfterIssueArtifactFailure = async (args) => {
|
|
5690
|
+
// 설계 B 트립: halt 시점까지의 배치 outcome 전체(완료 포함)를 결과
|
|
5691
|
+
// 아티팩트에 보존한다 — 완료 stance 유닛 행이 없으면 continuation
|
|
5692
|
+
// ledger가 missing으로 도출해 이미 지불한 완료분을 재디스패치한다.
|
|
5693
|
+
const batchOutcomes = args.error instanceof ReviewIssueArtifactDispatchError
|
|
5694
|
+
? args.error.batchOutcomes
|
|
5695
|
+
: [];
|
|
5696
|
+
for (const outcome of batchOutcomes) {
|
|
5697
|
+
if (!issueArtifactOutcomes.some((existing) => existing.dispatch.unit_id === outcome.dispatch.unit_id)) {
|
|
5698
|
+
issueArtifactOutcomes.push(outcome);
|
|
5699
|
+
}
|
|
5700
|
+
}
|
|
5005
5701
|
const failureOutcome = issueArtifactOutcomeFromError(args.error);
|
|
5006
5702
|
if (failureOutcome &&
|
|
5007
5703
|
!issueArtifactOutcomes.some((outcome) => outcome.dispatch.unit_id === failureOutcome.dispatch.unit_id)) {
|
|
@@ -5013,7 +5709,10 @@ export async function executeReviewPromptExecution(params) {
|
|
|
5013
5709
|
.filter((failure) => failure.unit_kind === "lens")
|
|
5014
5710
|
.map((failure) => failure.unit_id);
|
|
5015
5711
|
const executionCompletedAtMs = Date.now();
|
|
5016
|
-
const
|
|
5712
|
+
const haltReasonOverride = args.error instanceof ReviewIssueArtifactDispatchError
|
|
5713
|
+
? args.error.haltReason
|
|
5714
|
+
: null;
|
|
5715
|
+
const haltReason = haltReasonOverride ?? `Issue artifact generation failed: ${failureMessage}`;
|
|
5017
5716
|
await writeExecutionResultArtifact(executionPlan, {
|
|
5018
5717
|
session_id: executionPlan.session_id,
|
|
5019
5718
|
session_root: sessionRoot,
|
|
@@ -5119,6 +5818,15 @@ export async function executeReviewPromptExecution(params) {
|
|
|
5119
5818
|
unitTimeoutMs,
|
|
5120
5819
|
retryPolicy,
|
|
5121
5820
|
reviewExecutionProfile: params.reviewExecutionProfile,
|
|
5821
|
+
// Continuation: stance 수집 스테이지가 run-owing 유닛만 디스패치하고
|
|
5822
|
+
// 완료 유닛은 prior result에서 복원한다 (규칙 5 — 회복 재디스패치
|
|
5823
|
+
// 집합 == 미완료 집합).
|
|
5824
|
+
...(continuationMode
|
|
5825
|
+
? {
|
|
5826
|
+
runUnitIds: continuationRunUnitIds,
|
|
5827
|
+
preservedResultsByUnitId: previousResultsByUnitId,
|
|
5828
|
+
}
|
|
5829
|
+
: {}),
|
|
5122
5830
|
}));
|
|
5123
5831
|
return null;
|
|
5124
5832
|
}
|
|
@@ -5351,7 +6059,9 @@ export async function executeReviewPromptExecution(params) {
|
|
|
5351
6059
|
throw new Error("post-lens frontier loop did not converge (max iterations)");
|
|
5352
6060
|
}
|
|
5353
6061
|
const frontier = await computeReviewFrontier(sessionRoot);
|
|
5354
|
-
|
|
6062
|
+
// Convergence = no unit owes work: trusted output OR terminally
|
|
6063
|
+
// resolved (demoted complete-with-failure — 설계 A).
|
|
6064
|
+
if (frontier.unitLedger.units.every((unit) => isResolvedLedgerUnit(unit))) {
|
|
5355
6065
|
break;
|
|
5356
6066
|
}
|
|
5357
6067
|
const ready = frontier.frontierUnits.filter((unit) => unit.dispatchDecision !== "skip");
|