pi-daddy 0.23.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.md +20 -6
- package/contracts/ledger/v4/README.md +1 -1
- package/dist/cli.d.ts +3 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +49 -3
- package/dist/cli.js.map +1 -1
- package/dist/daily-dashboard-host.d.ts +123 -0
- package/dist/daily-dashboard-host.d.ts.map +1 -0
- package/dist/daily-dashboard-host.js +141 -0
- package/dist/daily-dashboard-host.js.map +1 -0
- package/dist/dashboard-cli.d.ts.map +1 -1
- package/dist/dashboard-cli.js +9 -2
- package/dist/dashboard-cli.js.map +1 -1
- package/dist/dashboard-handshake.d.ts.map +1 -1
- package/dist/dashboard-handshake.js +13 -4
- package/dist/dashboard-handshake.js.map +1 -1
- package/dist/dashboard-harness.d.ts +3 -0
- package/dist/dashboard-harness.d.ts.map +1 -1
- package/dist/dashboard-harness.js +15 -1
- package/dist/dashboard-harness.js.map +1 -1
- package/dist/dashboard-herdr.d.ts +1 -0
- package/dist/dashboard-herdr.d.ts.map +1 -1
- package/dist/dashboard-herdr.js +10 -8
- package/dist/dashboard-herdr.js.map +1 -1
- package/dist/dashboard-host-contract.d.ts +15 -1
- package/dist/dashboard-host-contract.d.ts.map +1 -1
- package/dist/dashboard-host-contract.js +1 -1
- package/dist/dashboard-host-contract.js.map +1 -1
- package/dist/dashboard-host-transport.d.ts +1 -0
- package/dist/dashboard-host-transport.d.ts.map +1 -1
- package/dist/dashboard-host-transport.js +5 -3
- package/dist/dashboard-host-transport.js.map +1 -1
- package/dist/dashboard-host.d.ts +62 -0
- package/dist/dashboard-host.d.ts.map +1 -1
- package/dist/dashboard-host.js +71 -6
- package/dist/dashboard-host.js.map +1 -1
- package/dist/dashboard-observation.d.ts.map +1 -1
- package/dist/dashboard-observation.js +6 -5
- package/dist/dashboard-observation.js.map +1 -1
- package/dist/experiment-store.d.ts +3 -1
- package/dist/experiment-store.d.ts.map +1 -1
- package/dist/experiment-store.js +20 -3
- package/dist/experiment-store.js.map +1 -1
- package/dist/experiment.d.ts.map +1 -1
- package/dist/experiment.js +9 -4
- package/dist/experiment.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/intent-application.d.ts.map +1 -1
- package/dist/intent-application.js +3 -2
- package/dist/intent-application.js.map +1 -1
- package/dist/measured-order.d.ts +249 -0
- package/dist/measured-order.d.ts.map +1 -0
- package/dist/measured-order.js +251 -0
- package/dist/measured-order.js.map +1 -0
- package/dist/measured-session.d.ts +97 -0
- package/dist/measured-session.d.ts.map +1 -0
- package/dist/measured-session.js +134 -0
- package/dist/measured-session.js.map +1 -0
- package/dist/work-command.d.ts +32 -0
- package/dist/work-command.d.ts.map +1 -0
- package/dist/work-command.js +161 -0
- package/dist/work-command.js.map +1 -0
- package/extensions/daily-dashboard-session.ts +31 -0
- package/extensions/daily-work-session.ts +20 -0
- package/extensions/delegation.ts +21 -8
- package/extensions/execute-child.ts +10 -1
- package/extensions/executor-session.ts +9 -0
- package/extensions/grants-command.ts +16 -15
- package/extensions/grants-connected-command.ts +19 -0
- package/extensions/grants.ts +25 -3
- package/extensions/primary-shadow.ts +11 -0
- package/extensions/run-delegation.ts +2 -0
- package/extensions/session.ts +7 -19
- package/extensions/work-runtime.ts +44 -0
- package/package.json +13 -1
- package/src/cli.ts +41 -4
- package/src/daily-dashboard-host.ts +88 -0
- package/src/dashboard-cli.ts +7 -2
- package/src/dashboard-handshake.ts +14 -5
- package/src/dashboard-harness.ts +10 -1
- package/src/dashboard-herdr.ts +12 -9
- package/src/dashboard-host-contract.ts +8 -2
- package/src/dashboard-host-transport.ts +8 -6
- package/src/dashboard-host.ts +37 -9
- package/src/dashboard-observation.ts +4 -2
- package/src/experiment-store.ts +11 -3
- package/src/experiment.ts +8 -3
- package/src/index.ts +17 -0
- package/src/intent-application.ts +3 -2
- package/src/measured-order.ts +67 -0
- package/src/measured-session.ts +70 -0
- package/src/work-command.ts +185 -0
package/extensions/session.ts
CHANGED
|
@@ -21,8 +21,7 @@ import { makeCatalog, skillPathsFromCatalog, type Catalog } from "../src/catalog
|
|
|
21
21
|
import type { SkillDefinition } from "../src/definitions.ts";
|
|
22
22
|
import { DELEGATE_CAPABILITY, type DelegationContext } from "../src/delegate.ts";
|
|
23
23
|
import { budgetFromEnv } from "../src/fanout.ts";
|
|
24
|
-
import { chooseExecutor,
|
|
25
|
-
import { probeHerdr } from "../src/herdr-cli.ts";
|
|
24
|
+
import { chooseExecutor, ENV_HERDR, type ExecutorChoice } from "../src/executor.ts";
|
|
26
25
|
import { WILDCARD } from "../src/pi-tools.ts";
|
|
27
26
|
import {
|
|
28
27
|
childEnv,
|
|
@@ -49,7 +48,7 @@ import { republishable } from "./approvals.ts";
|
|
|
49
48
|
import { storedGrantSessionState } from "./stored-grant-session.ts";
|
|
50
49
|
import { nativeSessionRootFromEnv, type NativeSessionHost } from "../src/native-session-target.ts";
|
|
51
50
|
import { ENV_ALLOW_UNRESOLVED_MODELS } from "../src/model-preflight.ts";
|
|
52
|
-
|
|
51
|
+
import type { DeclaredWorkState } from "../src/work-command.ts";
|
|
53
52
|
/**
|
|
54
53
|
* Run governed children in herdr panes instead of captured child processes.
|
|
55
54
|
*
|
|
@@ -80,6 +79,7 @@ export { ENV_HERDR_WORKSPACE } from "../src/herdr-cli.ts";
|
|
|
80
79
|
/** Keep each child's pane after it finishes, for inspection. Off by default: fan-out would flood it. */
|
|
81
80
|
export const ENV_HERDR_KEEP_PANE = "PI_GRANTS_HERDR_KEEP_PANE";
|
|
82
81
|
|
|
82
|
+
export interface VariantRunAccounting {runId:string;primaryExecutionId:string;shadowExecutionIds:string[];state:"running"|"settled";outcomes:null|{executionId:string;role:"primary"|"shadow";ok:boolean;reason:string|null}[]}
|
|
83
83
|
export interface GrantsSession extends NativeSessionHost {
|
|
84
84
|
/**
|
|
85
85
|
* False when neither `PI_GRANTS_GRANT` nor a stored grant applies: the session holds the wildcard and
|
|
@@ -127,6 +127,9 @@ export interface GrantsSession extends NativeSessionHost {
|
|
|
127
127
|
readonly modelResolutionCache: Map<string, boolean>;
|
|
128
128
|
/** Path to this extension, so a child granted `tool:delegate` can delegate in turn. */
|
|
129
129
|
readonly extensionPath?: string;
|
|
130
|
+
declaredWork?: DeclaredWorkState; // Explicit operator selection; absence leaves execution visibly unbound.
|
|
131
|
+
/** Primary-return fan-outs retained by this original session; bounded and human-readable via /grants variants. */
|
|
132
|
+
readonly variantRuns: Map<string, VariantRunAccounting>;
|
|
130
133
|
|
|
131
134
|
/** Approval keys approved for this session. In memory only — this dies with the process. */
|
|
132
135
|
readonly sessionApprovals: Set<string>;
|
|
@@ -235,22 +238,6 @@ export async function loadProjectDefinitions(session: GrantsSession, cwd: string
|
|
|
235
238
|
session.catalog = await session.catalogReady;
|
|
236
239
|
}
|
|
237
240
|
|
|
238
|
-
/**
|
|
239
|
-
* Probe for herdr and settle this session's executor — ADR-0031.
|
|
240
|
-
*
|
|
241
|
-
* **Once, at session start, and never per spawn.** A fan-out whose children ran under two executors would put
|
|
242
|
-
* two different things under one call in the ledger, and the two plans differ (`--print` is withheld on the
|
|
243
|
-
* herdr path). A herdr server that dies mid-session therefore surfaces as a failed `tab create`, reported as
|
|
244
|
-
* the spawn error it is, rather than as a silent relocation of the remaining children.
|
|
245
|
-
*
|
|
246
|
-
* `probeHerdr` never throws, so this cannot either — which matters because it runs *before* the line that
|
|
247
|
-
* discloses what it decided (R-60: a throw here would cancel that line and every control after it).
|
|
248
|
-
*/
|
|
249
|
-
export async function resolveExecutor(session: GrantsSession): Promise<void> {
|
|
250
|
-
const raw = process.env[ENV_HERDR];
|
|
251
|
-
session.executor = chooseExecutor(raw, needsProbe(raw) ? await probeHerdr() : null);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
241
|
export function createGrantsSession(extensionPath: string | undefined): GrantsSession {
|
|
255
242
|
// Governance is opt-in: with PI_GRANTS_GRANT unset AND no stored grant for this directory, the session
|
|
256
243
|
// holds the wildcard and nothing is blocked. This extension must never silently tighten a normal
|
|
@@ -322,6 +309,7 @@ export function createGrantsSession(extensionPath: string | undefined): GrantsSe
|
|
|
322
309
|
nativeSessionRoot: nativeSessionRootFromEnv(process.env),
|
|
323
310
|
modelResolutionCache: new Map<string, boolean>(),
|
|
324
311
|
extensionPath,
|
|
312
|
+
variantRuns: new Map(),
|
|
325
313
|
|
|
326
314
|
sessionApprovals: new Set<string>(),
|
|
327
315
|
sessionApprovalBindings: new Map<string, ApprovalBinding>(),
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Delegation } from "../src/delegate.ts";
|
|
2
|
+
import { retentionConfigurationDigest } from "../src/execution-retention.ts";
|
|
3
|
+
import { appendDeclaredWorkOccurrence } from "../src/work-command.ts";
|
|
4
|
+
import type { WorkOccurrencePayload } from "../src/work-ledger.ts";
|
|
5
|
+
import type { GrantsSession } from "./session.ts";
|
|
6
|
+
import type { PreparedWorkspace } from "./workspace-runtime.ts";
|
|
7
|
+
|
|
8
|
+
export async function beginDeclaredWorkAttempt(input: {
|
|
9
|
+
session: GrantsSession;
|
|
10
|
+
plan: Delegation;
|
|
11
|
+
childId: string;
|
|
12
|
+
executionId: string;
|
|
13
|
+
parentExecutionId: string | null;
|
|
14
|
+
toolCallId?: string;
|
|
15
|
+
preparedWorkspace?: PreparedWorkspace;
|
|
16
|
+
configuredTimeoutMs: number;
|
|
17
|
+
startedAt: Date;
|
|
18
|
+
}): Promise<null | { finish(state: Extract<WorkOccurrencePayload["state"], "completed" | "failed">): Promise<void> }> {
|
|
19
|
+
const state = input.session.declaredWork;
|
|
20
|
+
if (!state) return null;
|
|
21
|
+
const argument = (flag: string): string | null => {
|
|
22
|
+
const index = input.plan.args.indexOf(flag);
|
|
23
|
+
return index >= 0 ? input.plan.args[index + 1] ?? null : null;
|
|
24
|
+
};
|
|
25
|
+
const identity = {
|
|
26
|
+
executionId: input.executionId,
|
|
27
|
+
parentExecutionId: input.parentExecutionId,
|
|
28
|
+
childId: input.childId,
|
|
29
|
+
variantId: input.plan.correlation?.context_id ?? null,
|
|
30
|
+
toolCallId: input.toolCallId ?? null,
|
|
31
|
+
taskId: input.plan.correlation?.task_id ?? input.plan.taskDigest,
|
|
32
|
+
workspaceId: input.preparedWorkspace?.workspace.workspaceId ?? input.plan.correlation?.workspace_id ?? null,
|
|
33
|
+
definitionDigest: input.plan.definitionDigest?.sha256 ?? null,
|
|
34
|
+
configurationDigest: retentionConfigurationDigest({
|
|
35
|
+
args: input.plan.args,
|
|
36
|
+
effective: input.plan.effective,
|
|
37
|
+
timeoutMs: input.configuredTimeoutMs,
|
|
38
|
+
}),
|
|
39
|
+
modelId: argument("--model"),
|
|
40
|
+
effortId: argument("--thinking"),
|
|
41
|
+
};
|
|
42
|
+
await appendDeclaredWorkOccurrence(state, { ...identity, now: input.startedAt }, "starting");
|
|
43
|
+
return { finish: (terminal) => appendDeclaredWorkOccurrence(state, { ...identity, now: new Date() }, terminal) };
|
|
44
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-daddy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
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",
|
|
@@ -53,6 +53,10 @@
|
|
|
53
53
|
"types": "./dist/dashboard-harness.d.ts",
|
|
54
54
|
"default": "./dist/dashboard-harness.js"
|
|
55
55
|
},
|
|
56
|
+
"./daily-dashboard-host": {
|
|
57
|
+
"types": "./dist/daily-dashboard-host.d.ts",
|
|
58
|
+
"default": "./dist/daily-dashboard-host.js"
|
|
59
|
+
},
|
|
56
60
|
"./dashboard-host": {
|
|
57
61
|
"types": "./dist/dashboard-host.d.ts",
|
|
58
62
|
"default": "./dist/dashboard-host.js"
|
|
@@ -94,6 +98,14 @@
|
|
|
94
98
|
"types": "./dist/effect-profile.d.ts",
|
|
95
99
|
"default": "./dist/effect-profile.js"
|
|
96
100
|
},
|
|
101
|
+
"./measured-session": {
|
|
102
|
+
"types": "./dist/measured-session.d.ts",
|
|
103
|
+
"default": "./dist/measured-session.js"
|
|
104
|
+
},
|
|
105
|
+
"./measured-order": {
|
|
106
|
+
"types": "./dist/measured-order.d.ts",
|
|
107
|
+
"default": "./dist/measured-order.js"
|
|
108
|
+
},
|
|
97
109
|
"./execution-retention": {
|
|
98
110
|
"types": "./dist/execution-retention.d.ts",
|
|
99
111
|
"default": "./dist/execution-retention.js"
|
package/src/cli.ts
CHANGED
|
@@ -21,12 +21,15 @@ import { UnsafeGrantError } from "./grant-env.ts";
|
|
|
21
21
|
import { applyInit, countDeclaring, planInit, type InitPlan } from "./init.ts";
|
|
22
22
|
import { registeredWorkspaceIds } from "./workspace.ts";
|
|
23
23
|
import { discoverSkillPackages, skillPackageRoots, type RefusedSkill, type SkillPackage } from "./skill-packages.ts";
|
|
24
|
+
import { declareWork } from "./work-command.ts";
|
|
24
25
|
|
|
25
26
|
const USAGE = `pi-daddy — capability governance for pi sub-agents
|
|
26
27
|
|
|
27
28
|
Usage:
|
|
28
29
|
pi-daddy init [--force] [--dir <path>] scaffold .pi/skills/ and .pi/grants.env from installed
|
|
29
30
|
packages that declare skills (package.json "pi": {"skills": …})
|
|
31
|
+
pi-daddy work add --id <id> --outcome <text> [--dir <path>]
|
|
32
|
+
declare one current obligation for ordinary delegation
|
|
30
33
|
pi-daddy --help | --version
|
|
31
34
|
|
|
32
35
|
init copies each declared SKILL.md into .pi/skills/ and writes a grant naming exactly what those files
|
|
@@ -38,9 +41,11 @@ placeholder and stays unspawnable until you fill it in. Capabilities that can ch
|
|
|
38
41
|
It never rewrites .pi/grants.env — delete that file if you want it regenerated.`;
|
|
39
42
|
|
|
40
43
|
export interface ParsedArgs {
|
|
41
|
-
command: "init" | "help" | "version";
|
|
44
|
+
command: "init" | "work-add" | "help" | "version";
|
|
42
45
|
dir?: string;
|
|
43
46
|
force: boolean;
|
|
47
|
+
id?: string;
|
|
48
|
+
outcome?: string;
|
|
44
49
|
/** Non-empty means refuse: argv said something this program does not understand. */
|
|
45
50
|
errors: string[];
|
|
46
51
|
}
|
|
@@ -58,16 +63,28 @@ export function parseArgs(argv: string[]): ParsedArgs {
|
|
|
58
63
|
if (args.includes("--help") || args.includes("-h")) return { command: "help", force: false, errors: [] };
|
|
59
64
|
if (args.includes("--version") || args.includes("-v")) return { command: "version", force: false, errors: [] };
|
|
60
65
|
|
|
61
|
-
const [command, ...
|
|
62
|
-
if (command !== "init") return { command: "help", force: false, errors: [`unknown command "${command}"`] };
|
|
66
|
+
const [command, ...tail] = args;
|
|
67
|
+
if (command !== "init" && command !== "work") return { command: "help", force: false, errors: [`unknown command "${command}"`] };
|
|
68
|
+
const work = command === "work";
|
|
69
|
+
const rest = work && tail[0] === "add" ? tail.slice(1) : tail;
|
|
70
|
+
if (work && tail[0] !== "add") return { command: "work-add", force: false, errors: ["work needs subcommand add"] };
|
|
63
71
|
|
|
64
72
|
const errors: string[] = [];
|
|
65
73
|
let dir: string | undefined;
|
|
66
74
|
let force = false;
|
|
75
|
+
let id: string | undefined, outcome: string | undefined;
|
|
67
76
|
for (let i = 0; i < rest.length; i += 1) {
|
|
68
77
|
const arg = rest[i];
|
|
69
|
-
if (arg === "--force") {
|
|
78
|
+
if (arg === "--force" && !work) {
|
|
70
79
|
force = true;
|
|
80
|
+
} else if (arg === "--id" && work) {
|
|
81
|
+
const value = rest[i + 1];
|
|
82
|
+
if (value === undefined || value.startsWith("-")) errors.push("--id needs a value");
|
|
83
|
+
else { id = value; i += 1; }
|
|
84
|
+
} else if (arg === "--outcome" && work) {
|
|
85
|
+
const value = rest[i + 1];
|
|
86
|
+
if (value === undefined || value.startsWith("-")) errors.push("--outcome needs text");
|
|
87
|
+
else { outcome = value; i += 1; }
|
|
71
88
|
} else if (arg === "--dir") {
|
|
72
89
|
const value = rest[i + 1];
|
|
73
90
|
// A flag is not a path. Without this, `--dir --force` consumed the flag as the directory AND left
|
|
@@ -87,6 +104,11 @@ export function parseArgs(argv: string[]): ParsedArgs {
|
|
|
87
104
|
}
|
|
88
105
|
}
|
|
89
106
|
|
|
107
|
+
if (work) {
|
|
108
|
+
if (!id && !errors.some(error => error.startsWith("--id"))) errors.push("--id needs a value");
|
|
109
|
+
if (!outcome && !errors.some(error => error.startsWith("--outcome"))) errors.push("--outcome needs text");
|
|
110
|
+
return { command: "work-add", force: false, errors, ...(id ? { id } : {}), ...(outcome ? { outcome } : {}), ...(dir ? { dir } : {}) };
|
|
111
|
+
}
|
|
90
112
|
return { command: "init", dir, force, errors };
|
|
91
113
|
}
|
|
92
114
|
|
|
@@ -260,6 +282,21 @@ export async function main(argv: string[]): Promise<number> {
|
|
|
260
282
|
console.log(USAGE);
|
|
261
283
|
return 0;
|
|
262
284
|
}
|
|
285
|
+
if (parsed.command === "work-add") {
|
|
286
|
+
const declared = await declareWork({
|
|
287
|
+
cwd: resolvePath(parsed.dir ?? process.cwd()),
|
|
288
|
+
id: parsed.id!,
|
|
289
|
+
outcome: parsed.outcome!,
|
|
290
|
+
});
|
|
291
|
+
console.log(
|
|
292
|
+
`pi-daddy work: declared ${declared.id}\n` +
|
|
293
|
+
` ledger ${declared.ledgerPath}\n` +
|
|
294
|
+
` selection ${declared.statePath}\n\n` +
|
|
295
|
+
`Ordinary governed delegations from this project will record attempts after the Pi extension loads it.\n` +
|
|
296
|
+
`Run /reload in an existing Pi session, then /grants dashboard. Runtime success is not acceptance.`,
|
|
297
|
+
);
|
|
298
|
+
return 0;
|
|
299
|
+
}
|
|
263
300
|
|
|
264
301
|
// ABSOLUTE, always. `readSkill` compares a resolved entry against the package directory, and a relative
|
|
265
302
|
// `--dir` made that comparison false for every entry: every declared skill of every package was reported
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { isAbsolute, resolve, join } from "node:path";
|
|
4
|
+
import { createDashboardHost, dashboardHostDigest, dashboardHostRequestDigest, type DashboardHostAuthority, type DashboardHostConfig, type DashboardHostRequest } from "./dashboard-host.ts";
|
|
5
|
+
import type { DashboardHarness } from "./dashboard-host-contract.ts";
|
|
6
|
+
import { loadedDashboardHarnessDigest } from "./dashboard-harness.ts";
|
|
7
|
+
import { createIntentBudget, openResourceBudget, resourceBindingDigest } from "./resource-budget.ts";
|
|
8
|
+
import { dispatchRequestDigest, type DispatchRequest } from "./dispatch-control.ts";
|
|
9
|
+
import { serveDashboardHost } from "./dashboard-host-transport.ts";
|
|
10
|
+
import { rebindDeclaredWork, type DeclaredWorkState } from "./work-command.ts";
|
|
11
|
+
import { ordinaryCancellation, ordinaryCancellationDigest, type OrdinaryChildren, type OrdinaryTarget } from "./ordinary-children.ts";
|
|
12
|
+
import { dataDigest } from "./debrief-contract.ts";
|
|
13
|
+
import { bindWorkIntent } from "./intent-application.ts";
|
|
14
|
+
import { intentKey, intentRequest, intentRequestDigest, type IntentPriority, type IntentRequest, type IntentSelection } from "./intent-control.ts";
|
|
15
|
+
import { parseWorkLedgerText, type WorkRevisionEvent, type WorkSnapshotEvent } from "./work-ledger.ts";
|
|
16
|
+
import { resolveWorkSnapshotText } from "./work-ledger-snapshot.ts";
|
|
17
|
+
|
|
18
|
+
export interface DailyIntentAction {key:string;label:string;action:"revise-scope"|"reprioritize"|"select-alternative";fromSelection:IntentSelection;events:(WorkRevisionEvent|WorkSnapshotEvent)[];selection:IntentSelection;priorities:IntentPriority[]}
|
|
19
|
+
export interface DailyDashboardHostInput {id:string;cwd:string;directory:string;socketPath?:string;declared:DeclaredWorkState;ordinary:OrdinaryChildren;harness:DashboardHarness;author:string;priorities?:IntentPriority[];intentActions?:DailyIntentAction[];presence?:()=>{present:boolean;closing:boolean;evidenceDigest:string;expiresAt:number}|null;onDeclaredWorkChanged?:(state:DeclaredWorkState)=>void|Promise<void>}
|
|
20
|
+
const idPattern=/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
21
|
+
const hash=(value:string)=>createHash("sha256").update(value).digest("hex");
|
|
22
|
+
const same=(a:unknown,b:unknown)=>intentKey(a)===intentKey(b);
|
|
23
|
+
/** Recorded P01 snapshots become labelled proposals; choosing a label still traverses intent validation and exact host/CAS authority. */
|
|
24
|
+
export function discoverDailyIntentActions(text:string,selection:IntentSelection,priorities:readonly IntentPriority[]):DailyIntentAction[]{
|
|
25
|
+
const parsed=parseWorkLedgerText(text);if(!parsed.complete||parsed.events.length>512)throw Error("bounded complete recorded proposal ledger required");const label=(value:string)=>value.length<=80?value:`${value.slice(0,67)}…${hash(value).slice(0,12)}`;
|
|
26
|
+
const current=resolveWorkSnapshotText(text,selection);if(current.scopeState!=="valid"||!current.scope||!current.snapshot)throw Error("current declared work unavailable");
|
|
27
|
+
const actions:DailyIntentAction[]=[];for(const priority of priorities.slice(1)){
|
|
28
|
+
const ordered=[priority,...priorities.filter(x=>!same(x.obligation,priority.obligation))].map((x,rank)=>({...x,rank}));
|
|
29
|
+
actions.push({key:`priority-${hash(intentKey(priority.obligation)).slice(0,12)}`,label:`Put ${label(priority.obligation.id)} first`,action:"reprioritize",fromSelection:selection,events:[],selection,priorities:ordered});
|
|
30
|
+
}
|
|
31
|
+
for(const event of parsed.events){if(event.event!=="work_snapshot")continue;const candidate={snapshot:{id:event.payload.snapshot.snapshotId,digest:event.payload.snapshot.digest},event:{eventId:event.eventId,digest:event.digest}};if(same(candidate,selection))continue;
|
|
32
|
+
const next=resolveWorkSnapshotText(text,candidate);if(next.scopeState!=="valid"||!next.scope||!next.snapshot||next.snapshot.bindings.length>32)continue;
|
|
33
|
+
const action=next.scope.digest===current.scope.digest?"select-alternative":next.scope.predecessor&&same(next.scope.predecessor,{kind:current.scope.kind,id:current.scope.id,revision:current.scope.revision,digest:current.scope.digest})?"revise-scope":null;if(!action)continue;
|
|
34
|
+
actions.push({key:`${action==="revise-scope"?"scope":"alternative"}-${hash(intentKey(candidate)).slice(0,12)}`,label:`Select recorded ${action==="revise-scope"?"scope":"alternative"} ${label(event.payload.snapshot.snapshotId)}`,action,fromSelection:selection,events:[],selection:candidate,priorities:next.snapshot.bindings.map((b,rank)=>({obligation:b.obligation,rank}))});
|
|
35
|
+
}
|
|
36
|
+
return actions.sort((a,b)=>a.key.localeCompare(b.key)).slice(0,8);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** One explicitly started, process-owned daily host. No authority, controller or prior host is recovered from disk. */
|
|
40
|
+
export async function startDailyDashboardHost(input:DailyDashboardHostInput){
|
|
41
|
+
if(!idPattern.test(input.id)||!isAbsolute(input.cwd)||resolve(input.cwd)!==input.cwd||!isAbsolute(input.directory)||resolve(input.directory)!==input.directory||!input.author.trim()||input.author.length>256)throw Error("explicit bounded daily host configuration required");
|
|
42
|
+
const harnessDigest=loadedDashboardHarnessDigest(input.harness);if(!harnessDigest)throw Error("loaded skill-harness bridge required");
|
|
43
|
+
if(input.declared.ledgerPath!==join(input.cwd,".pi","work.jsonl"))throw Error("current declared work must belong to this host project");
|
|
44
|
+
await mkdir(input.directory,{mode:0o700});const archiveRoot=join(input.directory,"archive");await mkdir(archiveRoot,{mode:0o700});
|
|
45
|
+
const reference=input.harness.retainArchiveSource(archiveRoot,{sourceId:`host-${input.id}-bootstrap`,parser:{id:"daily-host-bootstrap",version:"1"},retention:"exact",bytes:Buffer.from("explicit daily host bootstrap")});
|
|
46
|
+
const component={kind:"detector",id:"coverage_gap",version:"work-signals-v1:declared-work-v1",population:`daily:${input.id}`};
|
|
47
|
+
const trustInput={archiveRoot,component,seed:hash(`daily-host\0${input.id}`),maxUnflagged:0,cohort:[{incidentId:"bootstrap",manifestId:reference.manifestId,flagged:true,split:"calibration"}],exposure:null};
|
|
48
|
+
const trustPolicyId=input.harness.trustPolicyDigest(trustInput),trustDirectory=join(input.directory,"trust");input.harness.createTrustLifecycle(trustDirectory,trustInput,[trustPolicyId]);
|
|
49
|
+
const workRelative="work.jsonl",factsRelative="facts.jsonl",workMirror=join(input.directory,workRelative);await writeFile(workMirror,await readFile(input.declared.ledgerPath),{mode:0o600,flag:"wx"});
|
|
50
|
+
const refreshWork=async()=>{const temporary=`${workMirror}.${process.pid}.${randomUUID()}.tmp`;try{await writeFile(temporary,await readFile(input.declared.ledgerPath),{mode:0o600,flag:"wx"});await rename(temporary,workMirror);}finally{await rm(temporary,{force:true});}};
|
|
51
|
+
const facts={scopeDigest:input.declared.selectedSnapshot.snapshot.digest,version:"declared-work-v1",population:`daily:${input.id}`,expectedWaits:[],checkpoints:[],violations:[],priorAccepted:[]};await writeFile(join(input.directory,factsRelative),JSON.stringify(facts)+"\n",{mode:0o600,flag:"wx"});
|
|
52
|
+
const policy={version:"archive-policy-v2",id:`daily-${input.id}`,revision:"1",sourceRoot:input.directory,archiveRoot,maxBytes:8*1024*1024,retention:"exact",expiresAt:new Date(Date.now()+24*60*60*1000).toISOString(),sources:[{id:"work",path:workRelative,parser:{id:"pi-daddy-work-ledger",version:"4"},contentPolicy:"manifest-only"},{id:"facts",path:factsRelative,parser:{id:"pi-daddy-work-signal-facts",version:"1"},contentPolicy:"manifest-only"}]};
|
|
53
|
+
const policyPath=join(input.directory,"archive-policy.json"),policyBytes=JSON.stringify(policy);await writeFile(policyPath,policyBytes,{mode:0o600,flag:"wx"});
|
|
54
|
+
const authorityDigest=dataDigest({kind:"daily-dashboard-host",id:input.id,cwd:input.cwd}),initialPriorities=input.priorities??[{obligation:input.declared.obligation,rank:0}];
|
|
55
|
+
const workIntent=await bindWorkIntent({path:input.declared.ledgerPath,grantLedgerPath:input.declared.grantLedgerPath,selection:input.declared.selectedSnapshot,priorities:initialPriorities});
|
|
56
|
+
const budget=await createIntentBudget({directory:join(input.directory,"budget"),authorityDigest,limits:{maxAttempts:6,maxInputBytes:65536,maxConcurrent:3}},workIntent);
|
|
57
|
+
const config:DashboardHostConfig={version:"producer-dashboard-host-v1",trustDirectory,trustPolicyId,archiveRoot,scope:`daily:${input.id}`,author:input.author,policyPath,policySha256:hash(policyBytes),sources:[{id:"work",kind:"work"},{id:"facts",kind:"facts"}],selection:input.declared.selectedSnapshot,cases:null,blind:null,budgetDigest:resourceBindingDigest(budget),experimentDigest:null,harnessArtifactDigest:harnessDigest,ordinaryDigest:input.ordinary.bindingDigest};
|
|
58
|
+
const proposals=input.intentActions??[];if(proposals.length>8||new Set(proposals.map(x=>x.key)).size!==proposals.length||proposals.some(x=>!idPattern.test(x.key)||typeof x.label!=="string"||!x.label.trim()||Buffer.byteLength(x.label)>120))throw Error("bounded friendly intent actions required");
|
|
59
|
+
const requestDigests=new Set<string>(),dispatchDigests=new Set<string>(),ordinaryDigests=new Set<string>();let host:ReturnType<typeof createDashboardHost>,factsResultManifest:string|null=null,workSelection:IntentSelection=input.declared.selectedSnapshot,declaredState=input.declared,reboundSnapshotId:string|null=null;
|
|
60
|
+
const authority=():DashboardHostAuthority=>({hostDigests:[dashboardHostDigest(config)],requestDigests:[...requestDigests],workContext:{selectedSnapshot:workSelection,authority:null},dispatch:{authorityDigest,requestDigests:[...dispatchDigests]},experiment:null,ordinary:{bindingDigest:input.ordinary.bindingDigest,requestDigests:[...ordinaryDigests]}});
|
|
61
|
+
const request=(context:{hostDigest:string;selectionDigest:string;tip:string},operation:DashboardHostRequest["operation"],payload:unknown,requestId:string):DashboardHostRequest=>({version:"1.0",requestId,hostDigest:context.hostDigest,selectionDigest:context.selectionDigest,expectedTip:context.tip,operation,payload});
|
|
62
|
+
host=createDashboardHost({harness:input.harness,config,budget,ordinary:input.ordinary,authority,beforeObservation:sourceId=>sourceId==="work"?refreshWork():Promise.resolve(),humanActions:async context=>{
|
|
63
|
+
const state=await openResourceBudget(budget).controls(null).inspect(),action=state.paused?"resume-dispatch":"pause-dispatch",key=state.paused?"resume-dispatch":"pause-new-dispatch";
|
|
64
|
+
const native:DispatchRequest={version:"1.0",requestId:`${action}:${state.revision}`,bindingDigest:config.budgetDigest,expectedRevision:state.revision,action,targetExecutionId:null};dispatchDigests.add(dispatchRequestDigest(native));
|
|
65
|
+
const hostRequest=request(context,"dispatch",native,`host-${action}:${state.revision}`);requestDigests.add(dashboardHostRequestDigest(hostRequest));
|
|
66
|
+
const previous=context.observations.find(x=>x.sourceId==="work")?.checkpointId??null,refresh=request(context,"observe",{sourceId:"work",previousCheckpointId:previous,facts:context.observations.find(x=>x.sourceId==="facts")?.checkpointId?factsResultManifest:null},`refresh-work:${state.revision}:${context.tip.slice(0,12)}`);requestDigests.add(dashboardHostRequestDigest(refresh));
|
|
67
|
+
const intentState=await openResourceBudget(budget).intentControls(null).inspect(),automatic=discoverDailyIntentActions(await readFile(input.declared.ledgerPath,"utf8"),intentState.selection,intentState.priorities);
|
|
68
|
+
const candidates=[...proposals,...automatic].filter((p,index,all)=>all.findIndex(x=>x.key===p.key)===index).filter(p=>p.action==="reprioritize"||Boolean(input.onDeclaredWorkChanged)).slice(0,8);
|
|
69
|
+
const intents=(input.ordinary.quiescent()?candidates:[]).filter(p=>intentKey(p.fromSelection)===intentKey(intentState.selection)).map(p=>{
|
|
70
|
+
const native=intentRequest({version:"intent-request-v1",requestId:`${p.key}:${intentState.revision}`,bindingDigest:config.budgetDigest,expectedRevision:intentState.revision,expectedSelection:intentState.selection,action:p.action,events:p.events,selection:p.selection,priorities:p.priorities});dispatchDigests.add(intentRequestDigest(native));
|
|
71
|
+
const proposed=request(context,"intent",native,`host-intent:${p.key}:${intentState.revision}`);requestDigests.add(dashboardHostRequestDigest(proposed));return {key:p.key,label:p.label,request:proposed};
|
|
72
|
+
});
|
|
73
|
+
const ordinary=input.ordinary.inspect() as {revision:number;children:{target:OrdinaryTarget;state:string}[]},accounting=await openResourceBudget(budget).inspect(),presence=input.presence?.();
|
|
74
|
+
const presentation:{key:string;label:string;request:DashboardHostRequest}[]=[];
|
|
75
|
+
if(context.preparedPresentationDigest){const shown=request(context,"presented",{frameDigest:context.preparedPresentationDigest},`host-presented:${state.revision}:${context.tip.slice(0,12)}`);requestDigests.add(dashboardHostRequestDigest(shown));presentation.push({key:"acknowledge-case-cards",label:"Acknowledge these case cards are visible",request:shown});}
|
|
76
|
+
else if(presence?.present===true&&presence.closing===true&&presence.expiresAt>Date.now()&&state.paused&&accounting.active===0&&!ordinary.children.some(row=>row.state==="active")){const present=request(context,"present",{userPresent:true,closing:true,evidenceDigest:presence.evidenceDigest,dispatchRevision:state.revision},`host-present:${state.revision}:${context.tip.slice(0,12)}`);requestDigests.add(dashboardHostRequestDigest(present));presentation.push({key:"prepare-case-cards",label:"Prepare up to five retained case cards",request:present});}
|
|
77
|
+
const cancellations=ordinary.children.filter(row=>row.state==="active").sort((a,b)=>a.target.executionId.localeCompare(b.target.executionId)).slice(0,8).map(row=>{
|
|
78
|
+
const native=ordinaryCancellation({version:"ordinary-cancel-v1",requestId:`cancel:${row.target.executionId.slice(5)}`,bindingDigest:input.ordinary.bindingDigest,expectedRevision:ordinary.revision,target:row.target});ordinaryDigests.add(ordinaryCancellationDigest(native));
|
|
79
|
+
const cancel=request(context,"ordinary-cancel",native,`host-cancel:${row.target.executionId.slice(5)}:${ordinary.revision}`);requestDigests.add(dashboardHostRequestDigest(cancel));
|
|
80
|
+
return {key:`cancel-${row.target.executionId.replace(":","-")}`,label:`Cancel running attempt ${row.target.executionId}`,request:cancel};
|
|
81
|
+
});
|
|
82
|
+
return [{key,label:state.paused?"Resume new governed dispatch":"Pause new governed dispatch; running children continue",request:hostRequest},{key:"refresh-current-work",label:reboundSnapshotId?`Capture rebound declared work; next dispatch uses ${reboundSnapshotId}`:"Capture current declared work and attempts",request:refresh},...intents,...presentation,...cancellations];
|
|
83
|
+
},afterIntentApplied:async selection=>{const rebound=await rebindDeclaredWork(declaredState,selection);declaredState=rebound;workSelection=selection;reboundSnapshotId=selection.snapshot.id;await input.onDeclaredWorkChanged?.(rebound);},presence:input.presence});
|
|
84
|
+
const before=await host.frame(),observeFacts=request({hostDigest:host.hostDigest,selectionDigest:before.selectionDigest,tip:before.tip},"observe",{sourceId:"facts",previousCheckpointId:null,facts:null},"observe-current-facts");requestDigests.add(dashboardHostRequestDigest(observeFacts));const factsResult=await host.action(observeFacts) as {result:{sourceManifestId:string}};factsResultManifest=factsResult.result.sourceManifestId;
|
|
85
|
+
const afterFacts=await host.frame(),observeWork=request({hostDigest:host.hostDigest,selectionDigest:afterFacts.selectionDigest,tip:afterFacts.tip},"observe",{sourceId:"work",previousCheckpointId:null,facts:factsResult.result.sourceManifestId},"observe-current-work");requestDigests.add(dashboardHostRequestDigest(observeWork));await host.action(observeWork);
|
|
86
|
+
const socketPath=input.socketPath??join(input.directory,"dashboard.sock"),server=await serveDashboardHost(socketPath,host);
|
|
87
|
+
let closed=false;return Object.freeze({host,socketPath,directory:input.directory,async close(){if(closed)return;closed=true;await server.close();}});
|
|
88
|
+
}
|
package/src/dashboard-cli.ts
CHANGED
|
@@ -52,7 +52,10 @@ export interface DashboardFrameOptions {
|
|
|
52
52
|
}
|
|
53
53
|
export async function dashboardHostAction(host:DashboardConnection,line:string){
|
|
54
54
|
if(!isDashboardConnection(host)||Buffer.byteLength(line)>60000)throw Error("genuine bounded dashboard connection required");
|
|
55
|
-
|
|
55
|
+
const command=line.trim();
|
|
56
|
+
if(command.startsWith("{"))return host.action(dashboardHostRequest(parseRetentionJson(command,60000) as DashboardHostRequest));
|
|
57
|
+
if(!/^[a-zA-Z0-9:_-]{1,64}$/.test(command))throw Error("dashboard action must be a listed command key");
|
|
58
|
+
return host.humanAction(command);
|
|
56
59
|
}
|
|
57
60
|
|
|
58
61
|
function shellQuote(value: string): string {
|
|
@@ -96,9 +99,11 @@ export async function dashboardFrame(options: DashboardFrameOptions): Promise<st
|
|
|
96
99
|
try{view=await options.connected.frame();}catch{return "PI-DADDY — CONNECTED HOST UNAVAILABLE\nNo prior acceptance, presentation or continuity substituted. No action retried.";}
|
|
97
100
|
if(options.dailyJson||options.debriefJson)return JSON.stringify(view);
|
|
98
101
|
const source=view.source as {daily?:Parameters<typeof renderDailyView>[0]}|null;
|
|
102
|
+
const actions=(view.actions as {key:string;label:string}[]|undefined)??[];
|
|
99
103
|
return ["PI-DADDY — CONNECTED HOST (snapshot; identity/acceptance not authenticated)",source?.daily?renderDailyView(source.daily,options.width):"Source unavailable; no previous acceptance substituted.",
|
|
100
104
|
view.debrief?renderDebrief(view.debrief as Parameters<typeof renderDebrief>[0],options.width):"Debrief deferred/closed. No implicit presentation or steering.",
|
|
101
|
-
`Attention reserved ${view.attention.attentionUsed}/5 (not proof of delivery). Tip ${view.tip}.`,
|
|
105
|
+
`Attention reserved ${view.attention.attentionUsed}/5 (not proof of delivery). Tip ${view.tip}.`,
|
|
106
|
+
actions.length?`ACTIONS — type one command and Enter\n${actions.map(action=>` ${action.key} — ${action.label}`).join("\n")}`:"ACTIONS — none currently authorized; refresh never acts."].join("\n\n");
|
|
102
107
|
}
|
|
103
108
|
if (options.debrief || options.debriefJson) {
|
|
104
109
|
if (!isDebriefPresenter(options.debrief)) return "PI-DADDY — DEBRIEF UNAVAILABLE: genuine presenter/host missing";
|
|
@@ -88,22 +88,31 @@ export async function offerDashboardHandshake(input: DashboardHandshakeInput): P
|
|
|
88
88
|
if (preference) return "suppressed";
|
|
89
89
|
const plugin = await inspectDashboardPlugin(exec, input.pluginRoot);
|
|
90
90
|
if (plugin.state === "compatible") return "already-installed";
|
|
91
|
-
|
|
91
|
+
const replacingDifferentPackage = plugin.state === "incompatible" && plugin.incompatibility === "package-root";
|
|
92
|
+
if (plugin.state !== "absent" && !replacingDifferentPackage) {
|
|
92
93
|
input.ui.notify(`pi-daddy dashboard: ${plugin.diagnostic}`, "error");
|
|
93
94
|
return "failed";
|
|
94
95
|
}
|
|
95
96
|
|
|
97
|
+
const affirmative = replacingDifferentPackage ? "Relink and open" : "Install and open";
|
|
98
|
+
const choices = replacingDifferentPackage ? [affirmative, "Not now"] : [affirmative, "Not now", "Never ask"];
|
|
96
99
|
const choice = await input.ui.select(
|
|
97
|
-
|
|
98
|
-
|
|
100
|
+
replacingDifferentPackage
|
|
101
|
+
? "The pi-daddy dashboard is linked from a different package. Relink this bundled copy and open it?"
|
|
102
|
+
: "Herdr detected. Install and open the pi-daddy dashboard?",
|
|
103
|
+
choices,
|
|
99
104
|
);
|
|
100
|
-
if (choice
|
|
105
|
+
if (!choices.includes(choice ?? "")) {
|
|
101
106
|
// Escape, timeout and UI teardown return undefined. None is an operator choice, so none may become a
|
|
102
107
|
// durable preference merely because the dialog disappeared.
|
|
103
108
|
input.ui.notify("pi-daddy dashboard: installation prompt dismissed; no preference was stored.", "info");
|
|
104
109
|
return "deferred";
|
|
105
110
|
}
|
|
106
|
-
if (choice !==
|
|
111
|
+
if (choice !== affirmative) {
|
|
112
|
+
if (replacingDifferentPackage) {
|
|
113
|
+
input.ui.notify("pi-daddy dashboard: existing link unchanged; no preference was stored.", "info");
|
|
114
|
+
return "deferred";
|
|
115
|
+
}
|
|
107
116
|
const recorded = choice === "Never ask" ? "never" : "not-now";
|
|
108
117
|
await savePreference(input.preferencePath, recorded);
|
|
109
118
|
input.ui.notify(
|
package/src/dashboard-harness.ts
CHANGED
|
@@ -6,6 +6,15 @@ import { dataDigest,byteDigest,detached,sha } from "./debrief-contract.ts";
|
|
|
6
6
|
import { DASHBOARD_HARNESS_PIN,type DashboardHarness } from "./dashboard-host-contract.ts";
|
|
7
7
|
export interface DashboardHarnessArtifact {version:"dashboard-harness-artifact-v1";sourceCommit:typeof DASHBOARD_HARNESS_PIN;files:Record<string,string>;typeboxRoot:string;typeboxPackageSha256:string}
|
|
8
8
|
const loaded=new WeakMap<object,string>();
|
|
9
|
+
export const DASHBOARD_HARNESS_BRIDGE_SOURCE="d123257e53d48a2cad6919708976b5371dc7590e";
|
|
10
|
+
const bridgeFunctions=["learningJournal","createTrustLifecycle","openTrustLifecycle","trustPolicyDigest","archivePolicyBinding","ingestPolicySource","readArchiveCheckpoint","readArchiveSource","retainArchiveSource","captureArchivedWorkSignals","readRetainedExecution","projectRetainedExecutions","createWorkCaseReviewer","createWorkSignalReviewer","retainBlindIntervention","openBlindIntervention","retainLearningLifecycle","readLearningLifecycle"] as const;
|
|
11
|
+
/** Adopt the API published by the already-loaded skill-harness extension. Same-process source identity is not human authentication. */
|
|
12
|
+
export function adoptDashboardHarnessBridge(input:unknown):DashboardHarness{
|
|
13
|
+
const value=input as {version?:unknown;sourceCommit?:unknown;api?:unknown};
|
|
14
|
+
if(!value||typeof value!=="object"||Object.keys(value).sort().join()!=="api,sourceCommit,version"||value.version!=="skill-harness-dashboard-bridge-v1"||value.sourceCommit!==DASHBOARD_HARNESS_BRIDGE_SOURCE)throw Error("exact supported harness bridge required");
|
|
15
|
+
const api=value.api as DashboardHarness;if(!api||typeof api!=="object"||!Object.isFrozen(api)||bridgeFunctions.some(name=>typeof (api as unknown as Record<string,unknown>)[name]!=="function"))throw Error("frozen harness API with required source jobs required");
|
|
16
|
+
const digest=dataDigest({version:value.version,sourceCommit:value.sourceCommit,functions:bridgeFunctions});loaded.set(api,digest);return api;
|
|
17
|
+
}
|
|
9
18
|
export const loadedDashboardHarnessDigest=(h:unknown):string|null=>typeof h==="object"&&h!==null?loaded.get(h)??null:null;
|
|
10
19
|
async function privateDirectory(path:string){const stat=await lstat(path);if(!isAbsolute(path)||await realpath(path)!==path||!stat.isDirectory()||stat.mode&0o077||stat.uid!==process.getuid?.())throw Error("canonical private owned harness directory required");}
|
|
11
20
|
async function bytes(path:string){if(await realpath(path)!==path)throw Error("harness artifact alias");const fd=await open(path,constants.O_RDONLY|constants.O_NOFOLLOW|constants.O_NONBLOCK);try{const stat=await fd.stat();if(!stat.isFile()||stat.nlink!==1||stat.mode&0o022||stat.size>1048576)throw Error("bounded immutable artifact required");const value=Buffer.alloc(stat.size+1),n=await fd.read(value,0,value.length,0),after=await fd.stat();if(n.bytesRead!==stat.size||after.size!==stat.size||after.mtimeMs!==stat.mtimeMs||after.ctimeMs!==stat.ctimeMs)throw Error("artifact changed during read");return value.subarray(0,n.bytesRead);}finally{await fd.close();}}
|
|
@@ -21,7 +30,7 @@ export async function loadDashboardHarness(artifactRoot:string,input:DashboardHa
|
|
|
21
30
|
if(content.get("package.json")?.toString()!=='{"type":"module"}')throw Error("explicit ESM artifact root required");
|
|
22
31
|
const directory=await mkdtemp(join(parent,"loaded-harness-"));for(const[p,b]of content){await mkdir(dirname(join(directory,p)),{recursive:true,mode:0o700});await writeFile(join(directory,p),b,{flag:"wx",mode:0o400});}
|
|
23
32
|
await mkdir(join(directory,"node_modules"));await symlink(manifest.typeboxRoot,join(directory,"node_modules/typebox"));
|
|
24
|
-
const names=["learning-journal","trust-lifecycle","archive-policy","archive-observer","archive-checkpoint","archived-work","execution-retention-archive","work-case-review","blind-intervention","evidence-archive","work-signal-observation","work-signal-cases","work-case-archive"];
|
|
33
|
+
const names=["learning-journal","trust-lifecycle","archive-policy","archive-observer","archive-checkpoint","archived-work","execution-retention-archive","work-case-review","blind-intervention","learning-lifecycle","evidence-archive","work-signal-observation","work-signal-cases","work-case-archive"];
|
|
25
34
|
const api=Object.assign({},...await Promise.all([...names.map(n=>"packages/adapters/src/"+n+".js"),"core.js"].map(p=>{if(!content.has(p))throw Error("missing genuine harness entry");return import(pathToFileURL(join(directory,p)).href);}))) as DashboardHarness;
|
|
26
35
|
for(const[p,digest]of files)if(byteDigest(await bytes(join(directory,p)))!==digest)throw Error("loaded harness bytes changed");
|
|
27
36
|
Object.freeze(api);const digest=dataDigest(manifest);loaded.set(api,digest);return Object.freeze({api,artifactDigest:digest,directory,manifest});
|
package/src/dashboard-herdr.ts
CHANGED
|
@@ -97,6 +97,7 @@ export type DashboardPluginState = "absent" | "disabled" | "compatible" | "incom
|
|
|
97
97
|
export interface DashboardPluginStatus {
|
|
98
98
|
state: DashboardPluginState;
|
|
99
99
|
diagnostic: string;
|
|
100
|
+
incompatibility?: "package-root" | "protocol";
|
|
100
101
|
plugin?: Record<string, unknown>;
|
|
101
102
|
}
|
|
102
103
|
|
|
@@ -116,15 +117,8 @@ export async function inspectDashboardPlugin(
|
|
|
116
117
|
const plugins = Array.isArray(parsed.result?.plugins) ? parsed.result.plugins as Record<string, unknown>[] : [];
|
|
117
118
|
const plugin = plugins.find((candidate) => candidate.plugin_id === DASHBOARD_PLUGIN_ID);
|
|
118
119
|
if (!plugin) return { state: "absent", diagnostic: `Herdr plugin ${DASHBOARD_PLUGIN_ID} is not installed.` };
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
if (expectedPluginRoot && (typeof plugin.plugin_root !== "string" || resolve(plugin.plugin_root) !== resolve(expectedPluginRoot))) {
|
|
122
|
-
return {
|
|
123
|
-
state: "incompatible",
|
|
124
|
-
diagnostic: `Herdr plugin ${DASHBOARD_PLUGIN_ID} is linked from a different package; relink the bundled copy.`,
|
|
125
|
-
plugin,
|
|
126
|
-
};
|
|
127
|
-
}
|
|
120
|
+
// Protocol and provenance both precede enabled state. A root-only repair is offered only when the
|
|
121
|
+
// registered manifest already speaks this core's protocol; unknown software is never replaced by guesswork.
|
|
128
122
|
const major = protocolMajor(plugin.version);
|
|
129
123
|
if (major !== DASHBOARD_PROTOCOL_VERSION) {
|
|
130
124
|
return {
|
|
@@ -132,6 +126,15 @@ export async function inspectDashboardPlugin(
|
|
|
132
126
|
diagnostic:
|
|
133
127
|
`Herdr plugin ${DASHBOARD_PLUGIN_ID} uses protocol ${major ?? "unknown"}; ` +
|
|
134
128
|
`this pi-daddy core requires ${DASHBOARD_PROTOCOL_VERSION}. Relink both from the same package.`,
|
|
129
|
+
incompatibility: "protocol",
|
|
130
|
+
plugin,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
if (expectedPluginRoot && (typeof plugin.plugin_root !== "string" || resolve(plugin.plugin_root) !== resolve(expectedPluginRoot))) {
|
|
134
|
+
return {
|
|
135
|
+
state: "incompatible",
|
|
136
|
+
diagnostic: `Herdr plugin ${DASHBOARD_PLUGIN_ID} is linked from a different package; relink the bundled copy.`,
|
|
137
|
+
incompatibility: "package-root",
|
|
135
138
|
plugin,
|
|
136
139
|
};
|
|
137
140
|
}
|
|
@@ -5,20 +5,25 @@ import type { WorkProjectionContext, WorkFrozen } from "./work-ledger.ts";
|
|
|
5
5
|
import type { DispatchAuthority } from "./dispatch-control.ts";
|
|
6
6
|
import type { OrdinaryAuthority } from "./ordinary-children.ts";
|
|
7
7
|
import type { ExperimentAuthority } from "./experiment-contract.ts";
|
|
8
|
-
export const DASHBOARD_HARNESS_PIN = "
|
|
8
|
+
export const DASHBOARD_HARNESS_PIN = "d123257e53d48a2cad6919708976b5371dc7590e";
|
|
9
9
|
export interface HostEvent { id: string; prior: string | null; value: Record<string, unknown> }
|
|
10
10
|
export interface HostJournal { read(): HostEvent[]; append(prior: string, value: Record<string, unknown>): HostEvent }
|
|
11
11
|
/** Loaded trusted ports; an interface or caller-supplied pin is NOT module/human authentication. */
|
|
12
12
|
export interface DashboardHarness extends DebriefHarness {
|
|
13
13
|
learningJournal(path: string, initial?: Record<string, unknown>): HostJournal;
|
|
14
14
|
openTrustLifecycle(path: string): { history(): HostEvent[]; inspect(now: number): { policyId: string; attentionUsed: number; tip: string; grantExpansion: false }; expose(id: string, now: number): { mode: string; reason: string; replayed: boolean } };
|
|
15
|
+
createTrustLifecycle(path:string,input:unknown,authorized:readonly string[]):unknown;
|
|
16
|
+
trustPolicyDigest(input:unknown):string;
|
|
17
|
+
retainArchiveSource(root:string,input:unknown):{manifestId:string};
|
|
15
18
|
archivePolicyBinding(path: string, source: string): { policySha256: string; archiveRoot: string };
|
|
16
19
|
ingestPolicySource(path: string, source: string, previous?: string, expectedPolicy?: string): { checkpointId: string; sourceStatus: string; issues?: string[]; [key: string]: unknown };
|
|
17
20
|
readArchiveCheckpoint(root: string, checkpoint: string): { checkpoint: { sourceManifestId: string; issues: string[]; [key: string]: unknown } };
|
|
18
21
|
readArchiveSource(root: string, id: string): { status: string; bytes?: Uint8Array; reference?: { retention: string; sha256: string; [key: string]: unknown } };
|
|
19
|
-
captureArchivedWorkSignals(root: string, id: string, context: WorkFrozen<WorkProjectionContext>, facts: unknown): { caseBatchId: string; observationId: string; linkageManifestId: string };
|
|
22
|
+
captureArchivedWorkSignals(root: string, id: string, context: WorkFrozen<WorkProjectionContext>, facts: unknown): { caseBatchId: string; observationId: string; linkageManifestId: string; runtimeFactsManifestId: string };
|
|
20
23
|
readRetainedExecution(root: string, id: string): { projection: unknown };
|
|
21
24
|
projectRetainedExecutions(values: readonly unknown[]): unknown;
|
|
25
|
+
retainLearningLifecycle(root:string,input:unknown):string;
|
|
26
|
+
readLearningLifecycle(root:string,id:string):{version:string;state:string;predecessorManifestId:string|null;links:Record<string,unknown>};
|
|
22
27
|
}
|
|
23
28
|
export interface DashboardHostConfig {
|
|
24
29
|
version: "producer-dashboard-host-v1"; trustDirectory: string; trustPolicyId: string; archiveRoot: string; scope: string; author: string;
|
|
@@ -26,6 +31,7 @@ export interface DashboardHostConfig {
|
|
|
26
31
|
selection: WorkProjectionContext["selectedSnapshot"]; cases: CaseSelection | null; blind: DurableBlindBinding | null;
|
|
27
32
|
budgetDigest: string; experimentDigest: string | null; harnessArtifactDigest: string;
|
|
28
33
|
ordinaryDigest?: string;
|
|
34
|
+
learningLifecycleId?: string;
|
|
29
35
|
}
|
|
30
36
|
export interface DashboardHostRequest { version: "1.0"; requestId: string; hostDigest: string; expectedTip: string; selectionDigest: string; operation: "observe" | "present" | "presented" | "defer" | "debrief" | "dispatch" | "intent" | "cancel" | "dispatch-reconcile" | "intent-reconcile" | "ordinary-cancel"; payload: unknown }
|
|
31
37
|
export interface DashboardHostAuthority {
|
|
@@ -4,7 +4,7 @@ import { dirname, isAbsolute } from "node:path";
|
|
|
4
4
|
import { parseRetentionJson } from "./retention-json.ts";
|
|
5
5
|
import { isDashboardHost, dashboardHostRequest, type DashboardHost, type DashboardHostRequest } from "./dashboard-host.ts";
|
|
6
6
|
export const ENV_DASHBOARD_HOST_SOCKET="PI_DADDY_HOST_SOCKET";
|
|
7
|
-
export type DashboardConnection={frame:DashboardHost["frame"];action:DashboardHost["action"]};
|
|
7
|
+
export type DashboardConnection={frame:DashboardHost["frame"];action:DashboardHost["action"];humanAction:DashboardHost["humanAction"]};
|
|
8
8
|
const connections=new WeakSet<object>();
|
|
9
9
|
export const isDashboardConnection=(v:unknown):v is DashboardConnection=>isDashboardHost(v)||typeof v==="object"&&v!==null&&connections.has(v);
|
|
10
10
|
async function parent(path:string){if(!isAbsolute(path)||Buffer.byteLength(path)>100||await realpath(dirname(path))!==dirname(path))throw Error("canonical bounded private dashboard socket required");const s=await lstat(dirname(path));if(!s.isDirectory()||s.mode&0o077||s.uid!==process.getuid?.())throw Error("private owned dashboard socket parent required");}
|
|
@@ -13,9 +13,11 @@ export async function serveDashboardHost(path:string,host:DashboardHost){
|
|
|
13
13
|
if(!isDashboardHost(host))throw Error("original dashboard host required");await parent(path);const clients=new Set<Socket>(),tasks=new Set<Promise<void>>();
|
|
14
14
|
const server=createServer(socket=>{if(clients.size>=4){socket.destroy();return;}clients.add(socket);socket.on("close",()=>clients.delete(socket));socket.on("error",()=>{});socket.setTimeout(5000,()=>socket.destroy());let size=0,used=false;const chunks:Buffer[]=[];
|
|
15
15
|
socket.on("data",bytes=>{if(used)return;size+=bytes.length;if(size>65536){socket.destroy();return;}chunks.push(bytes);if(bytes.at(-1)!==10)return;used=true;
|
|
16
|
-
const task=(async()=>{try{const text=new TextDecoder("utf-8",{fatal:true}).decode(Buffer.concat(chunks));const r=parseRetentionJson(text,65536) as {operation:string;request?:DashboardHostRequest};let result:unknown;
|
|
16
|
+
const task=(async()=>{try{const text=new TextDecoder("utf-8",{fatal:true}).decode(Buffer.concat(chunks));const r=parseRetentionJson(text,65536) as {operation:string;request?:DashboardHostRequest;key?:string};let result:unknown;
|
|
17
17
|
if(Object.keys(r).join()==="operation"&&r.operation==="frame")result=await host.frame();
|
|
18
|
-
else if(Object.keys(r).sort().join()==="operation,request"&&r.operation==="action")result=await host.action(dashboardHostRequest(r.request!));
|
|
18
|
+
else if(Object.keys(r).sort().join()==="operation,request"&&r.operation==="action")result=await host.action(dashboardHostRequest(r.request!));
|
|
19
|
+
else if(Object.keys(r).sort().join()==="key,operation"&&r.operation==="human-action"&&typeof r.key==="string")result=await host.humanAction(r.key);
|
|
20
|
+
else throw Error("closed dashboard protocol required");
|
|
19
21
|
const output=JSON.stringify({ok:true,result})+"\n";if(Buffer.byteLength(output)>512*1024)throw Error("dashboard frame bound exceeded");socket.end(output);
|
|
20
22
|
}catch{socket.end(JSON.stringify({ok:false,error:"refused or acknowledgement unknown; explicit readback only"})+"\n");}})();tasks.add(task);void task.finally(()=>tasks.delete(task));
|
|
21
23
|
});
|
|
@@ -26,13 +28,13 @@ export async function serveDashboardHost(path:string,host:DashboardHost){
|
|
|
26
28
|
}
|
|
27
29
|
/** Explicit operator-selected endpoint. Socket ownership is access policy, not module/human authentication. */
|
|
28
30
|
export function connectDashboardHost(path:string):DashboardConnection{
|
|
29
|
-
const call=async(operation:string,request?:DashboardHostRequest)=>{await parent(path);const s=await lstat(path);if(!s.isSocket()||s.mode&0o077||s.uid!==process.getuid?.())throw Error("private original dashboard endpoint unavailable");
|
|
31
|
+
const call=async(operation:string,payload:{request?:DashboardHostRequest;key?:string}={})=>{await parent(path);const s=await lstat(path);if(!s.isSocket()||s.mode&0o077||s.uid!==process.getuid?.())throw Error("private original dashboard endpoint unavailable");
|
|
30
32
|
return new Promise<any>((resolve,reject)=>{const socket=createConnection(path);let size=0,done=false;const chunks:Buffer[]=[];const fail=(e:unknown)=>{if(!done){done=true;socket.destroy();reject(e);}};
|
|
31
|
-
socket.setTimeout(5000,()=>fail(Error("dashboard acknowledgement timeout; do not repeat action")));socket.on("error",fail);socket.on("connect",()=>socket.write(JSON.stringify({operation,...
|
|
33
|
+
socket.setTimeout(5000,()=>fail(Error("dashboard acknowledgement timeout; do not repeat action")));socket.on("error",fail);socket.on("connect",()=>socket.write(JSON.stringify({operation,...payload})+"\n"));
|
|
32
34
|
socket.on("data",b=>{size+=b.length;if(size>512*1024){fail(Error("dashboard frame limit"));return;}chunks.push(b);});
|
|
33
35
|
socket.on("end",()=>{try{const text=new TextDecoder("utf-8",{fatal:true}).decode(Buffer.concat(chunks));const r=parseRetentionJson(text,512*1024) as {ok:boolean;result:unknown};if(r.ok!==true)throw Error("dashboard refused or acknowledgement unknown");if(operation==="frame"&&(r.result as {version?:string})?.version!=="producer-dashboard-frame-v1")throw Error("incompatible dashboard host");if(!done){done=true;resolve(r.result);}}catch(e){fail(e);}});
|
|
34
36
|
socket.on("close",()=>{if(!done)fail(Error("dashboard disconnected; acknowledgement unknown"));});
|
|
35
37
|
});
|
|
36
38
|
};
|
|
37
|
-
const api={frame:()=>call("frame"),action:(r:DashboardHostRequest)=>call("action",dashboardHostRequest(r))};connections.add(api);return Object.freeze(api);
|
|
39
|
+
const api={frame:()=>call("frame"),action:(r:DashboardHostRequest)=>call("action",{request:dashboardHostRequest(r)}),humanAction:(key:string)=>call("human-action",{key})};connections.add(api);return Object.freeze(api);
|
|
38
40
|
}
|