@skill-harness/adapters 0.7.0 → 0.9.0

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/index.d.ts CHANGED
@@ -2,3 +2,4 @@ import type { HarnessAdapter } from "@skill-harness/core";
2
2
  import { piAdapter } from "./pi.js";
3
3
  export declare function getAdapter(name: string): HarnessAdapter;
4
4
  export { piAdapter };
5
+ export * from "./trajectory.js";
package/dist/index.js CHANGED
@@ -14,4 +14,5 @@ export function getAdapter(name) {
14
14
  return a;
15
15
  }
16
16
  export { piAdapter };
17
+ export * from "./trajectory.js";
17
18
  //# sourceMappingURL=index.js.map
package/dist/pi-json.d.ts CHANGED
@@ -1,4 +1,25 @@
1
1
  import type { ExecutionTraceV1, ModelRef, RunMode } from "@skill-harness/core";
2
+ /**
3
+ * Run `pi --mode json` and build an execution trace, **streaming**.
4
+ *
5
+ * The streaming is not an optimization, it is the requirement. pi's
6
+ * `message_update` events re-send the entire accumulated message on every delta,
7
+ * so stdout is quadratic in the answer's length — a trivial three-tool-call run
8
+ * measured **52 MB** of stdout wrapping 12 KB of terminal events. Buffering that
9
+ * into a string (which is what the shared `exec()` helper does) would exhaust
10
+ * memory partway through a long wave, taking the whole run with it.
11
+ *
12
+ * So this deliberately does NOT reuse `exec()`. The two quadratic event types are
13
+ * dropped as each line arrives, so the giant ones are never retained; the
14
+ * remainder — a few KB of terminal events — is held until `close` and parsed
15
+ * once. What this bounds is the 52 MB, not the residue.
16
+ */
17
+ /**
18
+ * The two quadratic event types, matched at the head of the object where pi
19
+ * emits `type`. Line-anchored so a value inside the payload cannot masquerade as
20
+ * the event kind.
21
+ */
22
+ export declare const SKIPPED_TYPE_RE: RegExp;
2
23
  export interface PiJsonRunOptions {
3
24
  args: string[];
4
25
  cwd: string;
package/dist/pi-json.js CHANGED
@@ -21,7 +21,7 @@ import { parseTrace } from "@skill-harness/core";
21
21
  * emits `type`. Line-anchored so a value inside the payload cannot masquerade as
22
22
  * the event kind.
23
23
  */
24
- const SKIPPED_TYPE_RE = /^\s*\{\s*"type"\s*:\s*"(?:message_update|tool_execution_update)"/;
24
+ export const SKIPPED_TYPE_RE = /^\s*\{\s*"type"\s*:\s*"(?:message_update|tool_execution_update)"/;
25
25
  /** How much stderr to retain — enough to diagnose, bounded so a loop cannot blow up. */
26
26
  const MAX_STDERR_CHARS = 8000;
27
27
  export function runPiJson(opts) {
package/dist/pi.js CHANGED
@@ -2,7 +2,8 @@ import { existsSync, mkdtempSync, readFileSync, statSync } from "node:fs";
2
2
  import { tmpdir, homedir } from "node:os";
3
3
  import { join, resolve } from "node:path";
4
4
  import { runPiJson } from "./pi-json.js";
5
- import { exec, onPath, envNum } from "@skill-harness/core";
5
+ import { collectTrajectorySources, normalizePiTraces, resequence } from "./trajectory.js";
6
+ import { exec, onPath, envNum, traceSha256 } from "@skill-harness/core";
6
7
  const PI_TIMEOUT_MS = envNum("PI_TIMEOUT_MS", 300_000);
7
8
  /**
8
9
  * Refuse to hand pi a skill dir it will silently ignore.
@@ -205,13 +206,40 @@ export const piAdapter = {
205
206
  ` (exit ${r.code}${r.malformedLines ? `, ${r.malformedLines} malformed line(s)` : ""})` +
206
207
  (r.stderr.trim() ? `: ${r.stderr.trim()}` : ""));
207
208
  }
209
+ if (r.malformedLines > 0) {
210
+ r.trace.capture_errors = [`pi JSONL contained ${r.malformedLines} malformed line(s); absence-based trace assertions are unsafe`];
211
+ r.trace.trace_sha256 = traceSha256(r.trace);
212
+ }
208
213
  traces.push(r.trace);
209
214
  parts.push(header(i + 1, total, req.turns[i]));
210
215
  parts.push(`<<< ASSISTANT:\n${r.trace.final_text.trim()}\n`);
211
216
  if (r.code !== 0)
212
217
  parts.push(`[pi exited ${r.code} on turn ${i + 1}]\n${r.stderr.trim()}\n`);
213
218
  }
214
- return { transcript: parts.join("\n"), traces };
219
+ const native = req.eventSources?.length
220
+ ? collectTrajectorySources(req.cwd, req.eventSources)
221
+ : { events: [], errors: [] };
222
+ const piEvents = normalizePiTraces(traces);
223
+ const combined = [...piEvents, ...native.events];
224
+ const chronologyErrors = [];
225
+ if (piEvents.length && native.events.length) {
226
+ if (combined.some((event) => !event.at || !Number.isFinite(Date.parse(event.at)))) {
227
+ chronologyErrors.push("pi/native events cannot be globally ordered because at least one event has no valid `at` timestamp");
228
+ }
229
+ else {
230
+ const piTimes = new Set(piEvents.map((event) => Date.parse(event.at)));
231
+ if (native.events.some((event) => piTimes.has(Date.parse(event.at)))) {
232
+ chronologyErrors.push("pi/native events contain equal timestamps, so strict cross-source order is ambiguous");
233
+ }
234
+ }
235
+ }
236
+ const eventErrors = [...native.errors, ...chronologyErrors];
237
+ return {
238
+ transcript: parts.join("\n"),
239
+ traces,
240
+ events: resequence(combined),
241
+ ...(eventErrors.length ? { eventErrors } : {}),
242
+ };
215
243
  },
216
244
  /**
217
245
  * Run the judge: no skills, no context files, no session, single prompt.
@@ -0,0 +1,19 @@
1
+ import type { ExecutionTraceV1, TrajectoryEventSource, TrajectoryEventV1 } from "@skill-harness/core";
2
+ export interface CollectedTrajectorySources {
3
+ events: TrajectoryEventV1[];
4
+ errors: string[];
5
+ }
6
+ /** Read and normalize declared workspace-local native ledger files. */
7
+ export declare function collectTrajectorySources(cwd: string, sources: TrajectoryEventSource[]): CollectedTrajectorySources;
8
+ /** Combine independently sequenced sources by recorded time while retaining each native sequence. */
9
+ export declare function resequence(events: TrajectoryEventV1[]): TrajectoryEventV1[];
10
+ /** Normalize pi's structured calls into adapter-neutral start/completion events. */
11
+ export declare function normalizePiTraces(traces: ExecutionTraceV1[]): TrajectoryEventV1[];
12
+ /** Normalize principal-pi-skills' immutable assurance event schema v1.0. */
13
+ export declare function normalizePrincipalAssuranceLedger(text: string): TrajectoryEventV1[];
14
+ /**
15
+ * Normalize both the current unversioned pi-daddy 0.17 grant ledger and the
16
+ * explicit v1 governance supplement. Legacy omissions remain omissions: no
17
+ * task/workspace/expiry field is ever inferred as successful governance.
18
+ */
19
+ export declare function normalizePiDaddyLedger(text: string): TrajectoryEventV1[];
@@ -0,0 +1,480 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFileSync, readdirSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { TRAJECTORY_EVENT_VERSION, deserializeTrajectoryEvents, matchesGlob, redactArgs } from "@skill-harness/core";
5
+ /** Read and normalize declared workspace-local native ledger files. */
6
+ export function collectTrajectorySources(cwd, sources) {
7
+ const files = walkFiles(cwd);
8
+ const streams = [];
9
+ const errors = [];
10
+ const seenFiles = new Set();
11
+ for (const source of sources) {
12
+ const matched = files.filter((file) => matchesGlob(source.path, file));
13
+ if (matched.length === 0) {
14
+ if (source.required)
15
+ errors.push(`required event source ${source.adapter}:${source.path} is missing`);
16
+ continue;
17
+ }
18
+ for (const file of matched.sort()) {
19
+ const sourceFile = `${source.adapter}:${file}`;
20
+ if (seenFiles.has(sourceFile)) {
21
+ errors.push(`event source ${sourceFile} was declared more than once`);
22
+ continue;
23
+ }
24
+ seenFiles.add(sourceFile);
25
+ try {
26
+ const text = readFileSync(join(cwd, file), "utf8");
27
+ const normalized = source.adapter === "principal-assurance-v1"
28
+ ? normalizePrincipalAssuranceLedger(text)
29
+ : source.adapter === "pi-daddy-v1"
30
+ ? normalizePiDaddyLedger(text)
31
+ : deserializeTrajectoryEvents(text);
32
+ if (!normalized)
33
+ throw new Error("normalized-v1 source is empty, malformed, or unsupported");
34
+ const times = normalized.map((event) => validTime(event.at) ? Date.parse(event.at) : null);
35
+ if (times.every((time) => time !== null) && times.some((time, index) => index > 0 && time < times[index - 1])) {
36
+ throw new Error("native event timestamps move backwards relative to the source's recorded sequence");
37
+ }
38
+ streams.push({ file, adapter: source.adapter, events: normalized });
39
+ }
40
+ catch (error) {
41
+ errors.push(`${source.adapter}:${file}: ${error instanceof Error ? error.message : String(error)}`);
42
+ }
43
+ }
44
+ }
45
+ if (streams.length > 1) {
46
+ if (streams.some((stream) => stream.events.some((event) => !validTime(event.at)))) {
47
+ errors.push("multiple native event files cannot be globally ordered because at least one event has no valid `at` timestamp");
48
+ }
49
+ const owners = new Map();
50
+ for (const stream of streams)
51
+ for (const event of stream.events) {
52
+ if (!event.at)
53
+ continue;
54
+ const instant = String(Date.parse(event.at));
55
+ const filesAtTime = owners.get(instant) ?? new Set();
56
+ filesAtTime.add(stream.file);
57
+ owners.set(instant, filesAtTime);
58
+ }
59
+ if ([...owners.values()].some((filesAtTime) => filesAtTime.size > 1)) {
60
+ errors.push("native event files contain equal timestamps, so strict cross-source order is ambiguous");
61
+ }
62
+ const principalRuns = new Map();
63
+ for (const stream of streams.filter((entry) => entry.adapter === "principal-assurance-v1")) {
64
+ for (const runId of new Set(stream.events.map((event) => event.run_id).filter((value) => Boolean(value)))) {
65
+ const prior = principalRuns.get(runId);
66
+ if (prior && prior !== stream.file)
67
+ errors.push(`principal assurance run ${runId} appears in multiple ledger files (${prior}, ${stream.file})`);
68
+ else
69
+ principalRuns.set(runId, stream.file);
70
+ }
71
+ }
72
+ }
73
+ return { events: resequence(streams.flatMap((stream) => stream.events)), errors };
74
+ }
75
+ /** Combine independently sequenced sources by recorded time while retaining each native sequence. */
76
+ export function resequence(events) {
77
+ const native = events.map((event, index) => ({
78
+ event,
79
+ index,
80
+ at: validTime(event.at) ? Date.parse(event.at) : null,
81
+ }));
82
+ if (native.every((entry) => entry.at !== null))
83
+ native.sort((a, b) => a.at - b.at || a.index - b.index);
84
+ return native.map(({ event }, index) => ({
85
+ ...event,
86
+ seq: index + 1,
87
+ attributes: { native_seq: event.seq, ...(event.attributes ?? {}) },
88
+ }));
89
+ }
90
+ /** Normalize pi's structured calls into adapter-neutral start/completion events. */
91
+ export function normalizePiTraces(traces) {
92
+ const events = [];
93
+ let seq = 1;
94
+ for (const trace of [...traces].sort((a, b) => a.turn - b.turn)) {
95
+ const base = { scenario_id: trace.scenario_id, rep: trace.rep, turn: trace.turn };
96
+ const calls = [...trace.tool_calls].sort((a, b) => a.issueIndex - b.issueIndex);
97
+ for (const call of calls) {
98
+ events.push({
99
+ event_version: TRAJECTORY_EVENT_VERSION,
100
+ seq: seq++,
101
+ type: "tool_started",
102
+ source: "pi",
103
+ at: call.started_at,
104
+ tool: call.name,
105
+ attributes: { ...base, tool_call_id: call.id, args: call.args, issue_index: call.issueIndex },
106
+ });
107
+ }
108
+ for (const call of calls.filter((item) => item.completionIndex >= 0).sort((a, b) => a.completionIndex - b.completionIndex)) {
109
+ events.push({
110
+ event_version: TRAJECTORY_EVENT_VERSION,
111
+ seq: seq++,
112
+ type: "tool_completed",
113
+ source: "pi",
114
+ at: call.completed_at,
115
+ tool: call.name,
116
+ attributes: {
117
+ ...base,
118
+ tool_call_id: call.id,
119
+ success: !call.isError,
120
+ issue_index: call.issueIndex,
121
+ completion_index: call.completionIndex,
122
+ result_sha256: call.result.sha256,
123
+ ...(call.result.details ? { details: call.result.details } : {}),
124
+ },
125
+ });
126
+ }
127
+ }
128
+ return events;
129
+ }
130
+ /** Normalize principal-pi-skills' immutable assurance event schema v1.0. */
131
+ export function normalizePrincipalAssuranceLedger(text) {
132
+ const records = parseJsonl(text, "principal assurance");
133
+ validatePrincipalIntegrity(records);
134
+ return records.map((record, index) => {
135
+ if (record.schema_version !== "1.0") {
136
+ throw new Error(`unsupported principal assurance schema version ${JSON.stringify(record.schema_version)} at line ${index + 1}; expected \"1.0\"`);
137
+ }
138
+ if (!Number.isInteger(record.seq) || Number(record.seq) < 1 || typeof record.type !== "string" || typeof record.run_id !== "string") {
139
+ throw new Error(`invalid principal assurance v1 event at line ${index + 1}: seq, type, and run_id are required`);
140
+ }
141
+ const packet = object(record.packet);
142
+ const definitionDigests = object(packet?.definition_digests);
143
+ const definition = typeof record.definition_digest === "string"
144
+ ? record.definition_digest
145
+ : typeof definitionDigests?.["skill:build"] === "string"
146
+ ? definitionDigests["skill:build"]
147
+ : undefined;
148
+ const taskId = string(record.task_id) ?? string(packet?.task_id);
149
+ const workspaceId = string(record.workspace_id) ?? string(packet?.workspace_id);
150
+ const plan = string(record.plan_digest) ?? string(packet?.plan_digest);
151
+ const head = string(record.head_sha);
152
+ const tree = string(record.tree_sha);
153
+ const attributes = without(record, [
154
+ "schema_version", "seq", "type", "at", "run_id", "task_id", "workspace_id", "context_id",
155
+ "finding_id", "phase", "plan_digest", "definition_digest", "head_sha", "tree_sha", "exit_code",
156
+ ]);
157
+ return cleanEvent({
158
+ event_version: TRAJECTORY_EVENT_VERSION,
159
+ seq: Number(record.seq),
160
+ type: record.type,
161
+ source: "principal-assurance-v1",
162
+ at: string(record.at),
163
+ run_id: record.run_id,
164
+ task_id: taskId,
165
+ workspace_id: workspaceId,
166
+ context_id: string(record.context_id),
167
+ finding_id: string(record.finding_id),
168
+ phase: string(record.phase),
169
+ exit_code: Number.isInteger(record.exit_code) ? Number(record.exit_code) : undefined,
170
+ digests: anyDefined({ plan, definition, head, tree }),
171
+ requirements: stringArray(record.requirements),
172
+ attributes: sanitizeAttributes(attributes),
173
+ });
174
+ });
175
+ }
176
+ /**
177
+ * Normalize both the current unversioned pi-daddy 0.17 grant ledger and the
178
+ * explicit v1 governance supplement. Legacy omissions remain omissions: no
179
+ * task/workspace/expiry field is ever inferred as successful governance.
180
+ */
181
+ export function normalizePiDaddyLedger(text) {
182
+ const records = parseJsonl(text, "pi-daddy");
183
+ const out = [];
184
+ let seq = 1;
185
+ records.forEach((record, index) => {
186
+ if (record.schema_version !== undefined && record.schema_version !== "1.0") {
187
+ throw new Error(`unsupported pi-daddy ledger schema version ${JSON.stringify(record.schema_version)} at line ${index + 1}; expected unversioned 0.17 grant records or \"1.0\" governance records`);
188
+ }
189
+ if (record.schema_version === "1.0") {
190
+ out.push(normalizePiDaddyV1(record, seq++, index));
191
+ return;
192
+ }
193
+ for (const event of normalizeLegacyGrant(record, index))
194
+ out.push({ ...event, seq: seq++ });
195
+ });
196
+ return out;
197
+ }
198
+ function normalizeLegacyGrant(record, index) {
199
+ const requiredArrays = ["requested", "parentGrant", "effective", "denied", "clipped", "gatedBlocked"];
200
+ if (typeof record.ts !== "string" || typeof record.parentId !== "string" || typeof record.childId !== "string" ||
201
+ !Number.isInteger(record.depth) || typeof record.blocked !== "boolean" || typeof record.executor !== "string" ||
202
+ requiredArrays.some((field) => !Array.isArray(record[field]) || !record[field].every((value) => typeof value === "string"))) {
203
+ throw new Error(`invalid unversioned pi-daddy grant record at line ${index + 1}; expected the 0.17 GrantRecord shape`);
204
+ }
205
+ const requested = record.requested;
206
+ const effective = record.effective;
207
+ const denied = record.denied;
208
+ const gated = record.gatedBlocked;
209
+ const digest = object(record.definitionDigest);
210
+ const common = {
211
+ event_version: TRAJECTORY_EVENT_VERSION,
212
+ source: "pi-daddy-0.17",
213
+ at: record.ts,
214
+ parent_id: record.parentId,
215
+ child_id: record.childId,
216
+ };
217
+ const attributes = sanitizeAttributes({
218
+ native_record: index + 1,
219
+ depth: record.depth,
220
+ agent_type: record.agentType,
221
+ executor: record.executor,
222
+ parent_grant: record.parentGrant,
223
+ clipped: record.clipped,
224
+ gated_blocked: gated,
225
+ gate_outcome: record.gateOutcome,
226
+ human_denied: record.humanDenied === true,
227
+ reason: record.reason,
228
+ definition_name: digest?.name,
229
+ legacy_schema: "pi-daddy-grant-ledger/0.17",
230
+ });
231
+ const refusal = record.blocked ? legacyRefusalCode(record) : undefined;
232
+ const spawn = cleanEvent({
233
+ ...common,
234
+ type: record.blocked ? "child_spawn_refused" : "child_started",
235
+ requested_capabilities: requested,
236
+ effective_capabilities: effective,
237
+ refusal_code: refusal,
238
+ digests: anyDefined({ definition: string(digest?.sha256) }),
239
+ attributes,
240
+ });
241
+ const events = [
242
+ ...requested.map((capability) => ({ ...common, type: "capability_requested", capability, requested_capabilities: requested, effective_capabilities: effective, attributes })),
243
+ ...effective.map((capability) => ({ ...common, type: "capability_granted", capability, requested_capabilities: requested, effective_capabilities: effective, attributes })),
244
+ ...[...new Set([...denied, ...gated])].map((capability) => ({ ...common, type: "capability_refused", capability, requested_capabilities: requested, effective_capabilities: effective, refusal_code: denied.includes(capability) ? "CAPABILITY_ESCALATION" : refusal, attributes })),
245
+ ];
246
+ const sources = object(record.approvalSources);
247
+ const scopes = object(record.approvalScopes);
248
+ for (const capability of stringArray(record.approved) ?? []) {
249
+ events.push(cleanEvent({
250
+ ...common,
251
+ type: "approval_used",
252
+ capability,
253
+ approval: {
254
+ capability,
255
+ source: string(sources?.[capability]) ?? string(record.approvalSource),
256
+ scope: string(scopes?.[capability]) ?? string(record.approvalScope),
257
+ used_at: record.ts,
258
+ },
259
+ attributes,
260
+ }));
261
+ }
262
+ events.push(spawn);
263
+ return events;
264
+ }
265
+ function normalizePiDaddyV1(record, seq, index) {
266
+ const recordType = string(record.record_type);
267
+ const action = string(record.action);
268
+ if (!recordType || !action || !string(record.ts))
269
+ throw new Error(`invalid pi-daddy governance v1 record at line ${index + 1}: record_type, action, and ts are required`);
270
+ let type;
271
+ if (recordType === "writer_lease") {
272
+ if (!new Set(["acquired", "refused", "conflict", "released"]).has(action))
273
+ throw new Error(`invalid writer_lease action ${JSON.stringify(action)} at line ${index + 1}`);
274
+ type = action === "conflict" ? "writer_lease_conflict" : `writer_lease_${action}`;
275
+ }
276
+ else if (recordType === "approval") {
277
+ if (!new Set(["granted", "used", "refused"]).has(action))
278
+ throw new Error(`invalid approval action ${JSON.stringify(action)} at line ${index + 1}`);
279
+ type = `approval_${action}`;
280
+ }
281
+ else if (recordType === "child_lifecycle") {
282
+ if (!new Set(["started", "completed", "refused"]).has(action))
283
+ throw new Error(`invalid child_lifecycle action ${JSON.stringify(action)} at line ${index + 1}`);
284
+ type = action === "refused" ? "child_spawn_refused" : `child_${action}`;
285
+ }
286
+ else if (recordType === "capability") {
287
+ if (!new Set(["requested", "granted", "refused"]).has(action))
288
+ throw new Error(`invalid capability action ${JSON.stringify(action)} at line ${index + 1}`);
289
+ type = `capability_${action}`;
290
+ }
291
+ else {
292
+ throw new Error(`unsupported pi-daddy governance v1 record_type ${JSON.stringify(recordType)} at line ${index + 1}`);
293
+ }
294
+ return cleanEvent({
295
+ event_version: TRAJECTORY_EVENT_VERSION,
296
+ seq,
297
+ type,
298
+ source: "pi-daddy-v1",
299
+ at: string(record.ts),
300
+ run_id: string(record.run_id),
301
+ task_id: string(record.task_id),
302
+ workspace_id: string(record.workspace_id),
303
+ context_id: string(record.context_id),
304
+ parent_id: string(record.parent_id),
305
+ child_id: string(record.child_id),
306
+ capability: string(record.capability),
307
+ requested_capabilities: stringArray(record.requested),
308
+ effective_capabilities: stringArray(record.effective),
309
+ refusal_code: string(record.refusal_code),
310
+ digests: anyDefined({ task: string(record.task_digest), definition: string(record.definition_digest) }),
311
+ approval: recordType === "approval" ? cleanObject({
312
+ id: string(record.approval_id),
313
+ capability: string(record.capability),
314
+ subject: string(record.subject),
315
+ source: string(record.source),
316
+ scope: string(record.scope),
317
+ approved_at: string(record.approved_at),
318
+ expires_at: string(record.expires_at),
319
+ used_at: string(record.used_at),
320
+ }) : undefined,
321
+ attributes: sanitizeAttributes(without(record, ["schema_version", "record_type", "ts", "action", "run_id", "task_id", "workspace_id", "context_id", "parent_id", "child_id", "capability", "requested", "effective", "refusal_code", "task_digest", "definition_digest", "approval_id", "subject", "source", "scope", "approved_at", "expires_at", "used_at"])),
322
+ });
323
+ }
324
+ function legacyRefusalCode(record) {
325
+ const denied = stringArray(record.denied) ?? [];
326
+ const gated = stringArray(record.gatedBlocked) ?? [];
327
+ const reason = string(record.reason) ?? "";
328
+ if (denied.length)
329
+ return "CAPABILITY_ESCALATION";
330
+ if (/declares no `allowed-tools`/i.test(reason))
331
+ return "UNDECLARED_CAPABILITIES";
332
+ if (/unknown capabilit/i.test(reason))
333
+ return "UNKNOWN_CAPABILITY";
334
+ if (/depth limit/i.test(reason))
335
+ return "DEPTH_LIMIT";
336
+ if (/needs a task/i.test(reason))
337
+ return "MISSING_TASK";
338
+ if (/universal capability|cannot narrow/i.test(reason))
339
+ return "NON_NARROWING_GRANT";
340
+ if (gated.length) {
341
+ if (record.humanDenied === true || record.gateOutcome === "declined")
342
+ return "APPROVAL_DECLINED";
343
+ if (record.gateOutcome === "no-ui")
344
+ return "APPROVAL_NO_UI";
345
+ if (record.gateOutcome === "dismissed")
346
+ return "APPROVAL_DISMISSED";
347
+ if (record.gateOutcome === "error")
348
+ return "APPROVAL_ERROR";
349
+ return "APPROVAL_REQUIRED";
350
+ }
351
+ return "LEGACY_UNCLASSIFIED";
352
+ }
353
+ function validatePrincipalIntegrity(records) {
354
+ let previous = null;
355
+ let previousTime = null;
356
+ let runId = null;
357
+ records.forEach((record, index) => {
358
+ const line = index + 1;
359
+ if (record.schema_version !== "1.0") {
360
+ throw new Error(`unsupported principal assurance schema version ${JSON.stringify(record.schema_version)} at line ${line}; expected "1.0"`);
361
+ }
362
+ if (record.seq !== line)
363
+ throw new Error(`principal assurance integrity failure at line ${line}: sequence mismatch`);
364
+ if (index === 0 && record.type !== "run_initialized")
365
+ throw new Error("principal assurance integrity failure: first event must initialize the run");
366
+ if (typeof record.run_id !== "string" || !record.run_id)
367
+ throw new Error(`principal assurance integrity failure at line ${line}: run_id is missing`);
368
+ if (runId === null)
369
+ runId = record.run_id;
370
+ else if (record.run_id !== runId)
371
+ throw new Error(`principal assurance integrity failure at line ${line}: run_id changed`);
372
+ if (record.prev_digest !== previous)
373
+ throw new Error(`principal assurance integrity failure at line ${line}: previous digest mismatch`);
374
+ if (typeof record.event_digest !== "string" || !/^[a-f0-9]{64}$/i.test(record.event_digest)) {
375
+ throw new Error(`principal assurance integrity failure at line ${line}: event_digest is invalid`);
376
+ }
377
+ const copy = { ...record };
378
+ delete copy.event_digest;
379
+ const expected = createHash("sha256").update(canonicalJson(copy)).digest("hex");
380
+ if (record.event_digest !== expected)
381
+ throw new Error(`principal assurance integrity failure at line ${line}: event digest mismatch`);
382
+ if (!validTime(typeof record.at === "string" ? record.at : undefined))
383
+ throw new Error(`invalid principal assurance v1 event at line ${line}: at must be a date-time`);
384
+ const at = Date.parse(record.at);
385
+ if (previousTime !== null && at < previousTime)
386
+ throw new Error(`principal assurance integrity failure at line ${line}: timestamp moves backwards`);
387
+ previousTime = at;
388
+ previous = record.event_digest;
389
+ });
390
+ }
391
+ function canonicalJson(value) {
392
+ if (value === null || typeof value === "string" || typeof value === "boolean")
393
+ return JSON.stringify(value);
394
+ if (typeof value === "number") {
395
+ if (!Number.isFinite(value))
396
+ throw new Error("principal assurance event contains a non-finite number");
397
+ return JSON.stringify(value);
398
+ }
399
+ if (Array.isArray(value))
400
+ return `[${value.map(canonicalJson).join(",")}]`;
401
+ if (!value || typeof value !== "object")
402
+ throw new Error("principal assurance event contains a non-JSON value");
403
+ return `{${Object.entries(value)
404
+ .sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)
405
+ .map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`)
406
+ .join(",")}}`;
407
+ }
408
+ function validTime(value) {
409
+ if (typeof value !== "string")
410
+ return false;
411
+ const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.exec(value);
412
+ if (!match || !Number.isFinite(Date.parse(value)))
413
+ return false;
414
+ const [, year, month, day, hour, minute, second] = match.map(Number);
415
+ if (month < 1 || month > 12 || hour > 23 || minute > 59 || second > 59)
416
+ return false;
417
+ return day >= 1 && day <= new Date(Date.UTC(year, month, 0)).getUTCDate();
418
+ }
419
+ function sanitizeAttributes(value) {
420
+ const redacted = redactArgs(value);
421
+ const sensitiveKey = /(secret|token|password|passphrase|api[_-]?key|authorization|cookie|credential)/i;
422
+ const freeTextKey = /^(request|command|stdout|stderr|output|prompt|content)$/i;
423
+ const walk = (current, key = "") => {
424
+ if (sensitiveKey.test(key))
425
+ return "[REDACTED]";
426
+ if (typeof current === "string" && freeTextKey.test(key)) {
427
+ return `[REDACTED sha256:${createHash("sha256").update(current).digest("hex")}]`;
428
+ }
429
+ if (Array.isArray(current))
430
+ return current.map((entry) => walk(entry));
431
+ if (current && typeof current === "object")
432
+ return Object.fromEntries(Object.entries(current).map(([childKey, entry]) => [childKey, walk(entry, childKey)]));
433
+ return current;
434
+ };
435
+ return walk(redacted);
436
+ }
437
+ function parseJsonl(text, label) {
438
+ const lines = text.split("\n").filter((line) => line.trim());
439
+ if (!lines.length)
440
+ throw new Error(`${label} ledger is empty`);
441
+ return lines.map((line, index) => {
442
+ try {
443
+ const value = JSON.parse(line);
444
+ if (!value || typeof value !== "object" || Array.isArray(value))
445
+ throw new Error("record is not an object");
446
+ return value;
447
+ }
448
+ catch (error) {
449
+ throw new Error(`${label} ledger line ${index + 1} is invalid JSON: ${error instanceof Error ? error.message : error}`);
450
+ }
451
+ });
452
+ }
453
+ function object(value) { return value && typeof value === "object" && !Array.isArray(value) ? value : undefined; }
454
+ function string(value) { return typeof value === "string" && value.length ? value : undefined; }
455
+ function stringArray(value) { return Array.isArray(value) && value.every((item) => typeof item === "string") ? value : undefined; }
456
+ function anyDefined(value) { return Object.values(value).some((entry) => entry !== undefined) ? value : undefined; }
457
+ function cleanObject(value) { return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined)); }
458
+ function cleanEvent(event) { return cleanObject(event); }
459
+ function without(record, keys) { const omitted = new Set(keys); return Object.fromEntries(Object.entries(record).filter(([key, value]) => !omitted.has(key) && value !== undefined)); }
460
+ function walkFiles(root, relative = "") {
461
+ const out = [];
462
+ let entries;
463
+ try {
464
+ entries = readdirSync(join(root, relative), { withFileTypes: true });
465
+ }
466
+ catch {
467
+ return out;
468
+ }
469
+ for (const entry of entries) {
470
+ const path = relative ? `${relative}/${entry.name}` : entry.name;
471
+ if (entry.isDirectory())
472
+ out.push(...walkFiles(root, path));
473
+ else if (entry.isFile())
474
+ out.push(path);
475
+ // Symlinks are not followed: an event source is workspace-local evidence,
476
+ // not a route for a spec to read an arbitrary host path.
477
+ }
478
+ return out;
479
+ }
480
+ //# sourceMappingURL=trajectory.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skill-harness/adapters",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "skill-harness harness adapters — pi runner + claude-code judge routing (internal API)",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -40,6 +40,6 @@
40
40
  "prepack": "cp ../../LICENSE ./LICENSE"
41
41
  },
42
42
  "dependencies": {
43
- "@skill-harness/core": "0.7.0"
43
+ "@skill-harness/core": "0.9.0"
44
44
  }
45
45
  }