pi-daddy 0.48.3 → 0.49.1

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 (68) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +55 -0
  3. package/dist/cli.d.ts +3 -1
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/cli.js +18 -0
  6. package/dist/cli.js.map +1 -1
  7. package/dist/governance/retention-contract.js +3 -3
  8. package/dist/governance/retention-contract.js.map +1 -1
  9. package/dist/governance/retention-validator.d.ts +7 -0
  10. package/dist/governance/retention-validator.d.ts.map +1 -0
  11. package/dist/governance/retention-validator.js +100 -0
  12. package/dist/governance/retention-validator.js.map +1 -0
  13. package/dist/index.d.ts +1 -0
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +1 -0
  16. package/dist/index.js.map +1 -1
  17. package/dist/kernel/selected-definitions.d.ts.map +1 -1
  18. package/dist/kernel/selected-definitions.js +6 -3
  19. package/dist/kernel/selected-definitions.js.map +1 -1
  20. package/dist/products/dashboard-cli.d.ts.map +1 -1
  21. package/dist/products/dashboard-cli.js +46 -1
  22. package/dist/products/dashboard-cli.js.map +1 -1
  23. package/dist/products/dashboard-display-controls.d.ts +1 -1
  24. package/dist/products/dashboard-display-controls.d.ts.map +1 -1
  25. package/dist/products/dashboard-display-controls.js +11 -3
  26. package/dist/products/dashboard-display-controls.js.map +1 -1
  27. package/dist/products/dashboard-screen.d.ts +3 -1
  28. package/dist/products/dashboard-screen.d.ts.map +1 -1
  29. package/dist/products/dashboard-screen.js +21 -8
  30. package/dist/products/dashboard-screen.js.map +1 -1
  31. package/dist/products/dashboard-session-client.d.ts +9 -0
  32. package/dist/products/dashboard-session-client.d.ts.map +1 -1
  33. package/dist/products/dashboard-session-client.js.map +1 -1
  34. package/dist/products/dashboard-settings.d.ts +28 -0
  35. package/dist/products/dashboard-settings.d.ts.map +1 -0
  36. package/dist/products/dashboard-settings.js +42 -0
  37. package/dist/products/dashboard-settings.js.map +1 -0
  38. package/dist/products/diagnostic-export.d.ts +23 -0
  39. package/dist/products/diagnostic-export.d.ts.map +1 -0
  40. package/dist/products/diagnostic-export.js +264 -0
  41. package/dist/products/diagnostic-export.js.map +1 -0
  42. package/dist/products/diagnostic-filter.d.ts +7 -0
  43. package/dist/products/diagnostic-filter.d.ts.map +1 -0
  44. package/dist/products/diagnostic-filter.js +135 -0
  45. package/dist/products/diagnostic-filter.js.map +1 -0
  46. package/extensions/dashboard-session-server.ts +17 -2
  47. package/extensions/dashboard-settings.ts +149 -0
  48. package/extensions/delegation-operations.ts +81 -2
  49. package/extensions/execute-child.ts +9 -11
  50. package/extensions/execution-controls.ts +7 -2
  51. package/extensions/grants-command.ts +3 -1
  52. package/extensions/grants.ts +3 -0
  53. package/extensions/reload-environment.ts +3 -0
  54. package/extensions/run-delegation.ts +3 -1
  55. package/extensions/runtime-snapshot.ts +9 -1
  56. package/package.json +1 -1
  57. package/src/cli.ts +24 -1
  58. package/src/governance/retention-contract.ts +3 -3
  59. package/src/governance/retention-validator.ts +110 -0
  60. package/src/index.ts +2 -0
  61. package/src/kernel/selected-definitions.ts +5 -3
  62. package/src/products/dashboard-cli.ts +38 -1
  63. package/src/products/dashboard-display-controls.ts +10 -4
  64. package/src/products/dashboard-screen.ts +21 -10
  65. package/src/products/dashboard-session-client.ts +7 -1
  66. package/src/products/dashboard-settings.ts +72 -0
  67. package/src/products/diagnostic-export.ts +288 -0
  68. package/src/products/diagnostic-filter.ts +140 -0
@@ -4,7 +4,7 @@ import { realpath } from "node:fs/promises";
4
4
  import { Type } from "typebox";
5
5
  import { shouldSeekApproval } from "../src/kernel/approval.ts";
6
6
  import { planDelegation } from "../src/kernel/delegate.ts";
7
- import type { OperationClaim } from "../src/kernel/dispatch-operation.ts";
7
+ import type { OperationClaim, OperationReference } from "../src/kernel/dispatch-operation.ts";
8
8
  import { validOperationId } from "../src/kernel/dispatch-operation.ts";
9
9
  import { GovernanceRefusal } from "../src/kernel/refusals.ts";
10
10
  import { assertDefinitionIdentity } from "./definition-describe.ts";
@@ -15,6 +15,79 @@ import type { ChildSpec, DelegationToolContext } from "./run-delegation.ts";
15
15
  import type { ExecutionOccurrenceIds } from "./execution-occurrence.ts";
16
16
  import type { DelegationOutcome } from "./execute-child.ts";
17
17
 
18
+ // Short-lived exact finals for explicit workflow retention. The operation registry/socket
19
+ // remains identity-only; losing this observation never changes the original child result.
20
+ // The native-owner lifecycle survives extension reloads, which create a new GrantsSession.
21
+ const FINAL_BYTES = 4 * 1024 * 1024,
22
+ FINAL_ENTRIES = 128;
23
+ type Final = Extract<NonNullable<DelegationOutcome["final"]>, { state: "complete" }>;
24
+ export interface OperationFinalStore {
25
+ lifecycle: object;
26
+ owner: string;
27
+ bytes: number;
28
+ entries: Map<string, { executionId: string; final: Final }>;
29
+ }
30
+ export function bindOperationFinals(session: GrantsSession, sessionId: string, cwd: string) {
31
+ const owner = JSON.stringify([sessionId, cwd]),
32
+ old = session.reloadLifecycle.operationFinals;
33
+ if (!old || old.lifecycle !== session.reloadLifecycle || old.owner !== owner)
34
+ session.reloadLifecycle.operationFinals = {
35
+ lifecycle: session.reloadLifecycle,
36
+ owner,
37
+ bytes: 0,
38
+ entries: new Map(),
39
+ };
40
+ }
41
+ /** Capture before admitting work; a late child cannot write a replacement session's store. */
42
+ export function operationFinalScope(session: GrantsSession): object | undefined {
43
+ return session.reloadLifecycle?.operationFinals;
44
+ }
45
+ export function operationFinal(session: GrantsSession, operation: OperationReference) {
46
+ const store = session.reloadLifecycle?.operationFinals,
47
+ item = store?.entries.get(operation.operationId);
48
+ if (!store || store.lifecycle !== session.reloadLifecycle || !item || item.executionId !== operation.executionId)
49
+ return {
50
+ state: "unavailable" as const,
51
+ reason:
52
+ "exact native final was not retained, expired from the 4 MiB session buffer, or belongs to an earlier session",
53
+ };
54
+ const observed = operation.runtime?.final,
55
+ final = item.final;
56
+ if (
57
+ operation.state !== "settled" ||
58
+ observed?.state !== "complete" ||
59
+ ["sessionId", "messageId", "leafId", "sha256"].some(
60
+ (key) => observed[key as keyof typeof observed] !== final[key as keyof Final],
61
+ )
62
+ )
63
+ return { state: "unavailable" as const, reason: "native final identity no longer matches the operation" };
64
+ return { ...final };
65
+ }
66
+ function retainOperationFinal(
67
+ session: GrantsSession,
68
+ claim: OperationClaim,
69
+ result: DelegationOutcome,
70
+ scope: object | undefined,
71
+ ) {
72
+ const store = session.reloadLifecycle?.operationFinals,
73
+ final = result.final;
74
+ if (!store || store !== scope || store.lifecycle !== session.reloadLifecycle || final?.state !== "complete") return;
75
+ const bytes = Buffer.byteLength(final.text);
76
+ if (!bytes || bytes > FINAL_BYTES || createHash("sha256").update(final.text).digest("hex") !== final.sha256) return;
77
+ const old = store.entries.get(claim.operation.operationId);
78
+ if (old) {
79
+ store.bytes -= Buffer.byteLength(old.final.text);
80
+ store.entries.delete(claim.operation.operationId);
81
+ }
82
+ while (store.bytes + bytes > FINAL_BYTES || store.entries.size >= FINAL_ENTRIES) {
83
+ const first = store.entries.entries().next().value!;
84
+ store.bytes -= Buffer.byteLength(first[1].final.text);
85
+ store.entries.delete(first[0]);
86
+ }
87
+ store.entries.set(claim.operation.operationId, { executionId: claim.operation.executionId, final: { ...final } });
88
+ store.bytes += bytes;
89
+ }
90
+
18
91
  export const operationIdShape = () =>
19
92
  Type.Optional(
20
93
  Type.String({
@@ -121,7 +194,12 @@ export function existingOperationOutcome(claim: OperationClaim, depth: number):
121
194
  operation: { ...claim.operation, reused: true },
122
195
  };
123
196
  }
124
- export async function finishDelegationOperation(claim: OperationClaim, result: DelegationOutcome) {
197
+ export async function finishDelegationOperation(
198
+ claim: OperationClaim,
199
+ result: DelegationOutcome,
200
+ session: GrantsSession,
201
+ finalScope: object | undefined,
202
+ ) {
125
203
  const cleanup = result.cleanup;
126
204
  const state =
127
205
  cleanup?.state === "settled"
@@ -161,6 +239,7 @@ export async function finishDelegationOperation(claim: OperationClaim, result: D
161
239
  }
162
240
  : {}),
163
241
  });
242
+ retainOperationFinal(session, claim, result, finalScope);
164
243
  result.operation = {
165
244
  ...claim.operation,
166
245
  state,
@@ -1,3 +1,4 @@
1
+ import { childExecutionLimits } from "./dashboard-settings.ts";
1
2
  import { realpath } from "node:fs/promises";
2
3
  import { BoundedReadCleanupError } from "../src/kernel/bounded-read.ts";
3
4
  import { retainDiscoveryCleanupFailure } from "./session.ts";
@@ -16,13 +17,7 @@ import { appendLedgerEvent, buildChildLifecycleEvent } from "../src/governance/l
16
17
  import { hasFinalizerError } from "../src/governance/finalization.ts";
17
18
  import { mergeChildEnv } from "../src/kernel/propagation.ts";
18
19
  import type { Capability } from "../src/kernel/resolve.ts";
19
- import {
20
- DEFAULT_KILL_GRACE_MS,
21
- ENV_CHILD_IDLE_TIMEOUT,
22
- ENV_CHILD_TIMEOUT,
23
- idleTimeoutFromEnv,
24
- timeoutFromEnv,
25
- } from "../src/kernel/run-child.ts";
20
+ import { DEFAULT_KILL_GRACE_MS, ENV_CHILD_IDLE_TIMEOUT, ENV_CHILD_TIMEOUT } from "../src/kernel/run-child.ts";
26
21
  import { activitySessionFor } from "../src/executors/activity-session.ts";
27
22
  import { processTreeActivity } from "../src/executors/process-activity.ts";
28
23
  import { resolveWorkspace } from "../src/executors/herdr-cli.ts";
@@ -147,6 +142,7 @@ export async function executePlannedChild(input: ChildExecutionInput): Promise<D
147
142
  const runtime = input.session.reloadLifecycle?.runtimeSettlement;
148
143
  let registered = false;
149
144
  try {
145
+ const limits = childExecutionLimits(input.session);
150
146
  input = { ...input, cwd: await realpath(input.preparedWorkspace?.workspace.root ?? input.cwd) };
151
147
  runtime?.begin(input.executionId);
152
148
  registered = Boolean(runtime);
@@ -157,6 +153,7 @@ export async function executePlannedChild(input: ChildExecutionInput): Promise<D
157
153
  input.onExecutorEntry?.();
158
154
  },
159
155
  runtime,
156
+ limits,
160
157
  );
161
158
  try {
162
159
  await runtime?.finish(
@@ -220,7 +217,8 @@ export async function executePlannedChild(input: ChildExecutionInput): Promise<D
220
217
  async function executePreparedChild(
221
218
  input: ChildExecutionInput,
222
219
  onExecutorEntry: () => void,
223
- runtime?: import("../src/governance/runtime-settlement.ts").RuntimeSettlement,
220
+ runtime: import("../src/governance/runtime-settlement.ts").RuntimeSettlement | undefined,
221
+ limits: ReturnType<typeof childExecutionLimits>,
224
222
  ): Promise<DelegationOutcome> {
225
223
  const { session, plan, childId, executionId, parentExecutionId, preparedWorkspace, signal, onProgress } = input;
226
224
  const ledgerPath = session.ledgerPath;
@@ -269,10 +267,10 @@ async function executePreparedChild(
269
267
  );
270
268
  activityStarted = true;
271
269
  } catch {}
272
- const configuredTimeoutMs = timeoutFromEnv(process.env[ENV_CHILD_TIMEOUT]);
270
+ const configuredTimeoutMs = limits.wallMs;
273
271
  // PR 3e: the working bound is inactivity; `deadlineAt` below is the runaway ceiling. Every child gets a pi session
274
272
  // file so the parent can see it working when its stdout is quiet (pi appends each message and tool result to it).
275
- const configuredIdleMs = idleTimeoutFromEnv(process.env[ENV_CHILD_IDLE_TIMEOUT]);
273
+ const configuredIdleMs = limits.idleMs;
276
274
  const activitySession = await activitySessionFor(plan.args, executionId);
277
275
  // Disposed on every path (review finding: it leaked on every throw), except when the operator keeps a Herdr pane,
278
276
  // where the interactive pi in that pane is still alive and still appending to this file.
@@ -378,7 +376,7 @@ async function executePreparedChild(
378
376
  workspaceId: preparedWorkspace?.workspace.workspaceId ?? null,
379
377
  });
380
378
  const sessionFlag = plan.args.indexOf("--session");
381
- if (sessionFlag >= 0) retention.observeSession({ source: "pi-session-file", value: plan.args[sessionFlag + 1] });
379
+ // Pi creates the file during execution; observe its exact final below, not a guaranteed pre-spawn miss.
382
380
  let releaseReason = "failed";
383
381
  let retainWriterLease = false;
384
382
  let terminalAttempted = false;
@@ -1,3 +1,4 @@
1
+ import type { childExecutionLimits } from "./dashboard-settings.ts";
1
2
  /** Operator-facing execution settings; display never changes admission, deadlines or retention. */
2
3
  import { timeoutFromEnv, idleTimeoutFromEnv } from "../src/kernel/run-child.ts";
3
4
  import { timeoutMsFromEnv } from "../src/governance/approval-prompt.ts";
@@ -11,6 +12,7 @@ import {
11
12
  export function renderExecutionControls(
12
13
  nativeSessionRoot: string | undefined,
13
14
  env: NodeJS.ProcessEnv = process.env,
15
+ limits?: ReturnType<typeof childExecutionLimits>,
14
16
  ): string[] {
15
17
  let approval: string;
16
18
  try {
@@ -20,9 +22,12 @@ export function renderExecutionControls(
20
22
  approval = `${ENV_APPROVAL_TIMEOUT} invalid; a needed new prompt will refuse`;
21
23
  }
22
24
  const source = (key: string) => (env[key] === undefined ? "default" : `from ${key}; zero/invalid uses default`);
25
+ const wallSource = limits?.wallSource === "session" ? "session override; future children" : source(ENV_CHILD_TIMEOUT);
26
+ const idleSource =
27
+ limits?.idleSource === "session" ? "session override; future children" : source(ENV_CHILD_IDLE_TIMEOUT);
23
28
  return [
24
- ` child wall ${timeoutFromEnv(env[ENV_CHILD_TIMEOUT]) / 1000}s (${source(ENV_CHILD_TIMEOUT)})`,
25
- ` child idle ${idleTimeoutFromEnv(env[ENV_CHILD_IDLE_TIMEOUT]) / 1000}s (${source(ENV_CHILD_IDLE_TIMEOUT)})`,
29
+ ` child wall ${(limits?.wallMs ?? timeoutFromEnv(env[ENV_CHILD_TIMEOUT])) / 1000}s (${wallSource})`,
30
+ ` child idle ${(limits?.idleMs ?? idleTimeoutFromEnv(env[ENV_CHILD_IDLE_TIMEOUT])) / 1000}s (${idleSource})`,
26
31
  " process safety controls; no time/token budget is added to the model prompt",
27
32
  ` approval ${approval}`,
28
33
  ` transcripts ${
@@ -1,3 +1,4 @@
1
+ import type { childExecutionLimits } from "./dashboard-settings.ts";
1
2
  /**
2
3
  * The `/grants` command — session status, approvals, and ledger integrity.
3
4
  *
@@ -28,6 +29,7 @@ export interface GrantsCommandContext {
28
29
  governed: boolean;
29
30
  /** Host opt-in captured by this session, not a later environment reread. */
30
31
  nativeSessionRoot?: string;
32
+ executionLimits?: ReturnType<typeof childExecutionLimits>;
31
33
  autoModeStatus?: string;
32
34
  /** Explicit owner mutation; the command parser itself holds no permission authority. */
33
35
  changeAutoMode?: (enabled: boolean) => Promise<string>;
@@ -439,7 +441,7 @@ export const grantsCommand = {
439
441
  // ADR-0031, and its probe is only defensible because this line exists. Placed next to `holding` so the
440
442
  // two facts about what a spawn will be sit together.
441
443
  ` executor ${executor.disclosure}`,
442
- ...renderExecutionControls(ctx.grants.nativeSessionRoot),
444
+ ...renderExecutionControls(ctx.grants.nativeSessionRoot, process.env, ctx.grants.executionLimits),
443
445
  ` Auto ${ctx.grants.autoModeStatus ?? "OFF (default)"}`,
444
446
  ` depth ${depth} of max ${maxDepth}${maxDepth <= 0 ? " (spawning disabled)" : ""}`,
445
447
  ` ledger ${ledgerPath || "(not recording — set PI_DADDY_LEDGER)"}`,
@@ -1,3 +1,4 @@
1
+ import { registerDashboardSettings, childExecutionLimits } from "./dashboard-settings.ts";
1
2
  import { bindEcosystemVersions } from "./ecosystem-versions.ts";
2
3
  import {
3
4
  initializeSessionAutoMode,
@@ -90,6 +91,7 @@ export default function (pi: ExtensionAPI, trustedSession?: GrantsSession) {
90
91
  session.currentThinking = typeof pi.getThinkingLevel === "function" ? () => pi.getThinkingLevel() : undefined;
91
92
  registerActivityTimeline(pi, session);
92
93
  const runtimeSnapshot = registerRuntimeSnapshot(pi, session);
94
+ registerDashboardSettings(pi, session);
93
95
  const dashboardPluginRoot = fileURLToPath(new URL("../herdr-plugin/", import.meta.url));
94
96
  const dashboardPaths = defaultDashboardPaths(agentDir());
95
97
  // Definitions are registered only after owner-bound session_start. Until then there is no delegation
@@ -481,6 +483,7 @@ export default function (pi: ExtensionAPI, trustedSession?: GrantsSession) {
481
483
  }
482
484
  : {}),
483
485
  nativeSessionRoot: session.nativeSessionRoot,
486
+ executionLimits: childExecutionLimits(session),
484
487
  changeAutoMode: async (enabled: boolean) => {
485
488
  const { auto } = await setAutoApproval(session, enabled, "command");
486
489
  return `${auto.enabled ? "ON" : "OFF"} (${auto.source}; current session permissions)`;
@@ -5,6 +5,8 @@ import type { BoundedReadCleanupError } from "../src/kernel/bounded-read.ts";
5
5
 
6
6
  /** The root baseline and latest child publication for one real Pi session owner. */
7
7
  export interface ReloadLifecycle {
8
+ operationFinals?: import("./delegation-operations.ts").OperationFinalStore;
9
+ executionSettings?: { nativeSessionId: string; wallMs?: number; idleMs?: number };
8
10
  autoMode?: import("./session-auto-mode.ts").SessionAutoLifecycle;
9
11
  runtimeSnapshotUnsubscribe?: () => void;
10
12
  runtimeSnapshotGeneration?: object;
@@ -115,6 +117,7 @@ export function bindReloadLifecycle(
115
117
  existing = {
116
118
  root: current,
117
119
  autoMode: existing.autoMode,
120
+ executionSettings: existing.executionSettings,
118
121
  activityRootId: existing.activityRootId,
119
122
  episodeId: existing.episodeId,
120
123
  capacity: existing.capacity,
@@ -2,6 +2,7 @@ import {
2
2
  claimDelegationOperation,
3
3
  existingOperationOutcome,
4
4
  finishDelegationOperation,
5
+ operationFinalScope,
5
6
  } from "./delegation-operations.ts";
6
7
  import type { OperationClaim } from "../src/kernel/dispatch-operation.ts";
7
8
  import { reconcileDelegationCapacity, reserveDelegationCapacity } from "./session-capacity.ts";
@@ -552,6 +553,7 @@ export async function runOneDelegation(
552
553
  ...args: Parameters<typeof runOneDelegationImplementation>
553
554
  ): Promise<DelegationOutcome> {
554
555
  const [session, spec, ids, , ctx, signal] = args;
556
+ const finalScope = operationFinalScope(session);
555
557
  const options = args[6] ?? {};
556
558
  const claim = options.operationClaim ?? (await claimDelegationOperation(session, spec, ids, ctx, signal));
557
559
  if (claim?.reused) {
@@ -571,7 +573,7 @@ export async function runOneDelegation(
571
573
  const result = await runOneDelegationImplementation(...args);
572
574
  if (claim) {
573
575
  try {
574
- await finishDelegationOperation(claim, result);
576
+ await finishDelegationOperation(claim, result, session, finalScope);
575
577
  } catch (error) {
576
578
  // A post-execution observation failure must not erase the native final or settlement receipt.
577
579
  result.control = "failed";
@@ -5,6 +5,7 @@ import { openRuntimeSettlement, settlementHash } from "../src/governance/runtime
5
5
  import { reconcileDelegationCapacity } from "./session-capacity.ts";
6
6
  import { agentDir } from "../src/kernel/project-paths.ts";
7
7
  import type { GrantsSession } from "./session.ts";
8
+ import { bindOperationFinals, operationFinal } from "./delegation-operations.ts";
8
9
  export const OPERATION_STATUS_EVENT = "pi-daddy:operation-status:v1";
9
10
  export const RUNTIME_SNAPSHOT_EVENT = "pi-daddy:runtime-snapshot:v1";
10
11
  export function registerRuntimeSnapshot(
@@ -99,6 +100,7 @@ export function registerRuntimeSnapshot(
99
100
  sessionId?: string;
100
101
  cwd?: string;
101
102
  operationId?: string;
103
+ includeFinal?: boolean;
102
104
  reply?: (value: unknown) => void;
103
105
  };
104
106
  if (
@@ -139,7 +141,12 @@ export function registerRuntimeSnapshot(
139
141
  lifecycle.runtimeSnapshotGeneration !== generation
140
142
  )
141
143
  throw Error("operation owner changed during read");
142
- request.reply!({ ...envelope, qualified: true, operation });
144
+ request.reply!({
145
+ ...envelope,
146
+ qualified: true,
147
+ operation,
148
+ ...(request.includeFinal === true && operation ? { final: operationFinal(session, operation) } : {}),
149
+ });
143
150
  } catch (error) {
144
151
  request.reply!({ ...envelope, qualified: false, operation: null, reason: String(error) });
145
152
  }
@@ -172,6 +179,7 @@ export function registerRuntimeSnapshot(
172
179
  }
173
180
  if (sequence !== bindSequence || session.reloadLifecycle !== lifecycle) return;
174
181
  lifecycle.runtimeSettlement = runtime;
182
+ bindOperationFinals(session, sessionId, cwd);
175
183
  bound = { sessionId, cwd, lifecycle: session.reloadLifecycle };
176
184
  } catch (error) {
177
185
  if (sequence === bindSequence && session.reloadLifecycle === lifecycle) problem = String(error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-daddy",
3
- "version": "0.48.3",
3
+ "version": "0.49.1",
4
4
  "description": "Capability governance for pi sub-agents: spawn Agent Skills (SKILL.md) definitions whose allowed-tools becomes a grant that can only narrow going down a delegation tree, enforced by pi's own --tools allowlist, with an append-only ledger.",
5
5
  "keywords": [
6
6
  "pi-package",
package/src/cli.ts CHANGED
@@ -50,6 +50,7 @@ Usage:
50
50
  pi-daddy report [--since <date>] [--definition <name>] [--model <id>]
51
51
  [--group-by definition|model|thinking] [--json]
52
52
  pi-daddy outcomes append changed episode outcomes from Git and CI
53
+ pi-daddy diagnostics export <selection.json> <new-directory> private filtered local evidence
53
54
  pi-daddy --help | --version
54
55
 
55
56
  init references skills already enabled in Pi at their installed or local paths. Legacy unregistered npm
@@ -61,8 +62,11 @@ stays unspawnable. Capabilities that can change your machine
61
62
  It never rewrites ${SETTINGS_REL} — delete that file if you want it regenerated.`;
62
63
 
63
64
  export interface ParsedArgs {
64
- command: "init" | "ledger-repair" | "ledger-import" | "report" | "outcomes" | "help" | "version";
65
+ command:
66
+ "init" | "ledger-repair" | "ledger-import" | "report" | "outcomes" | "diagnostics-export" | "help" | "version";
65
67
  importTarget?: string;
68
+ selectionPath?: string;
69
+ exportDirectory?: string;
66
70
  /** `ledger repair <path>`: the ledger file; `yes` applies, otherwise preview only. */
67
71
  ledgerPath?: string;
68
72
  yes?: boolean;
@@ -91,6 +95,19 @@ export function parseArgs(argv: string[]): ParsedArgs {
91
95
  if (args.includes("--version") || args.includes("-v")) return { command: "version", force: false, errors: [] };
92
96
 
93
97
  const [command, ...tail] = args;
98
+ if (command === "diagnostics") {
99
+ const [verb, selectionPath, exportDirectory, ...extra] = tail;
100
+ if (
101
+ verb !== "export" ||
102
+ !selectionPath ||
103
+ !exportDirectory ||
104
+ selectionPath.startsWith("-") ||
105
+ exportDirectory.startsWith("-") ||
106
+ extra.length
107
+ )
108
+ return { command: "help", force: false, errors: ["diagnostics needs: export <selection.json> <new-directory>"] };
109
+ return { command: "diagnostics-export", force: false, errors: [], selectionPath, exportDirectory };
110
+ }
94
111
  if (command === "ledger") {
95
112
  // ADR-0076 PR 3d: `ledger repair <path> [--yes]` previews, --yes truncates; `ledger import <source> <target>`.
96
113
  const [verb, target, ...flags] = tail;
@@ -388,6 +405,12 @@ export async function main(
388
405
  console.log(USAGE);
389
406
  return 0;
390
407
  }
408
+ if (parsed.command === "diagnostics-export") {
409
+ const { exportDiagnostics } = await import("./products/diagnostic-export.ts");
410
+ const result = await exportDiagnostics(parsed.selectionPath!, parsed.exportDirectory!);
411
+ console.log(JSON.stringify(result));
412
+ return result.missing ? 1 : 0;
413
+ }
391
414
  if (parsed.command === "outcomes") {
392
415
  const cwd = process.cwd();
393
416
  const result = await updateEpisodeOutcomes({
@@ -1,4 +1,4 @@
1
- import { Compile } from "typebox/compile";
1
+ import { compileRetentionShape } from "./retention-validator.ts";
2
2
  import { parseRetentionJson } from "./retention-json.ts";
3
3
  import type { ExecutionRetentionManifest } from "./execution-retention.ts";
4
4
 
@@ -121,7 +121,7 @@ export const RETENTION_SCHEMA = freeze({
121
121
  },
122
122
  ],
123
123
  });
124
- const validator = Compile(RETENTION_SCHEMA);
124
+ const validateShape = compileRetentionShape(RETENTION_SCHEMA);
125
125
  function freeze<T>(value: T): T {
126
126
  if (value && typeof value === "object") {
127
127
  for (const child of Object.values(value)) freeze(child);
@@ -166,7 +166,7 @@ export function buildExecutionRetentionManifest(value: unknown): ExecutionRetent
166
166
  }
167
167
  };
168
168
  inspect(value, 0);
169
- if (!validator.Check(value)) throw new TypeError("invalid execution-retention 2.x manifest");
169
+ if (!validateShape(value)) throw new TypeError("invalid execution-retention 2.x manifest");
170
170
  const m = JSON.parse(JSON.stringify(value)) as ExecutionRetentionManifest;
171
171
  if (
172
172
  m.native.sessionId !== m.nativeSession.sessionId ||
@@ -0,0 +1,110 @@
1
+ /** Dependency-free evaluator for the fixed retention schema; unsupported vocabulary fails at construction. */
2
+ type Check = (value: unknown) => boolean;
3
+ type Schema = Record<string, any>;
4
+ const object = (value: unknown): value is Record<string, unknown> =>
5
+ value !== null && typeof value === "object" && !Array.isArray(value);
6
+ const keywords = new Set([
7
+ "$schema",
8
+ "$id",
9
+ "type",
10
+ "const",
11
+ "enum",
12
+ "properties",
13
+ "required",
14
+ "additionalProperties",
15
+ "minLength",
16
+ "maxLength",
17
+ "pattern",
18
+ "minimum",
19
+ "maximum",
20
+ "items",
21
+ "maxItems",
22
+ "uniqueItems",
23
+ "anyOf",
24
+ "oneOf",
25
+ "allOf",
26
+ "not",
27
+ "if",
28
+ "then",
29
+ "else",
30
+ ]);
31
+
32
+ /** This is internal retention vocabulary, not a general JSON Schema implementation. */
33
+ export function compileRetentionShape(schema: Schema): Check {
34
+ for (const key of Object.keys(schema))
35
+ if (!keywords.has(key)) throw new TypeError(`unsupported retention schema keyword: ${key}`);
36
+ const checks: Check[] = [];
37
+ if (schema.type !== undefined) {
38
+ const types: Record<string, Check> = {
39
+ object,
40
+ array: Array.isArray,
41
+ string: (value) => typeof value === "string",
42
+ boolean: (value) => typeof value === "boolean",
43
+ integer: Number.isInteger,
44
+ null: (value) => value === null,
45
+ };
46
+ if (!Object.hasOwn(types, schema.type)) throw new TypeError("unsupported retention schema type");
47
+ checks.push(types[schema.type]);
48
+ }
49
+ if (Object.hasOwn(schema, "const")) checks.push((value) => value === schema.const);
50
+ if (schema.enum) checks.push((value) => schema.enum.includes(value));
51
+ if (schema.properties) {
52
+ const properties = Object.entries(schema.properties).map(
53
+ ([key, shape]) => [key, compileRetentionShape(shape as Schema)] as const,
54
+ );
55
+ checks.push(
56
+ (value) => !object(value) || properties.every(([key, check]) => !Object.hasOwn(value, key) || check(value[key])),
57
+ );
58
+ }
59
+ if (schema.required)
60
+ checks.push((value) => !object(value) || schema.required.every((key: string) => Object.hasOwn(value, key)));
61
+ if (schema.additionalProperties !== undefined) {
62
+ if (schema.additionalProperties !== false) throw new TypeError("unsupported retention additionalProperties");
63
+ checks.push(
64
+ (value) => !object(value) || Object.keys(value).every((key) => Object.hasOwn(schema.properties ?? {}, key)),
65
+ );
66
+ }
67
+ if (schema.pattern) {
68
+ const pattern = new RegExp(schema.pattern);
69
+ checks.push((value) => typeof value !== "string" || pattern.test(value));
70
+ }
71
+ if (schema.minLength !== undefined || schema.maxLength !== undefined)
72
+ checks.push(
73
+ (value) =>
74
+ typeof value !== "string" ||
75
+ ([...value].length >= (schema.minLength ?? 0) && [...value].length <= (schema.maxLength ?? Infinity)),
76
+ );
77
+ if (schema.minimum !== undefined || schema.maximum !== undefined)
78
+ checks.push(
79
+ (value) =>
80
+ typeof value !== "number" || (value >= (schema.minimum ?? -Infinity) && value <= (schema.maximum ?? Infinity)),
81
+ );
82
+ if (schema.items) {
83
+ const check = compileRetentionShape(schema.items);
84
+ checks.push((value) => !Array.isArray(value) || value.every(check));
85
+ }
86
+ if (schema.maxItems !== undefined) checks.push((value) => !Array.isArray(value) || value.length <= schema.maxItems);
87
+ if (schema.uniqueItems) checks.push((value) => !Array.isArray(value) || new Set(value).size === value.length);
88
+ for (const kind of ["anyOf", "oneOf", "allOf"] as const) {
89
+ if (!schema[kind]) continue;
90
+ const alternatives = schema[kind].map(compileRetentionShape) as Check[];
91
+ checks.push((value) =>
92
+ kind === "anyOf"
93
+ ? alternatives.some((check) => check(value))
94
+ : kind === "allOf"
95
+ ? alternatives.every((check) => check(value))
96
+ : alternatives.filter((check) => check(value)).length === 1,
97
+ );
98
+ }
99
+ if (schema.not) {
100
+ const check = compileRetentionShape(schema.not);
101
+ checks.push((value) => !check(value));
102
+ }
103
+ if (schema.if) {
104
+ const condition = compileRetentionShape(schema.if);
105
+ const yes = schema.then ? compileRetentionShape(schema.then) : () => true;
106
+ const no = schema.else ? compileRetentionShape(schema.else) : () => true;
107
+ checks.push((value) => (condition(value) ? yes(value) : no(value)));
108
+ }
109
+ return (value) => checks.every((check) => check(value));
110
+ }
package/src/index.ts CHANGED
@@ -191,3 +191,5 @@ export {
191
191
  type WorkspacePins,
192
192
  type ParsedPin,
193
193
  } from "./kernel/workspace-pin.ts";
194
+
195
+ export { exportDiagnostics, parseDiagnosticSelection, type DiagnosticSelection } from "./products/diagnostic-export.ts";
@@ -12,7 +12,8 @@ export interface SelectedCommand {
12
12
  name: string;
13
13
  sourceInfo?: { path?: string };
14
14
  }
15
- const phases = ["plan", "build", "review", "debug", "investigate"];
15
+ const requiredPhases = ["plan", "build", "review", "debug", "investigate"];
16
+ const phases = [...requiredPhases, "test-review"];
16
17
  const hash = (value: string) => createHash("sha256").update(value, "utf8").digest("hex");
17
18
  const object = (x: unknown): x is Record<string, unknown> => !!x && typeof x === "object" && !Array.isArray(x);
18
19
  const exact = (x: Record<string, unknown>, keys: string[]) =>
@@ -57,10 +58,10 @@ async function principalBinding(
57
58
  manifest.version !== 1 ||
58
59
  manifest.package !== "principal-pi-skills" ||
59
60
  !object(manifest.bindings) ||
60
- !exact(manifest.bindings, phases)
61
+ !exact(manifest.bindings, Object.hasOwn(manifest.bindings, "test-review") ? phases : requiredPhases)
61
62
  )
62
63
  throw Error("invalid Principal binding manifest");
63
- for (const name of phases) {
64
+ for (const name of Object.keys(manifest.bindings)) {
64
65
  const row = manifest.bindings[name];
65
66
  if (
66
67
  !object(row) ||
@@ -74,6 +75,7 @@ async function principalBinding(
74
75
  )
75
76
  throw Error("invalid Principal binding row");
76
77
  }
78
+ if (!Object.hasOwn(manifest.bindings, phase)) throw Error("selected Principal phase has no binding row");
77
79
  const row = manifest.bindings[phase] as Record<string, string>;
78
80
  const agentPath = join(root, row.agent);
79
81
  if (
@@ -376,6 +376,17 @@ export async function runDashboard(argv = process.argv.slice(2), env: NodeJS.Pro
376
376
  notice = (await connection.change({ action: "set", edits: trimmed.slice(2).trim() }))
377
377
  ? "Session models updated"
378
378
  : connection.state.error || "Session controls unavailable";
379
+ } else if (trimmed.startsWith("limit ")) {
380
+ const edit = /^limit (wall|idle) ([0-9]+)$/.exec(trimmed);
381
+ if (!edit) notice = "Use limit wall <seconds> or limit idle <seconds>; 0 restores default";
382
+ else
383
+ notice = (await connection.change({
384
+ action: "set-limit",
385
+ key: edit[1] as "wall" | "idle",
386
+ seconds: Number(edit[2]),
387
+ }))
388
+ ? "Session limit updated for future children; running children unchanged"
389
+ : connection.state.error || "Settings unavailable";
379
390
  } else notice = display.input(line) ? "" : `Unknown command: ${trimmed}. Press ? for help.`;
380
391
  redraw();
381
392
  };
@@ -397,7 +408,7 @@ export async function runDashboard(argv = process.argv.slice(2), env: NodeJS.Pro
397
408
  redraw();
398
409
  return;
399
410
  }
400
- const action = display.key(
411
+ let action = display.key(
401
412
  key.name === "return"
402
413
  ? "return"
403
414
  : ["up", "down", "escape", "tab"].includes(key.name || "")
@@ -409,6 +420,32 @@ export async function runDashboard(argv = process.argv.slice(2), env: NodeJS.Pro
409
420
  return;
410
421
  }
411
422
  if (action === "command") command = "";
423
+ if (action === "setting") {
424
+ const selected = display.screen.settingKey ?? "auto";
425
+ const settings = connection.state.snapshot?.settings;
426
+ if (!settings?.editable) notice = "Settings can only be changed in the connected owning parent";
427
+ else if (selected === "auto") action = "auto";
428
+ else if (selected === "wall" || selected === "idle") command = `limit ${selected} `;
429
+ else if (selected === "descendants") notice = "Startup only: set PI_DADDY_FANOUT before restarting Pi";
430
+ else if (selected === "depth") notice = "Startup only: set PI_DADDY_MAX_DEPTH before restarting Pi";
431
+ else if (selected === "perCall") notice = "Children per call is a fixed safety bound";
432
+ else if (!settings.jev.available)
433
+ notice = "JEV controls unavailable; load skill-harness in the parent Pi session";
434
+ else {
435
+ void connection
436
+ .change({ action: "set-jev", enabled: !(settings.jev.enabled || settings.jev.pending) })
437
+ .then((changed) => {
438
+ const jev = connection.state.snapshot?.settings?.jev;
439
+ notice = !changed
440
+ ? connection.state.error || "JEV change already pending"
441
+ : jev?.pending
442
+ ? "Complete JEV paid-call and LoRA-storage choices in the parent Pi session"
443
+ : jev?.error ||
444
+ `JEV ${jev?.enabled ? "enabled" : "disabled"}; ${jev?.availability ?? "status unknown"}`;
445
+ redraw();
446
+ });
447
+ }
448
+ }
412
449
  if (action === "auto") {
413
450
  const snapshot = connection.state.snapshot;
414
451
  if (!snapshot) notice = "Daddy permission controls unavailable";