@skill-harness/adapters 0.10.0 → 0.12.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.
@@ -2,8 +2,9 @@ import { createHash } from "node:crypto";
2
2
  import { readFileSync, readdirSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { TRAJECTORY_EVENT_VERSION, deserializeTrajectoryEvents, matchesGlob, redactArgs, redactText } from "@skill-harness/core";
5
- import { assertSupportedSchema, declaredPropertyNames, validateClosedSchema } from "./closed-schema.js";
5
+ import { assertSupportedSchema, assertSupportedSchemaV3, declaredPropertyNames, validateClosedSchema, validateClosedSchemaV3, } from "./closed-schema.js";
6
6
  import { PI_DADDY_CONTRACT_COMMIT, PI_DADDY_LEDGER_V2_SCHEMA } from "./pi-daddy-ledger-v2.js";
7
+ import { PI_DADDY_LEDGER_V3_CONTRACT_COMMIT, PI_DADDY_LEDGER_V3_SCHEMA } from "./pi-daddy-ledger-v3.js";
7
8
  /** Read and normalize declared workspace-local native ledger files. */
8
9
  export function collectTrajectorySources(cwd, sources) {
9
10
  const files = walkFiles(cwd);
@@ -29,15 +30,19 @@ export function collectTrajectorySources(cwd, sources) {
29
30
  const normalized = source.adapter === "principal-assurance-v1"
30
31
  ? normalizePrincipalAssuranceLedger(text)
31
32
  : source.adapter === "pi-daddy-v1"
32
- ? normalizePiDaddyLedger(text)
33
- : deserializeTrajectoryEvents(text);
33
+ ? normalizePiDaddyLegacyLedger(text)
34
+ : source.adapter === "pi-daddy-ledger-v3"
35
+ ? normalizePiDaddyLedgerV3(text)
36
+ : deserializeTrajectoryEvents(text);
34
37
  if (!normalized)
35
38
  throw new Error("normalized-v1 source is empty, malformed, or unsupported");
36
39
  const times = normalized.map((event) => validTime(event.at) ? Date.parse(event.at) : null);
37
40
  if (times.every((time) => time !== null)) {
38
41
  const highWaterByStream = new Map();
39
42
  for (let index = 0; index < times.length; index += 1) {
40
- const stream = source.adapter === "pi-daddy-v1" ? normalizedPiDaddyStreamKey(normalized[index], index) : "source";
43
+ const stream = source.adapter === "pi-daddy-v1" || source.adapter === "pi-daddy-ledger-v3"
44
+ ? normalizedPiDaddyStreamKey(normalized[index], index)
45
+ : "source";
41
46
  const highWater = highWaterByStream.get(stream);
42
47
  if (highWater !== undefined && times[index] < highWater && !isAllowedPiDaddyReceiptInversion(source.adapter, normalized, index)) {
43
48
  throw new Error("native event timestamps move backwards relative to the source's recorded sequence");
@@ -137,6 +142,26 @@ export function normalizePiTraces(traces) {
137
142
  }
138
143
  return events;
139
144
  }
145
+ const PRINCIPAL_ASSURANCE_SOURCES = new Set([
146
+ "default", "flag", "alias", "natural-language", "policy", "user", "user-downgrade",
147
+ ]);
148
+ function validatePrincipalAssurance(record, line) {
149
+ if (record.assurance === undefined)
150
+ return;
151
+ const assurance = object(record.assurance);
152
+ if (!assurance || typeof assurance.source !== "string" || !PRINCIPAL_ASSURANCE_SOURCES.has(assurance.source)) {
153
+ throw new Error(`invalid principal assurance v1 event at line ${line}: assurance.source is not a recognized source`);
154
+ }
155
+ const scope = object(assurance.scope);
156
+ if (!scope || Object.keys(scope).some((key) => key !== "type" && key !== "selectors") ||
157
+ (scope.type !== "entire-run" && scope.type !== "selectors") || !Array.isArray(scope.selectors) ||
158
+ scope.selectors.some((selector) => typeof selector !== "string" || !selector) ||
159
+ new Set(scope.selectors).size !== scope.selectors.length ||
160
+ (scope.type === "entire-run" && scope.selectors.length !== 0) ||
161
+ (scope.type === "selectors" && scope.selectors.length === 0)) {
162
+ throw new Error(`invalid principal assurance v1 event at line ${line}: assurance.scope is not a closed structured scope`);
163
+ }
164
+ }
140
165
  /** Normalize principal-pi-skills' immutable assurance event schema v1.0. */
141
166
  export function normalizePrincipalAssuranceLedger(text) {
142
167
  const records = parseJsonl(text, "principal assurance");
@@ -148,6 +173,7 @@ export function normalizePrincipalAssuranceLedger(text) {
148
173
  if (!Number.isInteger(record.seq) || Number(record.seq) < 1 || typeof record.type !== "string" || typeof record.run_id !== "string") {
149
174
  throw new Error(`invalid principal assurance v1 event at line ${index + 1}: seq, type, and run_id are required`);
150
175
  }
176
+ validatePrincipalAssurance(record, index + 1);
151
177
  const packet = object(record.packet);
152
178
  const definitionDigests = object(packet?.definition_digests);
153
179
  const definition = typeof record.definition_digest === "string"
@@ -184,10 +210,28 @@ export function normalizePrincipalAssuranceLedger(text) {
184
210
  });
185
211
  }
186
212
  /**
187
- * Normalize pi-daddy's public ledgers: unversioned 0.17 GrantRecord lines and
188
- * ledgerVersion 2 runtime events emitted by 0.18.0. Version detection precedes
189
- * the legacy fallback so a new event can never be misdiagnosed as an old grant.
213
+ * Normalize pi-daddy's public ledgers: unversioned 0.17 GrantRecord lines,
214
+ * frozen ledgerVersion 2 events, and separately pinned production ledgerVersion
215
+ * 3 events. Explicit version dispatch precedes legacy fallback, so an unknown or
216
+ * malformed new record is never reinterpreted as an old grant.
190
217
  */
218
+ /** Historical selector behavior: unversioned 0.17 and frozen ledger v2 only. */
219
+ export function normalizePiDaddyLegacyLedger(text) {
220
+ const records = parseJsonl(text, "pi-daddy");
221
+ const explicitV3 = records.findIndex((record) => record.ledgerVersion === 3);
222
+ if (explicitV3 >= 0)
223
+ throw new Error(`pi-daddy-v1 selector does not admit ledgerVersion 3 at line ${explicitV3 + 1}; use pi-daddy-ledger-v3`);
224
+ return normalizePiDaddyLedger(text);
225
+ }
226
+ /** Separately versioned selector: every line must be an explicit ledgerVersion 3 event. */
227
+ export function normalizePiDaddyLedgerV3(text) {
228
+ const records = parseJsonl(text, "pi-daddy");
229
+ const wrong = records.findIndex((record) => record.ledgerVersion !== 3);
230
+ if (wrong >= 0)
231
+ throw new Error(`pi-daddy-ledger-v3 requires explicit ledgerVersion 3 at line ${wrong + 1}`);
232
+ return normalizePiDaddyLedger(text);
233
+ }
234
+ /** Programmatic mixed-version dispatcher; source declarations use explicit selectors above. */
191
235
  export function normalizePiDaddyLedger(text) {
192
236
  const records = parseJsonl(text, "pi-daddy");
193
237
  validatePiDaddyTimestampOrder(records);
@@ -195,23 +239,30 @@ export function normalizePiDaddyLedger(text) {
195
239
  let seq = 1;
196
240
  records.forEach((record, index) => {
197
241
  if (record.ledgerVersion !== undefined) {
198
- if (record.ledgerVersion !== 2) {
199
- throw new Error(`unsupported pi-daddy ledgerVersion ${safeDiagnosticValue(record.ledgerVersion)} at line ${index + 1}; expected 2 or an unversioned 0.17 GrantRecord`);
242
+ if (record.ledgerVersion === 2) {
243
+ // The discriminator first (it names the four public variants), then the
244
+ // producer's own closed schema, then semantic normalization. Nothing
245
+ // downstream may assume a field the pinned contract has not admitted.
246
+ requireV2Discriminator(record, index + 1);
247
+ assertPinnedV2Contract(record, index + 1);
248
+ for (const event of normalizePiDaddyV2(record, index))
249
+ out.push({ ...event, seq: seq++ });
250
+ return;
200
251
  }
201
- // The discriminator first (it names the four public variants), then the
202
- // producer's own closed schema, then semantic normalization. Nothing
203
- // downstream may assume a field the pinned contract has not admitted.
204
- requireV2Discriminator(record, index + 1);
205
- assertPinnedV2Contract(record, index + 1);
206
- for (const event of normalizePiDaddyV2(record, index))
207
- out.push({ ...event, seq: seq++ });
208
- return;
252
+ if (record.ledgerVersion === 3) {
253
+ requireV3Discriminator(record, index + 1);
254
+ assertPinnedV3Contract(record, index + 1);
255
+ for (const event of normalizePiDaddyV3(record, index))
256
+ out.push({ ...event, seq: seq++ });
257
+ return;
258
+ }
259
+ throw new Error(`unsupported pi-daddy ledgerVersion ${safeDiagnosticValue(record.ledgerVersion)} at line ${index + 1}; expected 2, 3, or an unversioned 0.17 GrantRecord`);
209
260
  }
210
261
  if (record.schema_version !== undefined) {
211
- throw new Error(`pi-daddy schema_version/record_type at line ${index + 1} is not a public pi-daddy ledger format; expected ledgerVersion 2 or an unversioned 0.17 GrantRecord`);
262
+ throw new Error(`pi-daddy schema_version/record_type at line ${index + 1} is not a public pi-daddy ledger format; expected ledgerVersion 2, ledgerVersion 3, or an unversioned 0.17 GrantRecord`);
212
263
  }
213
264
  if (record.event !== undefined) {
214
- throw new Error(`pi-daddy event [REDACTED invalid value] at line ${index + 1} is missing explicit ledgerVersion 2`);
265
+ throw new Error(`pi-daddy event [REDACTED invalid value] at line ${index + 1} is missing explicit ledgerVersion 2 or 3`);
215
266
  }
216
267
  for (const event of normalizeLegacyGrant(record, index))
217
268
  out.push({ ...event, seq: seq++ });
@@ -257,6 +308,33 @@ function assertPinnedV2Contract(record, line) {
257
308
  throw new Error(`invalid pi-daddy v2 ${label} at line ${line}: closed contract violation — ${first.path ? `${first.path} ` : ""}${first.message}${extra}` +
258
309
  ` [pi-daddy ${PI_DADDY_CONTRACT_COMMIT.slice(0, 12)}]`);
259
310
  }
311
+ /** The five public `ledgerVersion: 3` event discriminators. */
312
+ const V3_EVENTS = new Set(["capability_decision", "workspace_lease", "child_lifecycle", "check_receipt", "workflow_fact"]);
313
+ function requireV3Discriminator(record, line) {
314
+ const nativeEvent = string(record.event);
315
+ if (!nativeEvent || !V3_EVENTS.has(nativeEvent)) {
316
+ throw new Error(`invalid pi-daddy v3 event at line ${line}: event discriminator is required and must be capability_decision, workspace_lease, child_lifecycle, check_receipt, or workflow_fact`);
317
+ }
318
+ return nativeEvent;
319
+ }
320
+ let pinnedV3ContractChecked = false;
321
+ let pinnedV3ContractFieldNames;
322
+ function assertPinnedV3Contract(record, line) {
323
+ if (!pinnedV3ContractChecked) {
324
+ assertSupportedSchemaV3(PI_DADDY_LEDGER_V3_SCHEMA, "pinned pi-daddy ledger v3 schema");
325
+ pinnedV3ContractFieldNames = declaredPropertyNames(PI_DADDY_LEDGER_V3_SCHEMA);
326
+ pinnedV3ContractChecked = true;
327
+ }
328
+ const violations = validateClosedSchemaV3(PI_DADDY_LEDGER_V3_SCHEMA, record, { knownFieldNames: pinnedV3ContractFieldNames });
329
+ if (violations.length === 0)
330
+ return;
331
+ const nativeEvent = string(record.event);
332
+ const label = nativeEvent && V3_EVENTS.has(nativeEvent) ? nativeEvent : "record";
333
+ const [first] = violations;
334
+ const extra = violations.length > 1 ? ` (+${violations.length - 1} more contract violation${violations.length > 2 ? "s" : ""})` : "";
335
+ throw new Error(`invalid pi-daddy v3 ${label} at line ${line}: closed contract violation — ${first.path ? `${first.path} ` : ""}${first.message}${extra}` +
336
+ ` [pi-daddy ${PI_DADDY_LEDGER_V3_CONTRACT_COMMIT.slice(0, 12)}]`);
337
+ }
260
338
  const V2_LEASE_OUTCOMES = new Set([
261
339
  "acquired", "uncontended", "refused", "released", "released-unrecorded", "lost", "retained", "timeout", "recovered",
262
340
  ]);
@@ -281,23 +359,14 @@ const V2_CORRELATION_NUMERIC_FIELDS = new Set(["event_seq", "last_change_seq", "
281
359
  const V2_APPROVAL_SOURCES = new Set(["prompt", "session", "persisted", "inherited"]);
282
360
  const V2_APPROVAL_SCOPES = new Set(["once", "session", "always"]);
283
361
  /**
284
- * pi-daddy's canonical refusal vocabulary, in the pinned contract's own order.
285
- *
286
- * This is a copy of `#/$defs/refusalCode` from the pinned schema and is exported
287
- * so `pi-daddy-contract.test.ts` can assert set equality against the producer
288
- * artifact — a hand-maintained second vocabulary without that drift assertion is
289
- * exactly how `GRANT_ID_MALFORMED` came to be rejected as "unsupported".
362
+ * pi-daddy's canonical refusal vocabulary, derived directly from the immutable
363
+ * pinned schema. The producer's `src/refusals.ts` is vendored and digest-checked
364
+ * separately, while the real-builder verifier proves its production export is
365
+ * set-equal to this schema-derived runtime set. There is no second list here to
366
+ * forget when pi-daddy adds a contract-valid code.
290
367
  */
291
- export const V2_REFUSAL_CODES = new Set([
292
- "CAPABILITY_ESCALATION", "GRANT_ID_MALFORMED", "DEFINITION_NOT_AUTHORIZED", "UNDECLARED_TOOLS", "UNKNOWN_TOOL",
293
- "GATED_UNAPPROVED", "APPROVAL_EXPIRED", "APPROVAL_SCOPE_MISMATCH", "APPROVAL_FLOW_FAILED",
294
- "DEPTH_EXCEEDED", "FANOUT_EXCEEDED", "EXECUTOR_UNAVAILABLE", "CHILD_TIMED_OUT", "CHILD_CANCELLED",
295
- "CHILD_EXIT_NONZERO", "TASK_MISSING", "UNKNOWN_DEFINITION", "CEILING_PATTERNS_UNRESOLVED",
296
- "NARROWING_VIOLATED", "DEFINITION_UNREADABLE", "CORRELATION_TOO_LARGE", "CORRELATION_INVALID",
297
- "LEDGER_WRITE_FAILED", "FANOUT_FAILED", "WORKSPACE_NOT_REGISTERED", "WORKSPACE_WRITE_CONFLICT",
298
- "WORKSPACE_LEASE_STALE", "CHECK_NOT_CONFIGURED", "CHECK_CONFIGURATION_INVALID",
299
- "CHECK_IDENTITY_UNAVAILABLE", "CHECK_IDENTITY_MISMATCH",
300
- ]);
368
+ export const V2_REFUSAL_CODES = new Set(PI_DADDY_LEDGER_V2_SCHEMA.$defs.refusalCode.enum);
369
+ export const V3_REFUSAL_CODES = new Set(PI_DADDY_LEDGER_V3_SCHEMA.$defs.refusalCode.enum);
301
370
  /**
302
371
  * Every vocabulary the adapter restates from the pinned contract, paired with the
303
372
  * place in the schema it must equal.
@@ -340,6 +409,11 @@ const V2_CORRELATION_MAX_SCOPE_BYTES = 4 * 1024;
340
409
  function piDaddyStreamKey(record, index) {
341
410
  if (record.ledgerVersion === undefined)
342
411
  return JSON.stringify(["legacy", string(record.childId) ?? `missing-child:${index}`]);
412
+ if (record.ledgerVersion === 3) {
413
+ return record.event === "workflow_fact"
414
+ ? JSON.stringify(["v3-fact", string(record.factId) ?? `missing-fact:${index}`])
415
+ : JSON.stringify(["v3-execution", string(record.executionId) ?? `missing-execution:${index}`]);
416
+ }
343
417
  const correlation = object(record.correlation);
344
418
  return JSON.stringify([
345
419
  string(correlation?.run_id) ?? `missing-run:${index}`,
@@ -351,6 +425,11 @@ function piDaddyStreamKey(record, index) {
351
425
  function normalizedPiDaddyStreamKey(event, index) {
352
426
  if (event.source === "pi-daddy-0.17")
353
427
  return JSON.stringify(["legacy", event.child_id ?? `missing-child:${index}`]);
428
+ if (event.source === "pi-daddy-v3") {
429
+ return event.workflow_fact_id
430
+ ? JSON.stringify(["v3-fact", event.workflow_fact_id])
431
+ : JSON.stringify(["v3-execution", event.execution_id ?? `missing-execution:${index}`]);
432
+ }
354
433
  const correlation = object(event.attributes?.correlation);
355
434
  return JSON.stringify([
356
435
  event.run_id ?? `missing-run:${index}`,
@@ -368,9 +447,9 @@ function sameRawCorrelationIdentity(left, right) {
368
447
  function validatePiDaddyTimestampOrder(records) {
369
448
  const highWaterByChild = new Map();
370
449
  records.forEach((record, index) => {
371
- const supportedV2 = record.ledgerVersion === 2;
450
+ const supportedVersion = record.ledgerVersion === 2 || record.ledgerVersion === 3;
372
451
  const legacy = record.ledgerVersion === undefined && record.schema_version === undefined && record.event === undefined;
373
- if (!supportedV2 && !legacy)
452
+ if (!supportedVersion && !legacy)
374
453
  return;
375
454
  const at = string(record.ts);
376
455
  if (!validTime(at))
@@ -387,17 +466,17 @@ function validatePiDaddyTimestampOrder(records) {
387
466
  function isRawPiDaddyReceiptInversion(records, index, receiptTime) {
388
467
  const receipt = records[index];
389
468
  const release = records[index - 1];
390
- if (receipt?.ledgerVersion !== 2 || receipt.event !== "check_receipt" || release?.ledgerVersion !== 2 || release.event !== "workspace_lease")
469
+ if ((receipt?.ledgerVersion !== 2 && receipt?.ledgerVersion !== 3) || receipt.event !== "check_receipt" || release?.ledgerVersion !== receipt.ledgerVersion || release.event !== "workspace_lease")
391
470
  return false;
392
471
  if (receipt.childId !== release.childId || receipt.workspaceId !== release.workspaceId || !sameRawCorrelationIdentity(receipt, release) || !V2_RECEIPT_RELEASE_OUTCOMES.has(string(release.outcome) ?? ""))
393
472
  return false;
394
- const previousLease = records.slice(0, index - 1).reverse().find((record) => record.ledgerVersion === 2 && record.event === "workspace_lease" && record.childId === receipt.childId &&
473
+ const previousLease = records.slice(0, index - 1).reverse().find((record) => record.ledgerVersion === receipt.ledgerVersion && record.event === "workspace_lease" && record.childId === receipt.childId &&
395
474
  record.workspaceId === receipt.workspaceId && sameRawCorrelationIdentity(receipt, record));
396
475
  return Boolean(previousLease && V2_RECEIPT_PRIOR_LEASE_OUTCOMES.has(string(previousLease.outcome) ?? "") &&
397
476
  validTime(string(previousLease.ts)) && Date.parse(string(previousLease.ts)) <= receiptTime);
398
477
  }
399
478
  function isAllowedPiDaddyReceiptInversion(adapter, events, index) {
400
- if (adapter !== "pi-daddy-v1")
479
+ if (adapter !== "pi-daddy-v1" && adapter !== "pi-daddy-ledger-v3")
401
480
  return false;
402
481
  const receipt = events[index];
403
482
  const release = events[index - 1];
@@ -662,6 +741,246 @@ function normalizePiDaddyV2(record, index) {
662
741
  }),
663
742
  })];
664
743
  }
744
+ function normalizePiDaddyV3(record, index) {
745
+ const line = index + 1;
746
+ const nativeEvent = requireV3Discriminator(record, line);
747
+ const at = requireV3String(record, "ts", nativeEvent, line);
748
+ const correlation = object(record.correlation) ?? {};
749
+ const correlationDigests = anyDefined({
750
+ correlation_plan: string(correlation.plan_digest),
751
+ correlation_task: string(correlation.task_digest),
752
+ correlation_definition: string(correlation.definition_digest),
753
+ correlation_base: string(correlation.base_sha),
754
+ correlation_head: string(correlation.head_sha),
755
+ correlation_tree: string(correlation.tree_sha),
756
+ });
757
+ const correlationAttributes = safeAttributes({
758
+ ledger_version: 3,
759
+ native_event: nativeEvent,
760
+ correlation: Object.keys(correlation).length ? sanitizeAttributes(correlation) : undefined,
761
+ event_seq: finiteNumber(correlation.event_seq),
762
+ last_change_seq: finiteNumber(correlation.last_change_seq),
763
+ last_authority_seq: finiteNumber(correlation.last_authority_seq),
764
+ check_receipt_id: string(correlation.check_receipt_id),
765
+ assurance: string(correlation.assurance),
766
+ assurance_effective: string(correlation.assurance_effective),
767
+ policy_label: string(correlation.policy_label),
768
+ assurance_source: string(correlation.assurance_source),
769
+ assurance_scope: correlation.assurance_scope,
770
+ activated_at: string(correlation.activated_at),
771
+ });
772
+ if (nativeEvent === "workflow_fact") {
773
+ return [cleanEvent({
774
+ event_version: TRAJECTORY_EVENT_VERSION,
775
+ type: "workflow_fact",
776
+ source: "pi-daddy-v3",
777
+ at,
778
+ run_id: string(correlation.run_id),
779
+ task_id: string(correlation.task_id),
780
+ workspace_id: string(correlation.workspace_id),
781
+ context_id: string(correlation.context_id),
782
+ phase: string(correlation.phase),
783
+ workflow_fact_id: requireV3String(record, "factId", nativeEvent, line),
784
+ digests: correlationDigests,
785
+ attributes: safeAttributes({
786
+ ...correlationAttributes,
787
+ source: string(record.source),
788
+ provenance: string(record.provenance),
789
+ fact_kind: string(record.kind),
790
+ fact_subject: string(record.subject),
791
+ fact_state: string(record.state),
792
+ }),
793
+ })];
794
+ }
795
+ const executionId = requireV3String(record, "executionId", nativeEvent, line);
796
+ const parentExecutionId = record.parentExecutionId === null ? null : requireV3String(record, "parentExecutionId", nativeEvent, line);
797
+ if (parentExecutionId === executionId)
798
+ throw new Error(`invalid pi-daddy v3 ${nativeEvent} at line ${line}: an execution cannot be its own parent`);
799
+ const childId = requireV3String(record, "childId", nativeEvent, line);
800
+ const carriesTopWorkspace = nativeEvent === "workspace_lease" || nativeEvent === "check_receipt";
801
+ const topWorkspace = carriesTopWorkspace ? string(record.workspaceId) : undefined;
802
+ const correlationWorkspace = string(correlation.workspace_id);
803
+ if (topWorkspace && correlationWorkspace && topWorkspace !== correlationWorkspace) {
804
+ throw new Error(`invalid pi-daddy v3 ${nativeEvent} at line ${line}: workspaceId disagrees with correlation.workspace_id`);
805
+ }
806
+ const common = {
807
+ event_version: TRAJECTORY_EVENT_VERSION,
808
+ source: "pi-daddy-v3",
809
+ at,
810
+ run_id: string(correlation.run_id),
811
+ task_id: string(correlation.task_id),
812
+ workspace_id: topWorkspace,
813
+ context_id: string(correlation.context_id),
814
+ child_id: childId,
815
+ execution_id: executionId,
816
+ parent_execution_id: parentExecutionId,
817
+ phase: string(correlation.phase),
818
+ digests: correlationDigests,
819
+ };
820
+ if (nativeEvent === "capability_decision") {
821
+ const requested = record.requested;
822
+ const parentGrant = record.parentGrant;
823
+ const effective = record.effective;
824
+ const denied = record.denied;
825
+ const clipped = record.clipped;
826
+ const gated = record.gatedBlocked;
827
+ const approved = record.approved;
828
+ validateCapabilityPartition(requested, effective, denied, clipped, gated, approved, Boolean(record.blocked), line, 3);
829
+ const approvalSources = object(record.approvalSources);
830
+ const approvalScopes = object(record.approvalScopes);
831
+ const approvalExpiresAt = object(record.approvalExpiresAt);
832
+ const approvalUses = object(record.approvalUses);
833
+ if (approvalUses && Object.values(approvalUses).some((use) => !object(use) || !Number.isInteger(use.max) || !Number.isInteger(use.remaining) || use.max < 0 || use.remaining < 0 || use.remaining > use.max))
834
+ throw new Error(`invalid pi-daddy v3 capability_decision at line ${line}: approvalUses requires remaining <= max integer bounds`);
835
+ validateApprovalEvidence(approved ?? [], string(record.approvalSource), approvalSources, approvalScopes, approvalExpiresAt, approvalUses, line, 3);
836
+ const refusal = structuredRefusal(record.refusal, nativeEvent, line, 3);
837
+ if (!record.blocked && refusal)
838
+ throw new Error(`invalid pi-daddy v3 capability_decision at line ${line}: an allowed decision cannot carry a refusal`);
839
+ const definition = object(record.definitionDigest);
840
+ const taskDigest = requireV3String(record, "taskDigest", nativeEvent, line);
841
+ const trustedDefinition = string(definition?.sha256);
842
+ const normalizedRequested = [...new Set(requested)];
843
+ const attributes = safeAttributes({
844
+ ...correlationAttributes,
845
+ depth: record.depth,
846
+ agent_type: string(record.agentType),
847
+ executor: string(record.executor),
848
+ task_from: string(record.taskFrom),
849
+ parent_grant: parentGrant,
850
+ denied,
851
+ clipped,
852
+ gated_blocked: gated,
853
+ blocked: record.blocked,
854
+ reason: string(record.reason),
855
+ approved,
856
+ approval_source: string(record.approvalSource),
857
+ approval_sources: approvalSources,
858
+ approval_scope: string(record.approvalScope),
859
+ approval_scopes: approvalScopes,
860
+ approval_expires_at: approvalExpiresAt,
861
+ approval_uses: approvalUses,
862
+ human_denied: record.humanDenied,
863
+ gate_outcome: string(record.gateOutcome),
864
+ definition_name: string(definition?.name),
865
+ definition_source: string(definition?.source),
866
+ structured_refusal: refusal,
867
+ });
868
+ const base = {
869
+ ...common,
870
+ parent_id: requireV3String(record, "parentId", nativeEvent, line),
871
+ task_from_execution_id: string(record.taskFromExecutionId),
872
+ requested_capabilities: normalizedRequested,
873
+ effective_capabilities: effective,
874
+ digests: anyDefined({ ...correlationDigests, task: taskDigest, definition: trustedDefinition }),
875
+ attributes,
876
+ };
877
+ const refusalCode = string(refusal?.code);
878
+ const events = normalizedRequested.map((capability) => ({ ...base, type: "capability_requested", capability }));
879
+ for (const capability of approved ?? []) {
880
+ events.push(cleanEvent({
881
+ ...base,
882
+ type: "approval_used",
883
+ capability,
884
+ approval: cleanObject({
885
+ capability,
886
+ subject: approvalSubject(record.agentType),
887
+ source: string(approvalSources?.[capability]) ?? string(record.approvalSource),
888
+ scope: string(approvalScopes?.[capability]) ?? string(record.approvalScope),
889
+ expires_at: string(approvalExpiresAt?.[capability]),
890
+ used_at: at,
891
+ }),
892
+ attributes: safeAttributes({ ...attributes, approval_uses: object(approvalUses?.[capability]) }),
893
+ }));
894
+ }
895
+ const approvedSet = new Set(approved ?? []);
896
+ if (!record.blocked)
897
+ events.push(...effective.map((capability) => ({ ...base, type: "capability_granted", capability })));
898
+ events.push(...[...new Set([...denied, ...gated.filter((capability) => !approvedSet.has(capability))])].map((capability) => ({
899
+ ...base,
900
+ type: "capability_refused",
901
+ capability,
902
+ refusal_code: denied.includes(capability) ? "CAPABILITY_ESCALATION" : refusalCode,
903
+ })));
904
+ events.push(cleanEvent({ ...base, type: record.blocked ? "child_spawn_refused" : "capability_decision", refusal_code: refusalCode }));
905
+ return events;
906
+ }
907
+ if (nativeEvent === "workspace_lease") {
908
+ const workspaceId = requireV3String(record, "workspaceId", nativeEvent, line);
909
+ const access = requireV3String(record, "access", nativeEvent, line);
910
+ const outcome = requireV3String(record, "outcome", nativeEvent, line);
911
+ const refusal = structuredRefusal(record.refusal, nativeEvent, line, 3);
912
+ const type = access === "read"
913
+ ? `workspace_read_${outcome.replaceAll("-", "_")}`
914
+ : outcome === "refused" && refusal?.code === "WORKSPACE_WRITE_CONFLICT"
915
+ ? "writer_lease_conflict"
916
+ : `writer_lease_${outcome.replaceAll("-", "_")}`;
917
+ return [cleanEvent({
918
+ ...common,
919
+ workspace_id: workspaceId,
920
+ type,
921
+ refusal_code: string(refusal?.code),
922
+ attributes: safeAttributes({
923
+ ...correlationAttributes,
924
+ root: string(record.root),
925
+ access,
926
+ outcome,
927
+ recovered: record.recovered,
928
+ release_reason: string(record.releaseReason),
929
+ structured_refusal: refusal,
930
+ }),
931
+ })];
932
+ }
933
+ if (nativeEvent === "child_lifecycle") {
934
+ const state = requireV3String(record, "state", nativeEvent, line);
935
+ const type = state === "starting" ? "child_started" : state === "running" ? "child_running" : state === "completed" ? "child_completed" : "child_failed";
936
+ return [cleanEvent({
937
+ ...common,
938
+ type,
939
+ deadline_at: string(record.deadlineAt),
940
+ exit_code: Number.isInteger(record.exitCode) ? Number(record.exitCode) : undefined,
941
+ attributes: safeAttributes({
942
+ ...correlationAttributes,
943
+ state,
944
+ executor: string(record.executor),
945
+ exit_code: record.exitCode,
946
+ signal: record.signal,
947
+ timed_out: record.timedOut,
948
+ aborted: record.aborted,
949
+ truncated: record.truncated,
950
+ reason: string(record.reason),
951
+ deadline_at: string(record.deadlineAt),
952
+ herdr_pane_id: string(record.herdrPaneId),
953
+ herdr_agent_name: string(record.herdrAgentName),
954
+ }),
955
+ })];
956
+ }
957
+ const workspaceId = requireV3String(record, "workspaceId", nativeEvent, line);
958
+ const receiptId = requireV3String(record, "receiptId", nativeEvent, line);
959
+ const treeSha = requireV3String(record, "treeSha", nativeEvent, line);
960
+ if (!/^(?:[a-fA-F0-9]{40}|[a-fA-F0-9]{64})$/.test(treeSha)) {
961
+ throw new Error(`invalid pi-daddy v3 check_receipt at line ${line}: treeSha must be a git object id for normalized tree evidence`);
962
+ }
963
+ return [cleanEvent({
964
+ ...common,
965
+ workspace_id: workspaceId,
966
+ type: "check_receipt_recorded",
967
+ digests: anyDefined({ ...correlationDigests, tree: treeSha }),
968
+ attributes: safeAttributes({
969
+ ...correlationAttributes,
970
+ receipt_id: receiptId,
971
+ check_id: string(record.checkId),
972
+ check_receipt_id: string(correlation.check_receipt_id),
973
+ }),
974
+ })];
975
+ }
976
+ function requireV3String(record, field, event, line) {
977
+ const value = string(record[field]);
978
+ if (!value)
979
+ throw new Error(`invalid pi-daddy v3 ${event} at line ${line}: ${field} is required`);
980
+ if (redactText(value) !== value)
981
+ throw new Error(`invalid pi-daddy v3 ${event} at line ${line}: ${field} contains a sensitive value`);
982
+ return value;
983
+ }
665
984
  function requireV2Correlation(record, event, line) {
666
985
  const correlation = object(record.correlation);
667
986
  if (!correlation) {
@@ -795,35 +1114,35 @@ function optionalV2ApprovalUses(value, event, line) {
795
1114
  }
796
1115
  return output;
797
1116
  }
798
- function validateCapabilityPartition(requested, effective, denied, clipped, gated, approved, blocked, line) {
1117
+ function validateCapabilityPartition(requested, effective, denied, clipped, gated, approved, blocked, line, version = 2) {
799
1118
  const groups = [effective, denied, clipped, gated];
800
1119
  if (groups.some((values) => new Set(values).size !== values.length)) {
801
- throw new Error(`invalid pi-daddy v2 capability_decision at line ${line}: result capability arrays must not contain duplicates`);
1120
+ throw new Error(`invalid pi-daddy v${version} capability_decision at line ${line}: result capability arrays must not contain duplicates`);
802
1121
  }
803
1122
  const requestedSet = new Set(requested);
804
1123
  if (groups.some((values) => values.some((capability) => !requestedSet.has(capability)))) {
805
- throw new Error(`invalid pi-daddy v2 capability_decision at line ${line}: effective, denied, clipped, and gatedBlocked must partition requested`);
1124
+ throw new Error(`invalid pi-daddy v${version} capability_decision at line ${line}: effective, denied, clipped, and gatedBlocked must partition requested`);
806
1125
  }
807
1126
  const flattened = groups.flat();
808
1127
  if (new Set(flattened).size !== flattened.length) {
809
- throw new Error(`invalid pi-daddy v2 capability_decision at line ${line}: effective, denied, clipped, and gatedBlocked must be disjoint subsets of requested`);
1128
+ throw new Error(`invalid pi-daddy v${version} capability_decision at line ${line}: effective, denied, clipped, and gatedBlocked must be disjoint subsets of requested`);
810
1129
  }
811
1130
  if ((approved ?? []).some((capability) => !requestedSet.has(capability) || (blocked ? !effective.includes(capability) && !gated.includes(capability) : !effective.includes(capability)))) {
812
- throw new Error(`invalid pi-daddy v2 capability_decision at line ${line}: approved capabilities must be requested and reflected in the resolved decision`);
1131
+ throw new Error(`invalid pi-daddy v${version} capability_decision at line ${line}: approved capabilities must be requested and reflected in the resolved decision`);
813
1132
  }
814
1133
  }
815
- function validateApprovalEvidence(approved, scalarSource, sources, scopes, expiries, uses, line) {
1134
+ function validateApprovalEvidence(approved, scalarSource, sources, scopes, expiries, uses, line, version = 2) {
816
1135
  const approvedSet = new Set(approved);
817
1136
  for (const [field, map] of [["approvalSources", sources], ["approvalScopes", scopes], ["approvalExpiresAt", expiries], ["approvalUses", uses]]) {
818
1137
  if (map && Object.keys(map).some((capability) => !approvedSet.has(capability))) {
819
- throw new Error(`invalid pi-daddy v2 capability_decision at line ${line}: ${field} keys must be approved capabilities`);
1138
+ throw new Error(`invalid pi-daddy v${version} capability_decision at line ${line}: ${field} keys must be approved capabilities`);
820
1139
  }
821
1140
  }
822
1141
  if (approved.some((capability) => !sources?.[capability] && !scalarSource)) {
823
- throw new Error(`invalid pi-daddy v2 capability_decision at line ${line}: each approved capability requires an approval source`);
1142
+ throw new Error(`invalid pi-daddy v${version} capability_decision at line ${line}: each approved capability requires an approval source`);
824
1143
  }
825
1144
  if (approved.length === 0 && (scalarSource || sources || scopes || expiries || uses)) {
826
- throw new Error(`invalid pi-daddy v2 capability_decision at line ${line}: approval evidence requires approved capabilities`);
1145
+ throw new Error(`invalid pi-daddy v${version} capability_decision at line ${line}: approval evidence requires approved capabilities`);
827
1146
  }
828
1147
  }
829
1148
  function optionalV2Boolean(record, field, event, line) {
@@ -838,23 +1157,24 @@ function approvalSubject(value) {
838
1157
  const agentType = string(value);
839
1158
  return agentType === undefined || agentType === "delegate" ? "<delegate>" : agentType;
840
1159
  }
841
- function structuredRefusal(value, event, line) {
1160
+ function structuredRefusal(value, event, line, version = 2) {
842
1161
  if (value === undefined)
843
1162
  return undefined;
844
1163
  const parsed = object(value);
845
1164
  const code = string(parsed?.code);
846
1165
  if (!parsed || !code || !string(parsed.message)) {
847
- throw new Error(`invalid pi-daddy v2 ${event} at line ${line}: refusal requires code and message`);
1166
+ throw new Error(`invalid pi-daddy v${version} ${event} at line ${line}: refusal requires code and message`);
848
1167
  }
849
- if (!V2_REFUSAL_CODES.has(code)) {
850
- throw new Error(`invalid pi-daddy v2 ${event} at line ${line}: refusal has unsupported code ${safeDiagnosticValue(code)}`);
1168
+ const refusalCodes = version === 3 ? V3_REFUSAL_CODES : V2_REFUSAL_CODES;
1169
+ if (!refusalCodes.has(code)) {
1170
+ throw new Error(`invalid pi-daddy v${version} ${event} at line ${line}: refusal has unsupported code ${safeDiagnosticValue(code)}`);
851
1171
  }
852
1172
  const unknown = Object.keys(parsed).filter((key) => !V2_REFUSAL_FIELDS.has(key));
853
1173
  if (unknown.length > 0)
854
- throw new Error(`invalid pi-daddy v2 ${event} at line ${line}: refusal carries unsupported fields`);
1174
+ throw new Error(`invalid pi-daddy v${version} ${event} at line ${line}: refusal carries unsupported fields`);
855
1175
  const details = parsed.details === undefined ? undefined : object(parsed.details);
856
1176
  if (parsed.details !== undefined && (!details || Object.values(details).some((entry) => !V2_REFUSAL_DETAIL_TYPES.has(entry === null ? "null" : typeof entry)))) {
857
- throw new Error(`invalid pi-daddy v2 ${event} at line ${line}: refusal.details must contain scalar values`);
1177
+ throw new Error(`invalid pi-daddy v${version} ${event} at line ${line}: refusal.details must contain scalar values`);
858
1178
  }
859
1179
  return parsed;
860
1180
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skill-harness/adapters",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "skill-harness harness adapters — pi runner + claude-code judge routing (internal API)",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -37,9 +37,9 @@
37
37
  "node": ">=20"
38
38
  },
39
39
  "scripts": {
40
- "prepack": "cp ../../LICENSE ./LICENSE"
40
+ "prepack": "node ../../scripts/release-pack.mjs --guard"
41
41
  },
42
42
  "dependencies": {
43
- "@skill-harness/core": "0.10.0"
43
+ "@skill-harness/core": "0.12.0"
44
44
  }
45
45
  }