@akagilnc/pi-workflow-roles 0.1.1883 → 0.1.1918
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/evidence-child-executor.js +153 -16
- package/dist/navigator-attendance.js +64 -29
- package/dist/public-cli/main.js +786 -373
- package/dist/reviewer-child-executor.js +1 -1
- package/dist/typed-provider-http.js +60 -0
- package/dist/upstream-error-testimony.js +19 -0
- package/package.json +1 -1
- package/scripts/build-package.mjs +2 -0
- package/src/evidence-child-executor.ts +177 -24
- package/src/factory-board.ts +60 -3
- package/src/navigator-attendance.ts +86 -33
- package/src/public-cli/cli.ts +96 -39
- package/src/public-cli/coder-run.ts +36 -11
- package/src/public-cli/collector-run.ts +8 -3
- package/src/public-cli/doctor-run.ts +1 -0
- package/src/public-cli/explicit-internal.ts +61 -19
- package/src/public-cli/fixer-run.ts +36 -11
- package/src/public-cli/invocation.ts +137 -101
- package/src/public-cli/judge-run.ts +39 -11
- package/src/public-cli/merger-run.ts +36 -11
- package/src/public-cli/reviewer-run.ts +35 -11
- package/src/public-cli/run-lifecycle.ts +89 -76
- package/src/public-cli/settlement.ts +256 -66
- package/src/reviewer-child-executor.ts +1 -1
- package/src/role-runtime.ts +18 -8
- package/src/ticket-frontmatter.ts +94 -0
- package/src/ticket-trajectory.ts +145 -5
- package/src/typed-provider-http.ts +99 -0
- package/src/upstream-error-testimony.ts +45 -0
|
@@ -13,6 +13,13 @@ import { DOCTOR_AUDIT_TOOL_NAME } from "../doctor-auditor.ts";
|
|
|
13
13
|
import { JUDGE_AUDIT_TOOL_NAME } from "../judge-auditor.ts";
|
|
14
14
|
import { REVIEWER_AUDIT_TOOL_NAME } from "../reviewer-auditor.ts";
|
|
15
15
|
import { knownFailureFromProviderStop, type ExplicitInternalKnownFailure, readReviewerDispatchRejection } from "./explicit-internal.ts";
|
|
16
|
+
import {
|
|
17
|
+
isV1ResumableProvider,
|
|
18
|
+
readLatestTypedProviderHttpObservation,
|
|
19
|
+
readTypedHttp429Observation,
|
|
20
|
+
type TypedHttp429Observation,
|
|
21
|
+
type TypedProviderHttpObservation,
|
|
22
|
+
} from "./run-lifecycle.ts";
|
|
16
23
|
import {
|
|
17
24
|
AUDITOR_COMPLIANCE_FAILURE_ENTRY_TYPE,
|
|
18
25
|
AUDITOR_PARENT_ATTEMPT_BINDING_ENTRY_TYPE,
|
|
@@ -367,14 +374,16 @@ export function classifyPostAdmissionFailure(input: {
|
|
|
367
374
|
input.knownDiagnostic !== undefined && input.knownDiagnostic.trim() !== ""
|
|
368
375
|
? input.knownDiagnostic
|
|
369
376
|
: conciseChildDiagnostic(input.stderr, fallback);
|
|
370
|
-
const {
|
|
377
|
+
const { timedOut: _knownTimedOut, ...knownDetails } =
|
|
371
378
|
input.knownDetails ?? {};
|
|
379
|
+
const remoteCode = knownDetails.code;
|
|
372
380
|
return {
|
|
373
381
|
cause: input.knownCause,
|
|
374
382
|
diagnostic,
|
|
375
383
|
details: {
|
|
376
384
|
...knownDetails,
|
|
377
|
-
code:
|
|
385
|
+
...(remoteCode === undefined ? {} : { code: remoteCode }),
|
|
386
|
+
exitCode: input.code,
|
|
378
387
|
...(input.timedOut ? { timedOut: true as const } : {}),
|
|
379
388
|
},
|
|
380
389
|
...(input.knownIdentity === undefined
|
|
@@ -386,7 +395,7 @@ export function classifyPostAdmissionFailure(input: {
|
|
|
386
395
|
return {
|
|
387
396
|
cause: "timeout",
|
|
388
397
|
diagnostic: "role run timed out",
|
|
389
|
-
details: { timedOut: true,
|
|
398
|
+
details: { timedOut: true, exitCode: input.code },
|
|
390
399
|
};
|
|
391
400
|
}
|
|
392
401
|
if (input.code !== 0) {
|
|
@@ -394,27 +403,27 @@ export function classifyPostAdmissionFailure(input: {
|
|
|
394
403
|
return {
|
|
395
404
|
cause: "activation",
|
|
396
405
|
diagnostic: conciseChildDiagnostic(input.stderr, fallback),
|
|
397
|
-
details: {
|
|
406
|
+
details: { exitCode: input.code },
|
|
398
407
|
};
|
|
399
408
|
}
|
|
400
409
|
if (input.session?.state === "missing") {
|
|
401
410
|
return {
|
|
402
411
|
cause: "session",
|
|
403
412
|
diagnostic: "role run left no readable session transcript",
|
|
404
|
-
details: {
|
|
413
|
+
details: { exitCode: input.code, session: "missing" },
|
|
405
414
|
};
|
|
406
415
|
}
|
|
407
416
|
if (input.session?.state === "unreadable") {
|
|
408
417
|
return {
|
|
409
418
|
cause: "session",
|
|
410
419
|
diagnostic: input.session.diagnostic,
|
|
411
|
-
details: {
|
|
420
|
+
details: { exitCode: input.code, session: "unreadable" },
|
|
412
421
|
};
|
|
413
422
|
}
|
|
414
423
|
return {
|
|
415
424
|
cause: "output",
|
|
416
425
|
diagnostic: "role run completed without a lawful typed terminal result",
|
|
417
|
-
details: {
|
|
426
|
+
details: { exitCode: input.code },
|
|
418
427
|
};
|
|
419
428
|
}
|
|
420
429
|
|
|
@@ -448,6 +457,15 @@ type SessionMessage = {
|
|
|
448
457
|
provider?: string;
|
|
449
458
|
model?: string;
|
|
450
459
|
api?: string;
|
|
460
|
+
rawStopReason?: string;
|
|
461
|
+
diagnostics?: unknown;
|
|
462
|
+
/** Typed HTTP / SDK structured fields when held on the call surface. */
|
|
463
|
+
statusCode?: number;
|
|
464
|
+
status?: number;
|
|
465
|
+
httpStatus?: number;
|
|
466
|
+
body?: unknown;
|
|
467
|
+
code?: unknown;
|
|
468
|
+
errno?: unknown;
|
|
451
469
|
};
|
|
452
470
|
|
|
453
471
|
type SessionEntry = {
|
|
@@ -546,19 +564,65 @@ async function readBoundSessionEntries(
|
|
|
546
564
|
}
|
|
547
565
|
|
|
548
566
|
/**
|
|
549
|
-
* Latest native assistant provider-stop in a session (stopReason
|
|
567
|
+
* Latest native assistant provider-stop in a session (stopReason error|aborted).
|
|
550
568
|
* Only the final assistant turn decides terminality — an older error followed by a
|
|
551
569
|
* later non-error stop is not a provider failure (would wash a no-lawful-output path).
|
|
552
570
|
* Typed production source for provider cause — not child stderr prose.
|
|
553
571
|
*/
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
): {
|
|
557
|
-
stopReason: "error";
|
|
572
|
+
type SessionProviderStop = {
|
|
573
|
+
stopReason: "error" | "aborted";
|
|
558
574
|
errorMessage?: string;
|
|
559
575
|
provider?: string;
|
|
560
576
|
model?: string;
|
|
561
|
-
|
|
577
|
+
api?: string;
|
|
578
|
+
rawStopReason?: string;
|
|
579
|
+
diagnostics?: unknown;
|
|
580
|
+
httpStatus?: number;
|
|
581
|
+
body?: unknown;
|
|
582
|
+
code?: unknown;
|
|
583
|
+
errno?: unknown;
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
function typedHttpStatusFromMessage(message: SessionMessage): number | undefined {
|
|
587
|
+
for (const candidate of [message.httpStatus, message.statusCode, message.status]) {
|
|
588
|
+
if (typeof candidate === "number" && (candidate < 200 || candidate >= 300)) return candidate;
|
|
589
|
+
}
|
|
590
|
+
return undefined;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function sessionProviderStopFromAssistant(message: SessionMessage | undefined): SessionProviderStop | undefined {
|
|
594
|
+
if (message?.role !== "assistant") return undefined;
|
|
595
|
+
if (message.stopReason !== "error" && message.stopReason !== "aborted") return undefined;
|
|
596
|
+
const httpStatus = typedHttpStatusFromMessage(message);
|
|
597
|
+
return {
|
|
598
|
+
stopReason: message.stopReason,
|
|
599
|
+
// Preserve held errorMessage bytes — emptiness check must not rewrite.
|
|
600
|
+
...(typeof message.errorMessage === "string" && message.errorMessage.trim() !== ""
|
|
601
|
+
? { errorMessage: message.errorMessage }
|
|
602
|
+
: {}),
|
|
603
|
+
...(typeof message.provider === "string" && message.provider.trim() !== ""
|
|
604
|
+
? { provider: message.provider }
|
|
605
|
+
: {}),
|
|
606
|
+
...(typeof message.model === "string" && message.model.trim() !== ""
|
|
607
|
+
? { model: message.model }
|
|
608
|
+
: {}),
|
|
609
|
+
...(typeof message.api === "string" && message.api.trim() !== ""
|
|
610
|
+
? { api: message.api }
|
|
611
|
+
: {}),
|
|
612
|
+
...(typeof message.rawStopReason === "string" && message.rawStopReason.trim() !== ""
|
|
613
|
+
? { rawStopReason: message.rawStopReason }
|
|
614
|
+
: {}),
|
|
615
|
+
...(message.diagnostics === undefined ? {} : { diagnostics: message.diagnostics }),
|
|
616
|
+
...(httpStatus === undefined ? {} : { httpStatus }),
|
|
617
|
+
...(message.body === undefined ? {} : { body: message.body }),
|
|
618
|
+
...(message.code === undefined ? {} : { code: message.code }),
|
|
619
|
+
...(message.errno === undefined ? {} : { errno: message.errno }),
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
export function extractSessionProviderStop(
|
|
624
|
+
entries: readonly SessionEntry[],
|
|
625
|
+
): SessionProviderStop | undefined {
|
|
562
626
|
// A resumed dispatch appends a typed top-level user turn to the same session.
|
|
563
627
|
// Retained audit state from an older attempt must not replace the newer attempt's
|
|
564
628
|
// native provider stop. Sessions without a user turn are the initial attempt.
|
|
@@ -578,14 +642,8 @@ export function extractSessionProviderStop(
|
|
|
578
642
|
const entry = entries[i];
|
|
579
643
|
if (entry?.type !== "custom" || entry.customType !== COMPLIANCE_RESPONSE_ENTRY_TYPE) continue;
|
|
580
644
|
const response = isRecord(entry.data) && isRecord(entry.data.response) ? entry.data.response : undefined;
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
stopReason: "error",
|
|
584
|
-
...(typeof response.errorMessage === "string" && response.errorMessage.trim() !== "" ? { errorMessage: response.errorMessage } : {}),
|
|
585
|
-
...(typeof response.provider === "string" && response.provider.trim() !== "" ? { provider: response.provider } : {}),
|
|
586
|
-
...(typeof response.model === "string" && response.model.trim() !== "" ? { model: response.model } : {}),
|
|
587
|
-
};
|
|
588
|
-
}
|
|
645
|
+
const stop = sessionProviderStopFromAssistant(response);
|
|
646
|
+
if (stop !== undefined) return stop;
|
|
589
647
|
break;
|
|
590
648
|
}
|
|
591
649
|
for (let i = entries.length - 1; i >= attemptStart; i -= 1) {
|
|
@@ -594,19 +652,7 @@ export function extractSessionProviderStop(
|
|
|
594
652
|
const message = entry.message;
|
|
595
653
|
if (message?.role !== "assistant") continue;
|
|
596
654
|
// Latest assistant in the current attempt only (reviewer-child-executor lastAssistant pattern).
|
|
597
|
-
|
|
598
|
-
return {
|
|
599
|
-
stopReason: "error",
|
|
600
|
-
...(typeof message.errorMessage === "string" && message.errorMessage.trim() !== ""
|
|
601
|
-
? { errorMessage: message.errorMessage }
|
|
602
|
-
: {}),
|
|
603
|
-
...(typeof message.provider === "string" && message.provider.trim() !== ""
|
|
604
|
-
? { provider: message.provider }
|
|
605
|
-
: {}),
|
|
606
|
-
...(typeof message.model === "string" && message.model.trim() !== ""
|
|
607
|
-
? { model: message.model }
|
|
608
|
-
: {}),
|
|
609
|
-
};
|
|
655
|
+
return sessionProviderStopFromAssistant(message);
|
|
610
656
|
}
|
|
611
657
|
return undefined;
|
|
612
658
|
}
|
|
@@ -614,15 +660,7 @@ export function extractSessionProviderStop(
|
|
|
614
660
|
/** Read the bound session principal and extract a typed provider-stop, if any. */
|
|
615
661
|
export async function readSessionProviderStop(
|
|
616
662
|
sessionFile: string,
|
|
617
|
-
): Promise<
|
|
618
|
-
| {
|
|
619
|
-
stopReason: "error";
|
|
620
|
-
errorMessage?: string;
|
|
621
|
-
provider?: string;
|
|
622
|
-
model?: string;
|
|
623
|
-
}
|
|
624
|
-
| undefined
|
|
625
|
-
> {
|
|
663
|
+
): Promise<SessionProviderStop | undefined> {
|
|
626
664
|
try {
|
|
627
665
|
const entries = await readBoundSessionEntries(sessionFile);
|
|
628
666
|
return extractSessionProviderStop(entries);
|
|
@@ -662,8 +700,7 @@ export async function readBoundEvidenceChildKnownFailure(
|
|
|
662
700
|
return {
|
|
663
701
|
...primary,
|
|
664
702
|
details: {
|
|
665
|
-
...(
|
|
666
|
-
...(stop.model === undefined ? {} : { model: stop.model }),
|
|
703
|
+
...(primary.details ?? {}),
|
|
667
704
|
secondaryEvidence: "evidence-child",
|
|
668
705
|
},
|
|
669
706
|
};
|
|
@@ -721,10 +758,10 @@ export async function readBoundAuditorKnownFailure(
|
|
|
721
758
|
if (entry?.type !== "custom" || entry.customType !== AUDITOR_COMPLIANCE_FAILURE_ENTRY_TYPE || !isRecord(entry.data)) continue;
|
|
722
759
|
const parent = isRecord(entry.data.parent) ? entry.data.parent : undefined;
|
|
723
760
|
const failure = isRecord(entry.data.failure) ? entry.data.failure : undefined;
|
|
724
|
-
if (parent?.sessionId !== parentId || parent.sessionFile !== sessionFile || parent.attemptEntryId !== attemptEntryId || failure?.cause !== "provider") continue;
|
|
761
|
+
if (parent?.sessionId !== parentId || parent.sessionFile !== sessionFile || parent.attemptEntryId !== attemptEntryId || (failure?.cause !== "provider" && failure?.cause !== "unrecognized")) continue;
|
|
725
762
|
const identity = isRecord(failure.identity) ? failure.identity : undefined;
|
|
726
763
|
return {
|
|
727
|
-
cause: "provider",
|
|
764
|
+
cause: failure.cause === "provider" ? "provider" : "unrecognized",
|
|
728
765
|
...(identity === undefined ? {} : { identity: {
|
|
729
766
|
...(typeof identity.name === "string" ? { name: identity.name } : {}),
|
|
730
767
|
...(typeof identity.code === "string" || typeof identity.code === "number" ? { code: identity.code } : {}),
|
|
@@ -737,8 +774,7 @@ export async function readBoundAuditorKnownFailure(
|
|
|
737
774
|
return {
|
|
738
775
|
...primary,
|
|
739
776
|
details: {
|
|
740
|
-
...(
|
|
741
|
-
...(stop.model === undefined ? {} : { model: stop.model }),
|
|
777
|
+
...(primary.details ?? {}),
|
|
742
778
|
secondaryEvidence: "unavailable",
|
|
743
779
|
},
|
|
744
780
|
};
|
|
@@ -778,26 +814,57 @@ function typedFailedTerminatingToolKnownFailure(
|
|
|
778
814
|
return undefined;
|
|
779
815
|
}
|
|
780
816
|
|
|
817
|
+
/**
|
|
818
|
+
* One audited-runner resolution: knownFailure plus the typed-HTTP sidecar outcome
|
|
819
|
+
* from the same read. Callers that also decide v1 resume must consume this once —
|
|
820
|
+
* never re-read the sidecar in presentControlledFailure.
|
|
821
|
+
*/
|
|
822
|
+
export type AuditedRunnerFailureResolution = {
|
|
823
|
+
readonly knownFailure?: ExplicitInternalKnownFailure;
|
|
824
|
+
/** Successful sidecar read (not absence). */
|
|
825
|
+
readonly typedHttpObservation?: TypedProviderHttpObservation;
|
|
826
|
+
/**
|
|
827
|
+
* True when this resolution already performed the typed-HTTP sidecar read
|
|
828
|
+
* (success, absence, or non-absence failure folded into knownFailure).
|
|
829
|
+
* False when an earlier evidence tier short-circuited before the sidecar.
|
|
830
|
+
*/
|
|
831
|
+
readonly typedHttpObservationSettled: boolean;
|
|
832
|
+
};
|
|
833
|
+
|
|
834
|
+
function resolutionOf(
|
|
835
|
+
knownFailure: ExplicitInternalKnownFailure | undefined,
|
|
836
|
+
typedHttp: {
|
|
837
|
+
readonly settled: boolean;
|
|
838
|
+
readonly observation?: TypedProviderHttpObservation;
|
|
839
|
+
} = { settled: false },
|
|
840
|
+
): AuditedRunnerFailureResolution {
|
|
841
|
+
return {
|
|
842
|
+
...(knownFailure === undefined ? {} : { knownFailure }),
|
|
843
|
+
...(typedHttp.observation === undefined ? {} : { typedHttpObservation: typedHttp.observation }),
|
|
844
|
+
typedHttpObservationSettled: typedHttp.settled,
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
|
|
781
848
|
/** Sole evidence-priority owner for public runners with Soul auditors. */
|
|
782
|
-
export async function
|
|
849
|
+
export async function resolveAuditedRunnerFailureResolution(input: {
|
|
783
850
|
runner: ExplicitInternalKnownFailure | undefined;
|
|
784
851
|
sessionFile: string;
|
|
785
852
|
credential: ExplicitInternalKnownFailure | undefined;
|
|
786
853
|
/** Reviewer only: recover child-written rejection page into knownFailure.details. */
|
|
787
854
|
runDirectory?: string;
|
|
788
|
-
}): Promise<
|
|
789
|
-
if (input.runner !== undefined) return input.runner;
|
|
855
|
+
}): Promise<AuditedRunnerFailureResolution> {
|
|
856
|
+
if (input.runner !== undefined) return resolutionOf(input.runner);
|
|
790
857
|
if (input.runDirectory !== undefined) {
|
|
791
858
|
try {
|
|
792
859
|
const rejection = await readReviewerDispatchRejection(input.runDirectory);
|
|
793
|
-
if (rejection !== undefined) return rejection;
|
|
860
|
+
if (rejection !== undefined) return resolutionOf(rejection);
|
|
794
861
|
} catch (error) {
|
|
795
862
|
const failure = error instanceof Error ? error : new Error(String(error));
|
|
796
|
-
return {
|
|
863
|
+
return resolutionOf({
|
|
797
864
|
cause: "activation",
|
|
798
865
|
identity: thrownIdentity(failure),
|
|
799
866
|
diagnostic: failure.message || failure.name,
|
|
800
|
-
};
|
|
867
|
+
});
|
|
801
868
|
}
|
|
802
869
|
}
|
|
803
870
|
// Bound auditor evidence outranks a parent failure that the auditor path itself
|
|
@@ -805,24 +872,28 @@ export async function resolveAuditedRunnerKnownFailure(input: {
|
|
|
805
872
|
// it outranks provider/credential and nonzero fallbacks, but not its recorded cause.
|
|
806
873
|
try {
|
|
807
874
|
const auditorFailure = await readBoundAuditorKnownFailure(input.sessionFile);
|
|
808
|
-
if (auditorFailure !== undefined) return auditorFailure;
|
|
875
|
+
if (auditorFailure !== undefined) return resolutionOf(auditorFailure);
|
|
809
876
|
} catch (error) {
|
|
810
877
|
const failure = sessionReadFailure(error, "failed to recover bound auditor failure");
|
|
811
|
-
return {
|
|
878
|
+
return resolutionOf({
|
|
812
879
|
cause: "session",
|
|
813
880
|
identity: thrownIdentity(failure),
|
|
814
881
|
diagnostic: failure.message || failure.name,
|
|
815
|
-
};
|
|
882
|
+
});
|
|
816
883
|
}
|
|
817
884
|
try {
|
|
818
885
|
const terminatingFailure = typedFailedTerminatingToolKnownFailure(
|
|
819
886
|
await readBoundSessionEntries(input.sessionFile),
|
|
820
887
|
);
|
|
821
|
-
if (terminatingFailure !== undefined) return terminatingFailure;
|
|
888
|
+
if (terminatingFailure !== undefined) return resolutionOf(terminatingFailure);
|
|
822
889
|
} catch (error) {
|
|
823
890
|
if (!isMissingPathError(error)) {
|
|
824
891
|
const failure = sessionReadFailure(error, "failed to recover typed terminating-tool failure");
|
|
825
|
-
return {
|
|
892
|
+
return resolutionOf({
|
|
893
|
+
cause: "session",
|
|
894
|
+
identity: thrownIdentity(failure),
|
|
895
|
+
diagnostic: failure.message || failure.name,
|
|
896
|
+
});
|
|
826
897
|
}
|
|
827
898
|
}
|
|
828
899
|
// Reviewer axis evidence-children are next: fixed two-axis dispatch fails
|
|
@@ -830,19 +901,138 @@ export async function resolveAuditedRunnerKnownFailure(input: {
|
|
|
830
901
|
// fallback; credential is last.
|
|
831
902
|
try {
|
|
832
903
|
const evidenceChildFailure = await readBoundEvidenceChildKnownFailure(input.sessionFile);
|
|
833
|
-
if (evidenceChildFailure !== undefined) return evidenceChildFailure;
|
|
904
|
+
if (evidenceChildFailure !== undefined) return resolutionOf(evidenceChildFailure);
|
|
834
905
|
} catch (error) {
|
|
835
906
|
const failure = sessionReadFailure(error, "failed to recover bound evidence-child failure");
|
|
836
|
-
return {
|
|
907
|
+
return resolutionOf({
|
|
837
908
|
cause: "session",
|
|
838
909
|
identity: thrownIdentity(failure),
|
|
839
910
|
diagnostic: failure.message || failure.name,
|
|
840
|
-
};
|
|
911
|
+
});
|
|
841
912
|
}
|
|
842
913
|
const parentStop = await readSessionProviderStop(input.sessionFile);
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
914
|
+
// Typed HTTP observation: ENOENT=absence; other read/parse/shape failures keep real cause.
|
|
915
|
+
// This is the single sidecar read for both knownFailure projection and v1 resume.
|
|
916
|
+
let httpObservation: TypedProviderHttpObservation | undefined;
|
|
917
|
+
if (input.runDirectory !== undefined) {
|
|
918
|
+
try {
|
|
919
|
+
httpObservation = await readLatestTypedProviderHttpObservation(input.runDirectory);
|
|
920
|
+
} catch (error) {
|
|
921
|
+
const failure = error instanceof Error ? error : new Error(String(error));
|
|
922
|
+
return resolutionOf(
|
|
923
|
+
{
|
|
924
|
+
cause: "session",
|
|
925
|
+
identity: thrownIdentity(failure),
|
|
926
|
+
diagnostic: failure.message || failure.name,
|
|
927
|
+
},
|
|
928
|
+
{ settled: true },
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
const typedHttp = {
|
|
933
|
+
settled: input.runDirectory !== undefined,
|
|
934
|
+
...(httpObservation === undefined ? {} : { observation: httpObservation }),
|
|
935
|
+
};
|
|
936
|
+
if (parentStop === undefined) {
|
|
937
|
+
if (input.credential !== undefined) return resolutionOf(input.credential, typedHttp);
|
|
938
|
+
if (httpObservation === undefined) return resolutionOf(undefined, typedHttp);
|
|
939
|
+
// Project the HTTP observation's status + provider/source association.
|
|
940
|
+
return resolutionOf(
|
|
941
|
+
knownFailureFromProviderStop({
|
|
942
|
+
stopReason: "error",
|
|
943
|
+
httpStatus: httpObservation.httpStatus,
|
|
944
|
+
provider: httpObservation.provider,
|
|
945
|
+
}),
|
|
946
|
+
typedHttp,
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
return resolutionOf(
|
|
950
|
+
knownFailureFromProviderStop({
|
|
951
|
+
...parentStop,
|
|
952
|
+
...(httpObservation === undefined
|
|
953
|
+
? {}
|
|
954
|
+
: {
|
|
955
|
+
httpStatus: httpObservation.httpStatus,
|
|
956
|
+
// Observation association outranks session-configured provider name alone.
|
|
957
|
+
provider: httpObservation.provider,
|
|
958
|
+
}),
|
|
959
|
+
}),
|
|
960
|
+
typedHttp,
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
/** Sole evidence-priority owner for public runners with Soul auditors. */
|
|
965
|
+
export async function resolveAuditedRunnerKnownFailure(input: {
|
|
966
|
+
runner: ExplicitInternalKnownFailure | undefined;
|
|
967
|
+
sessionFile: string;
|
|
968
|
+
credential: ExplicitInternalKnownFailure | undefined;
|
|
969
|
+
/** Reviewer only: recover child-written rejection page into knownFailure.details. */
|
|
970
|
+
runDirectory?: string;
|
|
971
|
+
}): Promise<ExplicitInternalKnownFailure | undefined> {
|
|
972
|
+
return (await resolveAuditedRunnerFailureResolution(input)).knownFailure;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/**
|
|
976
|
+
* v1 resume observation for controlled-failure settlement — at most one sidecar read.
|
|
977
|
+
* Prefer the pre-resolved outcome from resolveAuditedRunnerFailureResolution.
|
|
978
|
+
* Non-absence failures never throw: they return observationReadFailure for the
|
|
979
|
+
* existing controlled-failure → error.json chain.
|
|
980
|
+
*/
|
|
981
|
+
export async function resolveControlledFailureResumeObservation(input: {
|
|
982
|
+
readonly runDirectory: string;
|
|
983
|
+
readonly typedHttpObservationSettled?: boolean;
|
|
984
|
+
readonly typedHttpObservation?: TypedProviderHttpObservation;
|
|
985
|
+
}): Promise<{
|
|
986
|
+
readonly typedHttp429?: TypedHttp429Observation;
|
|
987
|
+
readonly observationReadFailure?: ExplicitInternalKnownFailure;
|
|
988
|
+
}> {
|
|
989
|
+
if (input.typedHttpObservationSettled === true) {
|
|
990
|
+
const observation = input.typedHttpObservation;
|
|
991
|
+
if (
|
|
992
|
+
observation !== undefined &&
|
|
993
|
+
observation.httpStatus === 429 &&
|
|
994
|
+
isV1ResumableProvider(observation.provider)
|
|
995
|
+
) {
|
|
996
|
+
return {
|
|
997
|
+
typedHttp429: { httpStatus: 429, provider: observation.provider },
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
return {};
|
|
1001
|
+
}
|
|
1002
|
+
try {
|
|
1003
|
+
const typedHttp429 = await readTypedHttp429Observation(input.runDirectory);
|
|
1004
|
+
return typedHttp429 === undefined ? {} : { typedHttp429 };
|
|
1005
|
+
} catch (error) {
|
|
1006
|
+
const failure = error instanceof Error ? error : new Error(String(error));
|
|
1007
|
+
return {
|
|
1008
|
+
observationReadFailure: {
|
|
1009
|
+
cause: "session",
|
|
1010
|
+
identity: thrownIdentity(failure),
|
|
1011
|
+
diagnostic: failure.message || failure.name,
|
|
1012
|
+
},
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
/** Spread into presentControlledFailure failureInput from one audited resolution. */
|
|
1018
|
+
export function controlledFailureInputFromResolution(
|
|
1019
|
+
resolution: AuditedRunnerFailureResolution,
|
|
1020
|
+
): {
|
|
1021
|
+
knownFailure?: ExplicitInternalKnownFailure;
|
|
1022
|
+
typedHttpObservationSettled?: true;
|
|
1023
|
+
typedHttpObservation?: TypedProviderHttpObservation;
|
|
1024
|
+
} {
|
|
1025
|
+
return {
|
|
1026
|
+
...(resolution.knownFailure === undefined ? {} : { knownFailure: resolution.knownFailure }),
|
|
1027
|
+
...(resolution.typedHttpObservationSettled
|
|
1028
|
+
? {
|
|
1029
|
+
typedHttpObservationSettled: true as const,
|
|
1030
|
+
...(resolution.typedHttpObservation === undefined
|
|
1031
|
+
? {}
|
|
1032
|
+
: { typedHttpObservation: resolution.typedHttpObservation }),
|
|
1033
|
+
}
|
|
1034
|
+
: {}),
|
|
1035
|
+
};
|
|
846
1036
|
}
|
|
847
1037
|
|
|
848
1038
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
@@ -14,7 +14,7 @@ export type ReviewerChildExecuteOptions = Readonly<{
|
|
|
14
14
|
export function projectSharedChildFailure(error: unknown): unknown {
|
|
15
15
|
if (typeof error === "object" && error !== null && "evidenceChildFailure" in error) {
|
|
16
16
|
const classification = (error as { evidenceChildFailure?: unknown }).evidenceChildFailure;
|
|
17
|
-
if (classification === "provider" || classification === "child") {
|
|
17
|
+
if (classification === "provider" || classification === "child" || classification === "unknown") {
|
|
18
18
|
Object.assign(error, { reviewerFailure: classification });
|
|
19
19
|
}
|
|
20
20
|
}
|
package/src/role-runtime.ts
CHANGED
|
@@ -44,7 +44,7 @@ import {
|
|
|
44
44
|
NAVIGATOR_INVOCATION_ENTRY,
|
|
45
45
|
resolveLifecycleInvocationPrincipal,
|
|
46
46
|
} from "./navigator-invocation-identity.ts";
|
|
47
|
-
import { recordTypedProviderHttpStatus } from "./
|
|
47
|
+
import { recordTypedProviderHttpStatus } from "./typed-provider-http.ts";
|
|
48
48
|
import {
|
|
49
49
|
ExplicitInternalActivationError,
|
|
50
50
|
recordReviewerDispatchRejectionSync,
|
|
@@ -807,22 +807,32 @@ export function createRoleRuntimeExtension(
|
|
|
807
807
|
await observe(() => observationFace.onEnd(event), ctx);
|
|
808
808
|
});
|
|
809
809
|
|
|
810
|
-
// Public Role run: record typed
|
|
811
|
-
//
|
|
812
|
-
//
|
|
810
|
+
// Public Role run: record typed non-success HTTP for error evidence + v1 resume.
|
|
811
|
+
// 2xx clears prior observation (see recordTypedProviderHttpStatus). Non-success
|
|
812
|
+
// write failure must surface — never silently drop authorized error evidence.
|
|
813
813
|
pi.on("after_provider_response", async (event, ctx) => {
|
|
814
814
|
const runDir = process.env.AK_ROLE_RUN_DIR;
|
|
815
815
|
if (typeof runDir !== "string" || runDir.trim() === "") return;
|
|
816
816
|
const provider = ctx.model?.provider;
|
|
817
817
|
if (typeof provider !== "string" || provider.trim() === "") return;
|
|
818
|
+
const status = event.status;
|
|
819
|
+
if (typeof status !== "number") return;
|
|
818
820
|
try {
|
|
819
821
|
await recordTypedProviderHttpStatus(runDir, {
|
|
820
|
-
httpStatus:
|
|
822
|
+
httpStatus: status,
|
|
821
823
|
provider,
|
|
822
824
|
});
|
|
823
|
-
} catch {
|
|
824
|
-
|
|
825
|
-
|
|
825
|
+
} catch (error) {
|
|
826
|
+
if (
|
|
827
|
+
status >= 200 &&
|
|
828
|
+
status < 300 &&
|
|
829
|
+
error instanceof Error &&
|
|
830
|
+
"code" in error &&
|
|
831
|
+
(error as { code?: unknown }).code === "ENOENT"
|
|
832
|
+
) {
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
failInfrastructure(error, ctx);
|
|
826
836
|
}
|
|
827
837
|
});
|
|
828
838
|
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed ticket face contract (#176 option A).
|
|
3
|
+
*
|
|
4
|
+
* Machine field lives in YAML frontmatter only:
|
|
5
|
+
* ---
|
|
6
|
+
* ticketNumber: <positive integer>
|
|
7
|
+
* ---
|
|
8
|
+
*
|
|
9
|
+
* Admission reads frozen attachment bytes for this field alone.
|
|
10
|
+
* No filename, path, or free-text inference. Absent / invalid / conflicting
|
|
11
|
+
* values are unbound (undefined) — never a reject gate.
|
|
12
|
+
*
|
|
13
|
+
* Buffer bodies use strict UTF-8 (decode failure → unbound). Opening and
|
|
14
|
+
* closing fences must each be an exclusive line of exactly `---`.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { exactUtf8 } from "./exact-utf8.ts";
|
|
18
|
+
|
|
19
|
+
/** Positive integer GitHub/issue identity carried on a typed ticket face. */
|
|
20
|
+
export type TicketIdentity = number;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Parse one attachment body for the typed frontmatter ticketNumber field.
|
|
24
|
+
* Returns undefined when the contract is absent or not well-formed.
|
|
25
|
+
*/
|
|
26
|
+
export function parseTicketNumberFrontmatter(
|
|
27
|
+
bytes: Buffer | string,
|
|
28
|
+
): TicketIdentity | undefined {
|
|
29
|
+
let text: string;
|
|
30
|
+
if (typeof bytes === "string") {
|
|
31
|
+
text = bytes;
|
|
32
|
+
} else {
|
|
33
|
+
try {
|
|
34
|
+
text = exactUtf8(bytes, "ticket face");
|
|
35
|
+
} catch {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Opening fence must be an exclusive first line of exactly `---`.
|
|
41
|
+
let cursor: number;
|
|
42
|
+
if (text.startsWith("---\n")) {
|
|
43
|
+
cursor = 4;
|
|
44
|
+
} else if (text.startsWith("---\r\n")) {
|
|
45
|
+
cursor = 5;
|
|
46
|
+
} else {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Closing fence must likewise be an exclusive line of exactly `---`.
|
|
51
|
+
const fmLines: string[] = [];
|
|
52
|
+
let closed = false;
|
|
53
|
+
while (cursor < text.length) {
|
|
54
|
+
const nl = text.indexOf("\n", cursor);
|
|
55
|
+
const lineEnd = nl === -1 ? text.length : nl;
|
|
56
|
+
let line = text.slice(cursor, lineEnd);
|
|
57
|
+
if (line.endsWith("\r")) line = line.slice(0, -1);
|
|
58
|
+
if (line === "---") {
|
|
59
|
+
closed = true;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
fmLines.push(line);
|
|
63
|
+
if (nl === -1) break;
|
|
64
|
+
cursor = nl + 1;
|
|
65
|
+
}
|
|
66
|
+
if (!closed) return undefined;
|
|
67
|
+
|
|
68
|
+
let found: number | undefined;
|
|
69
|
+
for (const line of fmLines) {
|
|
70
|
+
const match = /^[ \t]*ticketNumber[ \t]*:[ \t]*(\d+)[ \t]*$/.exec(line);
|
|
71
|
+
if (match === null) continue;
|
|
72
|
+
const value = Number(match[1]);
|
|
73
|
+
if (!Number.isInteger(value) || value < 1) return undefined;
|
|
74
|
+
if (found !== undefined && found !== value) return undefined;
|
|
75
|
+
found = value;
|
|
76
|
+
}
|
|
77
|
+
return found;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Resolve at most one ticketNumber across frozen attachment bodies.
|
|
82
|
+
* Zero matches → unbound. Multiple distinct values → unbound (do not guess).
|
|
83
|
+
*/
|
|
84
|
+
export function resolveTicketNumberFromAttachmentBodies(
|
|
85
|
+
bodies: readonly (Buffer | string)[],
|
|
86
|
+
): TicketIdentity | undefined {
|
|
87
|
+
const found = new Set<number>();
|
|
88
|
+
for (const body of bodies) {
|
|
89
|
+
const value = parseTicketNumberFrontmatter(body);
|
|
90
|
+
if (value !== undefined) found.add(value);
|
|
91
|
+
}
|
|
92
|
+
if (found.size !== 1) return undefined;
|
|
93
|
+
return found.values().next().value;
|
|
94
|
+
}
|