pi-daddy 0.23.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +12 -6
  3. package/contracts/ledger/v4/README.md +1 -1
  4. package/dist/cli.d.ts +3 -1
  5. package/dist/cli.d.ts.map +1 -1
  6. package/dist/cli.js +49 -3
  7. package/dist/cli.js.map +1 -1
  8. package/dist/daily-dashboard-host.d.ts +99 -0
  9. package/dist/daily-dashboard-host.d.ts.map +1 -0
  10. package/dist/daily-dashboard-host.js +71 -0
  11. package/dist/daily-dashboard-host.js.map +1 -0
  12. package/dist/dashboard-cli.d.ts.map +1 -1
  13. package/dist/dashboard-cli.js +9 -2
  14. package/dist/dashboard-cli.js.map +1 -1
  15. package/dist/dashboard-harness.d.ts +3 -0
  16. package/dist/dashboard-harness.d.ts.map +1 -1
  17. package/dist/dashboard-harness.js +14 -0
  18. package/dist/dashboard-harness.js.map +1 -1
  19. package/dist/dashboard-host-contract.d.ts +6 -1
  20. package/dist/dashboard-host-contract.d.ts.map +1 -1
  21. package/dist/dashboard-host-contract.js +1 -1
  22. package/dist/dashboard-host-contract.js.map +1 -1
  23. package/dist/dashboard-host-transport.d.ts +1 -0
  24. package/dist/dashboard-host-transport.d.ts.map +1 -1
  25. package/dist/dashboard-host-transport.js +5 -3
  26. package/dist/dashboard-host-transport.js.map +1 -1
  27. package/dist/dashboard-host.d.ts +54 -0
  28. package/dist/dashboard-host.d.ts.map +1 -1
  29. package/dist/dashboard-host.js +49 -2
  30. package/dist/dashboard-host.js.map +1 -1
  31. package/dist/dashboard-observation.d.ts.map +1 -1
  32. package/dist/dashboard-observation.js +5 -5
  33. package/dist/dashboard-observation.js.map +1 -1
  34. package/dist/experiment-store.d.ts +3 -1
  35. package/dist/experiment-store.d.ts.map +1 -1
  36. package/dist/experiment-store.js +20 -3
  37. package/dist/experiment-store.js.map +1 -1
  38. package/dist/work-command.d.ts +30 -0
  39. package/dist/work-command.d.ts.map +1 -0
  40. package/dist/work-command.js +148 -0
  41. package/dist/work-command.js.map +1 -0
  42. package/extensions/daily-dashboard-session.ts +30 -0
  43. package/extensions/daily-work-session.ts +20 -0
  44. package/extensions/delegation.ts +21 -8
  45. package/extensions/execute-child.ts +10 -1
  46. package/extensions/executor-session.ts +9 -0
  47. package/extensions/grants-command.ts +14 -15
  48. package/extensions/grants-connected-command.ts +19 -0
  49. package/extensions/grants.ts +25 -3
  50. package/extensions/primary-shadow.ts +11 -0
  51. package/extensions/run-delegation.ts +2 -0
  52. package/extensions/session.ts +7 -19
  53. package/extensions/work-runtime.ts +44 -0
  54. package/package.json +1 -1
  55. package/src/cli.ts +41 -4
  56. package/src/daily-dashboard-host.ts +49 -0
  57. package/src/dashboard-cli.ts +7 -2
  58. package/src/dashboard-harness.ts +9 -0
  59. package/src/dashboard-host-contract.ts +4 -1
  60. package/src/dashboard-host-transport.ts +8 -6
  61. package/src/dashboard-host.ts +26 -3
  62. package/src/dashboard-observation.ts +3 -2
  63. package/src/experiment-store.ts +11 -3
  64. package/src/work-command.ts +179 -0
@@ -37,7 +37,8 @@ import { registerDelegationTools } from "./delegation.ts";
37
37
  import { grantsCommand } from "./grants-command.ts";
38
38
  import { runInit } from "./init-command.ts";
39
39
  import { planWithApprovals } from "./run-delegation.ts";
40
- import { createGrantsSession, loadProjectDefinitions, resolveExecutor, type GrantsSession } from "./session.ts";
40
+ import { createGrantsSession, loadProjectDefinitions, type GrantsSession } from "./session.ts";
41
+ import { resolveExecutor } from "./executor-session.ts";
41
42
  import { reportSessionStart } from "./session-report.ts";
42
43
  import { SPAWN_TOOLS, tripwireReason } from "./tripwire.ts";
43
44
  import { reportGrantStoreRefusal } from "./grant-store-refusal.ts";
@@ -47,7 +48,10 @@ import {
47
48
  openDashboardCommand,
48
49
  } from "../src/dashboard-handshake.ts";
49
50
 
50
- import { associateOrdinaryHost } from "../src/ordinary-children.ts";
51
+ import { associateOrdinaryHost, ordinaryChildrenFor } from "../src/ordinary-children.ts";
52
+ import { loadDeclaredWork } from "../src/work-command.ts";
53
+ import { createDailyDashboardSession } from "./daily-dashboard-session.ts";
54
+ import { replacePublishedDailyWork, type PublishedDailyWork } from "./daily-work-session.ts";
51
55
  export default function (pi: ExtensionAPI) {
52
56
  // The path pi loads as the extension, so a child granted `tool:delegate` can be started with `-e <this>`.
53
57
  // Only this file can say so about itself, which is why the session takes it rather than deriving it.
@@ -61,6 +65,7 @@ export default function (pi: ExtensionAPI) {
61
65
 
62
66
  const session = createGrantsSession(extensionPath);
63
67
  associateOrdinaryHost(pi,session);
68
+ const dailyHost=createDailyDashboardSession({ordinary:()=>ordinaryChildrenFor(pi),declared:()=>session.declaredWork,cwd:()=>session.cwd,env:process.env,author:"local-operator"});
64
69
  const dashboardPluginRoot = fileURLToPath(new URL("../herdr-plugin/", import.meta.url));
65
70
  const dashboardPaths = defaultDashboardPaths(
66
71
  process.env.PI_CODING_AGENT_DIR?.trim() || join(homedir(), ".pi", "agent"),
@@ -70,6 +75,7 @@ export default function (pi: ExtensionAPI) {
70
75
  // because the hooks below have to be registered before the tools and both need to call it — the tool
71
76
  // schemas describe which definitions are spawnable, and nothing knows that until a hook has run (R-39).
72
77
  const delegation = { refreshSpawnable: () => {} };
78
+ const publishedDailyWork: PublishedDailyWork = {};
73
79
 
74
80
  pi.on("session_start", async (_event, ctx) => {
75
81
  session.cwd = ctx.cwd;
@@ -77,6 +83,19 @@ export default function (pi: ExtensionAPI) {
77
83
  // execution tree across workspaces. Resolve it once at the root's actual pi cwd; descendants inherit
78
84
  // this absolute identity verbatim, and resolve(abs) remains abs at every deeper session start.
79
85
  if (session.ledgerPath) session.ledgerPath = resolve(ctx.cwd, session.ledgerPath);
86
+ try {
87
+ try {
88
+ replacePublishedDailyWork(process.env, publishedDailyWork, undefined);
89
+ session.declaredWork = (await loadDeclaredWork(join(ctx.cwd, ".pi", "work-current.json"))) ?? undefined;
90
+ replacePublishedDailyWork(process.env, publishedDailyWork, session.declaredWork);
91
+ } catch (error) {
92
+ session.declaredWork = undefined;
93
+ ctx.ui.notify(
94
+ `grants: declared work is invalid (${error instanceof Error ? error.message : String(error)}); ` +
95
+ `delegations remain unbound and no previous selection was substituted.`,
96
+ "error",
97
+ );
98
+ }
80
99
  // The one case the stored-grant lookup can get wrong (ADR-0030). The factory reads the store keyed by
81
100
  // `process.cwd()` because it runs before any hook and therefore before `ctx` exists — and S-5 forces
82
101
  // that ordering, since whether `delegate` is registered is decided there. Almost always the two agree.
@@ -95,7 +114,6 @@ export default function (pi: ExtensionAPI) {
95
114
  "warning",
96
115
  );
97
116
  }
98
- try {
99
117
  try {
100
118
  await reportGrantStoreRefusal(session, ctx.ui);
101
119
  } catch (error) {
@@ -232,6 +250,8 @@ export default function (pi: ExtensionAPI) {
232
250
  * `exit` remains the backstop. SIGKILL still orphans panes, exactly as R-62 records, and no signal handler is
233
251
  * installed here for the reason R-62 gives: it would turn pi's "interrupt this turn" into "exit pi".
234
252
  */
253
+ pi.on("session_shutdown", async () => { await dailyHost.close().catch(() => undefined); });
254
+
235
255
  pi.on("agent_settled", async () => {
236
256
  try {
237
257
  if (session.executor.kind !== "herdr" || openPaneCount() === 0) return undefined;
@@ -350,6 +370,7 @@ export default function (pi: ExtensionAPI) {
350
370
  definitions: session.definitions,
351
371
  sessionApprovals: session.sessionApprovals,
352
372
  inheritedApprovals: session.inheritedApprovals,
373
+ variantRuns: session.variantRuns,
353
374
  snapshotOf: (subject: string) => snapshotOf(session, subject),
354
375
  // The REAL delegation path, minus the one thing a diagnostic must never do. `ctx: null` is what
355
376
  // says so: stored approvals count exactly as they would for a spawn, and no human is asked
@@ -360,6 +381,7 @@ export default function (pi: ExtensionAPI) {
360
381
  await loadProjectDefinitions(session, ctx.cwd);
361
382
  delegation.refreshSpawnable();
362
383
  }),
384
+ runHost: (target: string) => dailyHost.run(target),
363
385
  openDashboard: () => openDashboardCommand({
364
386
  env: process.env,
365
387
  pid: process.pid,
@@ -0,0 +1,11 @@
1
+ import type { DelegationOutcome } from "./execute-child.ts";
2
+ import type { ExecutionOccurrenceIds } from "./execution-occurrence.ts";
3
+ import type { GrantsSession, VariantRunAccounting } from "./session.ts";
4
+
5
+ /** Return one primary while the original session retains and settles every shadow outcome. */
6
+ export async function completePrimary(input:{session:GrantsSession;primaryIndex:number;occurrences:ExecutionOccurrenceIds[];pending:Promise<DelegationOutcome>[];settle:(outcomes:{ok:boolean}[])=>void}){
7
+ const runId=input.occurrences[input.primaryIndex].executionId,record:VariantRunAccounting={runId,primaryExecutionId:runId,shadowExecutionIds:input.occurrences.filter((_,i)=>i!==input.primaryIndex).map(x=>x.executionId),state:"running",outcomes:null};
8
+ input.session.variantRuns.set(runId,record);
9
+ void Promise.all(input.pending).then(outcomes=>{record.outcomes=outcomes.map((outcome,index)=>({executionId:input.occurrences[index].executionId,role:index===input.primaryIndex?"primary":"shadow",ok:outcome.ok,reason:outcome.ok?null:outcome.reason??"failed"}));record.state="settled";input.settle(outcomes);});
10
+ return {runId,primary:await input.pending[input.primaryIndex]};
11
+ }
@@ -44,6 +44,7 @@ interface ChildSpec {
44
44
  agent?: string;
45
45
  tools?: string[];
46
46
  model?: string;
47
+ thinking?: string;
47
48
  correlation?: CorrelationMetadata;
48
49
  workspace?: DelegationWorkspaceSpec;
49
50
  }
@@ -248,6 +249,7 @@ export async function runOneDelegation(
248
249
  agent: spec.agent,
249
250
  tools: spec.tools,
250
251
  model: spec.model ?? defaultModel,
252
+ thinking: spec.thinking,
251
253
  correlation: spec.workspace
252
254
  ? { ...(spec.correlation ?? {}), workspace_id: spec.workspace.workspace_id }
253
255
  : spec.correlation,
@@ -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, needsProbe, ENV_HERDR, type ExecutorChoice } from "../src/executor.ts";
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.23.0",
3
+ "version": "0.24.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",
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, ...rest] = args;
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,49 @@
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 { createDispatchBudget, 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 type { DeclaredWorkState } from "./work-command.ts";
11
+ import type { OrdinaryChildren } from "./ordinary-children.ts";
12
+ import { dataDigest } from "./debrief-contract.ts";
13
+
14
+ export interface DailyDashboardHostInput {id:string;cwd:string;directory:string;socketPath?:string;declared:DeclaredWorkState;ordinary:OrdinaryChildren;harness:DashboardHarness;author:string}
15
+ const idPattern=/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
16
+ const hash=(value:string)=>createHash("sha256").update(value).digest("hex");
17
+
18
+ /** One explicitly started, process-owned daily host. No authority, controller or prior host is recovered from disk. */
19
+ export async function startDailyDashboardHost(input:DailyDashboardHostInput){
20
+ 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");
21
+ const harnessDigest=loadedDashboardHarnessDigest(input.harness);if(!harnessDigest)throw Error("loaded skill-harness bridge required");
22
+ if(input.declared.ledgerPath!==join(input.cwd,".pi","work.jsonl"))throw Error("current declared work must belong to this host project");
23
+ await mkdir(input.directory,{mode:0o700});const archiveRoot=join(input.directory,"archive");await mkdir(archiveRoot,{mode:0o700});
24
+ 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")});
25
+ const component={kind:"detector",id:"retained-work",version:"1",population:`daily:${input.id}`};
26
+ 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};
27
+ const trustPolicyId=input.harness.trustPolicyDigest(trustInput),trustDirectory=join(input.directory,"trust");input.harness.createTrustLifecycle(trustDirectory,trustInput,[trustPolicyId]);
28
+ const workRelative="work.jsonl",factsRelative="facts.jsonl",workMirror=join(input.directory,workRelative);await writeFile(workMirror,await readFile(input.declared.ledgerPath),{mode:0o600,flag:"wx"});
29
+ 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});}};
30
+ 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"});
31
+ 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"}]};
32
+ const policyPath=join(input.directory,"archive-policy.json"),policyBytes=JSON.stringify(policy);await writeFile(policyPath,policyBytes,{mode:0o600,flag:"wx"});
33
+ const authorityDigest=dataDigest({kind:"daily-dashboard-host",id:input.id,cwd:input.cwd}),budget=await createDispatchBudget({directory:join(input.directory,"budget"),authorityDigest,limits:{maxAttempts:6,maxInputBytes:65536,maxConcurrent:3}});
34
+ 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};
35
+ const requestDigests=new Set<string>(),dispatchDigests=new Set<string>();let host:ReturnType<typeof createDashboardHost>,factsResultManifest:string|null=null;
36
+ const authority=():DashboardHostAuthority=>({hostDigests:[dashboardHostDigest(config)],requestDigests:[...requestDigests],workContext:{selectedSnapshot:config.selection,authority:null},dispatch:{authorityDigest,requestDigests:[...dispatchDigests]},experiment:null});
37
+ 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});
38
+ host=createDashboardHost({harness:input.harness,config,budget,ordinary:input.ordinary,authority,beforeObservation:sourceId=>sourceId==="work"?refreshWork():Promise.resolve(),humanActions:async context=>{
39
+ const state=await openResourceBudget(budget).controls(null).inspect(),action=state.paused?"resume-dispatch":"pause-dispatch",key=state.paused?"resume-dispatch":"pause-new-dispatch";
40
+ const native:DispatchRequest={version:"1.0",requestId:`${action}:${state.revision}`,bindingDigest:config.budgetDigest,expectedRevision:state.revision,action,targetExecutionId:null};dispatchDigests.add(dispatchRequestDigest(native));
41
+ const hostRequest=request(context,"dispatch",native,`host-${action}:${state.revision}`);requestDigests.add(dashboardHostRequestDigest(hostRequest));
42
+ 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));
43
+ return [{key,label:state.paused?"Resume new governed dispatch":"Pause new governed dispatch; running children continue",request:hostRequest},{key:"refresh-current-work",label:"Capture current declared work and attempts",request:refresh}];
44
+ }});
45
+ 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;
46
+ 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);
47
+ const socketPath=input.socketPath??join(input.directory,"dashboard.sock"),server=await serveDashboardHost(socketPath,host);
48
+ let closed=false;return Object.freeze({host,socketPath,directory:input.directory,async close(){if(closed)return;closed=true;await server.close();}});
49
+ }
@@ -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
- return host.action(dashboardHostRequest(parseRetentionJson(line,60000) as DashboardHostRequest));
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}.`,"Explicit exact approved JSON requests only; refresh never acts."].join("\n\n");
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";
@@ -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="127b349310dd8f28e5d6b12148a063fce66a77dd";
10
+ const bridgeFunctions=["learningJournal","createTrustLifecycle","openTrustLifecycle","trustPolicyDigest","archivePolicyBinding","ingestPolicySource","readArchiveCheckpoint","readArchiveSource","retainArchiveSource","captureArchivedWorkSignals","readRetainedExecution","projectRetainedExecutions","createWorkCaseReviewer","createWorkSignalReviewer","retainBlindIntervention","openBlindIntervention"] 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();}}
@@ -5,13 +5,16 @@ 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 = "1c02194d4a3709d14890a5fbbad91ff5f0151f65";
8
+ export const DASHBOARD_HARNESS_PIN = "127b349310dd8f28e5d6b12148a063fce66a77dd";
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 } };
@@ -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!));else throw Error("closed dashboard protocol required");
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,...(request?{request}:{})})+"\n"));
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
  }
@@ -17,6 +17,10 @@ const hosts=new WeakSet<object>();
17
17
  export const isDashboardHost=(value:unknown):value is DashboardHost=>typeof value==="object"&&value!==null&&hosts.has(value);
18
18
  export type DashboardHost=ReturnType<typeof openDashboardHost>;
19
19
  export interface DashboardHostOptions { ordinary?:OrdinaryChildren; harness:DashboardHarness; config:DashboardHostConfig; budget:GovernedBudgetBinding; experiment?:ReturnType<typeof openExperiment>; authority:()=>DashboardHostAuthority|null;
20
+ /** Exact actions constructed and authorized by the owner; the dashboard exposes only human labels/keys. */
21
+ humanActions?:(context:{hostDigest:string;selectionDigest:string;tip:string;observations:readonly {sourceId:string;checkpointId:string}[]})=>readonly {key:string;label:string;request:DashboardHostRequest}[]|Promise<readonly {key:string;label:string;request:DashboardHostRequest}[]>;
22
+ /** Explicit approved observation effect only; refresh/frame never invokes it. */
23
+ beforeObservation?:(sourceId:string)=>Promise<void>;
20
24
  /** Independently sourced current declaration. Never inferred from idle, a PID, title or request fields. */
21
25
  presence?:()=>{present:boolean;closing:boolean;evidenceDigest:string;expiresAt:number}|null;
22
26
  }
@@ -53,6 +57,8 @@ export function openDashboardHost(options:DashboardHostOptions){
53
57
  const append=(value:Record<string,unknown>)=>journal.append(history().at(-1)!.id,value);
54
58
  const saved=():DebriefCheckpoint|null=>{const rows=history().filter(e=>e.value.type==="checkpoint");return rows.length?detached(rows.at(-1)!.value.checkpoint) as DebriefCheckpoint:null;};
55
59
  let presenter:DebriefPresenter|undefined,busy=false,poisoned=false,presentationRevision:number|null=null,presenceDigest:string|null=null,attentionDeferred:string|null=null;
60
+ const ordinaryDispatchHoldKey=dataDigest({hostDigest,control:"pause-new-ordinary-dispatch"});
61
+ let displayedActions:null|{tip:string;actions:{key:string;label:string;operation:DashboardHostRequest["operation"];request:DashboardHostRequest}[];digest:string}=null;
56
62
  const presence=()=>{const p=options.presence?.();return p&&p.present===true&&p.closing===true&&sha(p.evidenceDigest)&&Number.isFinite(p.expiresAt)&&p.expiresAt>Date.now()?p:null;};
57
63
  const persistence:DebriefPersistence={durability:"host-owned",load:saved,compareAndSwap(expected,next){
58
64
  const rows=history(),previous=saved();if((previous?dataDigest(previous):null)!==expected)throw Error("stale attention CAS");
@@ -65,6 +71,14 @@ export function openDashboardHost(options:DashboardHostOptions){
65
71
  const paused=async(revision:number)=>{const b=await boundary();if(ordinary&&!ordinary.quiescent())throw Error("original ordinary children not quiescent");if(experiment){const view=await experiment.inspect();if(view.budget?.active!==0||view.control!=="not-assessed")throw Error("original experiment not quiescent");}if(!b.dispatch.paused||b.dispatch.admission!=="paused"||b.dispatch.revision!==revision||b.active!==0)throw Error("safe paused selection unavailable");};
66
72
  const currentSelection=async()=>budget.binding.intent?(await budget.intentControls(null).inspect()).selection:c.selection;
67
73
  const nativeRead=async()=>({dispatch:await budget.controls(null).inspect(),intent:budget.binding.intent?await budget.intentControls(null).inspect():null,experiment:experiment?await experiment.inspect():null,ordinary:ordinary?ordinary.inspect():null});
74
+ const availableHumanActions=async()=>{
75
+ const rows=history(),tip=rows.at(-1)!.id,selection=await currentSelection(),seen=new Set<string>();
76
+ const declared=await options.humanActions?.({hostDigest,selectionDigest:dashboardSelectionDigest(selection),tip,observations:observation.latest().map(x=>({sourceId:x.sourceId,checkpointId:x.checkpointId}))})??[],a=authority();
77
+ return declared.map(value=>{const x=detached(value);controlShape(x,["key","label","request"]);const request=dashboardHostRequest(x.request);
78
+ if(!/^[a-zA-Z0-9:_-]{1,64}$/.test(x.key)||typeof x.label!=="string"||!x.label.trim()||Buffer.byteLength(x.label)>120||seen.has(x.key))throw Error("invalid human dashboard action");seen.add(x.key);
79
+ if(request.hostDigest!==hostDigest||request.expectedTip!==tip||request.selectionDigest!==dashboardSelectionDigest(selection)||!a?.requestDigests.includes(dashboardHostRequestDigest(request)))throw Error("human dashboard action is stale or not exactly authorized");
80
+ return {key:x.key,label:x.label,operation:request.operation,request};});
81
+ };
68
82
  const api={hostDigest,selectionDigest,
69
83
  async frame(){
70
84
  const rows=history(),tip=rows.at(-1)!.id,a=authority();let source:unknown=null,error:string|null=null;
@@ -74,14 +88,20 @@ export function openDashboardHost(options:DashboardHostOptions){
74
88
  let debrief:unknown=null;
75
89
  if(a&&presenter&&presentationRevision!==null&&presence()?.evidenceDigest===presenceDigest){try{await paused(presentationRevision);debrief=presenter.view();}catch{debrief=null;}}
76
90
  let controls:unknown=null;try{controls=await nativeRead();}catch(e){error=String(e);}
77
- const attention=trust.inspect(Date.now());
78
- return freeze(detached({version:"producer-dashboard-frame-v1",hostDigest,selectionDigest:dashboardSelectionDigest(selection),selectionState,tip,source,controls,debrief,attention,error,
91
+ const attention=trust.inspect(Date.now());let actions:{key:string;label:string;operation:string}[]=[];
92
+ try{const available=await availableHumanActions(),digest=dataDigest(available.map(({key,label,operation,request})=>({key,label,operation,requestDigest:dashboardHostRequestDigest(request)})));
93
+ if(displayedActions?.tip===tip&&displayedActions.digest!==digest)throw Error("displayed dashboard actions changed before journal advance");
94
+ displayedActions={tip,actions:available,digest};actions=available.map(({key,label,operation})=>({key,label,operation}));}catch(e){error=String(e);}
95
+ return freeze(detached({version:"producer-dashboard-frame-v1",hostDigest,selectionDigest:dashboardSelectionDigest(selection),selectionState,tip,source,controls,debrief,attention,error,actions,
79
96
  requests:rows.filter(e=>["claim","result","presentation","defer","ordinary-intent-pending"].includes(String(e.value.type))).map(e=>e.value),
80
97
  control:rows.some(e=>e.value.type==="host-failure")?"failed":rows.some(e=>e.value.type==="claim"&&!rows.some(r=>r.value.type==="result"&&r.value.requestId===e.value.requestId))?"unknown":"not-assessed",
81
98
  acknowledgement:poisoned?"unknown":"readback-only",identity:"independently-declared-host; not human/module authentication",activeBranch:null,acceptance:"not-assessed",freshness:"snapshot-unknown",workerInteractions:0}));
82
99
  },
83
100
  /** Explicit reconciliation is read-only. A retained claim is never replayed as an effect. */
84
101
  reconcile:()=>api.frame(),
102
+ async humanAction(key:string){const tip=history().at(-1)!.id;if(!displayedActions||displayedActions.tip!==tip)throw Error("dashboard actions must be displayed at the current tip");
103
+ const found=displayedActions.actions.find(action=>action.key===key);if(!found)throw Error(`unknown dashboard action ${JSON.stringify(key)}`);
104
+ const current=(await availableHumanActions()).find(action=>action.key===key);if(!current||current.label!==found.label||dashboardHostRequestDigest(current.request)!==dashboardHostRequestDigest(found.request))throw Error("displayed dashboard action changed; no effect attempted");return api.action(found.request);},
85
105
  async action(input:DashboardHostRequest){
86
106
  const request=dashboardHostRequest(input),digest=dashboardHostRequestDigest(request);
87
107
  if(busy||poisoned)throw Error("dashboard operation busy or acknowledgement unknown");busy=true;let attempted=false;
@@ -97,6 +117,7 @@ export function openDashboardHost(options:DashboardHostOptions){
97
117
  let result:unknown,releaseOrdinary:(()=>void)|undefined;
98
118
  try{
99
119
  if(request.operation==="observe"){
120
+ const sourceId=(request.payload as {sourceId?:unknown})?.sourceId;if(typeof sourceId!=="string")throw Error("exact observation source required");await options.beforeObservation?.(sourceId);
100
121
  const captured=observation.observe(request.payload,a!,selection);append({type:"observation",observation:captured});result=captured;
101
122
  }else if(request.operation==="present"){
102
123
  const p=detached(request.payload) as {userPresent:boolean;closing:boolean;evidenceDigest:string;dispatchRevision:number};
@@ -124,9 +145,11 @@ export function openDashboardHost(options:DashboardHostOptions){
124
145
  presenter?.close();presentationRevision=null;
125
146
  if(request.operation==="dispatch"||request.operation==="dispatch-reconcile"){
126
147
  const native=dispatchRequest(request.payload as DispatchRequest);if(native.action==="cancel-execution")throw Error("cancellation needs exact separately approved native bridge");
127
- const port=budget.controls(a!.dispatch);
148
+ const port=budget.controls(a!.dispatch),ordinaryHold=ordinary&&["pause-dispatch","resume-dispatch"].includes(native.action)?holdOrdinaryDispatch(ordinary,ordinaryDispatchHoldKey):undefined;
128
149
  if(request.operation==="dispatch-reconcile"&&(await port.inspect()).records.find(r=>r.request.requestId===native.requestId)?.digest!==dispatchRequestDigest(native))throw Error("exact original dispatch reconciliation required");
129
150
  result=request.operation==="dispatch"?await port.request(native):await port.reconcile(native.requestId);
151
+ const record=(result as {records:{request:{requestId:string};decision:string;application:string}[]}).records.find(r=>r.request.requestId===native.requestId);
152
+ if(ordinaryHold&&(record?.decision!=="approved"||native.action==="resume-dispatch"&&record.application==="applied"))ordinaryHold.release();
130
153
  }else if(request.operation==="intent"||request.operation==="intent-reconcile"){
131
154
  const native=detached(intentRequest(request.payload as IntentRequest)) as IntentRequest,port=budget.intentControls(a!.dispatch),digest=intentRequestDigest(native);
132
155
  if(c.ordinaryDigest&&!ordinary)throw Error("original ordinary boundary unavailable; no recovery");
@@ -20,8 +20,9 @@ export function dashboardObservations(h: DashboardHarness, c: DashboardHostConfi
20
20
  if(source.kind==="work"||source.kind==="facts"){
21
21
  const checkpoint=h.readArchiveCheckpoint(root,captured.checkpointId).checkpoint;sourceManifestId=checkpoint.sourceManifestId;bytes(sourceManifestId);
22
22
  if(source.kind==="facts") {if(input.facts!==null)throw Error("facts sources are declarations, not replacement authority");return {sourceId:source.id,kind:source.kind,checkpointId:captured.checkpointId,sourceManifestId,metadata:captured,cases:null,linkageManifestId:null};}
23
- const factSource=latest().find(r=>r.kind==="facts"&&r.sourceManifestId===input.facts);if(!factSource)throw Error("exact observed fact source required, not caller replacement facts");
24
- const facts=parseRetentionJson(new TextDecoder("utf-8",{fatal:true}).decode(bytes(factSource.sourceManifestId!)),256*1024);captured.factSourceManifestId=factSource.sourceManifestId;captured.factBasis="explicit-host-declarations-not-authenticated-truth";
23
+ const factSource=input.facts===null?null:latest().find(r=>r.kind==="facts"&&r.sourceManifestId===input.facts);if(input.facts!==null&&!factSource)throw Error("exact observed fact source required, not caller replacement facts");
24
+ const facts=factSource?parseRetentionJson(new TextDecoder("utf-8",{fatal:true}).decode(bytes(factSource.sourceManifestId!)),256*1024):null;
25
+ captured.factSourceManifestId=factSource?.sourceManifestId??null;captured.factBasis=factSource?"explicit-host-declarations-not-authenticated-truth":"derived-retained-work-only";
25
26
  if(dataDigest(authority.workContext.selectedSnapshot)!==dataDigest(selection))throw Error("independent source selection changed");
26
27
  try { const signals=h.captureArchivedWorkSignals(root,sourceManifestId,authority.workContext,facts);
27
28
  cases={version:"work-signals-v1",batchId:signals.caseBatchId,observationId:signals.observationId};linkageManifestId=signals.linkageManifestId;