@akagilnc/pi-workflow-roles 0.1.5368 → 0.1.5375
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/acp-host/production-host.js +719 -776
- package/dist/activation-ledger-git.js +2 -2
- package/dist/activation-ledger-topology.js +1 -7
- package/dist/analyst-entry.js +4 -8
- package/dist/analyst-gate-cycles-read.js +3 -11
- package/dist/analyst-index.js +3 -9
- package/dist/analyst-ledger.js +1 -11
- package/dist/analyst-metric-families/acceptance-success-rework.js +1 -3
- package/dist/analyst-metric-families/round-timeline.js +1 -3
- package/dist/analyst-page.js +2 -2
- package/dist/archivist-record-topology.js +2 -2
- package/dist/audit-escalation.js +2 -1
- package/dist/auditor-soul.js +2 -1
- package/dist/collector-config.js +6 -8
- package/dist/compliance-transport.js +2 -1
- package/dist/countersign-contracts.js +2 -1
- package/dist/diarist-contracts.js +3 -2
- package/dist/doctor-contracts.js +1 -3
- package/dist/doctor-evidence.js +7 -8
- package/dist/dossier-resolution.js +1 -3
- package/dist/engine-detour-usage.js +1 -3
- package/dist/gatekeeper-role.js +2 -9
- package/dist/gleaner-left-contracts.js +2 -1
- package/dist/headless-host/description.js +14 -17
- package/dist/headless-host/production-host.js +721 -780
- package/dist/host-session-record.js +3 -2
- package/dist/inspector-contracts.js +2 -1
- package/dist/ledger-session-read.js +1 -3
- package/dist/merger-contracts.js +9 -9
- package/dist/migrate-book-topology.js +348 -314
- package/dist/navigator-attendance.js +9 -11
- package/dist/navigator-invocation-identity.js +3 -2
- package/dist/navigator-public-session.js +3 -5
- package/dist/navigator-session-contracts.js +8 -10
- package/dist/navigator-work-base.js +5 -8
- package/dist/notary-contracts.js +3 -5
- package/dist/package-contracts/auditor-output.js +1 -3
- package/dist/package-contracts/fixer-packet.js +3 -6
- package/dist/package-contracts/gatekeeper-output.js +1 -3
- package/dist/package-contracts/navigator-output.js +8 -8
- package/dist/package-contracts/terminating-infrastructure.js +5 -9
- package/dist/package-contracts/terminating-tools.js +4 -3
- package/dist/public-cli/analyst-run.js +3 -2
- package/dist/public-cli/auto-resume.js +3 -8
- package/dist/public-cli/cli.js +9 -8
- package/dist/public-cli/config.js +8 -11
- package/dist/public-cli/countersign-run.js +1 -3
- package/dist/public-cli/host-providers.js +7 -14
- package/dist/public-cli/instruction-seat-run.js +4 -3
- package/dist/public-cli/invocation.js +9 -8
- package/dist/public-cli/main.js +1531 -1567
- package/dist/public-cli/post-admission.js +4 -3
- package/dist/public-cli/run-lifecycle.js +14 -26
- package/dist/public-cli/settlement.js +10 -26
- package/dist/public-role-summons.js +3 -2
- package/dist/receipt-delivery-policy.js +1 -3
- package/dist/role-run-placement.js +3 -5
- package/dist/role-run-relocation.js +12 -19
- package/dist/run-terminal-artifacts.js +1 -11
- package/dist/run-ticket-number.js +2 -4
- package/dist/session-assistant-usage.js +2 -1
- package/dist/session-dialogue.js +1 -14
- package/dist/sitian-reader.js +2 -4
- package/dist/submission-correctable-error.js +2 -1
- package/dist/submission-errors.js +2 -1
- package/dist/submission-gate.js +2 -1
- package/dist/submission-ledger.js +4 -5
- package/dist/ticket-provenance-contracts.js +1 -3
- package/dist/ticket-provenance.js +2 -1
- package/dist/typed-provider-http.js +4 -7
- package/dist/unknown-value.js +28 -0
- package/dist/user-dialogue-stdin.js +2 -1
- package/package.json +1 -1
- package/src/acp-host/role-turn-host.ts +4 -2
- package/src/activation-ledger-git.ts +3 -2
- package/src/activation-ledger-topology.ts +4 -12
- package/src/analyst-entry.ts +3 -11
- package/src/analyst-gate-cycles-read.ts +3 -15
- package/src/analyst-index.ts +3 -13
- package/src/analyst-ledger.ts +2 -16
- package/src/analyst-metric-families/acceptance-success-rework.ts +2 -4
- package/src/analyst-metric-families/round-timeline.ts +2 -4
- package/src/analyst-page.ts +2 -2
- package/src/archivist-record-topology.ts +2 -2
- package/src/audit-escalation.ts +2 -1
- package/src/auditor-soul.ts +3 -1
- package/src/book-topology-migration-placement.ts +5 -16
- package/src/book-topology-migrators-t11.ts +16 -15
- package/src/book-topology-mixed-volume-migrators.ts +3 -6
- package/src/book-topology-record-class-migrators.ts +7 -10
- package/src/book-topology-runs-migrator.ts +4 -3
- package/src/collector-config.ts +7 -9
- package/src/compliance-transport.ts +2 -1
- package/src/countersign-contracts.ts +2 -1
- package/src/diarist-contracts.ts +3 -2
- package/src/doctor-contracts.ts +3 -1
- package/src/doctor-evidence.ts +8 -7
- package/src/doctor-role.ts +4 -2
- package/src/dossier-resolution.ts +2 -4
- package/src/engine-detour-tool.ts +0 -2
- package/src/engine-detour-usage.ts +2 -4
- package/src/external-host-turn-loop.ts +3 -1
- package/src/factory-board.ts +1 -8
- package/src/gatekeeper-role.ts +2 -10
- package/src/gleaner-left-contracts.ts +3 -1
- package/src/headless-host/description.ts +15 -18
- package/src/headless-host/role-turn-host.ts +10 -9
- package/src/host-session-record.ts +4 -2
- package/src/inspector-contracts.ts +2 -1
- package/src/ledger-session-read.ts +2 -5
- package/src/merger-contracts.ts +10 -9
- package/src/navigator-attendance.ts +10 -12
- package/src/navigator-invocation-identity.ts +3 -2
- package/src/navigator-public-session.ts +4 -6
- package/src/navigator-session-contracts.ts +8 -11
- package/src/navigator-work-base.ts +5 -8
- package/src/navigator-work-context.ts +2 -1
- package/src/notary-contracts.ts +4 -6
- package/src/oauth-keepalive.ts +5 -18
- package/src/package-contracts/auditor-output.ts +2 -4
- package/src/package-contracts/fixer-packet.ts +4 -6
- package/src/package-contracts/gatekeeper-output.ts +2 -4
- package/src/package-contracts/judge-output.ts +0 -1
- package/src/package-contracts/navigator-output.ts +8 -8
- package/src/package-contracts/terminating-infrastructure.ts +6 -20
- package/src/package-contracts/terminating-tools.ts +8 -12
- package/src/pi/adapter.ts +0 -1
- package/src/pi/role-turn-host.ts +0 -1
- package/src/public-cli/analyst-run.ts +4 -2
- package/src/public-cli/auto-resume.ts +4 -11
- package/src/public-cli/cli.ts +10 -10
- package/src/public-cli/config.ts +8 -27
- package/src/public-cli/countersign-run.ts +2 -4
- package/src/public-cli/host-providers.ts +10 -27
- package/src/public-cli/instruction-seat-run.ts +5 -3
- package/src/public-cli/invocation.ts +11 -10
- package/src/public-cli/post-admission.ts +5 -3
- package/src/public-cli/run-lifecycle.ts +20 -41
- package/src/public-cli/settlement.ts +11 -33
- package/src/public-role-summons.ts +4 -3
- package/src/receipt-delivery-policy.ts +1 -4
- package/src/reviewer-role.ts +0 -1
- package/src/role-envelope.ts +5 -3
- package/src/role-run-placement.ts +4 -10
- package/src/role-run-relocation.ts +13 -23
- package/src/role-runtime.ts +9 -15
- package/src/run-terminal-artifacts.ts +2 -16
- package/src/run-ticket-number.ts +4 -10
- package/src/session-assistant-usage.ts +3 -1
- package/src/session-dialogue.ts +1 -4
- package/src/session-identity.ts +0 -1
- package/src/sitian-reader.ts +2 -4
- package/src/submission-correctable-error.ts +3 -1
- package/src/submission-errors.ts +2 -1
- package/src/submission-gate.ts +3 -1
- package/src/submission-ledger.ts +5 -6
- package/src/ticket-provenance-contracts.ts +1 -4
- package/src/ticket-provenance.ts +3 -1
- package/src/ticket-snapshot.ts +2 -4
- package/src/ticket-trajectory.ts +1 -8
- package/src/typed-provider-http.ts +5 -15
- package/src/unknown-value.ts +34 -0
- package/src/user-dialogue-stdin.ts +3 -1
- package/src/worker-role.ts +2 -5
- package/src/worker-submission-gates.ts +5 -7
|
@@ -36,11 +36,12 @@ import {
|
|
|
36
36
|
headlessTurnArgs,
|
|
37
37
|
isClaudePrintDescription,
|
|
38
38
|
isCodexExecDescription,
|
|
39
|
-
isPlainObject,
|
|
40
39
|
type HeadlessHostDescription,
|
|
41
40
|
} from "./description.ts";
|
|
42
41
|
import { NAVIGATOR_OUTPUT_TOOL_NAME } from "../package-contracts/navigator-output.ts";
|
|
43
42
|
|
|
43
|
+
import { errorText, isRecord } from "../unknown-value.ts";
|
|
44
|
+
|
|
44
45
|
export type HeadlessRoleTurnHostConfig = Readonly<{
|
|
45
46
|
description: HeadlessHostDescription;
|
|
46
47
|
sessionIdentity: SessionIdentityAuthority;
|
|
@@ -68,7 +69,7 @@ export type HeadlessCliResult = Readonly<{
|
|
|
68
69
|
* envelope without stream-json `type`). Intermediate stream-json events are not.
|
|
69
70
|
*/
|
|
70
71
|
function isHeadlessResultCandidate(value: unknown): value is HeadlessCliResult {
|
|
71
|
-
if (!
|
|
72
|
+
if (!isRecord(value)) return false;
|
|
72
73
|
const record = value as HeadlessCliResult & { type?: unknown };
|
|
73
74
|
return record.type === undefined
|
|
74
75
|
|| record.type === "result"
|
|
@@ -131,11 +132,11 @@ function createCodexExecTurnObserver(): {
|
|
|
131
132
|
|
|
132
133
|
return {
|
|
133
134
|
observe(value) {
|
|
134
|
-
if (!
|
|
135
|
+
if (!isRecord(value)) return;
|
|
135
136
|
const type = typeof value.type === "string" ? value.type : undefined;
|
|
136
137
|
if (type === "thread.started" && typeof value.thread_id === "string" && value.thread_id !== "") {
|
|
137
138
|
threadId = value.thread_id;
|
|
138
|
-
} else if (type === "item.completed" &&
|
|
139
|
+
} else if (type === "item.completed" && isRecord(value.item)) {
|
|
139
140
|
if (value.item.type === "agent_message" && typeof value.item.text === "string") {
|
|
140
141
|
finalMessage = value.item.text;
|
|
141
142
|
}
|
|
@@ -161,7 +162,7 @@ function createCodexExecTurnObserver(): {
|
|
|
161
162
|
|
|
162
163
|
function formatCodexFailurePayload(payload: unknown): string {
|
|
163
164
|
if (typeof payload === "string" && payload.trim() !== "") return payload;
|
|
164
|
-
if (
|
|
165
|
+
if (isRecord(payload)) {
|
|
165
166
|
if (typeof payload.message === "string" && payload.message.trim() !== "") return payload.message;
|
|
166
167
|
try {
|
|
167
168
|
return JSON.stringify(payload);
|
|
@@ -198,7 +199,7 @@ function resolveGitCommonDir(cwd: string): string | undefined {
|
|
|
198
199
|
encoding: "utf8",
|
|
199
200
|
});
|
|
200
201
|
} catch (error) {
|
|
201
|
-
const message =
|
|
202
|
+
const message = errorText(error);
|
|
202
203
|
throw new Error(`git rev-parse --git-common-dir failed: ${message}`);
|
|
203
204
|
}
|
|
204
205
|
if (result.error !== undefined) {
|
|
@@ -487,7 +488,7 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
|
|
|
487
488
|
...(gitCommonDir === undefined ? {} : { writableRoots: [gitCommonDir] }),
|
|
488
489
|
});
|
|
489
490
|
} catch (error) {
|
|
490
|
-
const message =
|
|
491
|
+
const message = errorText(error);
|
|
491
492
|
return {
|
|
492
493
|
status: "terminal",
|
|
493
494
|
result: failure("session", "HeadlessArgvFailure", "argv-failed", { diagnostic: message }, message),
|
|
@@ -559,7 +560,7 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
|
|
|
559
560
|
} catch (error) {
|
|
560
561
|
if (childExited) exitedSessionId = (codex ? codexObserver?.result().threadId : undefined) ?? sessionId;
|
|
561
562
|
if (isHostAbortedError(error)) throw error;
|
|
562
|
-
const message =
|
|
563
|
+
const message = errorText(error);
|
|
563
564
|
const observedHostFailure = codexObserver?.result().failureDiagnostic;
|
|
564
565
|
if (observedHostFailure !== undefined) {
|
|
565
566
|
return {
|
|
@@ -604,7 +605,7 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
|
|
|
604
605
|
} catch (error) {
|
|
605
606
|
// The host failure remains primary; retain persistence failure as
|
|
606
607
|
// secondary typed evidence instead of replacing the terminal.
|
|
607
|
-
sessionBindingDiagnostic =
|
|
608
|
+
sessionBindingDiagnostic = errorText(error);
|
|
608
609
|
}
|
|
609
610
|
}
|
|
610
611
|
|
|
@@ -26,6 +26,8 @@ import type { RoleTurnContinuation } from "./host-contracts.ts";
|
|
|
26
26
|
import { sitianReport } from "./sitian-facade.ts";
|
|
27
27
|
import type { SitianRecordInput } from "./sitian-contracts.ts";
|
|
28
28
|
|
|
29
|
+
import { errorText } from "./unknown-value.ts";
|
|
30
|
+
|
|
29
31
|
/** Volume category under `<run>/session/<kind>/records.jsonl`. */
|
|
30
32
|
export const HOST_SESSION_RECORD_KIND = "host-session" as const;
|
|
31
33
|
|
|
@@ -269,7 +271,7 @@ export function copyAndRecordHostDossier(options: {
|
|
|
269
271
|
payload: {
|
|
270
272
|
type: "native-session-warning",
|
|
271
273
|
sessionId: options.sessionId,
|
|
272
|
-
error:
|
|
274
|
+
error: errorText(error),
|
|
273
275
|
},
|
|
274
276
|
});
|
|
275
277
|
return;
|
|
@@ -334,7 +336,7 @@ export function copyAndRecordHostDossier(options: {
|
|
|
334
336
|
landingPath,
|
|
335
337
|
ordinal,
|
|
336
338
|
sessionId: options.sessionId,
|
|
337
|
-
error:
|
|
339
|
+
error: errorText(lastError),
|
|
338
340
|
},
|
|
339
341
|
});
|
|
340
342
|
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { REVIEW_QUEUE_STATUSES, REVIEW_SUBMISSION_OUTPUT_TOOL_NAME } from "./review-submission.ts";
|
|
8
|
+
import { isRecord } from "./unknown-value.ts";
|
|
8
9
|
|
|
9
10
|
export const INSPECTOR_OUTPUT_TOOL_NAME: string = REVIEW_SUBMISSION_OUTPUT_TOOL_NAME;
|
|
10
11
|
export const INSPECTOR_SOURCE_RUN_FLAG = {
|
|
@@ -21,7 +22,7 @@ export type InspectorOutput =
|
|
|
21
22
|
| { readonly status: "escalate"; readonly reason?: unknown; readonly findings?: unknown };
|
|
22
23
|
|
|
23
24
|
export function validateRecordedInspectorOutput(value: unknown): InspectorOutput {
|
|
24
|
-
if (
|
|
25
|
+
if (!isRecord(value)) {
|
|
25
26
|
throw new Error("Inspector output has no execution discriminator");
|
|
26
27
|
}
|
|
27
28
|
let status: unknown;
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { readFile } from "node:fs/promises";
|
|
6
6
|
|
|
7
|
+
import { isRecord } from "./unknown-value.ts";
|
|
8
|
+
|
|
7
9
|
export type LedgerSessionRow = Record<string, unknown>;
|
|
8
10
|
|
|
9
11
|
/**
|
|
@@ -32,10 +34,6 @@ export class LedgerSessionJsonlError extends Error {
|
|
|
32
34
|
}
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
36
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
37
|
/**
|
|
40
38
|
* One physical JSONL line as the single kernel saw it.
|
|
41
39
|
* `row` present = syntactically complete session object at that 1-based line.
|
|
@@ -339,4 +337,3 @@ export function intervalRowsAroundAnchor(
|
|
|
339
337
|
return { rows: rows.slice(start, end), closed: end < rows.length };
|
|
340
338
|
}
|
|
341
339
|
|
|
342
|
-
|
package/src/merger-contracts.ts
CHANGED
|
@@ -3,17 +3,19 @@ import { exactUtf8 } from "./exact-utf8.ts";
|
|
|
3
3
|
import { openToolObject } from "./open-tool-schema.ts";
|
|
4
4
|
import { withTerminatingOutputDeclarations } from "./package-contracts/terminating-infrastructure.ts";
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
import { isRecord } from "./unknown-value.ts";
|
|
7
|
+
|
|
8
|
+
const materialSchema = Type.Object({ bytesBase64: Type.String(), sha256: Type.String() });
|
|
9
|
+
const checkSchema = Type.Object({ name: Type.String({ minLength: 1 }), argv: Type.Array(Type.String({ minLength: 1 }), { minItems: 1 }) });
|
|
8
10
|
export const mergerInputSchema = Type.Object({
|
|
9
11
|
attemptId: Type.String({ minLength: 1, description: "对账 attempt 身份(transport)" }),
|
|
10
12
|
targetObjectId: Type.String({ description: "材料:target parent OID;可空" }),
|
|
11
13
|
sourceObjectId: Type.String({ description: "材料:source parent OID;可空" }),
|
|
12
|
-
materials: Type.Object({ task: materialSchema, authority: materialSchema, targetIntent: materialSchema, sourceIntent: materialSchema }
|
|
14
|
+
materials: Type.Object({ task: materialSchema, authority: materialSchema, targetIntent: materialSchema, sourceIntent: materialSchema }),
|
|
13
15
|
expectedConflictPaths: Type.Array(Type.String(), { description: "材料:当前未合并路径;可空" }),
|
|
14
16
|
resolutionScope: Type.Array(Type.String(), { description: "材料:解析范围提示;可空" }),
|
|
15
17
|
authorizedChecks: Type.Array(checkSchema),
|
|
16
|
-
}
|
|
18
|
+
});
|
|
17
19
|
// #836 r16 class 1: attemptId/report/diagnosis/mergeCommitId are LLM/human-read
|
|
18
20
|
// narrative content (candidate stored as submitted) — no code branches on their
|
|
19
21
|
// length. mergerInputSchema (host-authored material, not role output) is out of scope.
|
|
@@ -42,7 +44,6 @@ export type MergerOutput =
|
|
|
42
44
|
| { status: "escalate"; attemptId: string; diagnosis: string; report: string };
|
|
43
45
|
export const MERGER_OUTPUT_TOOL_NAME = "ak_merger_output";
|
|
44
46
|
|
|
45
|
-
const record = (v: unknown): v is Record<string, unknown> => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
46
47
|
const blank = (v: unknown) => typeof v !== "string" || v.trim().length === 0;
|
|
47
48
|
export class MergerInputContractError extends Error {
|
|
48
49
|
constructor(message = "Merger input violates its exact contract") { super(message); this.name = "MergerInputContractError"; }
|
|
@@ -53,16 +54,16 @@ const asString = (v: unknown): string => typeof v === "string" ? v : "";
|
|
|
53
54
|
const asStringArray = (v: unknown): string[] => Array.isArray(v) ? v.filter((item): item is string => typeof item === "string") : [];
|
|
54
55
|
|
|
55
56
|
function readMaterial(value: unknown, label: string): MergerMaterial {
|
|
56
|
-
if (!
|
|
57
|
+
if (!isRecord(value) || typeof value.bytesBase64 !== "string") fail(`Merger ${label} material is malformed`);
|
|
57
58
|
exactUtf8(Buffer.from(value.bytesBase64, "base64"), `Merger ${label} material`);
|
|
58
59
|
return { bytesBase64: value.bytesBase64, sha256: typeof value.sha256 === "string" ? value.sha256 : "" };
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
export function validateMergerInput(value: unknown): MergerInput {
|
|
62
|
-
if (!
|
|
63
|
-
if (!
|
|
63
|
+
if (!isRecord(value) || blank(value.attemptId)) fail("Merger input requires a non-empty attemptId for accounting");
|
|
64
|
+
if (!isRecord(value.materials)) fail("Merger materials are missing");
|
|
64
65
|
const materials = { task: readMaterial(value.materials.task, "task"), authority: readMaterial(value.materials.authority, "authority"), targetIntent: readMaterial(value.materials.targetIntent, "targetIntent"), sourceIntent: readMaterial(value.materials.sourceIntent, "sourceIntent") };
|
|
65
|
-
const authorizedChecks = Array.isArray(value.authorizedChecks) ? value.authorizedChecks.filter((check): check is Record<string, unknown> =>
|
|
66
|
+
const authorizedChecks = Array.isArray(value.authorizedChecks) ? value.authorizedChecks.filter((check): check is Record<string, unknown> => isRecord(check)).map(c => ({ name: asString(c.name), argv: asStringArray(c.argv).filter(a => a.trim().length > 0) })).filter(c => c.name.trim().length > 0 && c.argv.length > 0) : [];
|
|
66
67
|
return deepFreeze(structuredClone({ attemptId: (value.attemptId as string).trim(), targetObjectId: asString(value.targetObjectId), sourceObjectId: asString(value.sourceObjectId), materials, expectedConflictPaths: asStringArray(value.expectedConflictPaths), resolutionScope: asStringArray(value.resolutionScope), authorizedChecks }) as MergerInput);
|
|
67
68
|
}
|
|
68
69
|
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
1
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
2
|
import { homedir } from "node:os";
|
|
4
3
|
import { dirname, join, resolve } from "node:path";
|
|
@@ -64,6 +63,9 @@ import { createReceiptDeliveryPolicy, NO_RECEIPT_LIFECYCLE_ENTRY_TYPE } from "./
|
|
|
64
63
|
import { navigatorProseFromUnknown } from "./package-contracts/navigator-output.ts";
|
|
65
64
|
import { persistNavigatorWorkBase } from "./navigator-work-base.ts";
|
|
66
65
|
|
|
66
|
+
import { sha256Hex } from "./sha256.ts";
|
|
67
|
+
import { isRecord } from "./unknown-value.ts";
|
|
68
|
+
|
|
67
69
|
export const NAVIGATOR_EVENT_TYPE = "ak-navigator-attendance" as const;
|
|
68
70
|
export { NAVIGATOR_ROUTE_PLAYBOOK_FAILURE_ENTRY };
|
|
69
71
|
|
|
@@ -167,25 +169,21 @@ function unavailableKey(value: unknown): NavigatorUnavailableKey | undefined {
|
|
|
167
169
|
: undefined;
|
|
168
170
|
}
|
|
169
171
|
|
|
170
|
-
function exactRecord(value: unknown): value is Record<string, unknown> {
|
|
171
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
172
|
/** Correlate one rejected prepare call/result inside the just-finished prompt. */
|
|
175
173
|
function rejectedPrepareReason(entries: readonly unknown[], start: number): string | undefined {
|
|
176
174
|
const recent = entries.slice(start);
|
|
177
175
|
const prepareCalls = new Set<string>();
|
|
178
176
|
for (const entry of recent) {
|
|
179
|
-
if (!
|
|
177
|
+
if (!isRecord(entry) || entry.type !== "message" || !isRecord(entry.message)
|
|
180
178
|
|| entry.message.role !== "assistant" || !Array.isArray(entry.message.content)) continue;
|
|
181
179
|
for (const part of entry.message.content) {
|
|
182
|
-
if (
|
|
180
|
+
if (isRecord(part) && part.type === "toolCall" && part.name === NAVIGATOR_PREPARE_TOOL_NAME
|
|
183
181
|
&& typeof part.id === "string") prepareCalls.add(part.id);
|
|
184
182
|
}
|
|
185
183
|
}
|
|
186
184
|
let reason: string | undefined;
|
|
187
185
|
for (const entry of recent) {
|
|
188
|
-
if (!
|
|
186
|
+
if (!isRecord(entry) || entry.type !== "message" || !isRecord(entry.message)
|
|
189
187
|
|| entry.message.role !== "toolResult" || entry.message.isError !== true) continue;
|
|
190
188
|
const callId = entry.message.toolCallId;
|
|
191
189
|
if (entry.message.toolName !== NAVIGATOR_PREPARE_TOOL_NAME
|
|
@@ -194,7 +192,7 @@ function rejectedPrepareReason(entries: readonly unknown[], start: number): stri
|
|
|
194
192
|
}
|
|
195
193
|
const content = entry.message.content;
|
|
196
194
|
const text = Array.isArray(content)
|
|
197
|
-
? content.flatMap((part) =>
|
|
195
|
+
? content.flatMap((part) => isRecord(part) && typeof part.text === "string" ? [part.text] : []).join("")
|
|
198
196
|
: typeof content === "string" ? content : "";
|
|
199
197
|
if (text.trim() !== "") reason = text.trim();
|
|
200
198
|
}
|
|
@@ -220,7 +218,7 @@ export function navigatorSubjectKey(
|
|
|
220
218
|
if (provenance === "placeholder") return subjectRoot;
|
|
221
219
|
const normalized = subject.trim().replace(/\s+/g, " ");
|
|
222
220
|
if (normalized === "") return subjectRoot;
|
|
223
|
-
return `${subjectRoot}#${
|
|
221
|
+
return `${subjectRoot}#${sha256Hex(normalized).slice(0, 32)}`;
|
|
224
222
|
}
|
|
225
223
|
|
|
226
224
|
/**
|
|
@@ -485,10 +483,10 @@ export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
|
|
|
485
483
|
return undefined;
|
|
486
484
|
}
|
|
487
485
|
const nativeFailure = [...activeSession.entries()].reverse().find((entry: unknown) => {
|
|
488
|
-
if (!
|
|
486
|
+
if (!isRecord(entry) || entry.type !== "message" || !isRecord(entry.message)) return false;
|
|
489
487
|
return entry.message.role === "assistant" && typeof entry.message.errorMessage === "string" && entry.message.errorMessage.trim() !== "";
|
|
490
488
|
});
|
|
491
|
-
const nativeMessage =
|
|
489
|
+
const nativeMessage = isRecord(nativeFailure) && isRecord(nativeFailure.message) ? nativeFailure.message : undefined;
|
|
492
490
|
const errorMessage = nativeMessage !== undefined && typeof nativeMessage.errorMessage === "string"
|
|
493
491
|
? nativeMessage.errorMessage
|
|
494
492
|
: "Navigator did not submit direction advice";
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
import { LEGACY_REVIEW_OUTPUT_ROLES, PACKAGED_ROLE_REGISTRY } from "./packaged-role-registry.ts";
|
|
21
21
|
import { isUuidV7, uuidv7 } from "./uuidv7.ts";
|
|
22
|
+
import { isRecord } from "./unknown-value.ts";
|
|
22
23
|
import { workSubjectKeysEqual } from "./work-subject-identity.ts";
|
|
23
24
|
|
|
24
25
|
export const NAVIGATOR_INVOCATION_ENTRY = "ak-navigator-invocation" as const;
|
|
@@ -83,7 +84,7 @@ export function extractInfrastructureFailureEvidence(error: unknown): Record<str
|
|
|
83
84
|
* (ADR 0040 — discriminators select the branch, they do not ban extras).
|
|
84
85
|
*/
|
|
85
86
|
export function hasNavigatorInfrastructureFailureBase(value: unknown): boolean {
|
|
86
|
-
if (
|
|
87
|
+
if (!isRecord(value)) return false;
|
|
87
88
|
const record = value as Record<string, unknown>;
|
|
88
89
|
for (const key of NAVIGATOR_INFRASTRUCTURE_FAILURE_KEYS) {
|
|
89
90
|
if (!Object.hasOwn(record, key)) return false;
|
|
@@ -160,7 +161,7 @@ function invocationPhaseFromUnknown(value: unknown): InvocationPhase | undefined
|
|
|
160
161
|
export function parseInvocationMarkerIdentity(
|
|
161
162
|
data: unknown,
|
|
162
163
|
): InvocationMarkerIdentity | undefined {
|
|
163
|
-
if (
|
|
164
|
+
if (!isRecord(data)) return undefined;
|
|
164
165
|
const record = data as Record<string, unknown>;
|
|
165
166
|
const invocationId = record.invocationId;
|
|
166
167
|
if (typeof invocationId !== "string") return undefined;
|
|
@@ -29,6 +29,8 @@ import {
|
|
|
29
29
|
navigatorWorkContextFile,
|
|
30
30
|
} from "./navigator-work-base.ts";
|
|
31
31
|
|
|
32
|
+
import { isRecord } from "./unknown-value.ts";
|
|
33
|
+
|
|
32
34
|
/**
|
|
33
35
|
* Ledger process home for navigator attendance: admitted HostContext.runDirectory
|
|
34
36
|
* first, else a parent session file under .ak-roles. Never context.home / env HOME /
|
|
@@ -54,10 +56,6 @@ async function resolveNavigatorLedgerHome(context: HostContext): Promise<string
|
|
|
54
56
|
/** Resume key only — points at a host run principal, never stores advice prose. */
|
|
55
57
|
export const NAVIGATOR_HOST_RUN_POINTER_ENTRY = "ak-navigator-host-run";
|
|
56
58
|
|
|
57
|
-
function exactRecord(value: unknown): value is Record<string, unknown> {
|
|
58
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
59
|
export function runIdFromNavigatorDirectory(runDirectory: string): string | undefined {
|
|
62
60
|
const entry = basename(runDirectory);
|
|
63
61
|
const suffix = "@navigator";
|
|
@@ -69,10 +67,10 @@ export function runIdFromNavigatorDirectory(runDirectory: string): string | unde
|
|
|
69
67
|
export function readNavigatorHostRunPointer(entries: readonly unknown[]): string | undefined {
|
|
70
68
|
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
71
69
|
const entry = entries[index];
|
|
72
|
-
if (!
|
|
70
|
+
if (!isRecord(entry)) continue;
|
|
73
71
|
if (entry.type === "custom" && entry.customType === NAVIGATOR_HOST_RUN_POINTER_ENTRY) {
|
|
74
72
|
const data = entry.data;
|
|
75
|
-
if (
|
|
73
|
+
if (isRecord(data) && typeof data.runId === "string" && data.runId.trim() !== "") {
|
|
76
74
|
return data.runId.trim();
|
|
77
75
|
}
|
|
78
76
|
}
|
|
@@ -13,6 +13,7 @@ import { Value } from "typebox/value";
|
|
|
13
13
|
import type { HostContext, HostInstitutionalModelSelection } from "./host-contracts.ts";
|
|
14
14
|
import type { NoReceiptLifecycleFacts } from "./receipt-delivery-policy.ts";
|
|
15
15
|
|
|
16
|
+
import { isRecord, errorText, isEnoent } from "./unknown-value.ts";
|
|
16
17
|
|
|
17
18
|
export const NAVIGATOR_PREPARE_TOOL_NAME = "ak_navigator_prepare" as const;
|
|
18
19
|
|
|
@@ -53,16 +54,12 @@ export function navigatorUnavailableError(
|
|
|
53
54
|
error: unknown,
|
|
54
55
|
cause: NavigatorUnavailableKey = source,
|
|
55
56
|
): NavigatorUnavailableError {
|
|
56
|
-
const message =
|
|
57
|
+
const message = errorText(error);
|
|
57
58
|
return error instanceof NavigatorUnavailableError
|
|
58
59
|
? error
|
|
59
60
|
: new NavigatorUnavailableError(source, message, cause, error);
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
function exactRecord(value: unknown): value is Record<string, unknown> {
|
|
63
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
63
|
export function navigatorProviderFailureFromStatus(status: number | undefined): NavigatorProviderFailureFact | undefined {
|
|
67
64
|
if (status === 401 || status === 403) return { source: "auth", cause: "auth" };
|
|
68
65
|
if (status === 429) return { source: "quota", cause: "quota" };
|
|
@@ -86,7 +83,7 @@ export function navigatorProviderFailureFromError(error: unknown): NavigatorProv
|
|
|
86
83
|
if (cursor instanceof NavigatorUnavailableError) {
|
|
87
84
|
return { source: cursor.unavailableSource, cause: cursor.unavailableCause };
|
|
88
85
|
}
|
|
89
|
-
if (!
|
|
86
|
+
if (!isRecord(cursor)) {
|
|
90
87
|
cursor = cursor instanceof Error ? cursor.cause : undefined;
|
|
91
88
|
continue;
|
|
92
89
|
}
|
|
@@ -111,13 +108,13 @@ export function navigatorProviderFailureFromError(error: unknown): NavigatorProv
|
|
|
111
108
|
export function navigatorProviderFailureFromDiagnostics(diagnostics: unknown): NavigatorProviderFailureFact | undefined {
|
|
112
109
|
if (!Array.isArray(diagnostics)) return undefined;
|
|
113
110
|
for (const diagnostic of diagnostics) {
|
|
114
|
-
if (!
|
|
111
|
+
if (!isRecord(diagnostic)) continue;
|
|
115
112
|
if (diagnostic.type === "provider_transport_failure") return { source: "transport", cause: "transport" };
|
|
116
|
-
if (
|
|
113
|
+
if (isRecord(diagnostic.error)) {
|
|
117
114
|
const fromCode = navigatorProviderFailureFromCode(diagnostic.error.code);
|
|
118
115
|
if (fromCode !== undefined) return fromCode;
|
|
119
116
|
}
|
|
120
|
-
if (
|
|
117
|
+
if (isRecord(diagnostic.details)) {
|
|
121
118
|
const status = typeof diagnostic.details.status === "number"
|
|
122
119
|
? diagnostic.details.status
|
|
123
120
|
: typeof diagnostic.details.statusCode === "number"
|
|
@@ -225,13 +222,13 @@ export function navigatorModelSettingPath(): string {
|
|
|
225
222
|
export async function readNavigatorModelSetting(path = navigatorModelSettingPath()): Promise<string> {
|
|
226
223
|
try {
|
|
227
224
|
const raw = JSON.parse(await readFile(path, "utf8")) as unknown;
|
|
228
|
-
if (!
|
|
225
|
+
if (!isRecord(raw) || typeof raw.model !== "string" || raw.model.trim() === "") {
|
|
229
226
|
throw new Error("Navigator model setting is malformed");
|
|
230
227
|
}
|
|
231
228
|
return raw.model;
|
|
232
229
|
} catch (error) {
|
|
233
230
|
// #178: missing file is absence (no package default).
|
|
234
|
-
if (
|
|
231
|
+
if (isEnoent(error)) {
|
|
235
232
|
throw new Error("Navigator model setting is missing");
|
|
236
233
|
}
|
|
237
234
|
throw error;
|
|
@@ -8,6 +8,7 @@ import { readFile } from "node:fs/promises";
|
|
|
8
8
|
import { basename, dirname, join, resolve } from "node:path";
|
|
9
9
|
|
|
10
10
|
import { writeFileAtomically } from "./atomic-write.ts";
|
|
11
|
+
import { isEnoent, isRecord } from "./unknown-value.ts";
|
|
11
12
|
|
|
12
13
|
export const NAVIGATOR_WORK_CONTEXT_BASENAME = "work-context.json";
|
|
13
14
|
|
|
@@ -51,9 +52,7 @@ export async function persistNavigatorWorkBase(
|
|
|
51
52
|
const current = await readFile(path, "utf8");
|
|
52
53
|
if (current === next) return path;
|
|
53
54
|
} catch (error) {
|
|
54
|
-
if (!(error
|
|
55
|
-
throw error;
|
|
56
|
-
}
|
|
55
|
+
if (!isEnoent(error)) throw error;
|
|
57
56
|
}
|
|
58
57
|
await writeFileAtomically(path, next);
|
|
59
58
|
return path;
|
|
@@ -66,9 +65,7 @@ export async function readNavigatorWorkBase(path: string): Promise<NavigatorWork
|
|
|
66
65
|
try {
|
|
67
66
|
text = await readFile(canonical, "utf8");
|
|
68
67
|
} catch (error) {
|
|
69
|
-
if (
|
|
70
|
-
return undefined;
|
|
71
|
-
}
|
|
68
|
+
if (isEnoent(error)) return undefined;
|
|
72
69
|
throw error;
|
|
73
70
|
}
|
|
74
71
|
let parsed: unknown;
|
|
@@ -77,7 +74,7 @@ export async function readNavigatorWorkBase(path: string): Promise<NavigatorWork
|
|
|
77
74
|
} catch {
|
|
78
75
|
return undefined;
|
|
79
76
|
}
|
|
80
|
-
if (
|
|
77
|
+
if (!isRecord(parsed)) return undefined;
|
|
81
78
|
const record = parsed as Record<string, unknown>;
|
|
82
79
|
if (typeof record.subject !== "string" || typeof record.authority !== "string") return undefined;
|
|
83
80
|
if (record.authority.trim() === "") return undefined;
|
|
@@ -89,7 +86,7 @@ function jsonObjectFromPrompt(prompt: string): Record<string, unknown> | undefin
|
|
|
89
86
|
if (!head.startsWith("{")) return undefined;
|
|
90
87
|
try {
|
|
91
88
|
const parsed: unknown = JSON.parse(head);
|
|
92
|
-
if (
|
|
89
|
+
if (!isRecord(parsed)) return undefined;
|
|
93
90
|
return parsed as Record<string, unknown>;
|
|
94
91
|
} catch {
|
|
95
92
|
return undefined;
|
|
@@ -6,6 +6,7 @@ import { readFile } from "node:fs/promises";
|
|
|
6
6
|
import { resolve } from "node:path";
|
|
7
7
|
|
|
8
8
|
import { loadDoctorCase } from "./doctor-evidence.ts";
|
|
9
|
+
import { isEnoent } from "./unknown-value.ts";
|
|
9
10
|
import { runDirectoryFromHostContext, type HostContext } from "./host-contracts.ts";
|
|
10
11
|
import {
|
|
11
12
|
navigatorSubjectKey,
|
|
@@ -114,7 +115,7 @@ export async function loadNavigatorWorkContext(
|
|
|
114
115
|
break;
|
|
115
116
|
}
|
|
116
117
|
} catch (error) {
|
|
117
|
-
if (!(error
|
|
118
|
+
if (!isEnoent(error)) throw error;
|
|
118
119
|
}
|
|
119
120
|
}
|
|
120
121
|
const authority = resolveNavigatorAuthorityMaterial(input, authorityMaterial);
|
package/src/notary-contracts.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { REVIEW_SUBMISSION_OUTPUT_TOOL_NAME, reviewSubmissionSchema } from "./review-submission.ts";
|
|
7
7
|
|
|
8
|
+
import { isRecord } from "./unknown-value.ts";
|
|
9
|
+
|
|
8
10
|
export const NOTARY_OUTPUT_TOOL_NAME: string = REVIEW_SUBMISSION_OUTPUT_TOOL_NAME;
|
|
9
11
|
export const NOTARY_SOURCE_RUN_FLAG = {
|
|
10
12
|
name: "ak-notary-source-run",
|
|
@@ -41,10 +43,6 @@ export type NotaryOutput =
|
|
|
41
43
|
readonly findings?: unknown;
|
|
42
44
|
};
|
|
43
45
|
|
|
44
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
45
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
46
|
/**
|
|
49
47
|
* Recognize one lawful explicit Notary release (converged | continue | escalate).
|
|
50
48
|
* #753: no field rewrite — submitted params retained as-is (no disposition forge,
|
|
@@ -52,7 +50,7 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
52
50
|
*/
|
|
53
51
|
export function projectLawfulNotaryOutput(value: unknown): NotaryOutput | undefined {
|
|
54
52
|
// #836: no field drop
|
|
55
|
-
return (
|
|
53
|
+
return (isRecord(value))
|
|
56
54
|
? (value as NotaryOutput)
|
|
57
55
|
: undefined;
|
|
58
56
|
}
|
|
@@ -72,7 +70,7 @@ export function retainNotarySubmission(value: unknown): unknown {
|
|
|
72
70
|
* Does not gate role admission — callers must not use this to reject a submission.
|
|
73
71
|
*/
|
|
74
72
|
export function validateRecordedNotaryOutput(value: unknown): NotaryOutput {
|
|
75
|
-
if (
|
|
73
|
+
if (!isRecord(value)) {
|
|
76
74
|
throw new Error("Notary output is not an object");
|
|
77
75
|
}
|
|
78
76
|
return value as NotaryOutput;
|
package/src/oauth-keepalive.ts
CHANGED
|
@@ -10,6 +10,8 @@ import { readFileSync } from "node:fs";
|
|
|
10
10
|
import { homedir } from "node:os";
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
|
|
13
|
+
import { errorText, isEnoent, isRecord } from "./unknown-value.ts";
|
|
14
|
+
|
|
13
15
|
export const DEFAULT_OAUTH_KEEPALIVE_PROVIDERS = ["kimi-coding"] as const;
|
|
14
16
|
|
|
15
17
|
export const OAUTH_KEEPALIVE_INTERVAL_MS = 60_000;
|
|
@@ -25,10 +27,6 @@ export function oauthKeepaliveSettingPath(): string {
|
|
|
25
27
|
);
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
function isExactRecord(value: unknown): value is Record<string, unknown> {
|
|
29
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
30
|
/**
|
|
33
31
|
* Read the static keepalive provider list from the extension setting file.
|
|
34
32
|
* Missing file → default ["kimi-coding"]. Malformed content fails closed.
|
|
@@ -39,7 +37,7 @@ export function readOAuthKeepaliveProviders(
|
|
|
39
37
|
): readonly string[] {
|
|
40
38
|
try {
|
|
41
39
|
const raw = JSON.parse(readFileSync(path, "utf8")) as unknown;
|
|
42
|
-
if (!
|
|
40
|
+
if (!isRecord(raw) || !Array.isArray(raw.providers)) {
|
|
43
41
|
throw new Error(
|
|
44
42
|
"OAuth keepalive setting is malformed: expected { providers: string[] }",
|
|
45
43
|
);
|
|
@@ -56,13 +54,7 @@ export function readOAuthKeepaliveProviders(
|
|
|
56
54
|
return Object.freeze(providers);
|
|
57
55
|
} catch (error) {
|
|
58
56
|
// Absent optional setting uses the documented default; all other causes propagate.
|
|
59
|
-
if (
|
|
60
|
-
error instanceof Error &&
|
|
61
|
-
"code" in error &&
|
|
62
|
-
(error as NodeJS.ErrnoException).code === "ENOENT"
|
|
63
|
-
) {
|
|
64
|
-
return DEFAULT_OAUTH_KEEPALIVE_PROVIDERS;
|
|
65
|
-
}
|
|
57
|
+
if (isEnoent(error)) return DEFAULT_OAUTH_KEEPALIVE_PROVIDERS;
|
|
66
58
|
throw error;
|
|
67
59
|
}
|
|
68
60
|
}
|
|
@@ -125,12 +117,7 @@ function errorClassName(error: unknown): string {
|
|
|
125
117
|
|
|
126
118
|
function formatWarning(providerId: string, error: unknown): string {
|
|
127
119
|
const cls = errorClassName(error);
|
|
128
|
-
const detail =
|
|
129
|
-
error instanceof Error
|
|
130
|
-
? error.message
|
|
131
|
-
: typeof error === "string"
|
|
132
|
-
? error
|
|
133
|
-
: String(error);
|
|
120
|
+
const detail = errorText(error);
|
|
134
121
|
return `oauth-keepalive: refresh failed for provider ${providerId} (${cls}): ${detail}`;
|
|
135
122
|
}
|
|
136
123
|
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { REVIEW_SUBMISSION_OUTPUT_TOOL_NAME, reviewSubmissionSchema } from "../review-submission.ts";
|
|
6
6
|
|
|
7
|
+
import { isRecord } from "../unknown-value.ts";
|
|
8
|
+
|
|
7
9
|
export const AUDITOR_OUTPUT_TOOL_NAME: string = REVIEW_SUBMISSION_OUTPUT_TOOL_NAME;
|
|
8
10
|
|
|
9
11
|
export const auditorOutputSchema = reviewSubmissionSchema;
|
|
@@ -17,10 +19,6 @@ export type AuditorOutput =
|
|
|
17
19
|
readonly decisionGate?: unknown;
|
|
18
20
|
};
|
|
19
21
|
|
|
20
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
21
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
22
|
/** #836: no field drop — original object is the receipt. */
|
|
25
23
|
export function projectLawfulAuditorOutput(value: unknown): AuditorOutput | undefined {
|
|
26
24
|
return isRecord(value) ? (value as AuditorOutput) : undefined;
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { Type, type Static } from "typebox";
|
|
2
2
|
import { Value } from "typebox/value";
|
|
3
3
|
|
|
4
|
+
import { isRecord } from "../unknown-value.ts";
|
|
5
|
+
|
|
4
6
|
export const FIXER_PREREQUISITE_ID_PATTERN = "^[A-Za-z0-9][A-Za-z0-9._-]*$";
|
|
5
7
|
|
|
6
8
|
export const fixerPrerequisiteSchema = Type.Object({
|
|
7
9
|
id: Type.String({ pattern: FIXER_PREREQUISITE_ID_PATTERN }),
|
|
8
10
|
requirement: Type.String({ pattern: "\\S" }),
|
|
9
|
-
}
|
|
11
|
+
});
|
|
10
12
|
|
|
11
13
|
export const fixerPrerequisitesSchema = Type.Array(fixerPrerequisiteSchema);
|
|
12
14
|
export type FixerPrerequisite = Readonly<Static<typeof fixerPrerequisiteSchema>>;
|
|
@@ -45,13 +47,9 @@ function fail(cause: unknown): never {
|
|
|
45
47
|
function parseFailure(value: unknown): never {
|
|
46
48
|
if (!Array.isArray(value)) fail(new Error("Fixer prerequisites must be a JSON array"));
|
|
47
49
|
for (const entry of value) {
|
|
48
|
-
if (
|
|
50
|
+
if (!isRecord(entry)) {
|
|
49
51
|
fail(new Error("Fixer prerequisite entry must be an object with id and requirement fields"));
|
|
50
52
|
}
|
|
51
|
-
const keys = Object.keys(entry);
|
|
52
|
-
if (keys.length !== 2 || !keys.includes("id") || !keys.includes("requirement")) {
|
|
53
|
-
fail(new Error("Fixer prerequisite entry fields must be exactly id and requirement"));
|
|
54
|
-
}
|
|
55
53
|
if (
|
|
56
54
|
typeof (entry as Record<string, unknown>).id !== "string" ||
|
|
57
55
|
!(new RegExp(FIXER_PREREQUISITE_ID_PATTERN)).test((entry as { id: string }).id)
|
|
@@ -8,6 +8,8 @@ import { Type } from "typebox";
|
|
|
8
8
|
import { openToolObject } from "../open-tool-schema.ts";
|
|
9
9
|
import { withTerminatingOutputDeclarations } from "./terminating-infrastructure.ts";
|
|
10
10
|
|
|
11
|
+
import { isRecord } from "../unknown-value.ts";
|
|
12
|
+
|
|
11
13
|
export const GATEKEEPER_OUTPUT_TOOL_NAME = "ak_gatekeeper_output";
|
|
12
14
|
|
|
13
15
|
/** Same open decision shape the province uses inside audit sessions. */
|
|
@@ -33,10 +35,6 @@ export type GatekeeperDirectOutput =
|
|
|
33
35
|
| { readonly status: "dispatch"; readonly officer: "inspector" | "notary" }
|
|
34
36
|
| { readonly status: "pass"; readonly findings?: readonly string[] };
|
|
35
37
|
|
|
36
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
37
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
38
|
function asStringArray(value: unknown): readonly string[] {
|
|
41
39
|
if (!Array.isArray(value)) return [];
|
|
42
40
|
return value.filter((item): item is string => typeof item === "string");
|