pi-daddy 0.41.0 → 0.43.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 (138) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/contracts/ledger-record/v1/fixtures/capability-decision.json +3 -1
  3. package/contracts/ledger-record/v1/fixtures/child-lifecycle.json +25 -0
  4. package/contracts/ledger-record/v1/fixtures/episode-cost-gate.json +14 -0
  5. package/contracts/ledger-record/v1/fixtures/episode-outcome.json +12 -0
  6. package/contracts/ledger-record/v1/fixtures/ledger-record.jsonl +6 -3
  7. package/contracts/ledger-record/v1/fixtures/session-config.json +14 -0
  8. package/contracts/ledger-record/v1/governance-event.schema.json +356 -0
  9. package/dist/advisors/advisor.d.ts +24 -1
  10. package/dist/advisors/advisor.d.ts.map +1 -1
  11. package/dist/advisors/advisor.js +19 -1
  12. package/dist/advisors/advisor.js.map +1 -1
  13. package/dist/advisors/decider.d.ts +11 -0
  14. package/dist/advisors/decider.d.ts.map +1 -1
  15. package/dist/advisors/decider.js.map +1 -1
  16. package/dist/advisors/jev.d.ts.map +1 -1
  17. package/dist/advisors/jev.js +32 -1
  18. package/dist/advisors/jev.js.map +1 -1
  19. package/dist/cli.d.ts +7 -1
  20. package/dist/cli.d.ts.map +1 -1
  21. package/dist/cli.js +87 -1
  22. package/dist/cli.js.map +1 -1
  23. package/dist/executors/activity-session.d.ts +13 -0
  24. package/dist/executors/activity-session.d.ts.map +1 -1
  25. package/dist/executors/activity-session.js +44 -2
  26. package/dist/executors/activity-session.js.map +1 -1
  27. package/dist/governance/episode-cost-gate.d.ts +31 -0
  28. package/dist/governance/episode-cost-gate.d.ts.map +1 -0
  29. package/dist/governance/episode-cost-gate.js +112 -0
  30. package/dist/governance/episode-cost-gate.js.map +1 -0
  31. package/dist/governance/ledger-events.d.ts +108 -1
  32. package/dist/governance/ledger-events.d.ts.map +1 -1
  33. package/dist/governance/ledger-events.js +64 -0
  34. package/dist/governance/ledger-events.js.map +1 -1
  35. package/dist/governance/ledger-v3-validation.d.ts.map +1 -1
  36. package/dist/governance/ledger-v3-validation.js +124 -0
  37. package/dist/governance/ledger-v3-validation.js.map +1 -1
  38. package/dist/governance/ledger.d.ts +8 -2
  39. package/dist/governance/ledger.d.ts.map +1 -1
  40. package/dist/governance/ledger.js +18 -2
  41. package/dist/governance/ledger.js.map +1 -1
  42. package/dist/kernel/context-handoff.d.ts.map +1 -1
  43. package/dist/kernel/context-handoff.js +4 -1
  44. package/dist/kernel/context-handoff.js.map +1 -1
  45. package/dist/kernel/definitions.d.ts +5 -0
  46. package/dist/kernel/definitions.d.ts.map +1 -1
  47. package/dist/kernel/definitions.js +30 -1
  48. package/dist/kernel/definitions.js.map +1 -1
  49. package/dist/kernel/delegate-types.d.ts +4 -0
  50. package/dist/kernel/delegate-types.d.ts.map +1 -1
  51. package/dist/kernel/delegate-types.js.map +1 -1
  52. package/dist/kernel/delegate.d.ts.map +1 -1
  53. package/dist/kernel/delegate.js +20 -5
  54. package/dist/kernel/delegate.js.map +1 -1
  55. package/dist/kernel/env-names.d.ts +5 -0
  56. package/dist/kernel/env-names.d.ts.map +1 -1
  57. package/dist/kernel/env-names.js +9 -0
  58. package/dist/kernel/env-names.js.map +1 -1
  59. package/dist/kernel/grant-env.d.ts +2 -0
  60. package/dist/kernel/grant-env.d.ts.map +1 -1
  61. package/dist/kernel/grant-env.js +1 -0
  62. package/dist/kernel/grant-env.js.map +1 -1
  63. package/dist/kernel/propagation.d.ts +2 -2
  64. package/dist/kernel/propagation.d.ts.map +1 -1
  65. package/dist/kernel/propagation.js +7 -2
  66. package/dist/kernel/propagation.js.map +1 -1
  67. package/dist/products/dashboard-cli.d.ts +4 -0
  68. package/dist/products/dashboard-cli.d.ts.map +1 -1
  69. package/dist/products/dashboard-cli.js +32 -4
  70. package/dist/products/dashboard-cli.js.map +1 -1
  71. package/dist/products/dashboard-handshake.d.ts +8 -0
  72. package/dist/products/dashboard-handshake.d.ts.map +1 -1
  73. package/dist/products/dashboard-handshake.js +3 -0
  74. package/dist/products/dashboard-handshake.js.map +1 -1
  75. package/dist/products/dashboard-herdr.d.ts +4 -0
  76. package/dist/products/dashboard-herdr.d.ts.map +1 -1
  77. package/dist/products/dashboard-herdr.js +10 -2
  78. package/dist/products/dashboard-herdr.js.map +1 -1
  79. package/dist/products/dashboard-projection.d.ts.map +1 -1
  80. package/dist/products/dashboard-projection.js +13 -6
  81. package/dist/products/dashboard-projection.js.map +1 -1
  82. package/dist/products/dashboard-render.d.ts +10 -0
  83. package/dist/products/dashboard-render.d.ts.map +1 -1
  84. package/dist/products/dashboard-render.js +9 -0
  85. package/dist/products/dashboard-render.js.map +1 -1
  86. package/dist/products/dashboard-session-client.d.ts +17 -0
  87. package/dist/products/dashboard-session-client.d.ts.map +1 -0
  88. package/dist/products/dashboard-session-client.js +28 -0
  89. package/dist/products/dashboard-session-client.js.map +1 -0
  90. package/dist/products/episode-outcomes.d.ts +32 -0
  91. package/dist/products/episode-outcomes.d.ts.map +1 -0
  92. package/dist/products/episode-outcomes.js +280 -0
  93. package/dist/products/episode-outcomes.js.map +1 -0
  94. package/dist/products/episode-report.d.ts +59 -0
  95. package/dist/products/episode-report.d.ts.map +1 -0
  96. package/dist/products/episode-report.js +311 -0
  97. package/dist/products/episode-report.js.map +1 -0
  98. package/extensions/chain-ledger.ts +2 -0
  99. package/extensions/chain-plan.ts +4 -2
  100. package/extensions/context-shape.ts +19 -4
  101. package/extensions/dashboard-session-server.ts +86 -0
  102. package/extensions/definition-runtime.ts +93 -0
  103. package/extensions/delegate-chain.ts +31 -2
  104. package/extensions/delegation-ledger.ts +2 -0
  105. package/extensions/delegation.ts +24 -0
  106. package/extensions/effort-advice.ts +12 -0
  107. package/extensions/execute-child.ts +148 -9
  108. package/extensions/grants-command.ts +32 -2
  109. package/extensions/grants.ts +23 -1
  110. package/extensions/pruning-advice.ts +3 -1
  111. package/extensions/run-delegation.ts +54 -6
  112. package/extensions/session-model-prompt.ts +179 -0
  113. package/extensions/session.ts +104 -3
  114. package/package.json +1 -1
  115. package/src/advisors/advisor.ts +45 -2
  116. package/src/advisors/decider.ts +8 -0
  117. package/src/advisors/jev.ts +34 -1
  118. package/src/cli.ts +90 -2
  119. package/src/executors/activity-session.ts +56 -2
  120. package/src/governance/episode-cost-gate.ts +128 -0
  121. package/src/governance/ledger-events.ts +173 -1
  122. package/src/governance/ledger-v3-validation.ts +146 -0
  123. package/src/governance/ledger.ts +29 -1
  124. package/src/kernel/context-handoff.ts +5 -1
  125. package/src/kernel/definitions.ts +32 -1
  126. package/src/kernel/delegate-types.ts +4 -0
  127. package/src/kernel/delegate.ts +22 -5
  128. package/src/kernel/env-names.ts +10 -0
  129. package/src/kernel/grant-env.ts +3 -0
  130. package/src/kernel/propagation.ts +7 -0
  131. package/src/products/dashboard-cli.ts +32 -4
  132. package/src/products/dashboard-handshake.ts +5 -0
  133. package/src/products/dashboard-herdr.ts +15 -1
  134. package/src/products/dashboard-projection.ts +13 -6
  135. package/src/products/dashboard-render.ts +11 -0
  136. package/src/products/dashboard-session-client.ts +35 -0
  137. package/src/products/episode-outcomes.ts +317 -0
  138. package/src/products/episode-report.ts +372 -0
@@ -35,7 +35,14 @@ import { assertEpisodeId } from "../kernel/episode-id.ts";
35
35
  import { assertLedgerV3Wire } from "./ledger-v3-validation.ts";
36
36
 
37
37
  export const LEDGER_VERSION = 3 as const;
38
- export const LEDGER_EVENT_KINDS = ["capability_decision", "workspace_lease", "child_lifecycle"] as const;
38
+ export const LEDGER_EVENT_KINDS = [
39
+ "capability_decision",
40
+ "workspace_lease",
41
+ "child_lifecycle",
42
+ "cost_gate",
43
+ "session_config",
44
+ "episode_outcome",
45
+ ] as const;
39
46
  export type LedgerEventKind = (typeof LEDGER_EVENT_KINDS)[number];
40
47
  export const LEDGER_GATE_OUTCOMES = ["declined", "dismissed", "no-ui", "error"] as const;
41
48
  export type LedgerGateOutcome = (typeof LEDGER_GATE_OUTCOMES)[number];
@@ -144,6 +151,10 @@ export interface GrantRecord extends LedgerEventBase {
144
151
  * it. Absent for a `tools:`-style delegation, which has no definition.
145
152
  */
146
153
  definitionDigest?: DefinitionDigest;
154
+ /** SHA-256 of the complete SKILL.md used by this child. */
155
+ definitionHash?: string;
156
+ /** Installed package version exposed for the definition, when available. */
157
+ definitionPackageVersion?: string;
147
158
  /**
148
159
  * The context handoff this child RECEIVED (ADR-0078): the mode whose capability survived, and what crossed.
149
160
  * Absent means nothing crossed, which is the default and the overwhelming majority of records.
@@ -232,6 +243,8 @@ export function buildRecord(args: {
232
243
  humanDenied?: boolean;
233
244
  gateOutcome?: PromptOutcomeKind;
234
245
  definitionDigest?: DefinitionDigest;
246
+ definitionHash?: string;
247
+ definitionPackageVersion?: string;
235
248
  handoff?: GrantRecord["handoff"];
236
249
  /** Where the child ran (ADR-0031). Required: the probe's answer survives nowhere else. */
237
250
  executor: ExecutorKind;
@@ -255,6 +268,9 @@ export function buildRecord(args: {
255
268
  if (args.definitionDigest && !/^[a-f0-9]{64}$/i.test(args.definitionDigest.sha256)) {
256
269
  throw new TypeError("definitionDigest.sha256 must be a SHA-256 hex digest");
257
270
  }
271
+ if (args.definitionHash && !/^[a-f0-9]{64}$/i.test(args.definitionHash)) {
272
+ throw new TypeError("definitionHash must be a SHA-256 hex digest");
273
+ }
258
274
  // R-46: the scalar is a SUMMARY, emitted only when it cannot mislead. `buildRecord` derives it rather
259
275
  // than accepting it, so a call site cannot supply one that disagrees with the map beside it.
260
276
  const sources = args.approvalSources ?? {};
@@ -308,6 +324,8 @@ export function buildRecord(args: {
308
324
  // stops being a signal.
309
325
  ...(args.gateOutcome && args.gateOutcome !== "granted" ? { gateOutcome: args.gateOutcome } : {}),
310
326
  ...(args.definitionDigest ? { definitionDigest: args.definitionDigest } : {}),
327
+ ...(args.definitionHash ? { definitionHash: args.definitionHash } : {}),
328
+ ...(args.definitionPackageVersion ? { definitionPackageVersion: args.definitionPackageVersion } : {}),
311
329
  };
312
330
  if (args.taskDigest !== undefined) assertLedgerV3Wire(record);
313
331
  return record;
@@ -341,6 +359,10 @@ export function recordKindForEvent(event: { event?: string }): RecordKind {
341
359
  return "lifecycle";
342
360
  case "workspace_lease":
343
361
  return "lease";
362
+ case "cost_gate":
363
+ case "session_config":
364
+ case "episode_outcome":
365
+ return "fact";
344
366
  case "check_receipt": // retired kind, still imported from pre-format ledgers
345
367
  return "check";
346
368
  case "workflow_fact": // retired kind, still imported from pre-format ledgers
@@ -390,8 +412,14 @@ export {
390
412
  WORKSPACE_LEASE_OUTCOMES,
391
413
  WORKSPACE_RECOVERY_VALUES,
392
414
  buildChildLifecycleEvent,
415
+ buildEpisodeCostGateEvent,
416
+ buildEpisodeOutcomeEvent,
417
+ buildSessionConfigEvent,
393
418
  buildWorkspaceLeaseEvent,
394
419
  type CapabilityDecisionEvent,
420
+ type EpisodeCostGateEvent,
421
+ type EpisodeOutcomeEvent,
422
+ type SessionConfigEvent,
395
423
  type ChildLifecycleEvent,
396
424
  type ChildLifecycleState,
397
425
  type ChildProcessSignal,
@@ -114,7 +114,11 @@ export function parseContextRequest(raw: unknown): { request: ContextRequest } |
114
114
  if (!Array.isArray(value.files) || value.files.some((path) => typeof path !== "string" || path.length === 0))
115
115
  return { refusal: "context.files must be an array of non-empty paths" };
116
116
  if (value.files.length > MAX_CONTEXT_FILES)
117
- return { refusal: `context.files may name at most ${MAX_CONTEXT_FILES} paths` };
117
+ return {
118
+ refusal:
119
+ `context.files may name at most ${MAX_CONTEXT_FILES} paths; choose fewer, or omit context when the child ` +
120
+ "can inspect the same workspace",
121
+ };
118
122
  request.files = value.files as string[];
119
123
  }
120
124
  if (value.summary !== undefined) {
@@ -20,6 +20,7 @@
20
20
  */
21
21
 
22
22
  import { createHash } from "node:crypto";
23
+ import { join } from "node:path";
23
24
  import { readBoundedFile } from "./bounded-read.ts";
24
25
  import { resolveSkillResources, skillResourceName } from "./skill-resources.ts";
25
26
  import { CAPABILITY_NAMESPACE_PREFIXES } from "./capabilities.ts";
@@ -55,6 +56,10 @@ export function digestDefinition(definition: SkillDefinition): DefinitionDigest
55
56
  };
56
57
  }
57
58
 
59
+ export function isDefinitionPackageVersion(value: unknown): value is string {
60
+ return typeof value === "string" && /^[0-9A-Za-z][0-9A-Za-z.+_-]{0,127}$/.test(value);
61
+ }
62
+
58
63
  export interface SkillDefinition {
59
64
  /** From the path, never the frontmatter — see `parseSkillDefinition`. */
60
65
  name: string;
@@ -66,6 +71,10 @@ export interface SkillDefinition {
66
71
  /** Everything after the frontmatter — the child's system prompt. */
67
72
  body: string;
68
73
  source: string;
74
+ /** SHA-256 of the complete SKILL.md, matching skill-harness's raw `source_hashes["SKILL.md"]`. */
75
+ sourceHash?: string;
76
+ /** Installed package version when Pi exposes one for this definition. */
77
+ packageVersion?: string;
69
78
  }
70
79
 
71
80
  export interface DefinitionCeiling {
@@ -154,6 +163,7 @@ export function parseSkillDefinition(source: string, text: string): SkillDefinit
154
163
  metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
155
164
  body: text.slice(match[0].length).trim(),
156
165
  source,
166
+ sourceHash: createHash("sha256").update(text, "utf8").digest("hex"),
157
167
  };
158
168
  }
159
169
 
@@ -224,7 +234,8 @@ export async function loadDefinitions(
224
234
  skipped?: (path: string, reason: string) => void,
225
235
  ): Promise<Map<string, SkillDefinition>> {
226
236
  const definitions = new Map<string, SkillDefinition>();
227
- for (const { path } of (await resolveSkillResources(cwd)).skills) {
237
+ const packageVersions = new Map<string, string | undefined>();
238
+ for (const { path, metadata } of (await resolveSkillResources(cwd)).skills) {
228
239
  const read = await readBoundedFile(path, {
229
240
  maxBytes: DEFINITION_MAX_BYTES,
230
241
  timeoutMs: DEFINITION_READ_TIMEOUT_MS,
@@ -234,6 +245,26 @@ export async function loadDefinitions(
234
245
  continue;
235
246
  }
236
247
  const parsed = parseSkillDefinition(path, read.text);
248
+ if (parsed && metadata.origin === "package" && metadata.baseDir) {
249
+ if (!packageVersions.has(metadata.baseDir)) {
250
+ const manifest = await readBoundedFile(join(metadata.baseDir, "package.json"), {
251
+ maxBytes: DEFINITION_MAX_BYTES,
252
+ timeoutMs: DEFINITION_READ_TIMEOUT_MS,
253
+ });
254
+ let version: string | undefined;
255
+ if (manifest.ok) {
256
+ try {
257
+ const candidate = JSON.parse(manifest.text).version;
258
+ if (isDefinitionPackageVersion(candidate)) version = candidate;
259
+ } catch {
260
+ // A package without readable version metadata still supplies a valid definition; omit the optional stamp.
261
+ }
262
+ }
263
+ packageVersions.set(metadata.baseDir, version);
264
+ }
265
+ const packageVersion = packageVersions.get(metadata.baseDir);
266
+ if (packageVersion) parsed.packageVersion = packageVersion;
267
+ }
237
268
  if (!parsed) skipped?.(path, `${path} has no readable frontmatter with a description`);
238
269
  // Shadowing is legitimate — a project override is SUPPOSED to win over a package's copy — but review
239
270
  // pointed out it was the one remaining drop with no word said, in the very function being made loud.
@@ -219,6 +219,10 @@ export interface Delegation {
219
219
  * identify — and absent on an ADR-0017 authorisation refusal, which is decided before the file is read.
220
220
  */
221
221
  definitionDigest?: DefinitionDigest;
222
+ /** SHA-256 of the complete SKILL.md, matching skill-harness source hashes. */
223
+ definitionHash?: string;
224
+ /** Installed package version exposed for the definition, when available. */
225
+ definitionPackageVersion?: string;
222
226
  /** Trusted SHA-256 of the exact model-authored task. The task text itself is never stored. */
223
227
  taskDigest: string;
224
228
  /** Non-authoritative external join metadata, snapshotted at planning time. */
@@ -27,7 +27,7 @@ import {
27
27
  inheritableGrant,
28
28
  workspacePinEnv,
29
29
  } from "./propagation.ts";
30
- import { inheritApprovals, type InheritableApproval } from "./approval.ts";
30
+ import { DELEGATE_SUBJECT, inheritApprovals, type InheritableApproval } from "./approval.ts";
31
31
  import { explainDoubledNamespace, suggestForUnknown, unknownCapabilities, type Catalog } from "./catalog.ts";
32
32
  import { GovernanceRefusal, refusal, type RefusalCode, type StructuredRefusal } from "./refusals.ts";
33
33
  import { contextCapability, isContextCapability, parseContextRequest, type ContextRequest } from "./context-handoff.ts";
@@ -36,7 +36,7 @@ import { resolveDelegationApproval } from "./delegation-approval.ts";
36
36
  import type { Delegation, DelegationContext, DelegationRequest } from "./delegate-types.ts";
37
37
  import { assertCapabilitiesArePropagatable } from "./capabilities.ts";
38
38
  export type { Delegation, DelegationContext, DelegationRequest } from "./delegate-types.ts";
39
- import { GOVERNANCE_ENV_KEYS } from "./env-names.ts";
39
+ import { ENV_CHILD_DEFINITION, ENV_CHILD_EPISODE, ENV_CHILD_EXECUTION, GOVERNANCE_ENV_KEYS } from "./env-names.ts";
40
40
 
41
41
  export function planDelegation(request: DelegationRequest, ctx: DelegationContext): Delegation {
42
42
  const childDepth = ctx.depth + 1;
@@ -102,6 +102,8 @@ export function planDelegation(request: DelegationRequest, ctx: DelegationContex
102
102
  let requested: Capability[];
103
103
  let systemPrompt: string | undefined;
104
104
  let definitionDigest: DefinitionDigest | undefined;
105
+ let definitionHash: string | undefined;
106
+ let definitionPackageVersion: string | undefined;
105
107
  /** The definition being spawned, hoisted so the gate below can name its authorising id (ADR-0024). */
106
108
  let spawned: SkillDefinition | undefined;
107
109
 
@@ -160,7 +162,13 @@ export function planDelegation(request: DelegationRequest, ctx: DelegationContex
160
162
  // ninth return could forget, there is one, and forgetting it is not expressible. The success return
161
163
  // does not spread `empty`, so it names the field explicitly.
162
164
  definitionDigest = digestDefinition(definition);
163
- Object.assign(empty, { definitionDigest });
165
+ definitionHash = definition.sourceHash;
166
+ definitionPackageVersion = definition.packageVersion;
167
+ Object.assign(empty, {
168
+ definitionDigest,
169
+ ...(definitionHash ? { definitionHash } : {}),
170
+ ...(definitionPackageVersion ? { definitionPackageVersion } : {}),
171
+ });
164
172
 
165
173
  const ceiling = ceilingForDefinition(definition);
166
174
  if (ceiling.undeclared) {
@@ -389,8 +397,15 @@ export function planDelegation(request: DelegationRequest, ctx: DelegationContex
389
397
  // process boundaries with no shared state.
390
398
  if (ctx.fanoutBudget !== undefined) env[ENV_FANOUT] = String(ctx.fanoutBudget);
391
399
  if (ctx.childSpawnId) env[ENV_PARENT_ID] = ctx.childSpawnId;
392
- if (ctx.childExecutionId) env[ENV_EXECUTION_ID] = ctx.childExecutionId;
393
- if (ctx.episodeId) env[ENV_EPISODE_ID] = ctx.episodeId;
400
+ if (ctx.childExecutionId) {
401
+ env[ENV_EXECUTION_ID] = ctx.childExecutionId;
402
+ env[ENV_CHILD_EXECUTION] = ctx.childExecutionId;
403
+ }
404
+ if (ctx.episodeId) {
405
+ env[ENV_EPISODE_ID] = ctx.episodeId;
406
+ env[ENV_CHILD_EPISODE] = ctx.episodeId;
407
+ }
408
+ env[ENV_CHILD_DEFINITION] = request.agent ?? DELEGATE_SUBJECT;
394
409
  if (ctx.gated.length > 0) env[ENV_GATED] = ctx.gated.join(",");
395
410
  // Approvals ride down with the grant, but only ever for what this child actually received — so
396
411
  // `approved ⊆ grant` holds at every level (ADR-0010). Written even when empty, so this object states
@@ -433,5 +448,7 @@ export function planDelegation(request: DelegationRequest, ctx: DelegationContex
433
448
  ...(correlation ? { correlation } : {}),
434
449
  ...(approvalBinding ? { approvalBinding } : {}),
435
450
  ...(definitionDigest ? { definitionDigest } : {}),
451
+ ...(definitionHash ? { definitionHash } : {}),
452
+ ...(definitionPackageVersion ? { definitionPackageVersion } : {}),
436
453
  };
437
454
  }
@@ -24,6 +24,14 @@ export const ENV_FANOUT = "PI_DADDY_FANOUT";
24
24
  export const ENV_PARENT_ID = "PI_DADDY_PARENT_ID";
25
25
  export const ENV_EXECUTION_ID = "PI_DADDY_EXECUTION_ID";
26
26
  export const ENV_EPISODE_ID = "PI_DADDY_EPISODE_ID";
27
+ export const ENV_EPISODE_COST_CEILING = "PI_DADDY_EPISODE_COST_CEILING";
28
+
29
+ // Child-facing work attribution. These duplicate no authority: they let work produced outside pi-daddy carry the
30
+ // same identities as the ledger records that governed it.
31
+ export const ENV_CHILD_EPISODE = "PI_DADDY_EPISODE";
32
+ export const ENV_CHILD_DEFINITION = "PI_DADDY_DEFINITION";
33
+ export const ENV_CHILD_EXECUTION = "PI_DADDY_EXECUTION";
34
+ export const CHILD_ATTRIBUTION_ENV_KEYS = [ENV_CHILD_EPISODE, ENV_CHILD_DEFINITION, ENV_CHILD_EXECUTION] as const;
27
35
 
28
36
  // Operator preferences a child inherits unchanged. Still governance: they decide executor, deadlines, gates.
29
37
  export const ENV_HERDR = "PI_DADDY_HERDR";
@@ -80,6 +88,8 @@ export const GOVERNANCE_ENV_KEYS: readonly string[] = Object.freeze([
80
88
  ENV_PARENT_ID,
81
89
  ENV_EXECUTION_ID,
82
90
  ENV_EPISODE_ID,
91
+ ENV_EPISODE_COST_CEILING,
92
+ ...CHILD_ATTRIBUTION_ENV_KEYS,
83
93
  ENV_HERDR,
84
94
  ENV_HERDR_WORKSPACE,
85
95
  ENV_HERDR_KEEP_PANE,
@@ -140,6 +140,8 @@ export interface ProjectSettings {
140
140
  ledger: string;
141
141
  /** Gated by default (ADR-0012): closed under subsumption, so gating tool:write also gates tool:bash. */
142
142
  gatedByDefault: Capability[];
143
+ /** Cumulative provider-reported USD cost at which the episode asks whether to continue. */
144
+ episodeCostCeiling: number;
143
145
  }
144
146
 
145
147
  export function buildProjectSettings(input: GrantEnvInput): ProjectSettings {
@@ -166,6 +168,7 @@ export function buildProjectSettings(input: GrantEnvInput): ProjectSettings {
166
168
  cautions: [...input.cautions],
167
169
  ledger: PROJECT_FILES.ledger,
168
170
  gatedByDefault: ["tool:bash"],
171
+ episodeCostCeiling: 5,
169
172
  };
170
173
  }
171
174
 
@@ -32,6 +32,7 @@ import { WORKSPACE_WILDCARD } from "./resolve.ts";
32
32
  import { inheritApprovals, type InheritableApproval } from "./approval.ts";
33
33
  import { assertCapabilitiesArePropagatable } from "./capabilities.ts";
34
34
  import {
35
+ CHILD_ATTRIBUTION_ENV_KEYS,
35
36
  ENV_ADVISOR_MODEL,
36
37
  ENV_ADVISOR_TASK_EGRESS,
37
38
  ENV_ADVISOR_KEY,
@@ -41,6 +42,7 @@ import {
41
42
  ENV_PARENT_ID,
42
43
  ENV_EXECUTION_ID,
43
44
  ENV_EPISODE_ID,
45
+ ENV_EPISODE_COST_CEILING,
44
46
  ENV_DEPTH,
45
47
  ENV_MAX_DEPTH,
46
48
  ENV_GATED,
@@ -54,6 +56,7 @@ export {
54
56
  ENV_PARENT_ID,
55
57
  ENV_EXECUTION_ID,
56
58
  ENV_EPISODE_ID,
59
+ ENV_EPISODE_COST_CEILING,
57
60
  ENV_DEPTH,
58
61
  ENV_MAX_DEPTH,
59
62
  ENV_GATED,
@@ -102,6 +105,9 @@ export const GRANT_ENV_KEYS = [
102
105
  ENV_PARENT_ID,
103
106
  ENV_EXECUTION_ID,
104
107
  ENV_EPISODE_ID,
108
+ ENV_EPISODE_COST_CEILING,
109
+ // Child attribution identifies this process itself, so session publication must not clear it. `mergeChildEnv`
110
+ // strips it separately before applying the next child's plan.
105
111
  // ADR-0042: a child must never keep its parent's unnarrowed pin, so it is stripped like every other
106
112
  // governance value and re-supplied only by the spawn plan.
107
113
  ENV_WORKSPACE_PIN,
@@ -393,6 +399,7 @@ export function workspacePinEnv(
393
399
  export function mergeChildEnv(parentEnv: NodeJS.ProcessEnv, planEnv: Record<string, string>): NodeJS.ProcessEnv {
394
400
  const merged: NodeJS.ProcessEnv = { ...parentEnv };
395
401
  for (const key of GRANT_ENV_KEYS) delete merged[key];
402
+ for (const key of CHILD_ATTRIBUTION_ENV_KEYS) delete merged[key];
396
403
  return { ...merged, ...planEnv };
397
404
  }
398
405
 
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * `pi-daddy-dashboard` — the read-only view of one ledger or activity timeline.
3
+ * `pi-daddy-dashboard` — the view of one ledger or activity timeline plus session model controls.
4
4
  *
5
- * It reads a file and renders it; it holds no authority and connects to nothing. The Herdr pane opened by
6
- * `/grants dashboard` runs exactly this program with the ledger path in its environment.
5
+ * Ledger and cost views are read-only. When the owning pi session supplies its private endpoint, model edits are
6
+ * sent back to that process and use the same in-memory mutation and audit path as `/grants models`.
7
7
  */
8
8
  import { realpathSync } from "node:fs";
9
9
  import { readFile } from "node:fs/promises";
@@ -22,6 +22,7 @@ import {
22
22
  type TimelineFilter,
23
23
  } from "./activity-timeline.ts";
24
24
  import { createDashboardDisplayControls } from "./dashboard-display-controls.ts";
25
+ import { dashboardSessionRequest, type DashboardSessionSnapshot } from "./dashboard-session-client.ts";
25
26
  import { adoptLegacyEnvironment, ENV_LEDGER, legacyEnvironmentWarning } from "../kernel/env-names.ts";
26
27
  import { projectLedgerPath } from "../kernel/project-paths.ts";
27
28
 
@@ -29,6 +30,8 @@ export const DASHBOARD_PROTOCOL_VERSION = 1 as const;
29
30
  export const ENV_DASHBOARD_LEDGER = ENV_LEDGER; // one ledger path variable for children and the dashboard (ADR-0076 PR 3b)
30
31
  export const ENV_DASHBOARD_PROTOCOL = "PI_DADDY_DASHBOARD_PROTOCOL";
31
32
  export const ENV_DASHBOARD_KEY = "PI_DADDY_DASHBOARD_KEY";
33
+ export const ENV_DASHBOARD_SESSION_SOCKET = "PI_DADDY_DASHBOARD_SESSION_SOCKET";
34
+ export const ENV_DASHBOARD_SESSION_TOKEN = "PI_DADDY_DASHBOARD_SESSION_TOKEN";
32
35
  export const DASHBOARD_REFRESH_MS = 250;
33
36
 
34
37
  export interface DashboardFrameOptions {
@@ -43,6 +46,7 @@ export interface DashboardFrameOptions {
43
46
  activityDetail?: { taskKey: string; field: "prompt" | "final" };
44
47
  activityAliases?: ActivityTimelineAliases;
45
48
  now?: Date;
49
+ session?: DashboardSessionSnapshot;
46
50
  }
47
51
 
48
52
  function shellQuote(value: string): string {
@@ -139,6 +143,8 @@ export async function dashboardFrame(options: DashboardFrameOptions): Promise<st
139
143
  width: options.width,
140
144
  details: options.details,
141
145
  history: options.history,
146
+ modelRows: options.session?.rows,
147
+ episodeCost: options.session ? { cost: options.session.cost, ceiling: options.session.ceiling } : undefined,
142
148
  });
143
149
  return waiting ? `${rendered}\n\nwaiting for ledger ${ledgerPath}` : rendered;
144
150
  }
@@ -175,6 +181,8 @@ export async function runDashboard(argv = process.argv.slice(2), env: NodeJS.Pro
175
181
  const rawProtocol = env[ENV_DASHBOARD_PROTOCOL]?.trim();
176
182
  const protocol = rawProtocol === undefined || rawProtocol === "" ? undefined : Number(rawProtocol);
177
183
  const key = env[ENV_DASHBOARD_KEY]?.trim();
184
+ const sessionSocket = env[ENV_DASHBOARD_SESSION_SOCKET]?.trim();
185
+ const sessionToken = env[ENV_DASHBOARD_SESSION_TOKEN]?.trim();
178
186
  process.title = `pi-daddy-dashboard${key ? `:${key.slice(0, 12)}` : ""}`;
179
187
 
180
188
  const display = createDashboardDisplayControls(cli.details, true);
@@ -182,6 +190,13 @@ export async function runDashboard(argv = process.argv.slice(2), env: NodeJS.Pro
182
190
  let notice = "";
183
191
  let input: ReturnType<typeof createInterface> | null = null;
184
192
  const draw = async (clear: boolean): Promise<void> => {
193
+ let session: DashboardSessionSnapshot | undefined;
194
+ if (sessionSocket && sessionToken)
195
+ try {
196
+ session = await dashboardSessionRequest(sessionSocket, sessionToken, { action: "get" });
197
+ } catch (error) {
198
+ notice ||= `session controls unavailable: ${error instanceof Error ? error.message : String(error)}`;
199
+ }
185
200
  const view = await dashboardFrame({
186
201
  cwd,
187
202
  ledgerPath,
@@ -190,6 +205,7 @@ export async function runDashboard(argv = process.argv.slice(2), env: NodeJS.Pro
190
205
  width: process.stdout.columns || 80,
191
206
  ...display.state,
192
207
  activityAliases: display.aliases,
208
+ session,
193
209
  });
194
210
  const frame = notice ? `${notice}\n\n${view}` : view;
195
211
  if (frame === previous && !clear) return;
@@ -217,8 +233,20 @@ export async function runDashboard(argv = process.argv.slice(2), env: NodeJS.Pro
217
233
  });
218
234
  };
219
235
  input.on("line", (line) => {
236
+ const trimmed = line.trim();
237
+ if (trimmed.startsWith("m ") && sessionSocket && sessionToken) {
238
+ void dashboardSessionRequest(sessionSocket, sessionToken, { action: "set", edits: trimmed.slice(2).trim() })
239
+ .then(() => {
240
+ notice = "session model defaults updated";
241
+ })
242
+ .catch((error) => {
243
+ notice = `model edit refused: ${error instanceof Error ? error.message : String(error)}`;
244
+ })
245
+ .finally(redraw);
246
+ return;
247
+ }
220
248
  // Rule 8: a line the display controls do not understand is said so, never swallowed.
221
- notice = display.input(line) ? "" : `not a display command: ${JSON.stringify(line.trim())}. ${display.prompt()}`;
249
+ notice = display.input(line) ? "" : `not a display command: ${JSON.stringify(trimmed)}. ${display.prompt()}`;
222
250
  redraw();
223
251
  });
224
252
  const timer = setInterval(redraw, DASHBOARD_REFRESH_MS);
@@ -74,6 +74,7 @@ export interface DashboardHandshakeInput {
74
74
  pluginRoot: string;
75
75
  preferencePath: string;
76
76
  paneStatePath: string;
77
+ sessionEndpoint?: { socketPath: string; token: string };
77
78
  ui: DashboardHandshakeUI;
78
79
  }
79
80
 
@@ -101,6 +102,7 @@ export async function offerDashboardHandshake(input: DashboardHandshakeInput): P
101
102
  statePath: input.paneStatePath,
102
103
  pluginRoot: input.pluginRoot,
103
104
  allowInactive: true,
105
+ sessionEndpoint: input.sessionEndpoint,
104
106
  });
105
107
  input.ui.notify(`pi-daddy dashboard ${opened.kind} in pane ${opened.paneId} without changing focus.`, "info");
106
108
  return "already-installed";
@@ -152,6 +154,7 @@ export async function offerDashboardHandshake(input: DashboardHandshakeInput): P
152
154
  statePath: input.paneStatePath,
153
155
  pluginRoot: input.pluginRoot,
154
156
  allowInactive: true,
157
+ sessionEndpoint: input.sessionEndpoint,
155
158
  });
156
159
  input.ui.notify(`pi-daddy dashboard ${opened.kind} in pane ${opened.paneId} without changing focus.`, "info");
157
160
  return "installed";
@@ -172,6 +175,7 @@ export interface DashboardCommandInput {
172
175
  ledgerPath?: string;
173
176
  pluginRoot: string;
174
177
  paneStatePath: string;
178
+ sessionEndpoint?: { socketPath: string; token: string };
175
179
  }
176
180
 
177
181
  export type DashboardCommandResult =
@@ -211,6 +215,7 @@ export async function openDashboardCommand(input: DashboardCommandInput): Promis
211
215
  cwd: input.cwd,
212
216
  statePath: input.paneStatePath,
213
217
  pluginRoot: input.pluginRoot,
218
+ sessionEndpoint: input.sessionEndpoint,
214
219
  });
215
220
  }
216
221
 
@@ -6,6 +6,8 @@ import {
6
6
  ENV_DASHBOARD_KEY,
7
7
  ENV_DASHBOARD_LEDGER,
8
8
  ENV_DASHBOARD_PROTOCOL,
9
+ ENV_DASHBOARD_SESSION_SOCKET,
10
+ ENV_DASHBOARD_SESSION_TOKEN,
9
11
  DASHBOARD_PROTOCOL_VERSION,
10
12
  } from "./dashboard-cli.ts";
11
13
  import { parseReply, type HerdrExec } from "../executors/herdr-cli.ts";
@@ -291,6 +293,7 @@ export interface DashboardOpenInput {
291
293
  pluginRoot?: string;
292
294
  /** Installation handshake may open an explanatory pane before a ledger exists. */
293
295
  allowInactive?: boolean;
296
+ sessionEndpoint?: { socketPath: string; token: string };
294
297
  }
295
298
  export interface DashboardOpenResult {
296
299
  kind: "opened" | "reused";
@@ -308,7 +311,10 @@ export async function openOrReuseDashboard(input: DashboardOpenInput): Promise<D
308
311
  const ledgerPath = input.ledgerPath.trim() ? resolve(input.cwd, input.ledgerPath) : "";
309
312
  const cwd = resolve(input.cwd);
310
313
  const key = createHash("sha256")
311
- .update(`${input.host.workspaceId}\0${input.host.tabId}\0${ledgerPath}`, "utf8")
314
+ .update(
315
+ `${input.host.workspaceId}\0${input.host.tabId}\0${ledgerPath}\0${input.sessionEndpoint?.socketPath ?? ""}`,
316
+ "utf8",
317
+ )
312
318
  .digest("hex");
313
319
  await mkdir(dirname(input.statePath), { recursive: true });
314
320
 
@@ -368,6 +374,14 @@ export async function openOrReuseDashboard(input: DashboardOpenInput): Promise<D
368
374
  "--cwd",
369
375
  cwd,
370
376
  ...(ledgerPath ? ["--env", `${ENV_DASHBOARD_LEDGER}=${ledgerPath}`] : []),
377
+ ...(input.sessionEndpoint
378
+ ? [
379
+ "--env",
380
+ `${ENV_DASHBOARD_SESSION_SOCKET}=${input.sessionEndpoint.socketPath}`,
381
+ "--env",
382
+ `${ENV_DASHBOARD_SESSION_TOKEN}=${input.sessionEndpoint.token}`,
383
+ ]
384
+ : []),
371
385
  "--env",
372
386
  `${ENV_DASHBOARD_PROTOCOL}=${DASHBOARD_PROTOCOL_VERSION}`,
373
387
  "--env",
@@ -8,8 +8,8 @@ import {
8
8
  type LedgerV3Object as ObjectRecord,
9
9
  } from "../governance/ledger-v3-validation.ts";
10
10
  import { isLedgerCapabilityIdentifier, isLedgerDisplayIdentifier } from "../kernel/ledger-identifiers.ts";
11
+ import { LEDGER_EVENT_KINDS } from "../governance/ledger.ts";
11
12
  import { readRecords } from "../governance/record.ts";
12
- import { isRetiredLedgerEvent } from "../governance/ledger-v3-validation.ts";
13
13
 
14
14
  export type DashboardState =
15
15
  "authorised" | "starting" | "running" | "completed" | "failed" | "refused" | "incomplete" | "historical";
@@ -308,13 +308,20 @@ export function parseDashboardLedger(text: string, options: DashboardProjectionO
308
308
  return;
309
309
  }
310
310
 
311
- const invalid = validateV3(event);
312
- if (invalid) {
313
- corrupt.push({ line, reason: invalid });
311
+ if (event.ledgerVersion !== 3 || !nonEmpty(event.event)) {
312
+ corrupt.push({ line, reason: validateV3(event) ?? "invalid ledger v3 event" });
314
313
  return;
315
314
  }
316
- // Written by an earlier version (check receipts, workflow facts): valid history that is not a tree node.
317
- if (isRetiredLedgerEvent(event)) {
315
+ if (LEDGER_EVENT_KINDS.includes(event.event as (typeof LEDGER_EVENT_KINDS)[number])) {
316
+ const invalid = validateV3(event);
317
+ if (invalid) {
318
+ corrupt.push({ line, reason: invalid });
319
+ return;
320
+ }
321
+ }
322
+ // The dashboard renders only delegation-tree events. Other current, retired, or future additive event kinds
323
+ // remain valid ledger history without acquiring tree identity fields they do not have.
324
+ if (!["capability_decision", "child_lifecycle", "workspace_lease"].includes(event.event)) {
318
325
  orphanEvents += 1;
319
326
  return;
320
327
  }
@@ -8,6 +8,8 @@ export interface DashboardRenderOptions {
8
8
  history?: boolean;
9
9
  completedRoots?: number;
10
10
  completedChildren?: number;
11
+ modelRows?: Array<{ definition: string; model: string; thinking: string; source: string }>;
12
+ episodeCost?: { cost: number; ceiling: number };
11
13
  }
12
14
 
13
15
  const ACTIVE = new Set<DashboardState>(["authorised", "starting", "running"]);
@@ -209,6 +211,15 @@ export function renderDashboard(projection: DashboardProjection, options: Dashbo
209
211
  };
210
212
  const index = treeIndex(projection.nodes);
211
213
  const lines = [paint("PI-DADDY", 1, resolved.color), ""];
214
+ if (options.modelRows) {
215
+ lines.push(paint("SESSION MODELS", 1, resolved.color), "definition model thinking source");
216
+ for (const row of options.modelRows)
217
+ lines.push(`${clean(row.definition)} ${clean(row.model)} ${clean(row.thinking)} ${clean(row.source)}`);
218
+ lines.push("edit: m <definition> <provider:model> <thinking> | m all <provider:model> <thinking>");
219
+ if (options.episodeCost)
220
+ lines.push(`episode cost $${options.episodeCost.cost} / $${options.episodeCost.ceiling} ceiling`);
221
+ lines.push("");
222
+ }
212
223
 
213
224
  const renderRoots = (roots: DashboardNode[]): void => {
214
225
  const selected = selectedRoots(roots, index, resolved.completedRoots);
@@ -0,0 +1,35 @@
1
+ import { createConnection } from "node:net";
2
+
3
+ export interface DashboardSessionSnapshot {
4
+ rows: Array<{ definition: string; model: string; thinking: string; source: string }>;
5
+ cost: number;
6
+ ceiling: number;
7
+ }
8
+
9
+ export async function dashboardSessionRequest(
10
+ socketPath: string,
11
+ token: string,
12
+ request: { action: "get" } | { action: "set"; edits: string },
13
+ ): Promise<DashboardSessionSnapshot> {
14
+ return new Promise((resolve, reject) => {
15
+ const socket = createConnection(socketPath);
16
+ let output = "";
17
+ socket.setEncoding("utf8");
18
+ socket.setTimeout(2_000, () => socket.destroy(new Error("dashboard session request timed out")));
19
+ socket.once("error", reject);
20
+ socket.on("data", (chunk) => {
21
+ output += chunk;
22
+ if (output.length > 64 * 1024) socket.destroy(new Error("dashboard session response too large"));
23
+ });
24
+ socket.once("end", () => {
25
+ try {
26
+ const parsed = JSON.parse(output) as DashboardSessionSnapshot & { ok?: boolean; error?: string };
27
+ if (!parsed.ok) throw new Error(parsed.error ?? "dashboard session request failed");
28
+ resolve(parsed);
29
+ } catch (error) {
30
+ reject(error);
31
+ }
32
+ });
33
+ socket.once("connect", () => socket.write(`${JSON.stringify({ ...request, token })}\n`));
34
+ });
35
+ }