pi-daddy 0.24.0 → 0.25.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.md +12 -4
- package/THIRD_PARTY_LICENSES/herdr-Apache-2.0.txt +201 -0
- package/THIRD_PARTY_NOTICES.md +7 -0
- package/contracts/ledger/v4/README.md +1 -1
- package/dist/daily-dashboard-host.d.ts +26 -2
- package/dist/daily-dashboard-host.d.ts.map +1 -1
- package/dist/daily-dashboard-host.js +78 -8
- package/dist/daily-dashboard-host.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 +1 -1
- package/dist/dashboard-harness.js +3 -3
- 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 +10 -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.d.ts +8 -0
- package/dist/dashboard-host.d.ts.map +1 -1
- package/dist/dashboard-host.js +24 -6
- package/dist/dashboard-host.js.map +1 -1
- package/dist/dashboard-observation.d.ts.map +1 -1
- package/dist/dashboard-observation.js +1 -0
- package/dist/dashboard-observation.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/herdr-pi-lifecycle.d.ts +8 -0
- package/dist/herdr-pi-lifecycle.d.ts.map +1 -0
- package/dist/herdr-pi-lifecycle.js +22 -0
- package/dist/herdr-pi-lifecycle.js.map +1 -0
- package/dist/herdr-poll.d.ts +21 -0
- package/dist/herdr-poll.d.ts.map +1 -1
- package/dist/herdr-poll.js +52 -3
- package/dist/herdr-poll.js.map +1 -1
- package/dist/herdr-start.d.ts +9 -0
- package/dist/herdr-start.d.ts.map +1 -0
- package/dist/herdr-start.js +15 -0
- package/dist/herdr-start.js.map +1 -0
- 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/run-herdr.d.ts +3 -3
- package/dist/run-herdr.d.ts.map +1 -1
- package/dist/run-herdr.js +28 -33
- package/dist/run-herdr.js.map +1 -1
- package/dist/vendor/herdr-pi-lifecycle.d.ts +2 -0
- package/dist/vendor/herdr-pi-lifecycle.d.ts.map +1 -0
- package/dist/vendor/herdr-pi-lifecycle.js +223 -0
- package/dist/vendor/herdr-pi-lifecycle.js.map +1 -0
- package/dist/work-command.d.ts +2 -0
- package/dist/work-command.d.ts.map +1 -1
- package/dist/work-command.js +15 -2
- package/dist/work-command.js.map +1 -1
- package/extensions/daily-dashboard-session.ts +9 -8
- package/extensions/grants-command.ts +2 -0
- package/extensions/grants.ts +1 -1
- package/package.json +15 -1
- package/src/daily-dashboard-host.ts +49 -10
- package/src/dashboard-handshake.ts +14 -5
- package/src/dashboard-harness.ts +3 -3
- package/src/dashboard-herdr.ts +12 -9
- package/src/dashboard-host-contract.ts +5 -2
- package/src/dashboard-host.ts +14 -9
- package/src/dashboard-observation.ts +1 -0
- package/src/experiment.ts +8 -3
- package/src/herdr-pi-lifecycle.ts +21 -0
- package/src/herdr-poll.ts +52 -2
- package/src/herdr-start.ts +20 -0
- 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/run-herdr.ts +28 -40
- package/src/vendor/herdr-pi-lifecycle.ts +257 -0
- package/src/work-command.ts +8 -2
|
@@ -4,16 +4,37 @@ import { isAbsolute, resolve, join } from "node:path";
|
|
|
4
4
|
import { createDashboardHost, dashboardHostDigest, dashboardHostRequestDigest, type DashboardHostAuthority, type DashboardHostConfig, type DashboardHostRequest } from "./dashboard-host.ts";
|
|
5
5
|
import type { DashboardHarness } from "./dashboard-host-contract.ts";
|
|
6
6
|
import { loadedDashboardHarnessDigest } from "./dashboard-harness.ts";
|
|
7
|
-
import {
|
|
7
|
+
import { createIntentBudget, openResourceBudget, resourceBindingDigest } from "./resource-budget.ts";
|
|
8
8
|
import { dispatchRequestDigest, type DispatchRequest } from "./dispatch-control.ts";
|
|
9
9
|
import { serveDashboardHost } from "./dashboard-host-transport.ts";
|
|
10
|
-
import type
|
|
11
|
-
import type
|
|
10
|
+
import { rebindDeclaredWork, type DeclaredWorkState } from "./work-command.ts";
|
|
11
|
+
import { ordinaryCancellation, ordinaryCancellationDigest, type OrdinaryChildren, type OrdinaryTarget } from "./ordinary-children.ts";
|
|
12
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";
|
|
13
17
|
|
|
14
|
-
export interface
|
|
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>}
|
|
15
20
|
const idPattern=/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
16
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
|
+
}
|
|
17
38
|
|
|
18
39
|
/** One explicitly started, process-owned daily host. No authority, controller or prior host is recovered from disk. */
|
|
19
40
|
export async function startDailyDashboardHost(input:DailyDashboardHostInput){
|
|
@@ -22,7 +43,7 @@ export async function startDailyDashboardHost(input:DailyDashboardHostInput){
|
|
|
22
43
|
if(input.declared.ledgerPath!==join(input.cwd,".pi","work.jsonl"))throw Error("current declared work must belong to this host project");
|
|
23
44
|
await mkdir(input.directory,{mode:0o700});const archiveRoot=join(input.directory,"archive");await mkdir(archiveRoot,{mode:0o700});
|
|
24
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")});
|
|
25
|
-
const component={kind:"detector",id:"
|
|
46
|
+
const component={kind:"detector",id:"coverage_gap",version:"work-signals-v1:declared-work-v1",population:`daily:${input.id}`};
|
|
26
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};
|
|
27
48
|
const trustPolicyId=input.harness.trustPolicyDigest(trustInput),trustDirectory=join(input.directory,"trust");input.harness.createTrustLifecycle(trustDirectory,trustInput,[trustPolicyId]);
|
|
28
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"});
|
|
@@ -30,18 +51,36 @@ export async function startDailyDashboardHost(input:DailyDashboardHostInput){
|
|
|
30
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"});
|
|
31
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"}]};
|
|
32
53
|
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}),
|
|
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);
|
|
34
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};
|
|
35
|
-
const
|
|
36
|
-
const
|
|
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]}});
|
|
37
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});
|
|
38
62
|
host=createDashboardHost({harness:input.harness,config,budget,ordinary:input.ordinary,authority,beforeObservation:sourceId=>sourceId==="work"?refreshWork():Promise.resolve(),humanActions:async context=>{
|
|
39
63
|
const state=await openResourceBudget(budget).controls(null).inspect(),action=state.paused?"resume-dispatch":"pause-dispatch",key=state.paused?"resume-dispatch":"pause-new-dispatch";
|
|
40
64
|
const native:DispatchRequest={version:"1.0",requestId:`${action}:${state.revision}`,bindingDigest:config.budgetDigest,expectedRevision:state.revision,action,targetExecutionId:null};dispatchDigests.add(dispatchRequestDigest(native));
|
|
41
65
|
const hostRequest=request(context,"dispatch",native,`host-${action}:${state.revision}`);requestDigests.add(dashboardHostRequestDigest(hostRequest));
|
|
42
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));
|
|
43
|
-
|
|
44
|
-
|
|
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});
|
|
45
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;
|
|
46
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);
|
|
47
86
|
const socketPath=input.socketPath??join(input.directory,"dashboard.sock"),server=await serveDashboardHost(socketPath,host);
|
|
@@ -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,8 +6,8 @@ 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="
|
|
10
|
-
const bridgeFunctions=["learningJournal","createTrustLifecycle","openTrustLifecycle","trustPolicyDigest","archivePolicyBinding","ingestPolicySource","readArchiveCheckpoint","readArchiveSource","retainArchiveSource","captureArchivedWorkSignals","readRetainedExecution","projectRetainedExecutions","createWorkCaseReviewer","createWorkSignalReviewer","retainBlindIntervention","openBlindIntervention"] as const;
|
|
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
11
|
/** Adopt the API published by the already-loaded skill-harness extension. Same-process source identity is not human authentication. */
|
|
12
12
|
export function adoptDashboardHarnessBridge(input:unknown):DashboardHarness{
|
|
13
13
|
const value=input as {version?:unknown;sourceCommit?:unknown;api?:unknown};
|
|
@@ -30,7 +30,7 @@ export async function loadDashboardHarness(artifactRoot:string,input:DashboardHa
|
|
|
30
30
|
if(content.get("package.json")?.toString()!=='{"type":"module"}')throw Error("explicit ESM artifact root required");
|
|
31
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});}
|
|
32
32
|
await mkdir(join(directory,"node_modules"));await symlink(manifest.typeboxRoot,join(directory,"node_modules/typebox"));
|
|
33
|
-
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"];
|
|
34
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;
|
|
35
35
|
for(const[p,digest]of files)if(byteDigest(await bytes(join(directory,p)))!==digest)throw Error("loaded harness bytes changed");
|
|
36
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,7 +5,7 @@ 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. */
|
|
@@ -19,9 +19,11 @@ export interface DashboardHarness extends DebriefHarness {
|
|
|
19
19
|
ingestPolicySource(path: string, source: string, previous?: string, expectedPolicy?: string): { checkpointId: string; sourceStatus: string; issues?: string[]; [key: string]: unknown };
|
|
20
20
|
readArchiveCheckpoint(root: string, checkpoint: string): { checkpoint: { sourceManifestId: string; issues: string[]; [key: string]: unknown } };
|
|
21
21
|
readArchiveSource(root: string, id: string): { status: string; bytes?: Uint8Array; reference?: { retention: string; sha256: string; [key: string]: unknown } };
|
|
22
|
-
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 };
|
|
23
23
|
readRetainedExecution(root: string, id: string): { projection: unknown };
|
|
24
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>};
|
|
25
27
|
}
|
|
26
28
|
export interface DashboardHostConfig {
|
|
27
29
|
version: "producer-dashboard-host-v1"; trustDirectory: string; trustPolicyId: string; archiveRoot: string; scope: string; author: string;
|
|
@@ -29,6 +31,7 @@ export interface DashboardHostConfig {
|
|
|
29
31
|
selection: WorkProjectionContext["selectedSnapshot"]; cases: CaseSelection | null; blind: DurableBlindBinding | null;
|
|
30
32
|
budgetDigest: string; experimentDigest: string | null; harnessArtifactDigest: string;
|
|
31
33
|
ordinaryDigest?: string;
|
|
34
|
+
learningLifecycleId?: string;
|
|
32
35
|
}
|
|
33
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 }
|
|
34
37
|
export interface DashboardHostAuthority {
|
package/src/dashboard-host.ts
CHANGED
|
@@ -18,15 +18,18 @@ export const isDashboardHost=(value:unknown):value is DashboardHost=>typeof valu
|
|
|
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
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}[]>;
|
|
21
|
+
humanActions?:(context:{hostDigest:string;selectionDigest:string;tip:string;observations:readonly {sourceId:string;checkpointId:string}[];preparedPresentationDigest:string|null})=>readonly {key:string;label:string;request:DashboardHostRequest}[]|Promise<readonly {key:string;label:string;request:DashboardHostRequest}[]>;
|
|
22
22
|
/** Explicit approved observation effect only; refresh/frame never invokes it. */
|
|
23
23
|
beforeObservation?:(sourceId:string)=>Promise<void>;
|
|
24
|
+
/** Owner callback after the exact intent journal reports applied; failure keeps host acknowledgement unknown. */
|
|
25
|
+
afterIntentApplied?:(selection:IntentRequest["selection"])=>void|Promise<void>;
|
|
24
26
|
/** Independently sourced current declaration. Never inferred from idle, a PID, title or request fields. */
|
|
25
27
|
presence?:()=>{present:boolean;closing:boolean;evidenceDigest:string;expiresAt:number}|null;
|
|
26
28
|
}
|
|
27
29
|
function configuration(input:DashboardHostConfig){
|
|
28
|
-
const c=detached(input);controlShape(c,["version","trustDirectory","trustPolicyId","archiveRoot","scope","author","policyPath","policySha256","sources","selection","cases","blind","budgetDigest","experimentDigest","harnessArtifactDigest",...(Object.hasOwn(c,"ordinaryDigest")?["ordinaryDigest"]:[])]);
|
|
30
|
+
const c=detached(input);controlShape(c,["version","trustDirectory","trustPolicyId","archiveRoot","scope","author","policyPath","policySha256","sources","selection","cases","blind","budgetDigest","experimentDigest","harnessArtifactDigest",...(Object.hasOwn(c,"ordinaryDigest")?["ordinaryDigest"]:[]),...(Object.hasOwn(c,"learningLifecycleId")?["learningLifecycleId"]:[])]);
|
|
29
31
|
if(Object.hasOwn(c,"ordinaryDigest")&&!sha(c.ordinaryDigest))throw Error("invalid ordinary binding digest");
|
|
32
|
+
if(Object.hasOwn(c,"learningLifecycleId")&&!sha(c.learningLifecycleId))throw Error("invalid learning lifecycle manifest");
|
|
30
33
|
if(c.version!=="producer-dashboard-host-v1"||![c.trustDirectory,c.archiveRoot,c.policyPath].every(p=>typeof p==="string"&&isAbsolute(p))||![c.trustPolicyId,c.policySha256,c.budgetDigest,c.harnessArtifactDigest].every(sha)||!(c.experimentDigest===null||sha(c.experimentDigest))||typeof c.scope!=="string"||!c.scope||c.scope.length>128||typeof c.author!=="string"||!c.author||c.author.length>256||!Array.isArray(c.sources)||c.sources.length<1||c.sources.length>32||new Set(c.sources.map(s=>s.id)).size!==c.sources.length||c.sources.filter(s=>s.kind==="work").length>1||c.sources.some(s=>!/^[a-zA-Z0-9:_-]{1,128}$/.test(s.id)||!["work","retention","facts"].includes(s.kind)))throw Error("invalid explicit dashboard host binding");
|
|
31
34
|
return freeze(c);
|
|
32
35
|
}
|
|
@@ -56,7 +59,7 @@ export function openDashboardHost(options:DashboardHostOptions){
|
|
|
56
59
|
const observation=dashboardObservations(h,c,journal.read),history=()=>journal.read();
|
|
57
60
|
const append=(value:Record<string,unknown>)=>journal.append(history().at(-1)!.id,value);
|
|
58
61
|
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;};
|
|
59
|
-
let presenter:DebriefPresenter|undefined,busy=false,poisoned=false,presentationRevision:number|null=null,presenceDigest:string|null=null,attentionDeferred:string|null=null;
|
|
62
|
+
let presenter:DebriefPresenter|undefined,busy=false,poisoned=false,presentationRevision:number|null=null,presenceDigest:string|null=null,attentionDeferred:string|null=null,presentationAcknowledged=false;
|
|
60
63
|
const ordinaryDispatchHoldKey=dataDigest({hostDigest,control:"pause-new-ordinary-dispatch"});
|
|
61
64
|
let displayedActions:null|{tip:string;actions:{key:string;label:string;operation:DashboardHostRequest["operation"];request:DashboardHostRequest}[];digest:string}=null;
|
|
62
65
|
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;};
|
|
@@ -73,7 +76,7 @@ export function openDashboardHost(options:DashboardHostOptions){
|
|
|
73
76
|
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
77
|
const availableHumanActions=async()=>{
|
|
75
78
|
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();
|
|
79
|
+
const declared=await options.humanActions?.({hostDigest,selectionDigest:dashboardSelectionDigest(selection),tip,observations:observation.latest().map(x=>({sourceId:x.sourceId,checkpointId:x.checkpointId})),preparedPresentationDigest:presenter&&presentationRevision!==null&&!presentationAcknowledged?dataDigest(presenter.view()):null})??[],a=authority();
|
|
77
80
|
return declared.map(value=>{const x=detached(value);controlShape(x,["key","label","request"]);const request=dashboardHostRequest(x.request);
|
|
78
81
|
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
82
|
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");
|
|
@@ -88,11 +91,12 @@ export function openDashboardHost(options:DashboardHostOptions){
|
|
|
88
91
|
let debrief:unknown=null;
|
|
89
92
|
if(a&&presenter&&presentationRevision!==null&&presence()?.evidenceDigest===presenceDigest){try{await paused(presentationRevision);debrief=presenter.view();}catch{debrief=null;}}
|
|
90
93
|
let controls:unknown=null;try{controls=await nativeRead();}catch(e){error=String(e);}
|
|
94
|
+
let learning:unknown=null;if(c.learningLifecycleId)try{learning=h.readLearningLifecycle(c.archiveRoot,c.learningLifecycleId);}catch(e){error=String(e);}
|
|
91
95
|
const attention=trust.inspect(Date.now());let actions:{key:string;label:string;operation:string}[]=[];
|
|
92
96
|
try{const available=await availableHumanActions(),digest=dataDigest(available.map(({key,label,operation,request})=>({key,label,operation,requestDigest:dashboardHostRequestDigest(request)})));
|
|
93
97
|
if(displayedActions?.tip===tip&&displayedActions.digest!==digest)throw Error("displayed dashboard actions changed before journal advance");
|
|
94
98
|
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,
|
|
99
|
+
return freeze(detached({version:"producer-dashboard-frame-v1",hostDigest,selectionDigest:dashboardSelectionDigest(selection),selectionState,tip,source,controls,learning,debrief,attention,error,actions,
|
|
96
100
|
requests:rows.filter(e=>["claim","result","presentation","defer","ordinary-intent-pending"].includes(String(e.value.type))).map(e=>e.value),
|
|
97
101
|
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",
|
|
98
102
|
acknowledgement:poisoned?"unknown":"readback-only",identity:"independently-declared-host; not human/module authentication",activeBranch:null,acceptance:"not-assessed",freshness:"snapshot-unknown",workerInteractions:0}));
|
|
@@ -132,11 +136,11 @@ export function openDashboardHost(options:DashboardHostOptions){
|
|
|
132
136
|
attentionDeferred=null;const view=await presenter.open({mode:"manual",userPresent:true});await paused(p.dispatchRevision);
|
|
133
137
|
if(attentionDeferred){presenter.close();presentationRevision=null;append({type:"defer",requestId:request.requestId,reason:attentionDeferred,weekly:true});result={state:"deferred",reason:attentionDeferred};}
|
|
134
138
|
else {if(view.state!=="open")throw Error("presentation data/attention acknowledgement unavailable");
|
|
135
|
-
presentationRevision=p.dispatchRevision;presenceDigest=p.evidenceDigest;const frameDigest=dataDigest(view);append({type:"presentation",requestId:request.requestId,evidence:p,frameDigest,state:"prepared-not-delivery",automatic:"unqualified"});result={state:"prepared",frameDigest};}
|
|
139
|
+
presentationRevision=p.dispatchRevision;presenceDigest=p.evidenceDigest;presentationAcknowledged=false;const frameDigest=dataDigest(view);append({type:"presentation",requestId:request.requestId,evidence:p,frameDigest,state:"prepared-not-delivery",automatic:"unqualified"});result={state:"prepared",frameDigest};}
|
|
136
140
|
}
|
|
137
141
|
}else if(request.operation==="presented"){
|
|
138
142
|
const p=detached(request.payload) as {frameDigest:string};if(Object.keys(p).join()!=="frameDigest"||!presenter||presentationRevision===null)throw Error("original prepared presentation required");
|
|
139
|
-
await paused(presentationRevision);if(presence()?.evidenceDigest!==presenceDigest)throw Error("user absent/presence changed");if(p.frameDigest!==dataDigest(presenter.view()))throw Error("stale presentation acknowledgement");result={state:"host-acknowledged-presentation",frameDigest:p.frameDigest,humanAuthentication:"unqualified"};
|
|
143
|
+
await paused(presentationRevision);if(presence()?.evidenceDigest!==presenceDigest)throw Error("user absent/presence changed");if(p.frameDigest!==dataDigest(presenter.view()))throw Error("stale presentation acknowledgement");presentationAcknowledged=true;result={state:"host-acknowledged-presentation",frameDigest:p.frameDigest,humanAuthentication:"unqualified"};
|
|
140
144
|
}else if(request.operation==="defer"){
|
|
141
145
|
presenter?.close();presentationRevision=null;append({type:"defer",requestId:request.requestId,weekly:true,evidence:request.payload});result={state:"deferred"};
|
|
142
146
|
}else if(request.operation==="debrief"){
|
|
@@ -153,7 +157,7 @@ export function openDashboardHost(options:DashboardHostOptions){
|
|
|
153
157
|
}else if(request.operation==="intent"||request.operation==="intent-reconcile"){
|
|
154
158
|
const native=detached(intentRequest(request.payload as IntentRequest)) as IntentRequest,port=budget.intentControls(a!.dispatch),digest=intentRequestDigest(native);
|
|
155
159
|
if(c.ordinaryDigest&&!ordinary)throw Error("original ordinary boundary unavailable; no recovery");
|
|
156
|
-
if(!ordinary)result=request.operation==="intent"?await port.request(native):await port.reconcile(native);
|
|
160
|
+
if(!ordinary){result=request.operation==="intent"?await port.request(native):await port.reconcile(native);const snapshot=result as {selection:IntentRequest["selection"];records:{requestId:string;application:string}[]},outcome=snapshot.records.find(r=>r.requestId===native.requestId);if(outcome?.application==="applied")await options.afterIntentApplied?.(snapshot.selection);}
|
|
157
161
|
else {
|
|
158
162
|
const pending=history().filter(e=>e.value.type==="ordinary-intent-pending"&&e.value.nativeRequestId===native.requestId);
|
|
159
163
|
if(pending.some(e=>e.value.digest!==digest))throw Error("immutable pending ordinary intent ID");
|
|
@@ -167,8 +171,9 @@ export function openDashboardHost(options:DashboardHostOptions){
|
|
|
167
171
|
result={state:"pending-ordinary-boundary",nativeRequestId:native.requestId,application:"not-acknowledged"};
|
|
168
172
|
}else{
|
|
169
173
|
result=(request.operation==="intent"||pending.length>0&&!exists)?await port.request(native):await port.reconcile(native);
|
|
170
|
-
const
|
|
174
|
+
const snapshot=result as {selection:IntentRequest["selection"];records:{requestId:string;application:string}[]},outcome=snapshot.records.find(r=>r.requestId===native.requestId);
|
|
171
175
|
if(held&&!outcome)throw Error("native intent application acknowledgement unavailable");
|
|
176
|
+
if(outcome?.application==="applied")await options.afterIntentApplied?.(snapshot.selection);
|
|
172
177
|
if(held&&["applied","not-applied"].includes(outcome!.application))releaseOrdinary=held.release;
|
|
173
178
|
}
|
|
174
179
|
}
|
|
@@ -25,6 +25,7 @@ export function dashboardObservations(h: DashboardHarness, c: DashboardHostConfi
|
|
|
25
25
|
captured.factSourceManifestId=factSource?.sourceManifestId??null;captured.factBasis=factSource?"explicit-host-declarations-not-authenticated-truth":"derived-retained-work-only";
|
|
26
26
|
if(dataDigest(authority.workContext.selectedSnapshot)!==dataDigest(selection))throw Error("independent source selection changed");
|
|
27
27
|
try { const signals=h.captureArchivedWorkSignals(root,sourceManifestId,authority.workContext,facts);
|
|
28
|
+
captured.runtimeFactsManifestId=signals.runtimeFactsManifestId;
|
|
28
29
|
cases={version:"work-signals-v1",batchId:signals.caseBatchId,observationId:signals.observationId};linkageManifestId=signals.linkageManifestId;
|
|
29
30
|
} catch(error) { captured.semanticFailure=String(error); } // Retain checkpoint/gap even when nomination is unavailable.
|
|
30
31
|
} else {if(input.facts!==null||captured.kind!=="execution-retention-v2")throw Error("retention source requires exact referenced-blobs policy");h.readRetainedExecution(root,captured.checkpointId);}
|
package/src/experiment.ts
CHANGED
|
@@ -146,6 +146,12 @@ export function openExperiment(input: ExperimentBinding, hostAuthority: Experime
|
|
|
146
146
|
let resolve!: (success: boolean) => void;
|
|
147
147
|
return { promise: new Promise<boolean>(done => { resolve = done; }), done: (success: boolean) => resolve(success) };
|
|
148
148
|
});
|
|
149
|
+
// Public readiness includes the durable spawn observation, not merely process creation.
|
|
150
|
+
// This closes the process-start -> spawn-ledger race for inspect and cancellation callers.
|
|
151
|
+
const observedStarted = reserved.map((run, i) => run.started.then(async value => {
|
|
152
|
+
if (value === "spawned") await append({ type: "spawn", executionId: c.variants[i].executionId });
|
|
153
|
+
return value;
|
|
154
|
+
}));
|
|
149
155
|
const one = async (i: number) => {
|
|
150
156
|
const v = c.variants[i]; let success = false, launched = false, primaryResult: VariantRecord | null = null;
|
|
151
157
|
try {
|
|
@@ -155,8 +161,7 @@ export function openExperiment(input: ExperimentBinding, hostAuthority: Experime
|
|
|
155
161
|
launched = true;
|
|
156
162
|
const running = reserved[i].run(handles.get(v.executionId)!.signal);
|
|
157
163
|
// Consume both promises even on bookkeeping failure. No untracked rejection or worker.
|
|
158
|
-
const
|
|
159
|
-
const [result, observation] = await Promise.allSettled([running, observed]);
|
|
164
|
+
const [result, observation] = await Promise.allSettled([running, observedStarted[i]]);
|
|
160
165
|
if (result.status === "rejected" || observation.status === "rejected") throw new Error("runtime/accounting/observation unknown");
|
|
161
166
|
const data = Buffer.from(JSON.stringify(result.value) + "\n"), out = result.value.output;
|
|
162
167
|
const cancelled = out.aborted && (Boolean(out.signal) || await reserved[i].started === "settled-without-spawn");
|
|
@@ -218,7 +223,7 @@ export function openExperiment(input: ExperimentBinding, hostAuthority: Experime
|
|
|
218
223
|
}
|
|
219
224
|
const view = await inspect().catch(unknown); primary.resolve(view.variants[0]); resolveBoundary(view); return view;
|
|
220
225
|
})();
|
|
221
|
-
live = Object.freeze({ primary: primary.promise, completion, boundary: c.order ? boundary : completion, started: Object.freeze(
|
|
226
|
+
live = Object.freeze({ primary: primary.promise, completion, boundary: c.order ? boundary : completion, started: Object.freeze(observedStarted) }); return live;
|
|
222
227
|
} catch (error) {
|
|
223
228
|
fault = true;
|
|
224
229
|
for (const run of reserved) await run.run(AbortSignal.abort()).catch(() => {});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Locate the pinned, lifecycle-only Herdr Pi integration shipped with this package. */
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Explicit `-e` survives Pi's `--no-extensions`; only this audited source is loaded for a Herdr child.
|
|
7
|
+
*
|
|
8
|
+
* Source-loaded tests resolve the adjacent vendor file. Installed builds resolve the packed `src/` copy,
|
|
9
|
+
* deliberately rather than discovering an operator's extension directory.
|
|
10
|
+
*/
|
|
11
|
+
export function bundledHerdrPiLifecycleExtension(): string {
|
|
12
|
+
const candidates = [
|
|
13
|
+
new URL("./vendor/herdr-pi-lifecycle.ts", import.meta.url),
|
|
14
|
+
new URL("../src/vendor/herdr-pi-lifecycle.ts", import.meta.url),
|
|
15
|
+
];
|
|
16
|
+
for (const candidate of candidates) {
|
|
17
|
+
const path = fileURLToPath(candidate);
|
|
18
|
+
if (existsSync(path)) return path;
|
|
19
|
+
}
|
|
20
|
+
throw new Error("the bundled Herdr Pi lifecycle integration is missing from this package");
|
|
21
|
+
}
|
package/src/herdr-poll.ts
CHANGED
|
@@ -58,6 +58,15 @@ export function seqOf(result: Record<string, unknown> | undefined): number {
|
|
|
58
58
|
|
|
59
59
|
/** How often to poll `agent get` while waiting for the child to settle. */
|
|
60
60
|
export const POLL_INTERVAL_MS = 750;
|
|
61
|
+
/**
|
|
62
|
+
* A prompt accepted from a terminal state must produce an observable lifecycle change promptly.
|
|
63
|
+
*
|
|
64
|
+
* Herdr 0.8's `agent prompt --wait` documents this same five-second bound. We retain polling because it
|
|
65
|
+
* supplies the bounded live pane snapshots, but refuse rather than holding a parent for twenty minutes when
|
|
66
|
+
* the only reported state remains the pre-prompt terminal state. That is a detector/integration failure, not
|
|
67
|
+
* evidence that a child settled.
|
|
68
|
+
*/
|
|
69
|
+
export const FRESH_LIFECYCLE_TIMEOUT_MS = 5_000;
|
|
61
70
|
/** Lines of pane tail reported per poll. Matches the status block's own tail, so nothing is fetched unused. */
|
|
62
71
|
export const DEFAULT_SNAPSHOT_LINES = 3;
|
|
63
72
|
|
|
@@ -74,6 +83,31 @@ export const DEFAULT_SNAPSHOT_LINES = 3;
|
|
|
74
83
|
* `state_change_seq` has advanced past the value observed before prompting. `agent wait` is deliberately
|
|
75
84
|
* not used at all: its contract cannot express "settled *after* this point".
|
|
76
85
|
*/
|
|
86
|
+
/**
|
|
87
|
+
* Establish the bundled Pi lifecycle reporter's idle baseline before prompt dispatch.
|
|
88
|
+
*
|
|
89
|
+
* `screen_detection_skipped` is Herdr's reported fact that a full lifecycle authority owns this pane.
|
|
90
|
+
* Waiting for it prevents the reporter's asynchronously queued initial idle report from becoming a
|
|
91
|
+
* post-prompt sequence advance. This is not completion: no prompt has been sent at this point.
|
|
92
|
+
*/
|
|
93
|
+
export async function waitForLifecycleBaseline(
|
|
94
|
+
exec: HerdrExec,
|
|
95
|
+
request: PollTarget,
|
|
96
|
+
deadline: number,
|
|
97
|
+
): Promise<{ before?: number; aborted?: boolean; spawnError?: string }> {
|
|
98
|
+
const interval = request.pollIntervalMs ?? POLL_INTERVAL_MS;
|
|
99
|
+
for (;;) {
|
|
100
|
+
if (request.signal?.aborted) return { aborted: true };
|
|
101
|
+
if (Date.now() >= deadline) return { spawnError: "Herdr did not activate the bundled Pi lifecycle reporter before prompt dispatch" };
|
|
102
|
+
const reply = parseReply(await exec(["agent", "get", request.name]));
|
|
103
|
+
if (reply.error) return { spawnError: `herdr agent get failed: ${reply.error}` };
|
|
104
|
+
const agent = (reply.result?.agent ?? reply.result ?? {}) as { agent_status?: string; state_change_seq?: number; screen_detection_skipped?: boolean };
|
|
105
|
+
const seq = typeof agent.state_change_seq === "number" ? agent.state_change_seq : -1;
|
|
106
|
+
if (agent.screen_detection_skipped === true && agent.agent_status && TERMINAL.has(agent.agent_status) && seq >= 0) return { before: seq };
|
|
107
|
+
await new Promise<void>((resolve) => setTimeout(resolve, Math.min(interval, Math.max(0, deadline - Date.now()))));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
77
111
|
export async function waitForSettled(
|
|
78
112
|
exec: HerdrExec,
|
|
79
113
|
request: PollTarget,
|
|
@@ -88,6 +122,7 @@ export async function waitForSettled(
|
|
|
88
122
|
// No cross-poll state is kept, deliberately: the previous design remembered what it had reported so it could
|
|
89
123
|
// send a diff, and that is what broke — see `tailLines`. A snapshot needs no memory.
|
|
90
124
|
const keep = request.snapshotLines ?? DEFAULT_SNAPSHOT_LINES;
|
|
125
|
+
let staleTerminalSince: number | undefined;
|
|
91
126
|
|
|
92
127
|
for (;;) {
|
|
93
128
|
if (request.signal?.aborted) return { aborted: true };
|
|
@@ -114,9 +149,24 @@ export async function waitForSettled(
|
|
|
114
149
|
}
|
|
115
150
|
}
|
|
116
151
|
|
|
117
|
-
if (status && TERMINAL.has(status)
|
|
152
|
+
if (status && TERMINAL.has(status)) {
|
|
153
|
+
if (seq > before) return { status };
|
|
154
|
+
// Do not turn this pane's text into a completion signal. A known agent whose screen rule misses a new
|
|
155
|
+
// UI shape falls back to idle, precisely the state R-33 says is unsafe to accept. A stable stale
|
|
156
|
+
// terminal state for this bounded interval is instead a loud compatibility refusal (issue #43).
|
|
157
|
+
staleTerminalSince ??= Date.now();
|
|
158
|
+
if (Date.now() - staleTerminalSince >= FRESH_LIFECYCLE_TIMEOUT_MS) {
|
|
159
|
+
return {
|
|
160
|
+
spawnError:
|
|
161
|
+
`herdr reported ${status} without a post-prompt lifecycle change for ${FRESH_LIFECYCLE_TIMEOUT_MS}ms; ` +
|
|
162
|
+
"refusing to treat the pre-prompt terminal state as completion. Update Herdr's Pi detector or inspect its lifecycle reporting.",
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
staleTerminalSince = undefined;
|
|
167
|
+
}
|
|
118
168
|
|
|
119
|
-
await new Promise((
|
|
169
|
+
await new Promise<void>((resolve) => setTimeout(resolve, Math.min(interval, Math.max(0, deadline - Date.now()))));
|
|
120
170
|
}
|
|
121
171
|
}
|
|
122
172
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Start a Pi agent only after its newly-created Herdr pane reaches a shell prompt. */
|
|
2
|
+
import { parseReply, type HerdrExec } from "./herdr-cli.ts";
|
|
3
|
+
|
|
4
|
+
/** Retry cadence for Herdr's documented `agent_pane_busy` startup race. */
|
|
5
|
+
export const PANE_READY_POLL_MS = 300;
|
|
6
|
+
|
|
7
|
+
export async function startHerdrAgent(
|
|
8
|
+
exec: HerdrExec,
|
|
9
|
+
name: string,
|
|
10
|
+
paneId: string,
|
|
11
|
+
args: string[],
|
|
12
|
+
deadline: number,
|
|
13
|
+
): Promise<{ result?: Record<string, unknown>; error?: string }> {
|
|
14
|
+
for (;;) {
|
|
15
|
+
const reply = parseReply(await exec(["agent", "start", name, "--kind", "pi", "--pane", paneId, "--", ...args]));
|
|
16
|
+
if (!reply.error) return reply;
|
|
17
|
+
if (!/not an available shell|agent_pane_busy/.test(reply.error) || Date.now() >= deadline) return reply;
|
|
18
|
+
await new Promise<void>((resolve) => setTimeout(resolve, PANE_READY_POLL_MS));
|
|
19
|
+
}
|
|
20
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -37,6 +37,7 @@ export {
|
|
|
37
37
|
export { createDebriefPresenter, type DebriefPresenter, type DebriefHost, type DebriefFrame, type DebriefCheckpoint, type DebriefPersistence } from "./debrief.ts";
|
|
38
38
|
export { ordinaryChildrenFor, ordinaryCancellation, ordinaryCancellationDigest, isOrdinaryChildren, type OrdinaryChildren, type OrdinaryCancellation, type OrdinaryAuthority, type OrdinaryTarget } from "./ordinary-children.ts";
|
|
39
39
|
export { loadDashboardHarness, loadedDashboardHarnessDigest, type DashboardHarnessArtifact } from "./dashboard-harness.ts";
|
|
40
|
+
export { startDailyDashboardHost, discoverDailyIntentActions, type DailyDashboardHostInput, type DailyIntentAction } from "./daily-dashboard-host.ts";
|
|
40
41
|
export { createDashboardHost, openDashboardHost, dashboardHostDigest, dashboardHostRequestDigest, dashboardSelectionDigest, type DashboardHost, type DashboardHostConfig, type DashboardHostRequest, type DashboardHostAuthority, type DashboardHarness, type DashboardHostOptions } from "./dashboard-host.ts";
|
|
41
42
|
export { serveDashboardHost, connectDashboardHost, ENV_DASHBOARD_HOST_SOCKET, type DashboardConnection } from "./dashboard-host-transport.ts";
|
|
42
43
|
export { createRetainedDebrief, retainDebriefBlind, openDebriefBlindPreview, type DebriefHarness, type CaseSelection, type DurableBlindBinding } from "./debrief-host.ts";
|
|
@@ -154,6 +155,22 @@ export {
|
|
|
154
155
|
|
|
155
156
|
export { renderDashboard, type DashboardRenderOptions } from "./dashboard-render.ts";
|
|
156
157
|
|
|
158
|
+
export {
|
|
159
|
+
runMeasuredAgentSession,
|
|
160
|
+
piSdkMeasuredSessionHost,
|
|
161
|
+
verifyMeasuredSubscription,
|
|
162
|
+
MeasuredSessionFailure,
|
|
163
|
+
MeasuredSessionUnknownError,
|
|
164
|
+
type MeasuredSessionInput,
|
|
165
|
+
type MeasuredSessionHost,
|
|
166
|
+
type MeasuredSessionHostInput,
|
|
167
|
+
type MeasuredSessionHostResult,
|
|
168
|
+
type ProviderUsage,
|
|
169
|
+
} from "./measured-session.ts";
|
|
170
|
+
export { createMeasuredOrder, openMeasuredOrder, measuredOrderDigest, measuredOrderAcknowledgementDigest, qualifyMeasuredOutput,
|
|
171
|
+
type MeasuredOrder, type MeasuredOrderNode, type MeasuredOrderAttempt, type MeasuredOutputContract, type MeasuredOrderBinding, type MeasuredOrderAuthority,
|
|
172
|
+
type MeasuredOrderAcknowledgement, type MeasuredOrderNodeView } from "./measured-order.ts";
|
|
173
|
+
|
|
157
174
|
export {
|
|
158
175
|
createApprovalGate,
|
|
159
176
|
createApprovalGateProvider,
|
|
@@ -9,9 +9,10 @@ import { freezeWork } from "./work-ledger-json.ts";
|
|
|
9
9
|
import { intentKey, intentSelection, intentPriorities, type WorkIntentBinding, type IntentRequest, type IntentState, type IntentSelection, type IntentPriority } from "./intent-control.ts";
|
|
10
10
|
const ref = (r: WorkFrozen<WorkRevision>) => ({ kind: r.kind, id: r.id, revision: r.revision, digest: r.digest });
|
|
11
11
|
const entity = (r: { kind: string; id: string } | null) => r ? `${r.kind}:${r.id}` : null;
|
|
12
|
+
const allowedIntentPath=(path:string)=>!path.split("/").includes(".pi")||path.endsWith("/.pi/work.jsonl");
|
|
12
13
|
export function workIntentBinding(input: WorkIntentBinding): WorkIntentBinding {
|
|
13
14
|
controlShape(input, ["version", "path", "device", "inode", "grantLedgerPath", "selection", "priorities"]);
|
|
14
|
-
if (input.version !== "1.0" || typeof input.path !== "string" || input.path.length > 1024 || !isAbsolute(input.path) || resolve(input.path) !== input.path || input.path
|
|
15
|
+
if (input.version !== "1.0" || typeof input.path !== "string" || input.path.length > 1024 || !isAbsolute(input.path) || resolve(input.path) !== input.path || !allowedIntentPath(input.path) ||
|
|
15
16
|
![input.device, input.inode].every(x => typeof x === "string" && /^\d+$/.test(x)) || !(input.grantLedgerPath === null || typeof input.grantLedgerPath === "string" && input.grantLedgerPath.length <= 1024 && isAbsolute(input.grantLedgerPath))) throw new TypeError("invalid pinned work binding");
|
|
16
17
|
return JSON.parse(intentKey({ ...input, selection: intentSelection(input.selection), priorities: intentPriorities(input.priorities) }));
|
|
17
18
|
}
|
|
@@ -37,7 +38,7 @@ export function resolveIntent(text: string, selection: WorkFrozen<IntentSelectio
|
|
|
37
38
|
export async function bindWorkIntent(input: { path: string; grantLedgerPath: string | null; selection: IntentSelection; priorities: IntentPriority[] }): Promise<WorkFrozen<WorkIntentBinding>> {
|
|
38
39
|
controlShape(input, ["path", "grantLedgerPath", "selection", "priorities"]);
|
|
39
40
|
const selection = intentSelection(input.selection), priorities = intentPriorities(input.priorities), path = input.path, grantLedgerPath = input.grantLedgerPath;
|
|
40
|
-
if (typeof path !== "string" || !isAbsolute(path) || path
|
|
41
|
+
if (typeof path !== "string" || !isAbsolute(path) || !allowedIntentPath(path)) throw new TypeError("explicit work path required");
|
|
41
42
|
await workDestination(path, grantLedgerPath);
|
|
42
43
|
const parent = await lstat(dirname(path), { bigint: true });
|
|
43
44
|
if (!parent.isDirectory() || (parent.mode & 0o077n) || parent.uid !== BigInt(process.getuid!())) throw new Error("owner-private work parent required");
|