pi-daddy 0.46.1 → 0.47.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 (106) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +41 -11
  3. package/contracts/ledger-record/v1/governance-event.schema.json +65 -3
  4. package/dist/governance/approval-prompt.d.ts +4 -0
  5. package/dist/governance/approval-prompt.d.ts.map +1 -1
  6. package/dist/governance/approval-prompt.js +59 -4
  7. package/dist/governance/approval-prompt.js.map +1 -1
  8. package/dist/governance/auto-mode-policy.d.ts +11 -0
  9. package/dist/governance/auto-mode-policy.d.ts.map +1 -0
  10. package/dist/governance/auto-mode-policy.js +309 -0
  11. package/dist/governance/auto-mode-policy.js.map +1 -0
  12. package/dist/governance/execution-retention.d.ts +3 -0
  13. package/dist/governance/execution-retention.d.ts.map +1 -1
  14. package/dist/governance/execution-retention.js +30 -0
  15. package/dist/governance/execution-retention.js.map +1 -1
  16. package/dist/governance/ledger-events.d.ts +14 -1
  17. package/dist/governance/ledger-events.d.ts.map +1 -1
  18. package/dist/governance/ledger-events.js +13 -0
  19. package/dist/governance/ledger-events.js.map +1 -1
  20. package/dist/governance/ledger-report.d.ts.map +1 -1
  21. package/dist/governance/ledger-report.js +12 -3
  22. package/dist/governance/ledger-report.js.map +1 -1
  23. package/dist/governance/ledger-v3-validation.d.ts.map +1 -1
  24. package/dist/governance/ledger-v3-validation.js +17 -3
  25. package/dist/governance/ledger-v3-validation.js.map +1 -1
  26. package/dist/governance/ledger.d.ts +1 -1
  27. package/dist/governance/ledger.d.ts.map +1 -1
  28. package/dist/governance/ledger.js +1 -1
  29. package/dist/governance/ledger.js.map +1 -1
  30. package/dist/kernel/approval.d.ts +1 -1
  31. package/dist/kernel/approval.d.ts.map +1 -1
  32. package/dist/kernel/approval.js +1 -1
  33. package/dist/kernel/approval.js.map +1 -1
  34. package/dist/kernel/auto-mode.d.ts +35 -0
  35. package/dist/kernel/auto-mode.d.ts.map +1 -0
  36. package/dist/kernel/auto-mode.js +29 -0
  37. package/dist/kernel/auto-mode.js.map +1 -0
  38. package/dist/kernel/delegate-types.d.ts +2 -0
  39. package/dist/kernel/delegate-types.d.ts.map +1 -1
  40. package/dist/kernel/delegate-types.js.map +1 -1
  41. package/dist/kernel/delegate.d.ts.map +1 -1
  42. package/dist/kernel/delegate.js +2 -2
  43. package/dist/kernel/delegate.js.map +1 -1
  44. package/dist/kernel/env-names.d.ts +2 -0
  45. package/dist/kernel/env-names.d.ts.map +1 -1
  46. package/dist/kernel/env-names.js +4 -0
  47. package/dist/kernel/env-names.js.map +1 -1
  48. package/dist/kernel/propagation.d.ts +5 -1
  49. package/dist/kernel/propagation.d.ts.map +1 -1
  50. package/dist/kernel/propagation.js +8 -2
  51. package/dist/kernel/propagation.js.map +1 -1
  52. package/dist/products/dashboard-cli.d.ts +6 -0
  53. package/dist/products/dashboard-cli.d.ts.map +1 -1
  54. package/dist/products/dashboard-cli.js +249 -52
  55. package/dist/products/dashboard-cli.js.map +1 -1
  56. package/dist/products/dashboard-display-controls.d.ts +2 -1
  57. package/dist/products/dashboard-display-controls.d.ts.map +1 -1
  58. package/dist/products/dashboard-display-controls.js +61 -0
  59. package/dist/products/dashboard-display-controls.js.map +1 -1
  60. package/dist/products/dashboard-render.d.ts +2 -0
  61. package/dist/products/dashboard-render.d.ts.map +1 -1
  62. package/dist/products/dashboard-render.js +2 -2
  63. package/dist/products/dashboard-render.js.map +1 -1
  64. package/dist/products/dashboard-screen.d.ts +52 -0
  65. package/dist/products/dashboard-screen.d.ts.map +1 -0
  66. package/dist/products/dashboard-screen.js +262 -0
  67. package/dist/products/dashboard-screen.js.map +1 -0
  68. package/dist/products/dashboard-session-client.d.ts +34 -2
  69. package/dist/products/dashboard-session-client.d.ts.map +1 -1
  70. package/dist/products/dashboard-session-client.js +51 -0
  71. package/dist/products/dashboard-session-client.js.map +1 -1
  72. package/dist/products/ecosystem-versions.d.ts +19 -0
  73. package/dist/products/ecosystem-versions.d.ts.map +1 -0
  74. package/dist/products/ecosystem-versions.js +2 -0
  75. package/dist/products/ecosystem-versions.js.map +1 -0
  76. package/extensions/approvals.ts +3 -2
  77. package/extensions/dashboard-session-server.ts +32 -4
  78. package/extensions/delegate-chain.ts +2 -0
  79. package/extensions/delegation-ledger.ts +10 -6
  80. package/extensions/ecosystem-versions.ts +166 -0
  81. package/extensions/grants-command.ts +33 -3
  82. package/extensions/grants.ts +28 -2
  83. package/extensions/reload-environment.ts +16 -1
  84. package/extensions/run-delegation.ts +49 -3
  85. package/extensions/session-auto-mode.ts +132 -0
  86. package/extensions/session.ts +47 -37
  87. package/package.json +2 -2
  88. package/src/governance/approval-prompt.ts +62 -4
  89. package/src/governance/auto-mode-policy.ts +297 -0
  90. package/src/governance/execution-retention.ts +24 -0
  91. package/src/governance/ledger-events.ts +22 -1
  92. package/src/governance/ledger-report.ts +14 -3
  93. package/src/governance/ledger-v3-validation.ts +17 -3
  94. package/src/governance/ledger.ts +1 -0
  95. package/src/kernel/approval.ts +1 -1
  96. package/src/kernel/auto-mode.ts +56 -0
  97. package/src/kernel/delegate-types.ts +2 -0
  98. package/src/kernel/delegate.ts +2 -1
  99. package/src/kernel/env-names.ts +4 -0
  100. package/src/kernel/propagation.ts +11 -2
  101. package/src/products/dashboard-cli.ts +256 -53
  102. package/src/products/dashboard-display-controls.ts +52 -0
  103. package/src/products/dashboard-render.ts +2 -2
  104. package/src/products/dashboard-screen.ts +339 -0
  105. package/src/products/dashboard-session-client.ts +60 -1
  106. package/src/products/ecosystem-versions.ts +18 -0
@@ -211,8 +211,8 @@ export async function verifyLedger(path: string): Promise<LedgerReport> {
211
211
  executors: { herdr: 0, process: 0, unknown: 0 },
212
212
  definitions: [],
213
213
  approvals: {
214
- bySource: { prompt: 0, session: 0, persisted: 0, inherited: 0 },
215
- distinctBySource: { prompt: 0, session: 0, persisted: 0, inherited: 0 },
214
+ bySource: { prompt: 0, session: 0, persisted: 0, inherited: 0, auto: 0 },
215
+ distinctBySource: { prompt: 0, session: 0, persisted: 0, inherited: 0, auto: 0 },
216
216
  unattributed: 0,
217
217
  humanDenied: 0,
218
218
  humanDeniedPairs: 0,
@@ -243,13 +243,14 @@ export async function verifyLedger(path: string): Promise<LedgerReport> {
243
243
  let retired = 0;
244
244
  let escalationAttempts = 0;
245
245
  const executors = { herdr: 0, process: 0, unknown: 0 };
246
- const bySource: Record<ApprovalSource, number> = { prompt: 0, session: 0, persisted: 0, inherited: 0 };
246
+ const bySource: Record<ApprovalSource, number> = { prompt: 0, session: 0, persisted: 0, inherited: 0, auto: 0 };
247
247
  // `capability@subject` seen per source, so the report can state a bound as well as a raw count.
248
248
  const distinct: Record<ApprovalSource, Set<string>> = {
249
249
  prompt: new Set(),
250
250
  session: new Set(),
251
251
  persisted: new Set(),
252
252
  inherited: new Set(),
253
+ auto: new Set(),
253
254
  };
254
255
  let unattributed = 0;
255
256
  let humanDenied = 0;
@@ -288,6 +289,15 @@ export async function verifyLedger(path: string): Promise<LedgerReport> {
288
289
  events += 1;
289
290
  return;
290
291
  }
292
+ if (
293
+ event.ledgerVersion === 3 &&
294
+ (event.event === "session_config" || event.event === "cost_gate" || event.event === "episode_outcome")
295
+ ) {
296
+ // These facts passed the current v3 schema above. They are history, not capability decisions,
297
+ // and must not inflate approval, executor, lifecycle or retired-event counters.
298
+ events += 1;
299
+ return;
300
+ }
291
301
  if (event.ledgerVersion === 3 && isRetiredLedgerEvent(event)) {
292
302
  // Written by 0.30.0 or earlier; counted, never treated as corruption (see RETIRED_LEDGER_EVENT_KINDS).
293
303
  retired += 1;
@@ -386,6 +396,7 @@ export async function verifyLedger(path: string): Promise<LedgerReport> {
386
396
  session: distinct.session.size,
387
397
  persisted: distinct.persisted.size,
388
398
  inherited: distinct.inherited.size,
399
+ auto: distinct.auto.size,
389
400
  },
390
401
  unattributed,
391
402
  humanDenied,
@@ -21,7 +21,7 @@ export function isRetiredLedgerEvent(event: { event?: unknown }): boolean {
21
21
 
22
22
  const SHA256_RE = /^[a-f0-9]{64}$/i;
23
23
  const EXECUTORS = new Set(["process", "herdr"]);
24
- const APPROVAL_SOURCES = new Set(["prompt", "session", "persisted", "inherited"]);
24
+ const APPROVAL_SOURCES = new Set(["prompt", "session", "persisted", "inherited", "auto"]);
25
25
  const APPROVAL_SCOPES = new Set(["once", "session", "always"]);
26
26
  const GATE_OUTCOMES = new Set(["declined", "dismissed", "expired", "aborted", "no-ui", "error"]);
27
27
  const LEASE_OUTCOMES = new Set([
@@ -146,7 +146,7 @@ const FIELDS = {
146
146
  "outcome",
147
147
  "newCeiling",
148
148
  ]),
149
- session_config: new Set(["ledgerVersion", "event", "ts", "episodeId", "outcome", "trigger", "overrides"]),
149
+ session_config: new Set(["ledgerVersion", "event", "ts", "episodeId", "outcome", "trigger", "overrides", "autoMode"]),
150
150
  episode_outcome: new Set([
151
151
  "ledgerVersion",
152
152
  "event",
@@ -537,7 +537,7 @@ export function validateLedgerV3Event(event: LedgerV3Object): string | null {
537
537
  if (
538
538
  !isEpisodeId(event.episodeId) ||
539
539
  !["kept", "changed"].includes(String(event.outcome)) ||
540
- !["first-delegation", "grants-models"].includes(String(event.trigger)) ||
540
+ !["first-delegation", "grants-models", "auto-mode"].includes(String(event.trigger)) ||
541
541
  !isLedgerObject(event.overrides) ||
542
542
  !Object.entries(event.overrides).every(
543
543
  ([name, value]) =>
@@ -549,6 +549,20 @@ export function validateLedgerV3Event(event: LedgerV3Object): string | null {
549
549
  )
550
550
  )
551
551
  return "session config fields are invalid";
552
+ if (event.trigger === "auto-mode") {
553
+ const auto = event.autoMode;
554
+ if (
555
+ event.outcome !== "changed" ||
556
+ Object.keys(event.overrides).length !== 0 ||
557
+ !isLedgerObject(auto) ||
558
+ Object.keys(auto).length !== 3 ||
559
+ typeof auto.enabled !== "boolean" ||
560
+ !["default", "environment", "session"].includes(String(auto.source)) ||
561
+ !Number.isSafeInteger(auto.revision) ||
562
+ (auto.revision as number) < 0
563
+ )
564
+ return "auto mode config fields are invalid";
565
+ } else if (event.autoMode !== undefined) return "auto mode config requires its own trigger";
552
566
  return null;
553
567
  }
554
568
  if (kind === "episode_outcome") return validateEpisodeOutcome(event);
@@ -414,6 +414,7 @@ export {
414
414
  buildEpisodeCostGateEvent,
415
415
  buildEpisodeOutcomeEvent,
416
416
  buildSessionConfigEvent,
417
+ buildAutoModeConfigEvent,
417
418
  buildWorkspaceLeaseEvent,
418
419
  type CapabilityDecisionEvent,
419
420
  type EpisodeCostGateEvent,
@@ -18,7 +18,7 @@ export const APPROVAL_SCOPES = ["once", "session", "always"] as const;
18
18
  export type ApprovalScope = (typeof APPROVAL_SCOPES)[number];
19
19
 
20
20
  /** Where a yes came from, for the ledger. These call for different follow-ups, so they stay distinct. */
21
- export const APPROVAL_SOURCES = ["prompt", "session", "persisted", "inherited"] as const;
21
+ export const APPROVAL_SOURCES = ["prompt", "session", "persisted", "inherited", "auto"] as const;
22
22
  export type ApprovalSource = (typeof APPROVAL_SOURCES)[number];
23
23
 
24
24
  /** Which call site is asking. Determines the scopes offered — see `offeredScopes`. */
@@ -0,0 +1,56 @@
1
+ /** Live session permission policy. This does not widen a grant or convey stored approval. */
2
+ export type AutoModeSource = "default" | "environment" | "session";
3
+ export interface PendingApproval {
4
+ id: string;
5
+ subject: string;
6
+ capability: string;
7
+ }
8
+ export interface AutoModeSnapshot {
9
+ enabled: boolean;
10
+ source: AutoModeSource;
11
+ ownerId: string;
12
+ revision: number;
13
+ pendingApprovals: PendingApproval[];
14
+ }
15
+ export interface AutoModeRef {
16
+ socketPath: string;
17
+ token: string;
18
+ ownerId: string;
19
+ }
20
+ export interface AutoModeReader {
21
+ readonly reference: AutoModeRef;
22
+ read(): Promise<AutoModeSnapshot>;
23
+ /** Linearization point: only a current ON answer admits new work. No approval is banked. */
24
+ admit(signal?: AbortSignal): Promise<boolean>;
25
+ /** Notification only. A waiter must read again and later admit its own operation. */
26
+ waitEnabled(signal: AbortSignal): Promise<void>;
27
+ trackPending(approval: PendingApproval): () => void;
28
+ close(): Promise<void>;
29
+ }
30
+ export function parseAutoModeDefault(raw: string | undefined): { enabled: boolean; source: AutoModeSource } {
31
+ if (raw === undefined) return { enabled: false, source: "default" };
32
+ if (raw === "0" || raw === "1") return { enabled: raw === "1", source: "environment" };
33
+ throw new Error("PI_DADDY_AUTO_MODE must be literal 1 or 0 (unset defaults to OFF)");
34
+ }
35
+ export function parseAutoModeRef(raw: string): AutoModeRef {
36
+ let ref: AutoModeRef;
37
+ try {
38
+ ref = JSON.parse(raw);
39
+ } catch {
40
+ throw new Error("PI_DADDY_AUTO_MODE_REF is malformed");
41
+ }
42
+ if (
43
+ !ref ||
44
+ typeof ref !== "object" ||
45
+ Object.keys(ref).sort().join(",") !== "ownerId,socketPath,token" ||
46
+ typeof ref.socketPath !== "string" ||
47
+ !ref.socketPath.startsWith("/") ||
48
+ ref.socketPath.length > 107 ||
49
+ typeof ref.token !== "string" ||
50
+ !/^[a-f0-9]{48}$/.test(ref.token) ||
51
+ typeof ref.ownerId !== "string" ||
52
+ !/^[a-f0-9]{32}$/.test(ref.ownerId)
53
+ )
54
+ throw new Error("PI_DADDY_AUTO_MODE_REF is malformed");
55
+ return ref;
56
+ }
@@ -1,3 +1,4 @@
1
+ import type { AutoModeRef } from "./auto-mode.ts";
1
2
  /**
2
3
  * The shapes `planDelegation` consumes and produces. Split out of `./delegate.ts` only to stay under the
3
4
  * 400-line module ceiling this project enforces mechanically; `./delegate.ts` re-exports all three, so
@@ -58,6 +59,7 @@ export interface DelegationRequest {
58
59
  }
59
60
 
60
61
  export interface DelegationContext {
62
+ autoModeRef?: AutoModeRef;
61
63
  ownGrant: Capability[];
62
64
  /** Stable identity shared by the root session and every descendant. */
63
65
  episodeId?: string;
@@ -26,6 +26,7 @@ import {
26
26
  ENV_PARENT_ID,
27
27
  inheritableGrant,
28
28
  workspacePinEnv,
29
+ autoModeEnv,
29
30
  } from "./propagation.ts";
30
31
  import { DELEGATE_SUBJECT, inheritApprovals, type InheritableApproval } from "./approval.ts";
31
32
  import { explainDoubledNamespace, suggestForUnknown, unknownCapabilities, type Catalog } from "./catalog.ts";
@@ -421,7 +422,7 @@ export function planDelegation(request: DelegationRequest, ctx: DelegationContex
421
422
  // spelled once in `childEnv` and not here is a rule that does not hold on the path a delegated child
422
423
  // actually takes. Without this a routed grandchild inherits no pin and can route nowhere — fail-closed,
423
424
  // but wrong, and silently so.
424
- Object.assign(env, workspacePinEnv(ctx.workspacePin, inheritable));
425
+ Object.assign(env, workspacePinEnv(ctx.workspacePin, inheritable), autoModeEnv(ctx.autoModeRef));
425
426
  // Composition-supplied per-child environment (the activity timeline's observation identity today).
426
427
  // Never process-global grant state: a key in the governance namespace is a programming error in the
427
428
  // caller, refused loudly rather than letting a product widen what the child inherits.
@@ -20,6 +20,8 @@ export const ENV_MAX_DEPTH = "PI_DADDY_MAX_DEPTH";
20
20
  export const ENV_GATED = "PI_DADDY_GATED";
21
21
  export const ENV_LEDGER = "PI_DADDY_LEDGER";
22
22
  export const ENV_APPROVED = "PI_DADDY_APPROVED";
23
+ export const ENV_AUTO_MODE = "PI_DADDY_AUTO_MODE";
24
+ export const ENV_AUTO_MODE_REF = "PI_DADDY_AUTO_MODE_REF";
23
25
  export const ENV_FANOUT = "PI_DADDY_FANOUT";
24
26
  export const ENV_PARENT_ID = "PI_DADDY_PARENT_ID";
25
27
  export const ENV_EXECUTION_ID = "PI_DADDY_EXECUTION_ID";
@@ -86,6 +88,8 @@ export const GOVERNANCE_ENV_KEYS: readonly string[] = Object.freeze([
86
88
  ENV_GATED,
87
89
  ENV_LEDGER,
88
90
  ENV_APPROVED,
91
+ ENV_AUTO_MODE,
92
+ ENV_AUTO_MODE_REF,
89
93
  ENV_FANOUT,
90
94
  ENV_PARENT_ID,
91
95
  ENV_EXECUTION_ID,
@@ -25,7 +25,8 @@
25
25
  * interceptor and pi handed the child more than the parent held, the intersection clamps it back.
26
26
  */
27
27
 
28
- import { ENV_PUBLIC_EVIDENCE_DIR } from "./env-names.ts";
28
+ import { ENV_AUTO_MODE, ENV_AUTO_MODE_REF, ENV_PUBLIC_EVIDENCE_DIR } from "./env-names.ts";
29
+ import type { AutoModeRef } from "./auto-mode.ts";
29
30
  import type { Capability } from "./resolve.ts";
30
31
  import { attenuateWorkspacePin, formatWorkspacePin, type WorkspacePins } from "./workspace-pin.ts";
31
32
  import { WILDCARD } from "./pi-tools.ts";
@@ -87,6 +88,8 @@ export {
87
88
  * to give it.
88
89
  */
89
90
  export const GRANT_ENV_KEYS = [
91
+ ENV_AUTO_MODE,
92
+ ENV_AUTO_MODE_REF,
90
93
  ENV_PUBLIC_EVIDENCE_DIR,
91
94
  // Not governance state, but the same rule applies for a stronger reason: neither a credential the parent holds
92
95
  // nor the switch that points it at a third party is something a child inherits by being spawned (ADR-0077).
@@ -265,6 +268,7 @@ export function gatedFromEnv(raw: string | undefined): Capability[] {
265
268
  }
266
269
 
267
270
  export interface ChildEnvInput {
271
+ autoModeRef?: AutoModeRef;
268
272
  /** This session's own grant — becomes the child's inherited parent grant. */
269
273
  ownGrant: Capability[];
270
274
  /**
@@ -362,7 +366,7 @@ export function childEnv(input: ChildEnvInput): Record<string, string> {
362
366
  // `ENV_APPROVED` is: an omitted key does not overwrite, so a child would inherit the PARENT's unnarrowed pin
363
367
  // through the process-global publication path. An empty value parses back as "a pin was established and you
364
368
  // got nothing from it", which refuses at routing with a different message from "no pin exists".
365
- Object.assign(env, workspacePinEnv(input.workspacePin, inheritable));
369
+ Object.assign(env, workspacePinEnv(input.workspacePin, inheritable), autoModeEnv(input.autoModeRef));
366
370
  return env;
367
371
  }
368
372
 
@@ -427,3 +431,8 @@ export function observeToolNames(payload: unknown): string[] | null {
427
431
  }
428
432
  return null;
429
433
  }
434
+
435
+ /** A root preference is never copied; every descendant receives the same live owner reference. */
436
+ export function autoModeEnv(ref: AutoModeRef | undefined): Record<string, string> {
437
+ return { [ENV_AUTO_MODE]: "0", ...(ref ? { [ENV_AUTO_MODE_REF]: JSON.stringify(ref) } : {}) };
438
+ }
@@ -9,11 +9,12 @@ import { realpathSync } from "node:fs";
9
9
  import { readFile } from "node:fs/promises";
10
10
  import { resolve } from "node:path";
11
11
  import { pathToFileURL } from "node:url";
12
- import { createInterface } from "node:readline";
12
+ import { createInterface, emitKeypressEvents, type Key } from "node:readline";
13
13
  import { parseDashboardLedger } from "./dashboard-projection.ts";
14
14
  import { renderDashboard } from "./dashboard-render.ts";
15
15
  import {
16
16
  ActivityTimelineAliases,
17
+ activityTaskKey,
17
18
  defaultActivityTimelinePath,
18
19
  detailForTimeline,
19
20
  parseActivityTimeline,
@@ -22,9 +23,20 @@ import {
22
23
  type TimelineFilter,
23
24
  } from "./activity-timeline.ts";
24
25
  import { createDashboardDisplayControls } from "./dashboard-display-controls.ts";
25
- import { dashboardSessionRequest, type DashboardSessionSnapshot } from "./dashboard-session-client.ts";
26
+ import {
27
+ createDashboardConnection,
28
+ dashboardSessionRequest,
29
+ type DashboardSessionSnapshot,
30
+ } from "./dashboard-session-client.ts";
26
31
  import { adoptLegacyEnvironment, ENV_LEDGER, legacyEnvironmentWarning } from "../kernel/env-names.ts";
27
32
  import { projectLedgerPath } from "../kernel/project-paths.ts";
33
+ import {
34
+ activityDashboardItems,
35
+ ledgerDashboardItems,
36
+ renderDashboardScreen,
37
+ type DashboardItem,
38
+ type DashboardScreenState,
39
+ } from "./dashboard-screen.ts";
28
40
 
29
41
  export const DASHBOARD_PROTOCOL_VERSION = 1 as const;
30
42
  export const ENV_DASHBOARD_LEDGER = ENV_LEDGER; // one ledger path variable for children and the dashboard (ADR-0076 PR 3b)
@@ -47,6 +59,11 @@ export interface DashboardFrameOptions {
47
59
  activityAliases?: ActivityTimelineAliases;
48
60
  now?: Date;
49
61
  session?: DashboardSessionSnapshot;
62
+ height?: number;
63
+ screen?: DashboardScreenState;
64
+ notice?: string;
65
+ pending?: boolean;
66
+ command?: string;
50
67
  }
51
68
 
52
69
  function shellQuote(value: string): string {
@@ -102,7 +119,106 @@ async function renderTimelineFile(path: string, text: string, options: Dashboard
102
119
  });
103
120
  }
104
121
 
122
+ function isActivityTimelineText(text: string): boolean {
123
+ try {
124
+ const first = JSON.parse(text.trimStart().split("\n")[0] || "{}");
125
+ return first?.kind === "activity" || first?.version === 1;
126
+ } catch {
127
+ return false;
128
+ }
129
+ }
130
+
131
+ async function compactFrame(options: DashboardFrameOptions): Promise<string> {
132
+ const path = resolve(options.cwd, options.ledgerPath ?? defaultActivityTimelinePath(options.cwd));
133
+ let items: DashboardItem[] = [],
134
+ problem: string | undefined,
135
+ current: string | undefined,
136
+ currentStatus: string | undefined;
137
+ let content: { title: string; text: string } | undefined;
138
+ const aliases = options.activityAliases ?? new ActivityTimelineAliases();
139
+ if (options.protocol !== undefined && options.protocol !== DASHBOARD_PROTOCOL_VERSION)
140
+ return renderDashboardScreen({
141
+ ...options,
142
+ width: options.width ?? 80,
143
+ height: options.height!,
144
+ screen: options.screen!,
145
+ items: [],
146
+ problem: "INCOMPATIBLE dashboard protocol; relink this package.",
147
+ });
148
+ try {
149
+ const text = await readFile(path, "utf8");
150
+ if (!options.ledgerPath || isActivityTimelineText(text)) {
151
+ const timeline = parseActivityTimeline(text);
152
+ items = activityDashboardItems(timeline, aliases, options.now ?? new Date()).map((item) => ({
153
+ ...item,
154
+ timelinePath: path,
155
+ }));
156
+ problem = timeline.refusals[0];
157
+ } else {
158
+ const projection = parseDashboardLedger(text, { now: options.now });
159
+ items = ledgerDashboardItems(projection);
160
+ if (projection.corrupt.length) problem = `Ledger unavailable: ${projection.corrupt.length} corrupt line(s)`;
161
+ }
162
+ } catch (error) {
163
+ problem =
164
+ (error as { code?: string }).code === "ENOENT"
165
+ ? "Waiting for local work; open from /grants dashboard."
166
+ : `Evidence unavailable: ${error instanceof Error ? error.message : String(error)}`;
167
+ }
168
+ // The owner supplies this exact current turn. No sibling timeline or unrelated history is merged.
169
+ const activity = options.session?.activity;
170
+ if (activity) {
171
+ const key = activityTaskKey(activity.rootId, activity.taskId);
172
+ current = `Task ${activity.taskId}`;
173
+ try {
174
+ const timeline = parseActivityTimeline(await readFile(activity.path, "utf8"));
175
+ timeline.tasks = timeline.tasks.filter((task) => activityTaskKey(task.rootId, task.id) === key);
176
+ const item = activityDashboardItems(timeline, aliases, options.now ?? new Date())[0];
177
+ if (item) {
178
+ current = item.label;
179
+ currentStatus = item.status;
180
+ if (!items.some((existing) => existing.key === key)) items.unshift({ ...item, timelinePath: activity.path });
181
+ try {
182
+ current = (await detailForTimeline(activity.path, key, "prompt")).text.slice(0, 600);
183
+ } catch {
184
+ /* Metadata-only/off and unavailable retained content retain the exact task identity. */
185
+ }
186
+ }
187
+ } catch {
188
+ /* The owner identity remains useful when its local timeline is unavailable. */
189
+ }
190
+ }
191
+ if (options.activityDetail) {
192
+ try {
193
+ const requested = aliases.resolve(options.activityDetail.taskKey) ?? options.activityDetail.taskKey;
194
+ const key = items.find((value) => value.taskKey === requested)?.taskKey;
195
+ if (!key) throw Error("unknown task selector");
196
+ options.screen!.selectedKey = key;
197
+ const source = items.find((value) => value.taskKey === key)?.timelinePath;
198
+ if (!source) throw Error("task has no retained activity source");
199
+ content = {
200
+ title: `PRIVATE ${options.activityDetail.field.toUpperCase()}`,
201
+ text: (await detailForTimeline(source, key, options.activityDetail.field)).text,
202
+ };
203
+ } catch (error) {
204
+ problem = `Activity detail unavailable: ${error instanceof Error ? error.message : String(error)}`;
205
+ }
206
+ }
207
+ return renderDashboardScreen({
208
+ ...options,
209
+ width: options.width ?? 80,
210
+ height: options.height!,
211
+ screen: options.screen!,
212
+ items,
213
+ problem,
214
+ content,
215
+ current,
216
+ currentStatus,
217
+ });
218
+ }
219
+
105
220
  export async function dashboardFrame(options: DashboardFrameOptions): Promise<string> {
221
+ if (options.screen && options.height !== undefined) return compactFrame(options);
106
222
  if (options.protocol !== undefined && options.protocol !== DASHBOARD_PROTOCOL_VERSION) {
107
223
  return incompatibleFrame(options.protocol);
108
224
  }
@@ -136,7 +252,7 @@ export async function dashboardFrame(options: DashboardFrameOptions): Promise<st
136
252
  ].join("\n");
137
253
  }
138
254
  }
139
- const rendered = text.trimStart().startsWith('{"version":1')
255
+ const rendered = isActivityTimelineText(text)
140
256
  ? await renderTimelineFile(ledgerPath, text, options)
141
257
  : renderDashboard(parseDashboardLedger(text, { now: options.now }), {
142
258
  color: options.color,
@@ -186,17 +302,21 @@ export async function runDashboard(argv = process.argv.slice(2), env: NodeJS.Pro
186
302
  process.title = `pi-daddy-dashboard${key ? `:${key.slice(0, 12)}` : ""}`;
187
303
 
188
304
  const display = createDashboardDisplayControls(cli.details, true);
189
- let previous = "";
190
- let notice = "";
305
+ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY) && !cli.once;
306
+ const connection = createDashboardConnection(
307
+ sessionSocket && sessionToken
308
+ ? (action) => dashboardSessionRequest(sessionSocket, sessionToken, action)
309
+ : undefined,
310
+ );
311
+ let previous = "",
312
+ notice = "",
313
+ command: string | undefined;
191
314
  let input: ReturnType<typeof createInterface> | null = null;
315
+ let stopped = false,
316
+ drawing = false;
192
317
  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
- }
318
+ await connection.refresh();
319
+ if (stopped) return;
200
320
  const view = await dashboardFrame({
201
321
  cwd,
202
322
  ledgerPath,
@@ -205,62 +325,145 @@ export async function runDashboard(argv = process.argv.slice(2), env: NodeJS.Pro
205
325
  width: process.stdout.columns || 80,
206
326
  ...display.state,
207
327
  activityAliases: display.aliases,
208
- session,
328
+ session: connection.state.snapshot,
329
+ ...(interactive
330
+ ? {
331
+ height: process.stdout.rows || 24,
332
+ screen: display.screen,
333
+ notice: connection.state.error || notice,
334
+ pending: connection.state.pending,
335
+ command,
336
+ }
337
+ : {}),
209
338
  });
210
- const frame = notice ? `${notice}\n\n${view}` : view;
211
- if (frame === previous && !clear) return;
212
- previous = frame;
213
- const prompt = display.prompt();
214
- const draft = input?.line ?? "";
339
+ if (stopped) return; // A read may complete after q/SIGTERM restored the parent terminal.
340
+ const frame = interactive
341
+ ? view
342
+ : `${connection.state.error || notice ? `${connection.state.error || notice}\n\n` : ""}${view}`;
343
+ const prompt = display.prompt(),
344
+ draft = input?.line ?? "";
345
+ const rendered = `${frame}${!interactive && input ? `\n\n${prompt}${draft}` : ""}`;
346
+ if (rendered === previous) return;
347
+ previous = rendered;
215
348
  process.stdout.write(
216
- clear ? `\u001b]0;PI-DADDY\u0007\u001b[2J\u001b[H${frame}\n\n${input ? prompt + draft : ""}` : `${frame}\n`,
349
+ interactive
350
+ ? // Erase every row: clearing only below the final cursor leaves old suffixes on shorter rows.
351
+ `\u001b[H\u001b[2K${frame.replace(/\n/g, "\r\n\u001b[2K")}\u001b[J`
352
+ : clear
353
+ ? `\u001b]0;PI-DADDY\u0007\u001b[2J\u001b[H${rendered}`
354
+ : `${rendered}\n`,
217
355
  );
218
- if (input) input.setPrompt(prompt);
356
+ input?.setPrompt(prompt);
219
357
  };
220
-
221
358
  if (cli.once) {
222
359
  await draw(false);
223
360
  return;
224
361
  }
225
- input = createInterface({ input: process.stdin, output: process.stdout, terminal: Boolean(process.stdin.isTTY) });
226
- await draw(true);
227
- let drawing = false;
228
362
  const redraw = (): void => {
229
- if (drawing) return;
363
+ if (drawing || stopped) return;
230
364
  drawing = true;
231
- void draw(true).finally(() => {
232
- drawing = false;
233
- });
365
+ void draw(true)
366
+ .catch((error) => {
367
+ notice = `Dashboard unavailable: ${String(error)}`;
368
+ })
369
+ .finally(() => {
370
+ drawing = false;
371
+ });
234
372
  };
235
- input.on("line", (line) => {
373
+ const runCommand = async (line: string): Promise<void> => {
236
374
  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);
375
+ if (trimmed.startsWith("m ")) {
376
+ notice = (await connection.change({ action: "set", edits: trimmed.slice(2).trim() }))
377
+ ? "Session models updated"
378
+ : connection.state.error || "Session controls unavailable";
379
+ } else notice = display.input(line) ? "" : `Unknown command: ${trimmed}. Press ? for help.`;
380
+ redraw();
381
+ };
382
+ let stop: () => void = () => {};
383
+ const keypress = (text: string | undefined, key: Key): void => {
384
+ if (key.ctrl && key.name === "c") {
385
+ stop();
386
+ return;
387
+ }
388
+ if (command !== undefined) {
389
+ if (key.name === "escape") command = undefined;
390
+ else if (key.name === "return") {
391
+ const line = command;
392
+ command = undefined;
393
+ void runCommand(line);
394
+ } else if (key.name === "backspace") command = [...command].slice(0, -1).join("");
395
+ else if (text && !key.ctrl && !key.meta && !/[\p{Cc}\p{Cf}]/u.test(text) && command.length < 2048)
396
+ command += text;
397
+ redraw();
398
+ return;
399
+ }
400
+ const action = display.key(
401
+ key.name === "return"
402
+ ? "return"
403
+ : ["up", "down", "escape", "tab"].includes(key.name || "")
404
+ ? key.name!
405
+ : text || "",
406
+ );
407
+ if (action === "quit") {
408
+ stop();
246
409
  return;
247
410
  }
248
- // Rule 8: a line the display controls do not understand is said so, never swallowed.
249
- notice = display.input(line) ? "" : `not a display command: ${JSON.stringify(trimmed)}. ${display.prompt()}`;
411
+ if (action === "command") command = "";
412
+ if (action === "auto") {
413
+ const snapshot = connection.state.snapshot;
414
+ if (!snapshot) notice = "Daddy permission controls unavailable";
415
+ else {
416
+ const enabled = !snapshot.auto.enabled;
417
+ void connection.change({ action: "set-auto", enabled }).then((changed) => {
418
+ notice = changed
419
+ ? `Daddy permission Auto ${enabled ? "ON" : "OFF"}; ${enabled ? "future prompts auto-approved" : "admitted work continues"}`
420
+ : connection.state.error || "Permission change already pending";
421
+ redraw();
422
+ });
423
+ }
424
+ }
250
425
  redraw();
251
- });
252
- const timer = setInterval(redraw, DASHBOARD_REFRESH_MS);
253
- process.on("SIGWINCH", redraw);
254
- await new Promise<void>((settle) => {
255
- const stop = () => {
256
- clearInterval(timer);
257
- input?.close();
258
- process.off("SIGWINCH", redraw);
259
- settle();
260
- };
261
- process.once("SIGINT", stop);
262
- process.once("SIGTERM", stop);
263
- });
426
+ };
427
+ let timer: ReturnType<typeof setInterval> | undefined;
428
+ const oldRaw = process.stdin.isRaw;
429
+ try {
430
+ if (interactive) {
431
+ emitKeypressEvents(process.stdin);
432
+ process.stdin.setRawMode(true);
433
+ process.stdin.resume();
434
+ process.stdout.write("\u001b[?1049h\u001b[?25l");
435
+ } else {
436
+ input = createInterface({ input: process.stdin, output: process.stdout, terminal: false });
437
+ input.on("line", (line) => {
438
+ void runCommand(line);
439
+ });
440
+ }
441
+ await new Promise<void>((settle) => {
442
+ stop = () => {
443
+ stopped = true;
444
+ settle();
445
+ };
446
+ process.once("SIGINT", stop);
447
+ process.once("SIGTERM", stop);
448
+ if (interactive) process.stdin.on("keypress", keypress);
449
+ process.on("SIGWINCH", redraw);
450
+ timer = setInterval(redraw, DASHBOARD_REFRESH_MS);
451
+ redraw();
452
+ });
453
+ } finally {
454
+ stopped = true;
455
+ if (timer) clearInterval(timer);
456
+ process.off("SIGINT", stop);
457
+ process.off("SIGTERM", stop);
458
+ process.off("SIGWINCH", redraw);
459
+ input?.close();
460
+ if (interactive) {
461
+ process.stdin.off("keypress", keypress);
462
+ process.stdin.setRawMode(oldRaw || false);
463
+ process.stdin.pause();
464
+ process.stdout.write("\u001b[?25h\u001b[?1049l");
465
+ }
466
+ }
264
467
  }
265
468
 
266
469
  const invoked = (() => {