pi-daddy 0.25.1 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/dist/chain.d.ts +2 -0
- package/dist/chain.d.ts.map +1 -1
- package/dist/chain.js.map +1 -1
- package/dist/daily-dashboard-host.js +2 -2
- package/dist/daily-dashboard-host.js.map +1 -1
- package/dist/dashboard-cli.d.ts +2 -0
- package/dist/dashboard-cli.d.ts.map +1 -1
- package/dist/dashboard-cli.js +41 -12
- package/dist/dashboard-cli.js.map +1 -1
- 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/init.d.ts.map +1 -1
- package/dist/init.js +20 -1
- package/dist/init.js.map +1 -1
- package/dist/propagation.d.ts.map +1 -1
- package/dist/propagation.js +2 -1
- package/dist/propagation.js.map +1 -1
- package/dist/work-command.d.ts.map +1 -1
- package/dist/work-command.js +13 -3
- package/dist/work-command.js.map +1 -1
- package/extensions/chain-plan.ts +1 -0
- package/extensions/daily-dashboard-session.ts +12 -4
- package/extensions/delegate-chain.ts +6 -3
- package/extensions/delegation-activation.ts +17 -0
- package/extensions/delegation-authority.ts +12 -0
- package/extensions/delegation.ts +9 -8
- package/extensions/grants.ts +16 -21
- package/extensions/reload-environment.ts +63 -0
- package/extensions/session-environment.ts +45 -0
- package/extensions/session.ts +45 -34
- package/package.json +2 -1
- package/src/chain.ts +2 -0
- package/src/daily-dashboard-host.ts +3 -3
- package/src/dashboard-cli.ts +36 -11
- package/src/dashboard-host.ts +18 -5
- package/src/init.ts +18 -1
- package/src/propagation.ts +2 -1
- package/src/work-command.ts +10 -3
package/extensions/session.ts
CHANGED
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
ENV_LEDGER,
|
|
38
38
|
ENV_MAX_DEPTH,
|
|
39
39
|
ENV_PARENT_ID,
|
|
40
|
+
GRANT_ENV_KEYS,
|
|
40
41
|
parseList,
|
|
41
42
|
} from "../src/propagation.ts";
|
|
42
43
|
import type { Capability } from "../src/resolve.ts";
|
|
@@ -49,6 +50,8 @@ import { storedGrantSessionState } from "./stored-grant-session.ts";
|
|
|
49
50
|
import { nativeSessionRootFromEnv, type NativeSessionHost } from "../src/native-session-target.ts";
|
|
50
51
|
import { ENV_ALLOW_UNRESOLVED_MODELS } from "../src/model-preflight.ts";
|
|
51
52
|
import type { DeclaredWorkState } from "../src/work-command.ts";
|
|
53
|
+
import { beginExtensionLifecycle, rememberChildPublication, type ReloadLifecycle } from "./reload-environment.ts";
|
|
54
|
+
import { reconcileSessionEnvironment } from "./session-environment.ts";
|
|
52
55
|
/**
|
|
53
56
|
* Run governed children in herdr panes instead of captured child processes.
|
|
54
57
|
*
|
|
@@ -93,14 +96,15 @@ export interface GrantsSession extends NativeSessionHost {
|
|
|
93
96
|
*/
|
|
94
97
|
governed: boolean;
|
|
95
98
|
/** The upper bound handed down by the delegator, before this session's own tools are observed. */
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
+
inherited: Capability[];
|
|
100
|
+
depth: number;
|
|
101
|
+
maxDepth: number;
|
|
99
102
|
/** Bound variables that could not be read as non-negative integers — spawning is disabled, loudly. */
|
|
100
|
-
|
|
101
|
-
|
|
103
|
+
malformedBounds: string[];
|
|
104
|
+
gated: Capability[];
|
|
102
105
|
/** Resolved against the actual pi cwd at session start, then inherited verbatim by every descendant. */
|
|
103
106
|
ledgerPath?: string;
|
|
107
|
+
ledgerFromEnvironment: boolean;
|
|
104
108
|
/**
|
|
105
109
|
* Which executor runs this session's children — ADR-0031.
|
|
106
110
|
*
|
|
@@ -114,15 +118,17 @@ export interface GrantsSession extends NativeSessionHost {
|
|
|
114
118
|
*/
|
|
115
119
|
executor: ExecutorChoice;
|
|
116
120
|
/** This session's readable logical ledger identity; children descend from it (F8). */
|
|
117
|
-
|
|
121
|
+
ownSpawnId: string;
|
|
118
122
|
/** Unique identity when this session is itself a governed child; roots have no governed parent. */
|
|
119
|
-
|
|
123
|
+
ownExecutionId?: string;
|
|
120
124
|
/** Descendants this subtree may still create — the cardinality bound ADR-0008 never had. */
|
|
121
|
-
|
|
122
|
-
/** Whether
|
|
123
|
-
|
|
125
|
+
fanoutBudget: number;
|
|
126
|
+
/** Whether delegation tools are active. Reconciled against the owner-bound root at session_start. */
|
|
127
|
+
mayDelegate: boolean;
|
|
128
|
+
/** True only after session_start binds this instance to ctx.sessionManager. */
|
|
129
|
+
ownerBound: boolean;
|
|
124
130
|
/** Operator escape hatch for custom model resolution. Exact `1`, read once for the session. */
|
|
125
|
-
|
|
131
|
+
allowUnresolvedModels: boolean;
|
|
126
132
|
/** Results from pi's synchronous model catalogue, shared by every delegation in this session. */
|
|
127
133
|
readonly modelResolutionCache: Map<string, boolean>;
|
|
128
134
|
/** Path to this extension, so a child granted `tool:delegate` can delegate in turn. */
|
|
@@ -130,7 +136,8 @@ export interface GrantsSession extends NativeSessionHost {
|
|
|
130
136
|
declaredWork?: DeclaredWorkState; // Explicit operator selection; absence leaves execution visibly unbound.
|
|
131
137
|
/** Primary-return fan-outs retained by this original session; bounded and human-readable via /grants variants. */
|
|
132
138
|
readonly variantRuns: Map<string, VariantRunAccounting>;
|
|
133
|
-
|
|
139
|
+
/** Root identity keyed to ctx.sessionManager once session_start supplies it. */
|
|
140
|
+
reloadLifecycle: ReloadLifecycle;
|
|
134
141
|
/** Approval keys approved for this session. In memory only — this dies with the process. */
|
|
135
142
|
readonly sessionApprovals: Set<string>;
|
|
136
143
|
/** Exact bindings for correlated approvals; these never inherit across a delegation boundary. */
|
|
@@ -143,10 +150,9 @@ export interface GrantsSession extends NativeSessionHost {
|
|
|
143
150
|
* needs `session.definitions`, which does not exist until `session_start` — and this object is built
|
|
144
151
|
* before any hook has run.
|
|
145
152
|
*/
|
|
146
|
-
|
|
153
|
+
inheritedApprovals: Map<string, string | undefined>;
|
|
147
154
|
/** ONE single-flight queue for the whole session — see `obtainApprovals` for why it lives here. */
|
|
148
155
|
readonly approvalGateFor: ReturnType<typeof createApprovalGateProvider>;
|
|
149
|
-
|
|
150
156
|
/** Set at `session_start`; `process.cwd()` until then. */
|
|
151
157
|
cwd: string;
|
|
152
158
|
/** This session's own grant. Starts as the inherited upper bound, tightened once tools are observed. */
|
|
@@ -194,7 +200,7 @@ export interface GrantsSession extends NativeSessionHost {
|
|
|
194
200
|
*/
|
|
195
201
|
readonly storeCwd: string;
|
|
196
202
|
/** Invalid stored state fails closed and is reported/ledgered during session_start. */
|
|
197
|
-
|
|
203
|
+
grantStoreRefusal?: { reason: GrantStoreRefusalReason; path: string };
|
|
198
204
|
/**
|
|
199
205
|
* Adopt the project choice made DURING the session — grant plus optional default ledger — without restart.
|
|
200
206
|
*
|
|
@@ -207,6 +213,7 @@ export interface GrantsSession extends NativeSessionHost {
|
|
|
207
213
|
* widen its own session's ceiling by calling something.
|
|
208
214
|
*/
|
|
209
215
|
adoptGrant(grant: Capability[], projectLedger?: string): void;
|
|
216
|
+
reconcileEnvironment(environment: NodeJS.ProcessEnv, lifecycle: ReloadLifecycle): void;
|
|
210
217
|
}
|
|
211
218
|
|
|
212
219
|
/**
|
|
@@ -238,7 +245,10 @@ export async function loadProjectDefinitions(session: GrantsSession, cwd: string
|
|
|
238
245
|
session.catalog = await session.catalogReady;
|
|
239
246
|
}
|
|
240
247
|
|
|
241
|
-
export function createGrantsSession(extensionPath: string | undefined): GrantsSession {
|
|
248
|
+
export function createGrantsSession(extensionPath: string | undefined, lifecycle?: ReloadLifecycle): GrantsSession {
|
|
249
|
+
const started = lifecycle ? undefined : beginExtensionLifecycle();
|
|
250
|
+
const activeLifecycle = lifecycle ?? started!.lifecycle;
|
|
251
|
+
const environment = lifecycle ? process.env : started!.environment;
|
|
242
252
|
// Governance is opt-in: with PI_GRANTS_GRANT unset AND no stored grant for this directory, the session
|
|
243
253
|
// holds the wildcard and nothing is blocked. This extension must never silently tighten a normal
|
|
244
254
|
// workflow.
|
|
@@ -252,22 +262,21 @@ export function createGrantsSession(extensionPath: string | undefined): GrantsSe
|
|
|
252
262
|
// and therefore before `ctx` exists. That ordering is forced by S-5: whether `delegate` is registered at
|
|
253
263
|
// all is decided here, and a grant arriving later could not inform it. `session_start` re-checks the two
|
|
254
264
|
// against each other and says so if they differ, which is the only case this can get wrong.
|
|
255
|
-
const grantRaw =
|
|
265
|
+
const grantRaw = environment[ENV_GRANT];
|
|
256
266
|
const storeCwd = process.cwd();
|
|
257
267
|
// One root-only store read supplies both decisions made by `/grants init`. A child always has ENV_GRANT,
|
|
258
268
|
// so it cannot activate a ledger merely because its routed cwd happens to have a v2 store (ADR-0037).
|
|
259
269
|
const storedState = storedGrantSessionState(grantRaw, storeCwd);
|
|
260
270
|
const { governed, inherited, refusal: grantStoreRefusal } = storedState;
|
|
261
|
-
const ledgerRaw =
|
|
271
|
+
const ledgerRaw = environment[ENV_LEDGER];
|
|
262
272
|
// Capture provenance before publishChildEnv writes this session's derived default into process.env. A later
|
|
263
273
|
// `/grants init` for ctx.cwd must not mistake our own publication for an operator override.
|
|
264
274
|
const ledgerFromEnvironment = ledgerRaw !== undefined;
|
|
265
275
|
const storedLedger = storedState.defaultLedger;
|
|
266
276
|
// G7 / A-S4 + B-I4: strict, three-way parsing that fails CLOSED. A malformed bound used to yield
|
|
267
277
|
// `NaN`, and every comparison against `NaN` is false, so depth limiting switched itself off.
|
|
268
|
-
const bounds = depthConfig(
|
|
278
|
+
const bounds = depthConfig(environment[ENV_DEPTH], environment[ENV_MAX_DEPTH]);
|
|
269
279
|
const { depth, maxDepth } = bounds;
|
|
270
|
-
|
|
271
280
|
const emptyCatalog = makeCatalog([]);
|
|
272
281
|
|
|
273
282
|
const session: GrantsSession = {
|
|
@@ -281,41 +290,41 @@ export function createGrantsSession(extensionPath: string | undefined): GrantsSe
|
|
|
281
290
|
// did. Inside a session the operator already chose to govern, handing a child `bash` hands it an
|
|
282
291
|
// ungoverned-descendant escape hatch, and doing that silently is what changes here.
|
|
283
292
|
// `PI_GRANTS_GATED=""` turns the default off; absent and empty are deliberately distinguishable.
|
|
284
|
-
gated: governed ? gatedFromEnv(
|
|
293
|
+
gated: governed ? gatedFromEnv(environment[ENV_GATED]) : parseList(environment[ENV_GATED]),
|
|
285
294
|
// Presence wins, including an explicitly empty value for a one-run opt-out. The store is eligible only
|
|
286
295
|
// when ENV_GRANT was absent above, preserving the environment as the child's single authority channel.
|
|
287
296
|
ledgerPath: ledgerRaw !== undefined ? ledgerRaw : storedLedger,
|
|
297
|
+
ledgerFromEnvironment,
|
|
288
298
|
// The un-probed reading. `resolveExecutor` replaces it at session start; until then a `1` already reads as
|
|
289
299
|
// a refusal, which is the safe direction — a delegation that somehow ran before the probe would refuse
|
|
290
300
|
// rather than quietly use the wrong executor.
|
|
291
|
-
executor: chooseExecutor(
|
|
301
|
+
executor: chooseExecutor(environment[ENV_HERDR], null),
|
|
292
302
|
// `ownSpawnId` comes from the parent (F8), so ids form one tree across process boundaries instead of
|
|
293
303
|
// every level restarting at `d0` and the ledger becoming unjoinable.
|
|
294
|
-
ownSpawnId:
|
|
295
|
-
ownExecutionId:
|
|
304
|
+
ownSpawnId: environment[ENV_PARENT_ID]?.trim() || `d${depth}`,
|
|
305
|
+
ownExecutionId: environment[ENV_EXECUTION_ID]?.trim() || undefined,
|
|
296
306
|
// The cardinality bound ADR-0008 never had: it attenuates downward like depth, so a subtree can never
|
|
297
307
|
// create more descendants than its root was given — with no shared state, no lock and no counter file.
|
|
298
|
-
fanoutBudget: budgetFromEnv(
|
|
308
|
+
fanoutBudget: budgetFromEnv(environment[ENV_FANOUT]),
|
|
299
309
|
/**
|
|
300
310
|
* Review finding S-5, fixed. The comment on the tools has always claimed conditional registration; the
|
|
301
311
|
* call was unconditional, `DELEGATE_CAPABILITY` was imported and never used, and "withhold it and the
|
|
302
312
|
* child is a leaf" was simply untrue on this path.
|
|
303
313
|
*
|
|
304
|
-
*
|
|
305
|
-
* before any tools are observed. An ungoverned session registers it as before.
|
|
314
|
+
* Provisional before owner binding; session_start recomputes it from that owner's root before activation.
|
|
306
315
|
*/
|
|
307
316
|
mayDelegate: !governed || inherited.includes(DELEGATE_CAPABILITY) || inherited.includes(WILDCARD),
|
|
308
|
-
|
|
317
|
+
ownerBound: false,
|
|
318
|
+
allowUnresolvedModels: environment[ENV_ALLOW_UNRESOLVED_MODELS] === "1",
|
|
309
319
|
nativeSessionRoot: nativeSessionRootFromEnv(process.env),
|
|
310
320
|
modelResolutionCache: new Map<string, boolean>(),
|
|
311
321
|
extensionPath,
|
|
312
322
|
variantRuns: new Map(),
|
|
313
|
-
|
|
323
|
+
reloadLifecycle: activeLifecycle,
|
|
314
324
|
sessionApprovals: new Set<string>(),
|
|
315
325
|
sessionApprovalBindings: new Map<string, ApprovalBinding>(),
|
|
316
|
-
inheritedApprovals: parseInherited(
|
|
326
|
+
inheritedApprovals: parseInherited(environment[ENV_APPROVED]),
|
|
317
327
|
approvalGateFor: createApprovalGateProvider(),
|
|
318
|
-
|
|
319
328
|
cwd: process.cwd(),
|
|
320
329
|
ownGrant: deriveOwnGrant(inherited, null),
|
|
321
330
|
observed: false,
|
|
@@ -323,7 +332,6 @@ export function createGrantsSession(extensionPath: string | undefined): GrantsSe
|
|
|
323
332
|
definitions: new Map<string, SkillDefinition>(),
|
|
324
333
|
catalog: emptyCatalog,
|
|
325
334
|
catalogReady: Promise.resolve(emptyCatalog),
|
|
326
|
-
|
|
327
335
|
delegationContext: async (approved?: InheritableApproval[]) => ({
|
|
328
336
|
ownGrant: session.ownGrant,
|
|
329
337
|
depth: session.depth,
|
|
@@ -360,12 +368,13 @@ export function createGrantsSession(extensionPath: string | undefined): GrantsSe
|
|
|
360
368
|
session.ownGrant = grant;
|
|
361
369
|
// An environment ledger remains the explicit answer. Otherwise init's v2 choice becomes live now,
|
|
362
370
|
// before publishChildEnv gives the same absolute path to descendants.
|
|
363
|
-
if (!ledgerFromEnvironment && projectLedger !== undefined) {
|
|
371
|
+
if (!session.ledgerFromEnvironment && projectLedger !== undefined) {
|
|
364
372
|
session.ledgerPath = projectLedger;
|
|
365
373
|
}
|
|
366
374
|
session.publishChildEnv();
|
|
367
375
|
},
|
|
368
376
|
|
|
377
|
+
reconcileEnvironment: (environment, lifecycle) => reconcileSessionEnvironment(session, environment, lifecycle),
|
|
369
378
|
publishChildEnv: () => {
|
|
370
379
|
const env = childEnv({
|
|
371
380
|
ownGrant: session.ownGrant,
|
|
@@ -378,9 +387,11 @@ export function createGrantsSession(extensionPath: string | undefined): GrantsSe
|
|
|
378
387
|
// descendants too. Previously it exported its own observed tool surface as their grant.
|
|
379
388
|
governed: session.governed,
|
|
380
389
|
});
|
|
390
|
+
// Clear omitted fields: another owner's provenance must never reach this session's children.
|
|
391
|
+
for (const key of GRANT_ENV_KEYS) delete process.env[key];
|
|
381
392
|
for (const [key, value] of Object.entries(env)) process.env[key] = value;
|
|
393
|
+
rememberChildPublication(session.reloadLifecycle);
|
|
382
394
|
},
|
|
383
395
|
};
|
|
384
|
-
|
|
385
396
|
return session;
|
|
386
397
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-daddy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.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",
|
|
@@ -248,6 +248,7 @@
|
|
|
248
248
|
"build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json",
|
|
249
249
|
"typecheck": "tsc -p tsconfig.check.json",
|
|
250
250
|
"test": "node scripts/unit-tests.ts",
|
|
251
|
+
"test:dashboard-pty": "if [ \"$(uname -s)\" = Linux ]; then python3 test/dashboard-cli-pty.py; else echo 'dashboard PTY probe requires Unix pty/Unix sockets'; fi",
|
|
251
252
|
"test:integration": "node --test test-integration/*.it.ts",
|
|
252
253
|
"test:integration:ci": "node --test test-integration/approval.it.ts test-integration/governance.it.ts",
|
|
253
254
|
"contracts:generate": "node scripts/generate-ledger-v3-contract.ts",
|
package/src/chain.ts
CHANGED
|
@@ -156,6 +156,8 @@ export interface ChainStep {
|
|
|
156
156
|
agent?: string;
|
|
157
157
|
tools?: string[];
|
|
158
158
|
model?: string;
|
|
159
|
+
/** Requested Pi thinking level; validated by the same schema as delegate/delegate_all. */
|
|
160
|
+
thinking?: string;
|
|
159
161
|
correlation?: import("./correlation.ts").CorrelationMetadata;
|
|
160
162
|
workspace?: { workspace_id: string; access: import("./workspace.ts").WorkspaceAccess };
|
|
161
163
|
}
|
|
@@ -70,14 +70,14 @@ export async function startDailyDashboardHost(input:DailyDashboardHostInput){
|
|
|
70
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
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
72
|
});
|
|
73
|
-
const ordinary=input.ordinary.inspect() as {revision:number;children:{target:OrdinaryTarget;state:string}[]},accounting=await openResourceBudget(budget).inspect(),presence=input.presence?.();
|
|
73
|
+
const ordinary=input.ordinary.inspect() as {revision:number;children:{target:OrdinaryTarget;state:string;abortRequested:boolean}[]},accounting=await openResourceBudget(budget).inspect(),presence=input.presence?.();
|
|
74
74
|
const presentation:{key:string;label:string;request:DashboardHostRequest}[]=[];
|
|
75
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
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=>{
|
|
77
|
+
const cancellations=ordinary.children.filter(row=>row.state==="active"&&!row.abortRequested).sort((a,b)=>a.target.executionId.localeCompare(b.target.executionId)).slice(0,8).map(row=>{
|
|
78
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
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};
|
|
80
|
+
return {key:`cancel-${row.target.executionId.replace(":","-")}-${ordinary.revision.toString(36)}`,label:`Cancel running attempt ${row.target.executionId}`,request:cancel};
|
|
81
81
|
});
|
|
82
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
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});
|
package/src/dashboard-cli.ts
CHANGED
|
@@ -58,6 +58,20 @@ export async function dashboardHostAction(host:DashboardConnection,line:string){
|
|
|
58
58
|
return host.humanAction(command);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/** A fulfilled host call is not necessarily an applied native effect; keep those facts distinct in the UI. */
|
|
62
|
+
export function dashboardActionFeedback(command:string,outcome:unknown):string{
|
|
63
|
+
const value=outcome&&typeof outcome==="object"?outcome as {state?:unknown;result?:unknown}:{};
|
|
64
|
+
const state=typeof value.state==="string"?value.state:"unrecognised host response";
|
|
65
|
+
if(state==="failed-or-unknown"||state==="readback-only")return `NO ACTION CLAIM: ${command} returned ${state}; inspect/reconcile explicitly.`;
|
|
66
|
+
if(state!=="acknowledged")return `NOT APPLIED: ${command} returned ${state}.`;
|
|
67
|
+
const result=value.result&&typeof value.result==="object"?value.result as {application?:unknown}:null;
|
|
68
|
+
// A retained records[] snapshot has no outer dashboard request correlation. Never select an old receipt.
|
|
69
|
+
const nativeApplication=typeof result?.application==="string"?result.application:undefined;
|
|
70
|
+
if(nativeApplication==="applied")return `ACKNOWLEDGED: ${command}; native application applied.`;
|
|
71
|
+
if(nativeApplication)return `ACKNOWLEDGED: ${command}; native application ${nativeApplication}, so no applied effect is claimed.`;
|
|
72
|
+
return `ACKNOWLEDGED: ${command}; refreshed state is shown below (no native application claim).`;
|
|
73
|
+
}
|
|
74
|
+
|
|
61
75
|
function shellQuote(value: string): string {
|
|
62
76
|
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
63
77
|
}
|
|
@@ -96,14 +110,15 @@ export async function dashboardFrame(options: DashboardFrameOptions): Promise<st
|
|
|
96
110
|
if(options.connected){
|
|
97
111
|
if(!isDashboardConnection(options.connected))throw Error("original dashboard connection required");
|
|
98
112
|
let view:Awaited<ReturnType<DashboardConnection["frame"]>>;
|
|
99
|
-
try{view=await options.connected.frame();}catch{return
|
|
113
|
+
try{view=await options.connected.frame();}catch(error){return `PI-DADDY — CONNECTED HOST ERROR\n${error instanceof Error ? error.message : String(error)}\nNo prior acceptance, presentation or continuity substituted. No action retried.`;}
|
|
100
114
|
if(options.dailyJson||options.debriefJson)return JSON.stringify(view);
|
|
101
115
|
const source=view.source as {daily?:Parameters<typeof renderDailyView>[0]}|null;
|
|
102
116
|
const actions=(view.actions as {key:string;label:string}[]|undefined)??[];
|
|
103
|
-
|
|
117
|
+
const hostError=typeof view.error==="string"&&view.error?`HOST ERROR — ${view.error}`:null;
|
|
118
|
+
return ["PI-DADDY — CONNECTED HOST (snapshot; identity/acceptance not authenticated)",hostError,source?.daily?renderDailyView(source.daily,options.width):"Source unavailable; no previous acceptance substituted.",
|
|
104
119
|
view.debrief?renderDebrief(view.debrief as Parameters<typeof renderDebrief>[0],options.width):"Debrief deferred/closed. No implicit presentation or steering.",
|
|
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");
|
|
120
|
+
`Host control ${view.control}; acknowledgement ${view.acknowledgement}. Attention reserved ${view.attention.attentionUsed}/5 (not proof of delivery). Tip ${view.tip}.`,
|
|
121
|
+
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."].filter((line):line is string=>Boolean(line)).join("\n\n");
|
|
107
122
|
}
|
|
108
123
|
if (options.debrief || options.debriefJson) {
|
|
109
124
|
if (!isDebriefPresenter(options.debrief)) return "PI-DADDY — DEBRIEF UNAVAILABLE: genuine presenter/host missing";
|
|
@@ -213,6 +228,8 @@ export async function runDashboard(argv = process.argv.slice(2), env: NodeJS.Pro
|
|
|
213
228
|
}
|
|
214
229
|
const dailyReader = createDailyViewReader();
|
|
215
230
|
let previous = "";
|
|
231
|
+
let feedback = "";
|
|
232
|
+
let input: ReturnType<typeof createInterface> | null = null;
|
|
216
233
|
const draw = async (clear: boolean): Promise<void> => {
|
|
217
234
|
const frame = await dashboardFrame({
|
|
218
235
|
cwd,
|
|
@@ -223,15 +240,20 @@ export async function runDashboard(argv = process.argv.slice(2), env: NodeJS.Pro
|
|
|
223
240
|
details: cli.details,
|
|
224
241
|
dailyView, dailyReader, dailyJson: cli.dailyJson, debrief, debriefJson: cli.debriefJson, connected,
|
|
225
242
|
});
|
|
226
|
-
|
|
227
|
-
previous
|
|
228
|
-
|
|
243
|
+
const rendered = feedback ? `${frame}\n\n${feedback}` : frame;
|
|
244
|
+
if (rendered === previous && !clear) return;
|
|
245
|
+
previous = rendered;
|
|
246
|
+
const prompt = "COMMAND — type an exact listed key, then Enter (refresh never acts): ";
|
|
247
|
+
const draft = input?.line ?? "";
|
|
248
|
+
process.stdout.write(clear ? `\u001b]0;PI-DADDY\u0007\u001b[2J\u001b[H${rendered}\n\n${input ? prompt + draft : ""}` : `${rendered}\n`);
|
|
249
|
+
if (input) input.setPrompt(prompt);
|
|
229
250
|
};
|
|
230
251
|
|
|
231
252
|
if (cli.once) {
|
|
232
253
|
await draw(false);
|
|
233
254
|
return;
|
|
234
255
|
}
|
|
256
|
+
input = debrief || connected ? createInterface({ input: process.stdin, output: process.stdout, terminal: true }) : null;
|
|
235
257
|
await draw(true);
|
|
236
258
|
let drawing = false;
|
|
237
259
|
const redraw = (): void => {
|
|
@@ -239,13 +261,16 @@ export async function runDashboard(argv = process.argv.slice(2), env: NodeJS.Pro
|
|
|
239
261
|
drawing = true;
|
|
240
262
|
void draw(true).finally(() => { drawing = false; });
|
|
241
263
|
};
|
|
242
|
-
const input = debrief || connected ? createInterface({ input: process.stdin, terminal: false }) : null;
|
|
243
264
|
let acting = false;
|
|
244
265
|
input?.on("line", line => {
|
|
245
|
-
if (acting) return;
|
|
266
|
+
if (acting) return; // never replay a line typed while the prior exact command is still settling.
|
|
246
267
|
acting = true;
|
|
247
|
-
|
|
248
|
-
|
|
268
|
+
feedback = `COMMAND ${line.trim() || "(empty)"} — applying the displayed exact request…`;
|
|
269
|
+
void (connected?dashboardHostAction(connected,line):debriefAction(debrief!, line)).then(outcome => {
|
|
270
|
+
feedback = connected ? dashboardActionFeedback(line.trim() || "command", outcome) : `ACKNOWLEDGED: ${line.trim() || "command"}. Refreshed state is shown below.`;
|
|
271
|
+
}).catch(error => {
|
|
272
|
+
feedback = `REJECTED: ${error instanceof Error ? error.message : String(error)}. No command was retried; inspect the refreshed offered keys.`;
|
|
273
|
+
}).finally(() => { acting = false; redraw(); });
|
|
249
274
|
});
|
|
250
275
|
const timer = setInterval(redraw, DASHBOARD_REFRESH_MS);
|
|
251
276
|
process.on("SIGWINCH", redraw);
|
package/src/dashboard-host.ts
CHANGED
|
@@ -14,6 +14,9 @@ import { dashboardHostDigest, dashboardHostRequest, dashboardHostRequestDigest,
|
|
|
14
14
|
type DashboardHarness, type DashboardHostConfig, type DashboardHostRequest, type DashboardHostAuthority } from "./dashboard-host-contract.ts";
|
|
15
15
|
export * from "./dashboard-host-contract.ts";
|
|
16
16
|
const hosts=new WeakSet<object>();
|
|
17
|
+
/** Ordinary retention caps rows and cancellations at 1,024 each: attach, settle and cancel can advance 3,072 revisions.
|
|
18
|
+
* At eight visible targets per revision that is 24,576 immutable cancellation meanings; fixed controls fit inside 32,768. */
|
|
19
|
+
const MAX_DISPLAYED_ACTIONS_PER_TIP=32_768;
|
|
17
20
|
export const isDashboardHost=(value:unknown):value is DashboardHost=>typeof value==="object"&&value!==null&&hosts.has(value);
|
|
18
21
|
export type DashboardHost=ReturnType<typeof openDashboardHost>;
|
|
19
22
|
export interface DashboardHostOptions { ordinary?:OrdinaryChildren; harness:DashboardHarness; config:DashboardHostConfig; budget:GovernedBudgetBinding; experiment?:ReturnType<typeof openExperiment>; authority:()=>DashboardHostAuthority|null;
|
|
@@ -61,7 +64,9 @@ export function openDashboardHost(options:DashboardHostOptions){
|
|
|
61
64
|
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;};
|
|
62
65
|
let presenter:DebriefPresenter|undefined,busy=false,poisoned=false,presentationRevision:number|null=null,presenceDigest:string|null=null,attentionDeferred:string|null=null,presentationAcknowledged=false;
|
|
63
66
|
const ordinaryDispatchHoldKey=dataDigest({hostDigest,control:"pause-new-ordinary-dispatch"});
|
|
64
|
-
|
|
67
|
+
// A host frame is a read-only snapshot: ordinary children may attach or settle without advancing this journal.
|
|
68
|
+
// Within one tip, a key keeps its first exact request; humanAction revalidates it against current native authority.
|
|
69
|
+
let displayedActions:null|{tip:string;actions:Map<string,{key:string;label:string;operation:DashboardHostRequest["operation"];request:DashboardHostRequest}>}=null;
|
|
65
70
|
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;};
|
|
66
71
|
const persistence:DebriefPersistence={durability:"host-owned",load:saved,compareAndSwap(expected,next){
|
|
67
72
|
const rows=history(),previous=saved();if((previous?dataDigest(previous):null)!==expected)throw Error("stale attention CAS");
|
|
@@ -93,9 +98,17 @@ export function openDashboardHost(options:DashboardHostOptions){
|
|
|
93
98
|
let controls:unknown=null;try{controls=await nativeRead();}catch(e){error=String(e);}
|
|
94
99
|
let learning:unknown=null;if(c.learningLifecycleId)try{learning=h.readLearningLifecycle(c.archiveRoot,c.learningLifecycleId);}catch(e){error=String(e);}
|
|
95
100
|
const attention=trust.inspect(Date.now());let actions:{key:string;label:string;operation:string}[]=[];
|
|
96
|
-
try{const available=await availableHumanActions()
|
|
97
|
-
|
|
98
|
-
|
|
101
|
+
try{const available=await availableHumanActions();
|
|
102
|
+
// A frame that completed after its captured tip is stale and cannot replace a newer display generation.
|
|
103
|
+
if(history().at(-1)!.id===tip){
|
|
104
|
+
if(!displayedActions||displayedActions.tip!==tip)displayedActions={tip,actions:new Map()};
|
|
105
|
+
const additions=available.filter(action=>!displayedActions!.actions.has(action.key));
|
|
106
|
+
if(displayedActions.actions.size+additions.length>MAX_DISPLAYED_ACTIONS_PER_TIP)throw Error("displayed dashboard action capacity exhausted");
|
|
107
|
+
const shown=available.filter(action=>{const prior=displayedActions!.actions.get(action.key);
|
|
108
|
+
if(prior&&dashboardHostRequestDigest(prior.request)!==dashboardHostRequestDigest(action.request))return false;
|
|
109
|
+
if(!prior)displayedActions!.actions.set(action.key,action);return true;});
|
|
110
|
+
actions=shown.map(({key,label,operation})=>({key,label,operation}));
|
|
111
|
+
}}catch(e){error=String(e);}
|
|
99
112
|
return freeze(detached({version:"producer-dashboard-frame-v1",hostDigest,selectionDigest:dashboardSelectionDigest(selection),selectionState,tip,source,controls,learning,debrief,attention,error,actions,
|
|
100
113
|
requests:rows.filter(e=>["claim","result","presentation","defer","ordinary-intent-pending"].includes(String(e.value.type))).map(e=>e.value),
|
|
101
114
|
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",
|
|
@@ -104,7 +117,7 @@ export function openDashboardHost(options:DashboardHostOptions){
|
|
|
104
117
|
/** Explicit reconciliation is read-only. A retained claim is never replayed as an effect. */
|
|
105
118
|
reconcile:()=>api.frame(),
|
|
106
119
|
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");
|
|
107
|
-
const found=displayedActions.actions.
|
|
120
|
+
const found=displayedActions.actions.get(key);if(!found)throw Error(`unknown dashboard action ${JSON.stringify(key)}`);
|
|
108
121
|
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);},
|
|
109
122
|
async action(input:DashboardHostRequest){
|
|
110
123
|
const request=dashboardHostRequest(input),digest=dashboardHostRequestDigest(request);
|
package/src/init.ts
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* for, rather than a grant nobody decided.
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
-
import { mkdir, open, rm, writeFile } from "node:fs/promises";
|
|
25
|
+
import { lstat, mkdir, open, rm, writeFile } from "node:fs/promises";
|
|
26
26
|
import { join } from "node:path";
|
|
27
27
|
import { agentCapability, workspaceCapability } from "./capabilities.ts";
|
|
28
28
|
import { ceilingForDefinition } from "./definitions.ts";
|
|
@@ -372,6 +372,23 @@ async function replace(path: string, content: string, outcome: InitOutcome): Pro
|
|
|
372
372
|
*/
|
|
373
373
|
export async function applyInit(plan: InitPlan, options: { force?: boolean } = {}): Promise<InitOutcome> {
|
|
374
374
|
const outcome: InitOutcome = { written: [], kept: [], failed: [] };
|
|
375
|
+
const projectPi = join(plan.grantEnvPath, "..");
|
|
376
|
+
try {
|
|
377
|
+
// A new project gets a private control directory. Existing operator/Pi state is never chmodded or reused.
|
|
378
|
+
await mkdir(projectPi, { mode: 0o700 });
|
|
379
|
+
const created = await lstat(projectPi);
|
|
380
|
+
if (!created.isDirectory() || created.isSymbolicLink()) throw Error("private .pi directory creation failed");
|
|
381
|
+
} catch (error) {
|
|
382
|
+
if ((error as { code?: string }).code !== "EEXIST") {
|
|
383
|
+
outcome.failed.push({ path: projectPi, error: error instanceof Error ? error.message : String(error) });
|
|
384
|
+
return outcome;
|
|
385
|
+
}
|
|
386
|
+
const existing = await lstat(projectPi);
|
|
387
|
+
if (existing.isSymbolicLink() || !existing.isDirectory()) {
|
|
388
|
+
outcome.failed.push({ path: projectPi, error: "existing .pi state is not a directory; it was not followed or changed" });
|
|
389
|
+
return outcome;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
375
392
|
const force = options.force === true;
|
|
376
393
|
for (const skill of plan.skills) {
|
|
377
394
|
if (force) await replace(skill.targetPath, skill.content, outcome);
|
package/src/propagation.ts
CHANGED
|
@@ -285,7 +285,8 @@ export function childEnv(input: ChildEnvInput): Record<string, string> {
|
|
|
285
285
|
// whatever was there before — the parent's own, unclamped `PI_GRANTS_APPROVED` — visible to every child.
|
|
286
286
|
// `parseList("")` is `[]`, so an empty value reads back exactly as an absent one.
|
|
287
287
|
env[ENV_APPROVED] = inheritApprovals(input.approved ?? [], inheritable).join(",");
|
|
288
|
-
|
|
288
|
+
// Empty is an explicit one-run ledger opt-out and must overwrite a prior publication too.
|
|
289
|
+
if (input.ledgerPath !== undefined) env[ENV_LEDGER] = input.ledgerPath;
|
|
289
290
|
return env;
|
|
290
291
|
}
|
|
291
292
|
|
package/src/work-command.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { lstat, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
4
4
|
import {
|
|
5
5
|
appendWorkLedgerEventOnce,
|
|
@@ -101,8 +101,15 @@ export async function declareWork(input: DeclareWorkInput): Promise<WorkFrozen<D
|
|
|
101
101
|
}
|
|
102
102
|
if (!ID.test(input.id) || typeof input.outcome !== "string" || !input.outcome.trim()) throw new TypeError("work declaration requires an identifier and non-empty outcome");
|
|
103
103
|
const resolved = paths(input), outcomeDigest = sha256(input.outcome.trim());
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
const projectPi = dirname(resolved.ledgerPath);
|
|
105
|
+
try {
|
|
106
|
+
// Create only a new project control directory as private; never chmod or repurpose an existing one.
|
|
107
|
+
await mkdir(projectPi, { mode: 0o700 });
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if ((error as { code?: string }).code !== "EEXIST") throw error;
|
|
110
|
+
const existing = await lstat(projectPi);
|
|
111
|
+
if (!existing.isDirectory() || existing.isSymbolicLink()) throw Error("existing .pi state is not a directory; it was not followed or changed");
|
|
112
|
+
}
|
|
106
113
|
return withFileLock(resolved.statePath, "declared work", async () => {
|
|
107
114
|
const existing = await loadDeclaredWork(resolved.statePath);
|
|
108
115
|
if (existing) {
|