pi-daddy 0.19.0 → 0.20.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.
Files changed (125) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +105 -36
  3. package/contracts/ledger/v3/README.md +36 -0
  4. package/contracts/ledger/v3/fixtures/capability-decision.json +98 -0
  5. package/contracts/ledger/v3/fixtures/check-receipt.json +42 -0
  6. package/contracts/ledger/v3/fixtures/child-lifecycle.json +45 -0
  7. package/contracts/ledger/v3/fixtures/workflow-fact.json +41 -0
  8. package/contracts/ledger/v3/fixtures/workspace-lease.json +43 -0
  9. package/contracts/ledger/v3/ledger-event.schema.json +930 -0
  10. package/dist/check-runner.d.ts.map +1 -1
  11. package/dist/check-runner.js +14 -0
  12. package/dist/check-runner.js.map +1 -1
  13. package/dist/cli.js +0 -0
  14. package/dist/correlation.d.ts.map +1 -1
  15. package/dist/correlation.js +10 -0
  16. package/dist/correlation.js.map +1 -1
  17. package/dist/dashboard-cli.d.ts +18 -0
  18. package/dist/dashboard-cli.d.ts.map +1 -0
  19. package/dist/dashboard-cli.js +154 -0
  20. package/dist/dashboard-cli.js.map +1 -0
  21. package/dist/dashboard-handshake.d.ts +37 -0
  22. package/dist/dashboard-handshake.d.ts.map +1 -0
  23. package/dist/dashboard-handshake.js +127 -0
  24. package/dist/dashboard-handshake.js.map +1 -0
  25. package/dist/dashboard-herdr.d.ts +54 -0
  26. package/dist/dashboard-herdr.d.ts.map +1 -0
  27. package/dist/dashboard-herdr.js +286 -0
  28. package/dist/dashboard-herdr.js.map +1 -0
  29. package/dist/dashboard-projection.d.ts +73 -0
  30. package/dist/dashboard-projection.d.ts.map +1 -0
  31. package/dist/dashboard-projection.js +294 -0
  32. package/dist/dashboard-projection.js.map +1 -0
  33. package/dist/dashboard-render.d.ts +10 -0
  34. package/dist/dashboard-render.d.ts.map +1 -0
  35. package/dist/dashboard-render.js +208 -0
  36. package/dist/dashboard-render.js.map +1 -0
  37. package/dist/delegate-types.d.ts +6 -2
  38. package/dist/delegate-types.d.ts.map +1 -1
  39. package/dist/delegate-types.js.map +1 -1
  40. package/dist/delegate.d.ts.map +1 -1
  41. package/dist/delegate.js +4 -1
  42. package/dist/delegate.js.map +1 -1
  43. package/dist/execution-id.d.ts +6 -0
  44. package/dist/execution-id.d.ts.map +1 -0
  45. package/dist/execution-id.js +13 -0
  46. package/dist/execution-id.js.map +1 -0
  47. package/dist/index.d.ts +4 -1
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +4 -1
  50. package/dist/index.js.map +1 -1
  51. package/dist/ledger-events.d.ts +27 -4
  52. package/dist/ledger-events.d.ts.map +1 -1
  53. package/dist/ledger-events.js +30 -9
  54. package/dist/ledger-events.js.map +1 -1
  55. package/dist/ledger-identifiers.d.ts +7 -0
  56. package/dist/ledger-identifiers.d.ts.map +1 -0
  57. package/dist/ledger-identifiers.js +20 -0
  58. package/dist/ledger-identifiers.js.map +1 -0
  59. package/dist/ledger-report.d.ts +5 -2
  60. package/dist/ledger-report.d.ts.map +1 -1
  61. package/dist/ledger-report.js +38 -14
  62. package/dist/ledger-report.js.map +1 -1
  63. package/dist/ledger-v3-validation.d.ts +11 -0
  64. package/dist/ledger-v3-validation.d.ts.map +1 -0
  65. package/dist/ledger-v3-validation.js +265 -0
  66. package/dist/ledger-v3-validation.js.map +1 -0
  67. package/dist/ledger.d.ts +18 -5
  68. package/dist/ledger.d.ts.map +1 -1
  69. package/dist/ledger.js +29 -7
  70. package/dist/ledger.js.map +1 -1
  71. package/dist/propagation.d.ts +3 -1
  72. package/dist/propagation.d.ts.map +1 -1
  73. package/dist/propagation.js +3 -0
  74. package/dist/propagation.js.map +1 -1
  75. package/dist/run-child.d.ts +3 -1
  76. package/dist/run-child.d.ts.map +1 -1
  77. package/dist/run-child.js +30 -2
  78. package/dist/run-child.js.map +1 -1
  79. package/dist/run-herdr.d.ts +2 -0
  80. package/dist/run-herdr.d.ts.map +1 -1
  81. package/dist/run-herdr.js +8 -0
  82. package/dist/run-herdr.js.map +1 -1
  83. package/dist/workflow-fact-id.d.ts +4 -0
  84. package/dist/workflow-fact-id.d.ts.map +1 -0
  85. package/dist/workflow-fact-id.js +14 -0
  86. package/dist/workflow-fact-id.js.map +1 -0
  87. package/dist/workflow-facts.d.ts +34 -0
  88. package/dist/workflow-facts.d.ts.map +1 -0
  89. package/dist/workflow-facts.js +43 -0
  90. package/dist/workflow-facts.js.map +1 -0
  91. package/extensions/chain-ledger.ts +4 -0
  92. package/extensions/chain-plan.ts +98 -0
  93. package/extensions/delegate-chain.ts +63 -122
  94. package/extensions/delegation-ledger.ts +60 -0
  95. package/extensions/delegation.ts +13 -13
  96. package/extensions/execute-child.ts +85 -21
  97. package/extensions/execution-occurrence.ts +20 -0
  98. package/extensions/grants-command.ts +22 -3
  99. package/extensions/grants.ts +44 -0
  100. package/extensions/run-delegation.ts +36 -56
  101. package/extensions/session.ts +7 -2
  102. package/extensions/workspace-runtime.ts +10 -0
  103. package/herdr-plugin/herdr-plugin.toml +18 -0
  104. package/package.json +16 -4
  105. package/src/check-runner.ts +16 -0
  106. package/src/correlation.ts +11 -0
  107. package/src/dashboard-cli.ts +171 -0
  108. package/src/dashboard-handshake.ts +185 -0
  109. package/src/dashboard-herdr.ts +355 -0
  110. package/src/dashboard-projection.ts +390 -0
  111. package/src/dashboard-render.ts +254 -0
  112. package/src/delegate-types.ts +6 -2
  113. package/src/delegate.ts +4 -2
  114. package/src/execution-id.ts +18 -0
  115. package/src/index.ts +25 -0
  116. package/src/ledger-events.ts +54 -10
  117. package/src/ledger-identifiers.ts +21 -0
  118. package/src/ledger-report.ts +40 -19
  119. package/src/ledger-v3-validation.ts +266 -0
  120. package/src/ledger.ts +49 -10
  121. package/src/propagation.ts +3 -0
  122. package/src/run-child.ts +35 -3
  123. package/src/run-herdr.ts +9 -0
  124. package/src/workflow-fact-id.ts +16 -0
  125. package/src/workflow-facts.ts +65 -0
package/src/delegate.ts CHANGED
@@ -19,8 +19,8 @@ import {
19
19
  // charged to every caller for a line count they did not cause.
20
20
  export { DELEGATE_CAPABILITY, agentCapability, maySpawnDefinition, normaliseCapability } from "./capabilities.ts";
21
21
  import {
22
- ENV_APPROVED, ENV_DEPTH, ENV_FANOUT, ENV_GATED, ENV_GRANT, ENV_LEDGER, ENV_MAX_DEPTH, ENV_PARENT_ID,
23
- inheritableGrant,
22
+ ENV_APPROVED, ENV_DEPTH, ENV_EXECUTION_ID, ENV_FANOUT, ENV_GATED, ENV_GRANT, ENV_LEDGER, ENV_MAX_DEPTH,
23
+ ENV_PARENT_ID, inheritableGrant,
24
24
  } from "./propagation.ts";
25
25
  import { inheritApprovals, type InheritableApproval } from "./approval.ts";
26
26
  import { suggestForUnknown, unknownCapabilities, type Catalog } from "./catalog.ts";
@@ -309,6 +309,7 @@ export function planDelegation(request: DelegationRequest, ctx: DelegationContex
309
309
  // process boundaries with no shared state.
310
310
  if (ctx.fanoutBudget !== undefined) env[ENV_FANOUT] = String(ctx.fanoutBudget);
311
311
  if (ctx.childSpawnId) env[ENV_PARENT_ID] = ctx.childSpawnId;
312
+ if (ctx.childExecutionId) env[ENV_EXECUTION_ID] = ctx.childExecutionId;
312
313
  if (ctx.gated.length > 0) env[ENV_GATED] = ctx.gated.join(",");
313
314
  // Approvals ride down with the grant, but only ever for what this child actually received — so
314
315
  // `approved ⊆ grant` holds at every level (ADR-0010). Written even when empty, so this object states
@@ -330,6 +331,7 @@ export function planDelegation(request: DelegationRequest, ctx: DelegationContex
330
331
  childDepth,
331
332
  requested,
332
333
  childId: ctx.childSpawnId,
334
+ executionId: ctx.childExecutionId,
333
335
  taskDigest,
334
336
  ...(correlation ? { correlation } : {}),
335
337
  ...(approvalBinding ? { approvalBinding } : {}),
@@ -0,0 +1,18 @@
1
+ import { randomUUID } from "node:crypto";
2
+
3
+ /** Unique identity for one governed execution occurrence. Logical child ids remain readable positions. */
4
+ export type ExecutionId = `exec:${string}`;
5
+
6
+ const EXECUTION_ID_RE = /^exec:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
7
+
8
+ export function isExecutionId(value: unknown): value is ExecutionId {
9
+ return typeof value === "string" && EXECUTION_ID_RE.test(value);
10
+ }
11
+
12
+ export function newExecutionId(): ExecutionId {
13
+ return `exec:${randomUUID()}`;
14
+ }
15
+
16
+ export function assertExecutionId(value: unknown, field = "executionId"): asserts value is ExecutionId {
17
+ if (!isExecutionId(value)) throw new TypeError(`${field} must be a pi-daddy execution id`);
18
+ }
package/src/index.ts CHANGED
@@ -15,6 +15,10 @@ export {
15
15
  buildChildLifecycleEvent,
16
16
  buildRecord,
17
17
  buildWorkspaceLeaseEvent,
18
+ buildWorkflowFactEvent,
19
+ WORKFLOW_FACT_KINDS,
20
+ WORKFLOW_FACT_PROVENANCE,
21
+ WORKFLOW_FACT_STATES,
18
22
  isEscalationAttempt,
19
23
  LEDGER_VERSION,
20
24
  type CheckReceiptLedgerEvent,
@@ -24,6 +28,10 @@ export {
24
28
  type RuntimeLedgerEvent,
25
29
  type WorkspaceLeaseEvent,
26
30
  type WorkspaceLeaseOutcome,
31
+ type WorkflowFactEvent,
32
+ type WorkflowFactKind,
33
+ type WorkflowFactProvenance,
34
+ type WorkflowFactState,
27
35
  } from "./ledger.ts";
28
36
 
29
37
  export { planSpawn, type SpawnPlan, type SpawnPlanInput } from "./spawn.ts";
@@ -97,6 +105,23 @@ export {
97
105
  type CheckRegistry,
98
106
  } from "./check-runner.ts";
99
107
 
108
+ export {
109
+ isExecutionId,
110
+ newExecutionId,
111
+ type ExecutionId,
112
+ } from "./execution-id.ts";
113
+
114
+ export {
115
+ parseDashboardLedger,
116
+ type DashboardNode,
117
+ type DashboardProjection,
118
+ type DashboardState,
119
+ type DashboardWorkflow,
120
+ type DashboardWorkflowFact,
121
+ } from "./dashboard-projection.ts";
122
+
123
+ export { renderDashboard, type DashboardRenderOptions } from "./dashboard-render.ts";
124
+
100
125
  export {
101
126
  createApprovalGate,
102
127
  createApprovalGateProvider,
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The v2 runtime-event half of the ledger: workspace leases, child lifecycle and check receipts, with
2
+ * The v3 runtime-event half of the ledger: workspace leases, child lifecycle and check receipts, with
3
3
  * their builders. Split out of `./ledger.ts` only to stay under the 400-line module ceiling this
4
4
  * project enforces mechanically; `./ledger.ts` re-exports everything here, so "the ledger module"
5
5
  * remains one import.
@@ -8,6 +8,9 @@ import { LEDGER_VERSION, type LedgerEventBase, type GrantRecord } from "./ledger
8
8
  import type { ExecutorKind } from "./executor.ts";
9
9
  import type { CorrelationMetadata } from "./correlation.ts";
10
10
  import type { StructuredRefusal } from "./refusals.ts";
11
+ import { assertExecutionId } from "./execution-id.ts";
12
+ import type { WorkflowFactEvent } from "./workflow-facts.ts";
13
+ import { assertLedgerV3Wire } from "./ledger-v3-validation.ts";
11
14
 
12
15
  export const WORKSPACE_ACCESSES = ["read", "write"] as const;
13
16
  export type WorkspaceAccess = typeof WORKSPACE_ACCESSES[number];
@@ -15,10 +18,10 @@ export type WorkspaceAccess = typeof WORKSPACE_ACCESSES[number];
15
18
  export const WORKSPACE_RECOVERY_VALUES = [false, true, "unknown"] as const;
16
19
  export type WorkspaceRecovery = typeof WORKSPACE_RECOVERY_VALUES[number];
17
20
 
18
- export const CHILD_LIFECYCLE_STATES = ["starting", "completed", "failed"] as const;
21
+ export const CHILD_LIFECYCLE_STATES = ["starting", "running", "completed", "failed"] as const;
19
22
  export type ChildLifecycleState = typeof CHILD_LIFECYCLE_STATES[number];
20
23
 
21
- /** The complete Node signal vocabulary accepted by a v2 child-lifecycle event. */
24
+ /** The complete Node signal vocabulary accepted by a v3 child-lifecycle event. */
22
25
  export const CHILD_PROCESS_SIGNALS = [
23
26
  "SIGABRT", "SIGALRM", "SIGBUS", "SIGCHLD", "SIGCONT", "SIGFPE", "SIGHUP", "SIGILL", "SIGINT", "SIGIO",
24
27
  "SIGIOT", "SIGKILL", "SIGPIPE", "SIGPOLL", "SIGPROF", "SIGPWR", "SIGQUIT", "SIGSEGV", "SIGSTKFLT",
@@ -47,6 +50,8 @@ export type WorkspaceLeaseOutcome = typeof WORKSPACE_LEASE_OUTCOMES[number];
47
50
  export interface WorkspaceLeaseEvent extends LedgerEventBase {
48
51
  ledgerVersion: typeof LEDGER_VERSION;
49
52
  event: "workspace_lease";
53
+ executionId: string;
54
+ parentExecutionId: string | null;
50
55
  childId: string;
51
56
  workspaceId: string;
52
57
  root: string;
@@ -61,9 +66,16 @@ export interface WorkspaceLeaseEvent extends LedgerEventBase {
61
66
  export interface ChildLifecycleEvent extends LedgerEventBase {
62
67
  ledgerVersion: typeof LEDGER_VERSION;
63
68
  event: "child_lifecycle";
69
+ executionId: string;
70
+ parentExecutionId: string | null;
64
71
  childId: string;
65
72
  state: ChildLifecycleState;
66
73
  executor: ExecutorKind;
74
+ /** Bound after which a non-terminal start is known to be incomplete. */
75
+ deadlineAt?: string;
76
+ /** Herdr runtime identity is observational and never an enforcement boundary. */
77
+ herdrPaneId?: string;
78
+ herdrAgentName?: string;
67
79
  exitCode?: number | null;
68
80
  signal?: ChildProcessSignal | null;
69
81
  timedOut?: true;
@@ -75,6 +87,8 @@ export interface ChildLifecycleEvent extends LedgerEventBase {
75
87
  export interface CheckReceiptLedgerEvent extends LedgerEventBase {
76
88
  ledgerVersion: typeof LEDGER_VERSION;
77
89
  event: "check_receipt";
90
+ executionId: string;
91
+ parentExecutionId: string | null;
78
92
  childId: string;
79
93
  receiptId: string;
80
94
  workspaceId: string;
@@ -85,6 +99,8 @@ export interface CheckReceiptLedgerEvent extends LedgerEventBase {
85
99
  export type CapabilityDecisionEvent = GrantRecord & {
86
100
  ledgerVersion: typeof LEDGER_VERSION;
87
101
  event: "capability_decision";
102
+ executionId: string;
103
+ parentExecutionId: string | null;
88
104
  taskDigest: string;
89
105
  };
90
106
 
@@ -92,9 +108,12 @@ export type RuntimeLedgerEvent =
92
108
  | CapabilityDecisionEvent
93
109
  | WorkspaceLeaseEvent
94
110
  | ChildLifecycleEvent
95
- | CheckReceiptLedgerEvent;
111
+ | CheckReceiptLedgerEvent
112
+ | WorkflowFactEvent;
96
113
 
97
114
  export function buildWorkspaceLeaseEvent(args: {
115
+ executionId: string;
116
+ parentExecutionId: string | null;
98
117
  childId: string;
99
118
  workspaceId: string;
100
119
  root: string;
@@ -106,10 +125,13 @@ export function buildWorkspaceLeaseEvent(args: {
106
125
  correlation?: CorrelationMetadata;
107
126
  now: Date;
108
127
  }): WorkspaceLeaseEvent {
109
- return {
128
+ assertEventIdentity(args);
129
+ return assertLedgerV3Wire({
110
130
  ledgerVersion: LEDGER_VERSION,
111
131
  event: "workspace_lease",
112
132
  ts: args.now.toISOString(),
133
+ executionId: args.executionId,
134
+ parentExecutionId: args.parentExecutionId,
113
135
  childId: args.childId,
114
136
  workspaceId: args.workspaceId,
115
137
  root: args.root,
@@ -123,10 +145,12 @@ export function buildWorkspaceLeaseEvent(args: {
123
145
  ...(args.releaseReason ? { releaseReason: args.releaseReason } : {}),
124
146
  ...(args.refusal ? { refusal: structuredClone(args.refusal) } : {}),
125
147
  ...(args.correlation ? { correlation: structuredClone(args.correlation) } : {}),
126
- };
148
+ });
127
149
  }
128
150
 
129
151
  export function buildCheckReceiptLedgerEvent(args: {
152
+ executionId: string;
153
+ parentExecutionId: string | null;
130
154
  childId: string;
131
155
  receiptId: string;
132
156
  workspaceId: string;
@@ -135,26 +159,34 @@ export function buildCheckReceiptLedgerEvent(args: {
135
159
  correlation?: CorrelationMetadata;
136
160
  now: Date;
137
161
  }): CheckReceiptLedgerEvent {
162
+ assertEventIdentity(args);
138
163
  if (!/^[a-f0-9]{64}$/i.test(args.receiptId)) {
139
164
  throw new TypeError("receiptId must be a SHA-256 hex digest");
140
165
  }
141
- return {
166
+ return assertLedgerV3Wire({
142
167
  ledgerVersion: LEDGER_VERSION,
143
168
  event: "check_receipt",
144
169
  ts: args.now.toISOString(),
170
+ executionId: args.executionId,
171
+ parentExecutionId: args.parentExecutionId,
145
172
  childId: args.childId,
146
173
  receiptId: args.receiptId,
147
174
  workspaceId: args.workspaceId,
148
175
  checkId: args.checkId,
149
176
  treeSha: args.treeSha,
150
177
  ...(args.correlation ? { correlation: structuredClone(args.correlation) } : {}),
151
- };
178
+ });
152
179
  }
153
180
 
154
181
  export function buildChildLifecycleEvent(args: {
182
+ executionId: string;
183
+ parentExecutionId: string | null;
155
184
  childId: string;
156
185
  state: ChildLifecycleState;
157
186
  executor: ExecutorKind;
187
+ deadlineAt?: string;
188
+ herdrPaneId?: string;
189
+ herdrAgentName?: string;
158
190
  exitCode?: number | null;
159
191
  signal?: ChildProcessSignal | null;
160
192
  timedOut?: boolean;
@@ -164,13 +196,19 @@ export function buildChildLifecycleEvent(args: {
164
196
  correlation?: CorrelationMetadata;
165
197
  now: Date;
166
198
  }): ChildLifecycleEvent {
167
- return {
199
+ assertEventIdentity(args);
200
+ return assertLedgerV3Wire({
168
201
  ledgerVersion: LEDGER_VERSION,
169
202
  event: "child_lifecycle",
170
203
  ts: args.now.toISOString(),
204
+ executionId: args.executionId,
205
+ parentExecutionId: args.parentExecutionId,
171
206
  childId: args.childId,
172
207
  state: args.state,
173
208
  executor: args.executor,
209
+ ...(args.deadlineAt ? { deadlineAt: args.deadlineAt } : {}),
210
+ ...(args.herdrPaneId ? { herdrPaneId: args.herdrPaneId } : {}),
211
+ ...(args.herdrAgentName ? { herdrAgentName: args.herdrAgentName } : {}),
174
212
  ...(args.exitCode !== undefined ? { exitCode: args.exitCode } : {}),
175
213
  ...(args.signal !== undefined ? { signal: args.signal } : {}),
176
214
  ...(args.timedOut ? { timedOut: true } : {}),
@@ -178,5 +216,11 @@ export function buildChildLifecycleEvent(args: {
178
216
  ...(args.truncated ? { truncated: true } : {}),
179
217
  ...(args.reason ? { reason: args.reason } : {}),
180
218
  ...(args.correlation ? { correlation: structuredClone(args.correlation) } : {}),
181
- };
219
+ });
220
+ }
221
+
222
+ function assertEventIdentity(args: { executionId: string; parentExecutionId: string | null }): void {
223
+ assertExecutionId(args.executionId);
224
+ if (args.parentExecutionId !== null) assertExecutionId(args.parentExecutionId, "parentExecutionId");
225
+ if (args.parentExecutionId === args.executionId) throw new TypeError("an execution cannot be its own parent");
182
226
  }
@@ -0,0 +1,21 @@
1
+ const DISPLAY_IDENTIFIER = /^[A-Za-z0-9@*][A-Za-z0-9@*._:/-]{0,511}$/;
2
+ const CORRELATION_IDENTIFIER = /^[A-Za-z0-9@*][A-Za-z0-9@*._:/-]{0,127}$/;
3
+ const CAPABILITY_TAIL = /^[A-Za-z0-9@*][A-Za-z0-9@*._/-]{0,255}$/;
4
+ const CAPABILITY_PREFIXES = ["tool:", "skill:", "agent:", "workspace:", "ext:"] as const;
5
+
6
+ /** Identifier text safe to copy onto the operator's terminal; prose belongs in no ledger display field. */
7
+ export function isLedgerDisplayIdentifier(value: unknown): value is string {
8
+ return typeof value === "string" && DISPLAY_IDENTIFIER.test(value);
9
+ }
10
+
11
+ /** The tighter bound for model-facing correlation labels that are copied onto every event. */
12
+ export function isLedgerCorrelationIdentifier(value: unknown): value is string {
13
+ return typeof value === "string" && CORRELATION_IDENTIFIER.test(value);
14
+ }
15
+
16
+ /** A namespaced capability id with an ASCII, non-prose tail. Namespace wildcards remain representable. */
17
+ export function isLedgerCapabilityIdentifier(value: unknown): value is string {
18
+ if (typeof value !== "string") return false;
19
+ const prefix = CAPABILITY_PREFIXES.find((candidate) => value.startsWith(candidate));
20
+ return Boolean(prefix && CAPABILITY_TAIL.test(value.slice(prefix.length)));
21
+ }
@@ -18,11 +18,13 @@ import type { ApprovalSource } from "./approval.ts";
18
18
  import type { GrantRecord } from "./ledger.ts";
19
19
  import { isEscalationAttempt } from "./ledger.ts";
20
20
  import type { WorkspaceLeaseOutcome } from "./ledger-events.ts";
21
+ import { isExecutionId } from "./execution-id.ts";
22
+ import { validateLedgerV3Event } from "./ledger-v3-validation.ts";
21
23
 
22
24
  export interface LedgerReport {
23
25
  /** False when the file is absent — a configuration state, not damage. */
24
26
  exists: boolean;
25
- /** Capability-decision records (legacy or v2). */
27
+ /** Capability-decision records (legacy, v2, or v3). */
26
28
  records: number;
27
29
  /** Every parsed ledger event, including lifecycle and lease events. */
28
30
  events: number;
@@ -30,9 +32,11 @@ export interface LedgerReport {
30
32
  acquired: number; uncontended: number; refused: number; released: number;
31
33
  releasedUnrecorded: number; lost: number; retained: number; timeout: number; recovered: number;
32
34
  };
33
- lifecycle: { starting: number; completed: number; failed: number };
35
+ lifecycle: { starting: number; running: number; completed: number; failed: number };
36
+ /** Planned/observed/controller-validated facts; never counted as enforced child executions. */
37
+ workflowFacts: number;
34
38
  /** Lines that did not, with 1-based line numbers so the report is actionable. */
35
- corrupt: Array<{ line: number; text: string }>;
39
+ corrupt: Array<{ line: number; reason: string }>;
36
40
  /** Records where an agent asked for more than it held — ADR-0008's designated signal. */
37
41
  escalationAttempts: number;
38
42
  /**
@@ -139,18 +143,20 @@ const LEASE_OUTCOME_COUNTERS: Record<WorkspaceLeaseOutcome, keyof LedgerReport["
139
143
  recovered: "recovered",
140
144
  };
141
145
 
142
- function validV2Base(event: Record<string, unknown>): boolean {
143
- return event.ledgerVersion === 2 && typeof event.ts === "string";
144
- }
145
-
146
- function requireV2(event: Record<string, unknown>, fields: string[]): void {
147
- if (!validV2Base(event) || fields.some((field) => typeof event[field] !== "string" || event[field] === "")) {
146
+ function requireVersioned(event: Record<string, unknown>, fields: string[]): void {
147
+ if ((event.ledgerVersion !== 2 && event.ledgerVersion !== 3) || typeof event.ts !== "string" ||
148
+ fields.some((field) => typeof event[field] !== "string" || event[field] === "")) {
148
149
  throw new Error("invalid versioned ledger event");
149
150
  }
151
+ if (event.ledgerVersion === 3 &&
152
+ (!isExecutionId(event.executionId) ||
153
+ (event.parentExecutionId !== null && !isExecutionId(event.parentExecutionId)))) {
154
+ throw new Error("invalid v3 execution identity");
155
+ }
150
156
  }
151
157
 
152
158
  function requireCapabilityDecision(event: Record<string, unknown>): void {
153
- requireV2(event, ["parentId", "childId", "executor", "taskDigest"]);
159
+ requireVersioned(event, ["parentId", "childId", "executor", "taskDigest"]);
154
160
  for (const field of ["requested", "parentGrant", "effective", "denied", "clipped", "gatedBlocked"]) {
155
161
  if (!Array.isArray(event[field]) || !(event[field] as unknown[]).every((value) => typeof value === "string")) {
156
162
  throw new Error("invalid capability decision arrays");
@@ -172,7 +178,8 @@ export async function verifyLedger(path: string): Promise<LedgerReport> {
172
178
  records: 0,
173
179
  events: 0,
174
180
  workspaceLeases: { acquired: 0, uncontended: 0, refused: 0, released: 0, releasedUnrecorded: 0, lost: 0, retained: 0, timeout: 0, recovered: 0 },
175
- lifecycle: { starting: 0, completed: 0, failed: 0 },
181
+ lifecycle: { starting: 0, running: 0, completed: 0, failed: 0 },
182
+ workflowFacts: 0,
176
183
  corrupt: [],
177
184
  escalationAttempts: 0,
178
185
  executors: { herdr: 0, process: 0, unknown: 0 },
@@ -190,13 +197,14 @@ export async function verifyLedger(path: string): Promise<LedgerReport> {
190
197
  throw error;
191
198
  }
192
199
 
193
- const corrupt: Array<{ line: number; text: string }> = [];
200
+ const corrupt: Array<{ line: number; reason: string }> = [];
194
201
  // Keyed by name+digest: one name with two entries is the signal that the definition changed mid-ledger.
195
202
  const digests = new Map<string, { name: string; source: string; sha256: string; spawns: number }>();
196
203
  let records = 0;
197
204
  let events = 0;
198
205
  const workspaceLeases = { acquired: 0, uncontended: 0, refused: 0, released: 0, releasedUnrecorded: 0, lost: 0, retained: 0, timeout: 0, recovered: 0 };
199
- const lifecycle = { starting: 0, completed: 0, failed: 0 };
206
+ const lifecycle = { starting: 0, running: 0, completed: 0, failed: 0 };
207
+ let workflowFacts = 0;
200
208
  let escalationAttempts = 0;
201
209
  const executors = { herdr: 0, process: 0, unknown: 0 };
202
210
  const bySource: Record<ApprovalSource, number> = { prompt: 0, session: 0, persisted: 0, inherited: 0 };
@@ -217,11 +225,21 @@ export async function verifyLedger(path: string): Promise<LedgerReport> {
217
225
  if (raw.trim().length === 0) return;
218
226
  try {
219
227
  const event = JSON.parse(raw) as Record<string, unknown>;
220
- if (event.ledgerVersion !== undefined && event.event === undefined) {
221
- throw new Error("versioned ledger line has no event discriminator");
228
+ if (event.ledgerVersion !== undefined) {
229
+ if (event.event === undefined) throw new Error("versioned ledger line has no event discriminator");
230
+ if (event.ledgerVersion !== 2 && event.ledgerVersion !== 3) throw new Error("unsupported ledger version");
231
+ if (event.ledgerVersion === 3 && validateLedgerV3Event(event)) throw new Error("invalid ledger v3 event");
232
+ }
233
+ if (event.event === "workflow_fact") {
234
+ // v2 has no workflow facts; the exact v3 validator above already checked its vocabulary,
235
+ // correlation and provenance/state relation.
236
+ if (event.ledgerVersion !== 3) throw new Error("invalid workflow fact event");
237
+ workflowFacts += 1;
238
+ events += 1;
239
+ return;
222
240
  }
223
241
  if (event.event === "workspace_lease") {
224
- requireV2(event, ["childId", "workspaceId", "root", "access"]);
242
+ requireVersioned(event, ["childId", "workspaceId", "root", "access"]);
225
243
  const outcome = typeof event.outcome === "string" ? event.outcome : "";
226
244
  // Explicit map, not a name match: the ledger's outcome vocabulary and these counter names are
227
245
  // allowed to differ, and a valid event must never be counted as corruption because a counter
@@ -235,7 +253,7 @@ export async function verifyLedger(path: string): Promise<LedgerReport> {
235
253
  return;
236
254
  }
237
255
  if (event.event === "child_lifecycle") {
238
- requireV2(event, ["childId", "executor"]);
256
+ requireVersioned(event, ["childId", "executor"]);
239
257
  const state = typeof event.state === "string" ? event.state : "";
240
258
  if (!Object.hasOwn(lifecycle, state)) throw new Error("invalid child lifecycle event");
241
259
  lifecycle[state as keyof typeof lifecycle] += 1;
@@ -243,7 +261,7 @@ export async function verifyLedger(path: string): Promise<LedgerReport> {
243
261
  return;
244
262
  }
245
263
  if (event.event === "check_receipt") {
246
- requireV2(event, ["childId", "receiptId", "workspaceId", "checkId", "treeSha"]);
264
+ requireVersioned(event, ["childId", "receiptId", "workspaceId", "checkId", "treeSha"]);
247
265
  events += 1;
248
266
  return;
249
267
  }
@@ -305,7 +323,9 @@ export async function verifyLedger(path: string): Promise<LedgerReport> {
305
323
  else digests.set(key, { name: d.name, source: d.source, sha256: d.sha256, spawns: 1 });
306
324
  }
307
325
  } catch {
308
- corrupt.push({ line: index + 1, text: raw.slice(0, 120) });
326
+ // Content-free by contract: a torn or foreign line can contain exactly the task/output material the
327
+ // ledger and dashboard promise never to expose. The line number locates the evidence without copying it.
328
+ corrupt.push({ line: index + 1, reason: "invalid ledger line" });
309
329
  }
310
330
  });
311
331
 
@@ -315,6 +335,7 @@ export async function verifyLedger(path: string): Promise<LedgerReport> {
315
335
  events,
316
336
  workspaceLeases,
317
337
  lifecycle,
338
+ workflowFacts,
318
339
  corrupt,
319
340
  escalationAttempts,
320
341
  executors,