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.
- package/CHANGELOG.md +14 -0
- package/README.md +41 -11
- package/contracts/ledger-record/v1/governance-event.schema.json +65 -3
- package/dist/governance/approval-prompt.d.ts +4 -0
- package/dist/governance/approval-prompt.d.ts.map +1 -1
- package/dist/governance/approval-prompt.js +59 -4
- package/dist/governance/approval-prompt.js.map +1 -1
- package/dist/governance/auto-mode-policy.d.ts +11 -0
- package/dist/governance/auto-mode-policy.d.ts.map +1 -0
- package/dist/governance/auto-mode-policy.js +309 -0
- package/dist/governance/auto-mode-policy.js.map +1 -0
- package/dist/governance/execution-retention.d.ts +3 -0
- package/dist/governance/execution-retention.d.ts.map +1 -1
- package/dist/governance/execution-retention.js +30 -0
- package/dist/governance/execution-retention.js.map +1 -1
- package/dist/governance/ledger-events.d.ts +14 -1
- package/dist/governance/ledger-events.d.ts.map +1 -1
- package/dist/governance/ledger-events.js +13 -0
- package/dist/governance/ledger-events.js.map +1 -1
- package/dist/governance/ledger-report.d.ts.map +1 -1
- package/dist/governance/ledger-report.js +12 -3
- package/dist/governance/ledger-report.js.map +1 -1
- package/dist/governance/ledger-v3-validation.d.ts.map +1 -1
- package/dist/governance/ledger-v3-validation.js +17 -3
- package/dist/governance/ledger-v3-validation.js.map +1 -1
- package/dist/governance/ledger.d.ts +1 -1
- package/dist/governance/ledger.d.ts.map +1 -1
- package/dist/governance/ledger.js +1 -1
- package/dist/governance/ledger.js.map +1 -1
- package/dist/kernel/approval.d.ts +1 -1
- package/dist/kernel/approval.d.ts.map +1 -1
- package/dist/kernel/approval.js +1 -1
- package/dist/kernel/approval.js.map +1 -1
- package/dist/kernel/auto-mode.d.ts +35 -0
- package/dist/kernel/auto-mode.d.ts.map +1 -0
- package/dist/kernel/auto-mode.js +29 -0
- package/dist/kernel/auto-mode.js.map +1 -0
- package/dist/kernel/delegate-types.d.ts +2 -0
- package/dist/kernel/delegate-types.d.ts.map +1 -1
- package/dist/kernel/delegate-types.js.map +1 -1
- package/dist/kernel/delegate.d.ts.map +1 -1
- package/dist/kernel/delegate.js +2 -2
- package/dist/kernel/delegate.js.map +1 -1
- package/dist/kernel/env-names.d.ts +2 -0
- package/dist/kernel/env-names.d.ts.map +1 -1
- package/dist/kernel/env-names.js +4 -0
- package/dist/kernel/env-names.js.map +1 -1
- package/dist/kernel/propagation.d.ts +5 -1
- package/dist/kernel/propagation.d.ts.map +1 -1
- package/dist/kernel/propagation.js +8 -2
- package/dist/kernel/propagation.js.map +1 -1
- package/dist/products/dashboard-cli.d.ts +6 -0
- package/dist/products/dashboard-cli.d.ts.map +1 -1
- package/dist/products/dashboard-cli.js +249 -52
- package/dist/products/dashboard-cli.js.map +1 -1
- package/dist/products/dashboard-display-controls.d.ts +2 -1
- package/dist/products/dashboard-display-controls.d.ts.map +1 -1
- package/dist/products/dashboard-display-controls.js +61 -0
- package/dist/products/dashboard-display-controls.js.map +1 -1
- package/dist/products/dashboard-render.d.ts +2 -0
- package/dist/products/dashboard-render.d.ts.map +1 -1
- package/dist/products/dashboard-render.js +2 -2
- package/dist/products/dashboard-render.js.map +1 -1
- package/dist/products/dashboard-screen.d.ts +52 -0
- package/dist/products/dashboard-screen.d.ts.map +1 -0
- package/dist/products/dashboard-screen.js +262 -0
- package/dist/products/dashboard-screen.js.map +1 -0
- package/dist/products/dashboard-session-client.d.ts +34 -2
- package/dist/products/dashboard-session-client.d.ts.map +1 -1
- package/dist/products/dashboard-session-client.js +51 -0
- package/dist/products/dashboard-session-client.js.map +1 -1
- package/dist/products/ecosystem-versions.d.ts +19 -0
- package/dist/products/ecosystem-versions.d.ts.map +1 -0
- package/dist/products/ecosystem-versions.js +2 -0
- package/dist/products/ecosystem-versions.js.map +1 -0
- package/extensions/approvals.ts +3 -2
- package/extensions/dashboard-session-server.ts +32 -4
- package/extensions/delegate-chain.ts +2 -0
- package/extensions/delegation-ledger.ts +10 -6
- package/extensions/ecosystem-versions.ts +166 -0
- package/extensions/grants-command.ts +33 -3
- package/extensions/grants.ts +28 -2
- package/extensions/reload-environment.ts +16 -1
- package/extensions/run-delegation.ts +49 -3
- package/extensions/session-auto-mode.ts +132 -0
- package/extensions/session.ts +47 -37
- package/package.json +2 -2
- package/src/governance/approval-prompt.ts +62 -4
- package/src/governance/auto-mode-policy.ts +297 -0
- package/src/governance/execution-retention.ts +24 -0
- package/src/governance/ledger-events.ts +22 -1
- package/src/governance/ledger-report.ts +14 -3
- package/src/governance/ledger-v3-validation.ts +17 -3
- package/src/governance/ledger.ts +1 -0
- package/src/kernel/approval.ts +1 -1
- package/src/kernel/auto-mode.ts +56 -0
- package/src/kernel/delegate-types.ts +2 -0
- package/src/kernel/delegate.ts +2 -1
- package/src/kernel/env-names.ts +4 -0
- package/src/kernel/propagation.ts +11 -2
- package/src/products/dashboard-cli.ts +256 -53
- package/src/products/dashboard-display-controls.ts +52 -0
- package/src/products/dashboard-render.ts +2 -2
- package/src/products/dashboard-screen.ts +339 -0
- package/src/products/dashboard-session-client.ts +60 -1
- 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);
|
package/src/governance/ledger.ts
CHANGED
package/src/kernel/approval.ts
CHANGED
|
@@ -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;
|
package/src/kernel/delegate.ts
CHANGED
|
@@ -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.
|
package/src/kernel/env-names.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
-
|
|
190
|
-
|
|
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
|
-
|
|
194
|
-
if (
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
232
|
-
|
|
233
|
-
|
|
365
|
+
void draw(true)
|
|
366
|
+
.catch((error) => {
|
|
367
|
+
notice = `Dashboard unavailable: ${String(error)}`;
|
|
368
|
+
})
|
|
369
|
+
.finally(() => {
|
|
370
|
+
drawing = false;
|
|
371
|
+
});
|
|
234
372
|
};
|
|
235
|
-
|
|
373
|
+
const runCommand = async (line: string): Promise<void> => {
|
|
236
374
|
const trimmed = line.trim();
|
|
237
|
-
if (trimmed.startsWith("m ")
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
249
|
-
|
|
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
|
-
|
|
253
|
-
process.
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
process.
|
|
259
|
-
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
|
|
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 = (() => {
|