@akagilnc/pi-workflow-roles 0.1.5478 → 0.1.5504
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 +4945 -4980
- package/dist/analyst-gate-cycles-read.js +11 -29
- package/dist/analyst-ledger.js +10 -12
- package/dist/archivist-record-entry.js +4 -3
- package/dist/archivist-record-pointer.js +59 -32
- package/dist/atomic-write.js +2 -2
- package/dist/auditor-dossier-tool.js +8 -4
- package/dist/auditor-soul.js +2 -1
- package/dist/collector-config.js +6 -5
- package/dist/doctor-evidence.js +5 -1
- package/dist/engine-detour-usage.js +4 -10
- package/dist/headless-host/description.js +251 -0
- package/dist/headless-host/production-host.js +2787 -2658
- package/dist/ledger-session-read.js +5 -0
- package/dist/migrate-book-topology.js +129 -100
- package/dist/navigator-public-session.js +13 -17
- package/dist/notary-source-run.js +7 -15
- package/dist/packaged-role-registry.js +0 -12
- package/dist/pi/durable-principal.js +2 -3
- package/dist/pi/role-turn-host.js +27 -13
- package/dist/public-cli/instruction-seat-run.js +4 -4
- package/dist/public-cli/invocation.js +17 -45
- package/dist/public-cli/main.js +1074 -1085
- package/dist/public-cli/run-lifecycle.js +17 -47
- package/dist/public-cli/settlement.js +28 -128
- package/dist/public-cli/terminal.js +0 -12
- package/dist/role-run-placement.js +52 -5
- package/dist/role-run-relocation.js +39 -2
- package/dist/run-terminal-artifacts.js +2 -6
- package/dist/run-ticket-number.js +33 -4
- package/dist/session-assistant-usage.js +9 -18
- package/dist/session-identity.js +3 -4
- package/dist/sitian-appender.js +20 -6
- package/dist/submission-ledger.js +4 -4
- package/dist/ticket-provenance-contracts.js +2 -8
- package/dist/ticket-provenance.js +5 -4
- package/package.json +1 -1
- package/src/acp-host/role-turn-host.ts +2 -2
- package/src/analyst-gate-cycles-read.ts +11 -34
- package/src/analyst-ledger.ts +14 -14
- package/src/archivist-record-entry.ts +4 -3
- package/src/archivist-record-pointer.ts +70 -34
- package/src/atomic-write.ts +3 -1
- package/src/auditor-dossier-tool.ts +8 -4
- package/src/auditor-soul.ts +2 -1
- package/src/collector-config.ts +5 -3
- package/src/doctor-evidence.ts +5 -1
- package/src/engine-detour-usage.ts +10 -13
- package/src/factory-board.ts +16 -81
- package/src/headless-host/description.ts +248 -1
- package/src/headless-host/production-host.ts +1 -2
- package/src/headless-host/role-turn-host.ts +10 -9
- package/src/ledger-session-read.ts +6 -0
- package/src/navigator-public-session.ts +13 -18
- package/src/navigator-work-context.ts +3 -1
- package/src/notary-role.ts +2 -1
- package/src/notary-source-run.ts +7 -17
- package/src/packaged-role-registry.ts +0 -15
- package/src/pi/durable-principal.ts +2 -4
- package/src/pi/role-turn-host.ts +35 -12
- package/src/public-cli/instruction-seat-run.ts +4 -4
- package/src/public-cli/invocation.ts +29 -57
- package/src/public-cli/run-lifecycle.ts +20 -43
- package/src/public-cli/settlement.ts +30 -173
- package/src/public-cli/terminal.ts +0 -60
- package/src/role-envelope.ts +4 -2
- package/src/role-run-placement.ts +63 -4
- package/src/role-run-relocation.ts +39 -2
- package/src/role-runtime.ts +40 -59
- package/src/run-terminal-artifacts.ts +3 -5
- package/src/run-ticket-number.ts +41 -3
- package/src/session-assistant-usage.ts +8 -20
- package/src/session-identity.ts +3 -4
- package/src/sitian-appender.ts +20 -12
- package/src/submission-ledger.ts +4 -4
- package/src/ticket-provenance-contracts.ts +3 -6
- package/src/ticket-provenance.ts +5 -6
- package/src/ticket-snapshot.ts +3 -2
- package/src/ticket-trajectory.ts +85 -99
package/src/collector-config.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { readFile } from "node:fs/promises";
|
|
|
2
2
|
|
|
3
3
|
import { sha256Hex } from "./sha256.ts";
|
|
4
4
|
import { isRecord } from "./unknown-value.ts";
|
|
5
|
+
import { isTicketNumberString, parseTicketNumber } from "./run-ticket-number.ts";
|
|
5
6
|
|
|
6
7
|
export const COLLECTOR_OWNER_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/;
|
|
7
8
|
export const COLLECTOR_REPO_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,98}[A-Za-z0-9])?$/;
|
|
@@ -46,10 +47,11 @@ export function parseCollectorRepository(raw: unknown): CollectorRepository {
|
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
export function parseCollectorPrNumber(raw: unknown): number {
|
|
49
|
-
if (typeof raw === "string" && !/^[1-9][0-9]*$/.test(raw)) fail("Collector pull request number must be a positive safe integer string");
|
|
50
50
|
if (typeof raw !== "string" && typeof raw !== "number") fail("Collector pull request number is required");
|
|
51
|
-
const value =
|
|
52
|
-
if (
|
|
51
|
+
const value = parseTicketNumber(raw);
|
|
52
|
+
if (value === undefined) fail(typeof raw === "string" && !isTicketNumberString(raw)
|
|
53
|
+
? "Collector pull request number must be a positive safe integer string"
|
|
54
|
+
: "Collector pull request number must be a positive safe integer");
|
|
53
55
|
return value;
|
|
54
56
|
}
|
|
55
57
|
|
package/src/doctor-evidence.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readdir, readFile, realpath, stat } from "node:fs/promises";
|
|
2
2
|
import { dirname, relative, resolve, sep } from "node:path";
|
|
3
3
|
import { sha256Hex } from "./sha256.ts";
|
|
4
|
+
import { physicallyContainedIn } from "./activation-ledger-topology.ts";
|
|
4
5
|
import type { DoctorCase, DoctorCaseCost, DoctorCount, DoctorEvidenceEntry } from "./doctor-contracts.ts";
|
|
5
6
|
import { AcceptedDetailsContractError, acceptedFacts, isTerminatingToolName, validateAcceptedDetails } from "./package-contracts/terminating-tools.ts";
|
|
6
7
|
|
|
@@ -38,8 +39,11 @@ function deriveSession(content: string, id: string): SessionDerivation {
|
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
/** Read Pi's retained session directory as the sole raw material for one case. */
|
|
41
|
-
export async function loadDoctorCase(runsPath: string): Promise<DoctorCase> {
|
|
42
|
+
export async function loadDoctorCase(runsPath: string, projectRoot?: string): Promise<DoctorCase> {
|
|
42
43
|
const root = await realpath(runsPath);
|
|
44
|
+
if (projectRoot !== undefined && !physicallyContainedIn(projectRoot, root)) {
|
|
45
|
+
throw new Error("doctor --runs escapes the project root");
|
|
46
|
+
}
|
|
43
47
|
// Canonical <book>/<ticket>/runs; legacy issues/<n>/runs remains read-only compatible.
|
|
44
48
|
const match = root.split(sep).join("/").match(/\/\.ak-roles\/books\/[^/]+\/(?:issues\/)?([1-9]\d*)\/runs$/);
|
|
45
49
|
if (!match) throw new Error("Doctor case must be an .ak-roles/books/<book>/<n>/runs directory");
|
|
@@ -17,9 +17,15 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { randomUUID } from "node:crypto";
|
|
19
19
|
import { readFileSync } from "node:fs";
|
|
20
|
-
import {
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
|
|
22
|
+
export {
|
|
23
|
+
runDirectoryFromSessionDirectory,
|
|
24
|
+
sessionFileIn as sessionFileFromSessionDirectory,
|
|
25
|
+
} from "./role-run-placement.ts";
|
|
21
26
|
|
|
22
27
|
import { ENGINE_DETOUR_TOOL_NAME } from "./engine-detour.ts";
|
|
28
|
+
import { sitianRunVolumeDirectory, sitianVolumeRecordsFile } from "./sitian-appender.ts";
|
|
23
29
|
import {
|
|
24
30
|
readSitianRecords,
|
|
25
31
|
resolveSitianRecordPath,
|
|
@@ -40,8 +46,9 @@ export const ENGINE_DETOUR_TOOL_USAGE_FACT_KEY = "engineDetourToolUsage" as cons
|
|
|
40
46
|
* Openable once the run directory is known from the terminal's run id.
|
|
41
47
|
* Contains no runId bytes itself (#108 + #537).
|
|
42
48
|
*/
|
|
43
|
-
export const ENGINE_DETOUR_CALL_RECORD_FILE_RELATIVE =
|
|
44
|
-
|
|
49
|
+
export const ENGINE_DETOUR_CALL_RECORD_FILE_RELATIVE = sitianVolumeRecordsFile(
|
|
50
|
+
sitianRunVolumeDirectory("", ENGINE_DETOUR_CALL_KIND),
|
|
51
|
+
);
|
|
45
52
|
|
|
46
53
|
/** stdout UTF-8 byte length (ticket-frozen metric; empty stdout is real 0). */
|
|
47
54
|
export function engineDetourStdoutByteLength(stdout: string): number {
|
|
@@ -288,16 +295,6 @@ export function withEngineDetourToolUsageFact<
|
|
|
288
295
|
};
|
|
289
296
|
}
|
|
290
297
|
|
|
291
|
-
/** runDirectory owning a session directory (.../runs/<id>@role/session). */
|
|
292
|
-
export function runDirectoryFromSessionDirectory(sessionDirectory: string): string {
|
|
293
|
-
return dirname(sessionDirectory);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
/** session.jsonl under a session directory. */
|
|
297
|
-
export function sessionFileFromSessionDirectory(sessionDirectory: string): string {
|
|
298
|
-
return join(sessionDirectory, "session.jsonl");
|
|
299
|
-
}
|
|
300
|
-
|
|
301
298
|
/**
|
|
302
299
|
* Mint one public-invocation scope id.
|
|
303
300
|
* Call once at the public-entry boundary — never inside the auto-resume loop.
|
package/src/factory-board.ts
CHANGED
|
@@ -16,9 +16,7 @@
|
|
|
16
16
|
* output path outside every ledger and declares the bound; one-shot write does
|
|
17
17
|
* not advertise refresh. Snapshot bindings stay fixed; each tick re-reads ledgers.
|
|
18
18
|
*/
|
|
19
|
-
import {
|
|
20
|
-
import { lstat, mkdir, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
21
|
-
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
19
|
+
import { resolve } from "node:path";
|
|
22
20
|
|
|
23
21
|
import { packagedBoardPlacement } from "./packaged-role-registry.ts";
|
|
24
22
|
import {
|
|
@@ -29,6 +27,8 @@ import {
|
|
|
29
27
|
} from "./human-format.ts";
|
|
30
28
|
import {
|
|
31
29
|
DEFAULT_REFRESH_BOUNDARY_SECONDS,
|
|
30
|
+
assertOutputOutsideLedgers,
|
|
31
|
+
writePageOutsideLedgers,
|
|
32
32
|
buildTicketTrajectoryBookIndex,
|
|
33
33
|
loadTicketTrajectoryRuns,
|
|
34
34
|
loadUnboundTrajectoryRuns,
|
|
@@ -41,7 +41,7 @@ import {
|
|
|
41
41
|
} from "./ticket-trajectory.ts";
|
|
42
42
|
import type { BoardSnapshot, SnapshotTicket, TicketIssueState } from "./ticket-snapshot.ts";
|
|
43
43
|
|
|
44
|
-
import { isRecord
|
|
44
|
+
import { isRecord } from "./unknown-value.ts";
|
|
45
45
|
|
|
46
46
|
/** Unaccepted latest-run mtime bands (page-visible thresholds). */
|
|
47
47
|
export const UNACCEPTED_FLYING_MS = 2 * 60 * 1000;
|
|
@@ -322,11 +322,6 @@ function attr(value: string): string {
|
|
|
322
322
|
return escapeHtml(value);
|
|
323
323
|
}
|
|
324
324
|
|
|
325
|
-
function isPathInside(parent: string, child: string): boolean {
|
|
326
|
-
const rel = relative(parent, child);
|
|
327
|
-
return rel === "" || (!rel.startsWith(`..${sep}`) && rel !== ".." && !rel.startsWith(".."));
|
|
328
|
-
}
|
|
329
|
-
|
|
330
325
|
function activeBlockedBy(ticket: SnapshotTicket): number[] {
|
|
331
326
|
if (ticket.state === "closed") return [];
|
|
332
327
|
return ticket.blockedBy
|
|
@@ -1654,76 +1649,7 @@ ${boardPageScript()}
|
|
|
1654
1649
|
`;
|
|
1655
1650
|
}
|
|
1656
1651
|
|
|
1657
|
-
|
|
1658
|
-
books: readonly FactoryBoardBook[],
|
|
1659
|
-
outputPath: string,
|
|
1660
|
-
): Promise<{ ledgerRoots: string[]; outputAbsolute: string }> {
|
|
1661
|
-
const outputAbsolute = resolve(outputPath);
|
|
1662
|
-
const ledgerRoots: string[] = [];
|
|
1663
|
-
for (const book of books) {
|
|
1664
|
-
const ledgerResolved = resolve(book.ledgerDir);
|
|
1665
|
-
let ledgerRoot: string;
|
|
1666
|
-
try {
|
|
1667
|
-
ledgerRoot = await realpath(ledgerResolved);
|
|
1668
|
-
} catch (error) {
|
|
1669
|
-
if (!isMissingPathError(error)) throw error;
|
|
1670
|
-
ledgerRoot = ledgerResolved;
|
|
1671
|
-
}
|
|
1672
|
-
ledgerRoots.push(ledgerRoot);
|
|
1673
|
-
|
|
1674
|
-
const missingTail: string[] = [];
|
|
1675
|
-
let cursor = outputAbsolute;
|
|
1676
|
-
for (;;) {
|
|
1677
|
-
try {
|
|
1678
|
-
await lstat(cursor);
|
|
1679
|
-
break;
|
|
1680
|
-
} catch (error) {
|
|
1681
|
-
if (!isMissingPathError(error)) throw error;
|
|
1682
|
-
const parent = dirname(cursor);
|
|
1683
|
-
if (parent === cursor) break;
|
|
1684
|
-
missingTail.push(basename(cursor));
|
|
1685
|
-
cursor = parent;
|
|
1686
|
-
}
|
|
1687
|
-
}
|
|
1688
|
-
let realPrefix: string;
|
|
1689
|
-
try {
|
|
1690
|
-
realPrefix = await realpath(cursor);
|
|
1691
|
-
} catch (error) {
|
|
1692
|
-
if (!isMissingPathError(error)) throw error;
|
|
1693
|
-
realPrefix = resolve(cursor);
|
|
1694
|
-
}
|
|
1695
|
-
const prospectiveReal =
|
|
1696
|
-
missingTail.length === 0 ? realPrefix : resolve(realPrefix, ...missingTail.reverse());
|
|
1697
|
-
if (
|
|
1698
|
-
isPathInside(ledgerRoot, prospectiveReal) ||
|
|
1699
|
-
isPathInside(ledgerRoot, realPrefix) ||
|
|
1700
|
-
isPathInside(ledgerRoot, outputAbsolute)
|
|
1701
|
-
) {
|
|
1702
|
-
throw new Error("factory board outputPath must be outside every ledger directory");
|
|
1703
|
-
}
|
|
1704
|
-
}
|
|
1705
|
-
return { ledgerRoots, outputAbsolute };
|
|
1706
|
-
}
|
|
1707
|
-
|
|
1708
|
-
async function writeHtmlAtomically(outputAbsolute: string, html: string, ledgerRoots: string[]): Promise<string> {
|
|
1709
|
-
const parent = dirname(outputAbsolute);
|
|
1710
|
-
await mkdir(parent, { recursive: true });
|
|
1711
|
-
const parentReal = await realpath(parent);
|
|
1712
|
-
for (const root of ledgerRoots) {
|
|
1713
|
-
if (isPathInside(root, parentReal) || isPathInside(root, resolve(parentReal, basename(outputAbsolute)))) {
|
|
1714
|
-
throw new Error("factory board outputPath must be outside every ledger directory");
|
|
1715
|
-
}
|
|
1716
|
-
}
|
|
1717
|
-
const temporary = join(parent, `.factory-board-${randomUUID()}.html.tmp`);
|
|
1718
|
-
try {
|
|
1719
|
-
await writeFile(temporary, html, "utf8");
|
|
1720
|
-
await rename(temporary, outputAbsolute);
|
|
1721
|
-
} catch (error) {
|
|
1722
|
-
await rm(temporary, { force: true }).catch(() => undefined);
|
|
1723
|
-
throw error;
|
|
1724
|
-
}
|
|
1725
|
-
return realpath(outputAbsolute);
|
|
1726
|
-
}
|
|
1652
|
+
const BOARD_OUTPUT_REFUSAL = "factory board outputPath must be outside every ledger directory";
|
|
1727
1653
|
|
|
1728
1654
|
export async function writeFactoryBoardPage(input: {
|
|
1729
1655
|
books: readonly FactoryBoardBook[];
|
|
@@ -1732,7 +1658,11 @@ export async function writeFactoryBoardPage(input: {
|
|
|
1732
1658
|
outputPath: string;
|
|
1733
1659
|
refreshBoundarySeconds?: number;
|
|
1734
1660
|
}): Promise<{ outputPath: string; html: string }> {
|
|
1735
|
-
const gate = await
|
|
1661
|
+
const gate = await assertOutputOutsideLedgers(
|
|
1662
|
+
input.books.map((book) => book.ledgerDir),
|
|
1663
|
+
input.outputPath,
|
|
1664
|
+
BOARD_OUTPUT_REFUSAL,
|
|
1665
|
+
);
|
|
1736
1666
|
const html = await renderFactoryBoardHtml(
|
|
1737
1667
|
input.books,
|
|
1738
1668
|
input.view,
|
|
@@ -1741,7 +1671,12 @@ export async function writeFactoryBoardPage(input: {
|
|
|
1741
1671
|
? { refreshBoundarySeconds: input.refreshBoundarySeconds }
|
|
1742
1672
|
: undefined,
|
|
1743
1673
|
);
|
|
1744
|
-
const outputPath = await
|
|
1674
|
+
const outputPath = await writePageOutsideLedgers(
|
|
1675
|
+
gate.ledgerRoots,
|
|
1676
|
+
gate.outputAbsolute,
|
|
1677
|
+
html,
|
|
1678
|
+
BOARD_OUTPUT_REFUSAL,
|
|
1679
|
+
);
|
|
1745
1680
|
return { outputPath, html };
|
|
1746
1681
|
}
|
|
1747
1682
|
|
|
@@ -130,6 +130,253 @@ function codexTomlStringTable(entries: Readonly<Record<string, string>>): string
|
|
|
130
130
|
return `{${parts.join(",")}}`;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Free-form JSON leaf for Type.Unknown under Codex strict transport.
|
|
135
|
+
* Declared contract objects still close with additionalProperties:false.
|
|
136
|
+
* For Unknown, a $defs anyOf of JSON values with nested additionalProperties
|
|
137
|
+
* as $ref is accepted by Codex and keeps non-empty object receipts expressible
|
|
138
|
+
* (native probe). Package code still does not validate or reject the receipt.
|
|
139
|
+
*/
|
|
140
|
+
const CODEX_JSON_VALUE_DEF = "codexJsonValue";
|
|
141
|
+
const CODEX_JSON_VALUE_REF = `#/$defs/${CODEX_JSON_VALUE_DEF}`;
|
|
142
|
+
const CODEX_JSON_VALUE_SCHEMA = Object.freeze({
|
|
143
|
+
anyOf: Object.freeze([
|
|
144
|
+
Object.freeze({ type: "string" }),
|
|
145
|
+
Object.freeze({ type: "number" }),
|
|
146
|
+
Object.freeze({ type: "boolean" }),
|
|
147
|
+
Object.freeze({ type: "null" }),
|
|
148
|
+
Object.freeze({ type: "array", items: Object.freeze({ $ref: CODEX_JSON_VALUE_REF }) }),
|
|
149
|
+
Object.freeze({
|
|
150
|
+
type: "object",
|
|
151
|
+
properties: Object.freeze({}),
|
|
152
|
+
required: Object.freeze([] as string[]),
|
|
153
|
+
additionalProperties: Object.freeze({ $ref: CODEX_JSON_VALUE_REF }),
|
|
154
|
+
}),
|
|
155
|
+
]),
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Derive a Codex/OpenAI-strict transport schema from the package open schema.
|
|
160
|
+
* Legal open schema is untouched; this is a host-only transmission projection
|
|
161
|
+
* (#1148 / #646 / ADR 0057 / ADR 0054): every object closes, every property is
|
|
162
|
+
* required, originally-optional fields become a null union (official guidance),
|
|
163
|
+
* and originally-required declared nullability stays. Nested open-tool anyOf
|
|
164
|
+
* wrappers are flattened so every branch carries `type`, keeping descriptions.
|
|
165
|
+
* Type.Unknown / description-only leaves become the closed JSON-value $ref.
|
|
166
|
+
* Package code still does not validate or reject the receipt.
|
|
167
|
+
*/
|
|
168
|
+
export function closeJsonSchemaForCodex(
|
|
169
|
+
schema: Readonly<Record<string, unknown>>,
|
|
170
|
+
): Record<string, unknown> {
|
|
171
|
+
const closed = closeSchemaNode(schema) as Record<string, unknown>;
|
|
172
|
+
const existingDefs = isRecord(closed.$defs)
|
|
173
|
+
? (closed.$defs as Record<string, unknown>)
|
|
174
|
+
: {};
|
|
175
|
+
return {
|
|
176
|
+
...closed,
|
|
177
|
+
$defs: {
|
|
178
|
+
...existingDefs,
|
|
179
|
+
[CODEX_JSON_VALUE_DEF]: CODEX_JSON_VALUE_SCHEMA,
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Pure null leaf only — composite type|null is stripped in-leaf, not dropped whole. */
|
|
185
|
+
function isPureNullTypeSchema(value: unknown): boolean {
|
|
186
|
+
if (!isRecord(value)) return false;
|
|
187
|
+
if (value.type === "null") return true;
|
|
188
|
+
if (Array.isArray(value.type) && value.type.length > 0 && value.type.every((t) => t === "null")) {
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Declared nullability on the open schema (type|null, anyOf null leaf, or pure null). */
|
|
195
|
+
function schemaDeclaresNull(schema: unknown): boolean {
|
|
196
|
+
if (!isRecord(schema)) return false;
|
|
197
|
+
if (isPureNullTypeSchema(schema)) return true;
|
|
198
|
+
if (Array.isArray(schema.type) && schema.type.includes("null")) return true;
|
|
199
|
+
if (Array.isArray(schema.anyOf) && schema.anyOf.some(schemaDeclaresNull)) return true;
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Within a leaf, remove null members from type arrays.
|
|
205
|
+
* Null is re-added when the union or property edge originally declared it.
|
|
206
|
+
*/
|
|
207
|
+
function stripNullFromLeafType(leaf: unknown): unknown {
|
|
208
|
+
if (!isRecord(leaf) || !Array.isArray(leaf.type)) return leaf;
|
|
209
|
+
const nonNull = leaf.type.filter((t) => t !== "null");
|
|
210
|
+
if (nonNull.length === leaf.type.length) return leaf;
|
|
211
|
+
if (nonNull.length === 0) return { ...leaf, type: "null" };
|
|
212
|
+
if (nonNull.length === 1) return { ...leaf, type: nonNull[0] };
|
|
213
|
+
return { ...leaf, type: nonNull };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Flatten nested anyOf wrappers into concrete leaf schemas.
|
|
218
|
+
* Open-tool unions often wrap leaves in description-only anyOf shells that
|
|
219
|
+
* lack `type`; strict structured output rejects those intermediate nodes.
|
|
220
|
+
* Nested shell descriptions attach to leaves that lack their own (not onto $ref).
|
|
221
|
+
* The outermost union keeps its description on the closed node via the caller.
|
|
222
|
+
*/
|
|
223
|
+
function flattenUnionLeaves(schema: unknown, attachShellDescription = false): unknown[] {
|
|
224
|
+
if (!isRecord(schema)) return [schema];
|
|
225
|
+
if (!Array.isArray(schema.anyOf)) return [schema];
|
|
226
|
+
const shellDescription =
|
|
227
|
+
attachShellDescription && typeof schema.description === "string"
|
|
228
|
+
? schema.description
|
|
229
|
+
: undefined;
|
|
230
|
+
return schema.anyOf.flatMap((branch) => {
|
|
231
|
+
const leaves = flattenUnionLeaves(branch, true);
|
|
232
|
+
if (shellDescription === undefined) return leaves;
|
|
233
|
+
return leaves.map((leaf) => {
|
|
234
|
+
if (!isRecord(leaf) || typeof leaf.$ref === "string") return leaf;
|
|
235
|
+
if (typeof leaf.description === "string") return leaf;
|
|
236
|
+
return { ...leaf, description: shellDescription };
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Drop pure-null leaves after in-leaf null stripping; property edge may re-add null. */
|
|
242
|
+
function nonNullLeaves(schema: unknown): unknown[] {
|
|
243
|
+
return flattenUnionLeaves(schema)
|
|
244
|
+
.map(stripNullFromLeafType)
|
|
245
|
+
.filter((leaf) => !isPureNullTypeSchema(leaf));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function withDescription(closed: unknown, description: Readonly<{ description?: string }>): unknown {
|
|
249
|
+
if (description.description === undefined) return closed;
|
|
250
|
+
// Codex strict does not allow sibling keys on $ref; wrap so guidance is kept.
|
|
251
|
+
if (isRecord(closed) && typeof closed.$ref === "string") {
|
|
252
|
+
return { anyOf: [closed], ...description };
|
|
253
|
+
}
|
|
254
|
+
// Prefer a leaf's own description (nested anyOf shell) over the outer copy.
|
|
255
|
+
if (isRecord(closed) && typeof closed.description === "string") return closed;
|
|
256
|
+
return isRecord(closed) ? { ...closed, ...description } : closed;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Property edge under strict transport.
|
|
261
|
+
* Optional → closed leaf(s) + null. Required with declared nullability keeps null.
|
|
262
|
+
* Required without declared null stays non-nullable.
|
|
263
|
+
*/
|
|
264
|
+
function closePropertySchema(schema: unknown, optional: boolean): unknown {
|
|
265
|
+
const description =
|
|
266
|
+
isRecord(schema) && typeof schema.description === "string"
|
|
267
|
+
? { description: schema.description }
|
|
268
|
+
: {};
|
|
269
|
+
const keepNull = optional || schemaDeclaresNull(schema);
|
|
270
|
+
const leaves = nonNullLeaves(schema).map(closeSchemaNode);
|
|
271
|
+
if (leaves.length === 0) return { type: "null", ...description };
|
|
272
|
+
if (!keepNull) {
|
|
273
|
+
const closed = leaves.length === 1 ? leaves[0] : { anyOf: leaves };
|
|
274
|
+
return withDescription(closed, description);
|
|
275
|
+
}
|
|
276
|
+
return { anyOf: [...leaves, { type: "null" }], ...description };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Strict generators require every schema node to declare `type` (or a $ref).
|
|
281
|
+
* Type.Unknown / description-only leaves → free JSON $ref (not string): array
|
|
282
|
+
* and object receipts stay expressible under --output-schema.
|
|
283
|
+
*/
|
|
284
|
+
function ensureTypedLeaf(schema: Record<string, unknown>): Record<string, unknown> {
|
|
285
|
+
if (schema.type !== undefined) return schema;
|
|
286
|
+
if (typeof schema.$ref === "string") return schema;
|
|
287
|
+
if (isRecord(schema.properties) || schema.additionalProperties !== undefined) {
|
|
288
|
+
return { ...schema, type: "object" };
|
|
289
|
+
}
|
|
290
|
+
if (schema.items !== undefined) {
|
|
291
|
+
return { ...schema, type: "array" };
|
|
292
|
+
}
|
|
293
|
+
if (schema.const !== undefined) {
|
|
294
|
+
const value = schema.const;
|
|
295
|
+
if (value === null) return { ...schema, type: "null" };
|
|
296
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
297
|
+
return { ...schema, type: typeof value };
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (Array.isArray(schema.enum) && schema.enum.length > 0) {
|
|
301
|
+
const sample = schema.enum.find((item) => item !== null);
|
|
302
|
+
if (typeof sample === "string" || typeof sample === "number" || typeof sample === "boolean") {
|
|
303
|
+
return { ...schema, type: typeof sample };
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
// Free JSON via $defs. $ref must stand alone (strict rejects sibling keys).
|
|
307
|
+
return { $ref: CODEX_JSON_VALUE_REF };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function originalRequiredNames(node: Record<string, unknown>): ReadonlySet<string> {
|
|
311
|
+
if (!Array.isArray(node.required)) return new Set();
|
|
312
|
+
return new Set(node.required.filter((item): item is string => typeof item === "string"));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function closeSchemaNode(node: unknown): unknown {
|
|
316
|
+
if (!isRecord(node)) return node;
|
|
317
|
+
|
|
318
|
+
// Already a ref (free-JSON leaf or pre-existing) — do not retype.
|
|
319
|
+
if (typeof node.$ref === "string") return node;
|
|
320
|
+
|
|
321
|
+
// Union node: flatten then close each concrete leaf (do not keep untyped shells).
|
|
322
|
+
// Declared null stays — including nested unions such as array items.
|
|
323
|
+
if (Array.isArray(node.anyOf)) {
|
|
324
|
+
const description =
|
|
325
|
+
typeof node.description === "string" ? { description: node.description } : {};
|
|
326
|
+
const keepNull = schemaDeclaresNull(node);
|
|
327
|
+
const leaves = nonNullLeaves(node).map(closeSchemaNode);
|
|
328
|
+
if (leaves.length === 0) return { type: "null", ...description };
|
|
329
|
+
if (!keepNull) {
|
|
330
|
+
if (leaves.length === 1) return withDescription(leaves[0], description);
|
|
331
|
+
return { anyOf: leaves, ...description };
|
|
332
|
+
}
|
|
333
|
+
return { anyOf: [...leaves, { type: "null" }], ...description };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
let out: Record<string, unknown> = { ...node };
|
|
337
|
+
|
|
338
|
+
if (node.items !== undefined) {
|
|
339
|
+
out.items = closeSchemaNode(node.items);
|
|
340
|
+
}
|
|
341
|
+
if (isRecord(node.$defs)) {
|
|
342
|
+
out.$defs = Object.fromEntries(
|
|
343
|
+
Object.entries(node.$defs).map(([key, value]) => [key, closeSchemaNode(value)]),
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const hasProperties = isRecord(node.properties);
|
|
348
|
+
const isObjectType =
|
|
349
|
+
node.type === "object"
|
|
350
|
+
|| (Array.isArray(node.type) && node.type.includes("object"))
|
|
351
|
+
|| hasProperties
|
|
352
|
+
|| node.additionalProperties !== undefined;
|
|
353
|
+
|
|
354
|
+
if (hasProperties) {
|
|
355
|
+
const props = node.properties as Record<string, unknown>;
|
|
356
|
+
const wasRequired = originalRequiredNames(node);
|
|
357
|
+
const closedProps: Record<string, unknown> = {};
|
|
358
|
+
const required: string[] = [];
|
|
359
|
+
for (const [name, propSchema] of Object.entries(props)) {
|
|
360
|
+
required.push(name);
|
|
361
|
+
closedProps[name] = closePropertySchema(propSchema, !wasRequired.has(name));
|
|
362
|
+
}
|
|
363
|
+
out.properties = closedProps;
|
|
364
|
+
out.required = required;
|
|
365
|
+
out.additionalProperties = false;
|
|
366
|
+
if (out.type === undefined) out.type = "object";
|
|
367
|
+
// Object nodes must not also carry residual anyOf from the open copy.
|
|
368
|
+
delete out.anyOf;
|
|
369
|
+
} else if (isObjectType) {
|
|
370
|
+
out.additionalProperties = false;
|
|
371
|
+
if (!Array.isArray(out.required)) out.required = [];
|
|
372
|
+
if (out.type === undefined) out.type = "object";
|
|
373
|
+
} else {
|
|
374
|
+
out = ensureTypedLeaf(out);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return out;
|
|
378
|
+
}
|
|
379
|
+
|
|
133
380
|
/**
|
|
134
381
|
* Project shared-envelope MCP rows into `codex -c mcp_servers.<name>.*` argv pairs.
|
|
135
382
|
* Dot-path + TOML values per official config-advanced; spawn argv (no shell).
|
|
@@ -185,7 +432,7 @@ export function codexTurnArgs(options: {
|
|
|
185
432
|
readonly systemPromptPath: string;
|
|
186
433
|
/**
|
|
187
434
|
* Absolute path for the native `--output-schema` flag.
|
|
188
|
-
*
|
|
435
|
+
* File holds the Codex-strict transport projection of the open declaration (#1148).
|
|
189
436
|
* Optional for prose-exit seats (#959 navigator) so agent_message stays free text.
|
|
190
437
|
*/
|
|
191
438
|
readonly outputSchemaPath?: string;
|
|
@@ -7,8 +7,7 @@
|
|
|
7
7
|
* Intermediate AK tools ride the shared envelope MCP relay (Claude `--mcp-config`,
|
|
8
8
|
* codex `-c mcp_servers.*`). The terminating receipt is the host-native schema
|
|
9
9
|
* channel only (#750 submission-tool-is-schema-channel) — terminating tool is not
|
|
10
|
-
* listed on MCP (Claude `--json-schema`, codex `--output-schema`
|
|
11
|
-
* Codex native strict output cannot promise an open contract; the adapter reports that gap.
|
|
10
|
+
* listed on MCP (Claude `--json-schema`, codex `--output-schema` closed projection).
|
|
12
11
|
*/
|
|
13
12
|
import { randomUUID } from "node:crypto";
|
|
14
13
|
|
|
@@ -10,6 +10,7 @@ import { writeFile } from "node:fs/promises";
|
|
|
10
10
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
11
11
|
|
|
12
12
|
import type { RoleTurnHost, RoleTurnRequest, RoleTurnResult } from "../host-contracts.ts";
|
|
13
|
+
import { sessionDirectoryOf } from "../role-run-placement.ts";
|
|
13
14
|
import {
|
|
14
15
|
createSerializedRoleTurnHost,
|
|
15
16
|
driveExternalRoleTurnRounds,
|
|
@@ -32,6 +33,7 @@ import {
|
|
|
32
33
|
recordNativeSessionPointer,
|
|
33
34
|
} from "../host-session-record.ts";
|
|
34
35
|
import {
|
|
36
|
+
closeJsonSchemaForCodex,
|
|
35
37
|
codexTurnArgs,
|
|
36
38
|
headlessMcpConfigDocument,
|
|
37
39
|
headlessTurnArgs,
|
|
@@ -465,16 +467,15 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
|
|
|
465
467
|
let mcpConfigPath: string | undefined;
|
|
466
468
|
let outputSchemaPath: string | undefined;
|
|
467
469
|
if (codex) {
|
|
468
|
-
//
|
|
469
|
-
//
|
|
470
|
+
// #1148: Codex --output-schema requires a native strict transport schema.
|
|
471
|
+
// Project from the unique open declaration; do not alter package receipt rules.
|
|
470
472
|
if (prepared.terminatingToolName !== NAVIGATOR_OUTPUT_TOOL_NAME) {
|
|
471
473
|
outputSchemaPath = join(request.runDirectory, "headless-output-schema.json");
|
|
472
|
-
await writeFile(
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
});
|
|
474
|
+
await writeFile(
|
|
475
|
+
outputSchemaPath,
|
|
476
|
+
`${JSON.stringify(closeJsonSchemaForCodex(prepared.jsonSchema), null, 2)}\n`,
|
|
477
|
+
"utf8",
|
|
478
|
+
);
|
|
478
479
|
}
|
|
479
480
|
} else {
|
|
480
481
|
mcpConfigPath = join(request.runDirectory, "headless-mcp-config.json");
|
|
@@ -717,7 +718,7 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
|
|
|
717
718
|
try {
|
|
718
719
|
if (exitedSessionId !== undefined && exitedSessionId !== "") copyAndRecordHostDossier({
|
|
719
720
|
host: config.hostName, sessionId: exitedSessionId, cwd: request.cwd,
|
|
720
|
-
sessionDirectory:
|
|
721
|
+
sessionDirectory: sessionDirectoryOf(request.runDirectory), sessionParent,
|
|
721
722
|
continuation: request.continuation,
|
|
722
723
|
...(request.model !== undefined ? { model: request.model } : {}),
|
|
723
724
|
...(request.home !== undefined ? { home: request.home } : {}),
|
|
@@ -8,6 +8,12 @@ import { isRecord } from "./unknown-value.ts";
|
|
|
8
8
|
|
|
9
9
|
export type LedgerSessionRow = Record<string, unknown>;
|
|
10
10
|
|
|
11
|
+
/** Complete Pi volume decoding: malformed EOF is an error, with no row-shape filtering. */
|
|
12
|
+
export async function readStrictPiSessionJsonl(path: string): Promise<unknown[]> {
|
|
13
|
+
const text = await readFile(path, "utf8");
|
|
14
|
+
return text.trim().split("\n").filter(Boolean).map((line) => JSON.parse(line) as unknown);
|
|
15
|
+
}
|
|
16
|
+
|
|
11
17
|
/**
|
|
12
18
|
* Loud JSONL failure that still retains rows parsed before the bad line.
|
|
13
19
|
* Callers that only need the throw keep catching Error; owners that must
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { existsSync } from "node:fs";
|
|
10
10
|
import { basename } from "node:path";
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import { sitianReportSafe } from "./host-session-record.ts";
|
|
13
13
|
import {
|
|
14
14
|
NavigatorUnavailableError,
|
|
15
15
|
navigatorProviderFailureFromPublicTerminal,
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
} from "./navigator-session-contracts.ts";
|
|
22
22
|
import type { NoReceiptLifecycleFacts } from "./receipt-delivery-policy.ts";
|
|
23
23
|
import { runDirectoryFromHostContext, type HostContext } from "./host-contracts.ts";
|
|
24
|
+
import { parseRunLeaf } from "./role-run-placement.ts";
|
|
24
25
|
import type { PublicSummonResult } from "./public-role-summons.ts";
|
|
25
26
|
import { CliUsageError } from "./public-cli/cli-errors.ts";
|
|
26
27
|
import { isNavigatorSeat } from "./packaged-role-registry.ts";
|
|
@@ -57,11 +58,9 @@ async function resolveNavigatorLedgerHome(context: HostContext): Promise<string
|
|
|
57
58
|
export const NAVIGATOR_HOST_RUN_POINTER_ENTRY = "ak-navigator-host-run";
|
|
58
59
|
|
|
59
60
|
export function runIdFromNavigatorDirectory(runDirectory: string): string | undefined {
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const runId = entry.slice(0, entry.length - suffix.length);
|
|
64
|
-
return runId.length > 0 ? runId : undefined;
|
|
61
|
+
const parsed = parseRunLeaf(basename(runDirectory));
|
|
62
|
+
if (parsed === undefined || parsed.role !== "navigator") return undefined;
|
|
63
|
+
return parsed.runId;
|
|
65
64
|
}
|
|
66
65
|
|
|
67
66
|
export function readNavigatorHostRunPointer(entries: readonly unknown[]): string | undefined {
|
|
@@ -285,18 +284,14 @@ export function createNativeNavigatorSessionFactory(deps?: {
|
|
|
285
284
|
routePlaybookReadFailure: () => routePlaybookReadFailure,
|
|
286
285
|
appendEntry: (customType, data) => {
|
|
287
286
|
sessionManager.appendCustomEntry(customType, data);
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
});
|
|
297
|
-
} catch {
|
|
298
|
-
// best-effort
|
|
299
|
-
}
|
|
287
|
+
sitianReportSafe({
|
|
288
|
+
level: "event",
|
|
289
|
+
kind: "attendance",
|
|
290
|
+
cwd: context.cwd,
|
|
291
|
+
sessionParent: sessionManager.getSessionFile(),
|
|
292
|
+
payload: { customType, data },
|
|
293
|
+
source: "navigator-public-session",
|
|
294
|
+
});
|
|
300
295
|
},
|
|
301
296
|
entries: () => sessionManager.getEntries(),
|
|
302
297
|
setModel: async (next, nextThinking) => {
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
import { readFile } from "node:fs/promises";
|
|
6
6
|
import { resolve } from "node:path";
|
|
7
7
|
|
|
8
|
+
import { sessionDirectoryOf } from "./role-run-placement.ts";
|
|
9
|
+
|
|
8
10
|
import { loadDoctorCase } from "./doctor-evidence.ts";
|
|
9
11
|
import { isEnoent } from "./unknown-value.ts";
|
|
10
12
|
import { runDirectoryFromHostContext, type HostContext } from "./host-contracts.ts";
|
|
@@ -69,7 +71,7 @@ export async function loadNavigatorWorkContext(
|
|
|
69
71
|
const publicRunDir = runDirectoryFromHostContext(options.context);
|
|
70
72
|
const currentSessionDir = options.context.sessionManager.getSessionDir();
|
|
71
73
|
const isBoundPublicRun = publicRunDir !== undefined
|
|
72
|
-
&& resolve(currentSessionDir) === resolve(publicRunDir
|
|
74
|
+
&& resolve(currentSessionDir) === resolve(sessionDirectoryOf(publicRunDir));
|
|
73
75
|
if (subjectMode === "public-instruction" && isBoundPublicRun) {
|
|
74
76
|
let admitted;
|
|
75
77
|
try {
|
package/src/notary-role.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { RoleHost, HostContext } from "./host-contracts.ts";
|
|
2
2
|
import { registerFiledSubmissionTool } from "./filed-submission.ts";
|
|
3
3
|
import { roleSubmissionDeclaration } from "./role-submission-declarations.ts";
|
|
4
|
+
import { isSafePositiveTicketNumber } from "./run-ticket-number.ts";
|
|
4
5
|
/**
|
|
5
6
|
* Public Notary role runtime — direct officer seat (not through Gatekeeper province).
|
|
6
7
|
* Caller supplies only a source-run locator; Notary self-fetches authoritative materials.
|
|
@@ -44,7 +45,7 @@ export function readNotaryTicketFlag(flag: unknown): number | undefined {
|
|
|
44
45
|
}
|
|
45
46
|
if (flag.trim() === "") return undefined;
|
|
46
47
|
const n = Number(flag);
|
|
47
|
-
if (!
|
|
48
|
+
if (!isSafePositiveTicketNumber(n)) {
|
|
48
49
|
throw new Error(
|
|
49
50
|
"Notary ak-notary-ticket-number is present but not a safe positive integer string",
|
|
50
51
|
);
|