pi-daddy 0.23.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +12 -6
  3. package/contracts/ledger/v4/README.md +1 -1
  4. package/dist/cli.d.ts +3 -1
  5. package/dist/cli.d.ts.map +1 -1
  6. package/dist/cli.js +49 -3
  7. package/dist/cli.js.map +1 -1
  8. package/dist/daily-dashboard-host.d.ts +99 -0
  9. package/dist/daily-dashboard-host.d.ts.map +1 -0
  10. package/dist/daily-dashboard-host.js +71 -0
  11. package/dist/daily-dashboard-host.js.map +1 -0
  12. package/dist/dashboard-cli.d.ts.map +1 -1
  13. package/dist/dashboard-cli.js +9 -2
  14. package/dist/dashboard-cli.js.map +1 -1
  15. package/dist/dashboard-harness.d.ts +3 -0
  16. package/dist/dashboard-harness.d.ts.map +1 -1
  17. package/dist/dashboard-harness.js +14 -0
  18. package/dist/dashboard-harness.js.map +1 -1
  19. package/dist/dashboard-host-contract.d.ts +6 -1
  20. package/dist/dashboard-host-contract.d.ts.map +1 -1
  21. package/dist/dashboard-host-contract.js +1 -1
  22. package/dist/dashboard-host-contract.js.map +1 -1
  23. package/dist/dashboard-host-transport.d.ts +1 -0
  24. package/dist/dashboard-host-transport.d.ts.map +1 -1
  25. package/dist/dashboard-host-transport.js +5 -3
  26. package/dist/dashboard-host-transport.js.map +1 -1
  27. package/dist/dashboard-host.d.ts +54 -0
  28. package/dist/dashboard-host.d.ts.map +1 -1
  29. package/dist/dashboard-host.js +49 -2
  30. package/dist/dashboard-host.js.map +1 -1
  31. package/dist/dashboard-observation.d.ts.map +1 -1
  32. package/dist/dashboard-observation.js +5 -5
  33. package/dist/dashboard-observation.js.map +1 -1
  34. package/dist/experiment-store.d.ts +3 -1
  35. package/dist/experiment-store.d.ts.map +1 -1
  36. package/dist/experiment-store.js +20 -3
  37. package/dist/experiment-store.js.map +1 -1
  38. package/dist/work-command.d.ts +30 -0
  39. package/dist/work-command.d.ts.map +1 -0
  40. package/dist/work-command.js +148 -0
  41. package/dist/work-command.js.map +1 -0
  42. package/extensions/daily-dashboard-session.ts +30 -0
  43. package/extensions/daily-work-session.ts +20 -0
  44. package/extensions/delegation.ts +21 -8
  45. package/extensions/execute-child.ts +10 -1
  46. package/extensions/executor-session.ts +9 -0
  47. package/extensions/grants-command.ts +14 -15
  48. package/extensions/grants-connected-command.ts +19 -0
  49. package/extensions/grants.ts +25 -3
  50. package/extensions/primary-shadow.ts +11 -0
  51. package/extensions/run-delegation.ts +2 -0
  52. package/extensions/session.ts +7 -19
  53. package/extensions/work-runtime.ts +44 -0
  54. package/package.json +1 -1
  55. package/src/cli.ts +41 -4
  56. package/src/daily-dashboard-host.ts +49 -0
  57. package/src/dashboard-cli.ts +7 -2
  58. package/src/dashboard-harness.ts +9 -0
  59. package/src/dashboard-host-contract.ts +4 -1
  60. package/src/dashboard-host-transport.ts +8 -6
  61. package/src/dashboard-host.ts +26 -3
  62. package/src/dashboard-observation.ts +3 -2
  63. package/src/experiment-store.ts +11 -3
  64. package/src/work-command.ts +179 -0
@@ -13,7 +13,8 @@ export async function ownedDirectory(path: string) {
13
13
  if (!s.isDirectory() || s.isSymbolicLink() || s.uid !== BigInt(process.getuid!()) || (s.mode & 0o077n) !== 0n || await realpath(path) !== path) throw new Error("canonical private experiment directory required");
14
14
  return s;
15
15
  }
16
- export async function readExperimentFile(path: string, max: number): Promise<Buffer> {
16
+ export async function readExperimentFile(path: string, max: number, options: { appendOnly?: true } = {}): Promise<Buffer> {
17
+ if (Object.keys(options).some(key => key !== "appendOnly") || options.appendOnly !== undefined && options.appendOnly !== true) throw new TypeError("invalid experiment read mode");
17
18
  const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
18
19
  return runWithFinalizers(async () => {
19
20
  const s = await file.stat({ bigint: true });
@@ -21,7 +22,14 @@ export async function readExperimentFile(path: string, max: number): Promise<Buf
21
22
  const data = Buffer.alloc(Number(s.size)); let offset = 0;
22
23
  while (offset < data.length) { const n = (await file.read(data, offset, data.length - offset, offset)).bytesRead; if (!n) throw new Error("short experiment read"); offset += n; }
23
24
  const end = await file.stat({ bigint: true }), current = await lstat(path, { bigint: true });
24
- if (end.size !== s.size || end.mtimeNs !== s.mtimeNs || current.dev !== s.dev || current.ino !== s.ino || current.nlink !== 1n) throw new Error("experiment source changed");
25
+ const identityChanged = current.dev !== s.dev || current.ino !== s.ino || current.nlink !== 1n;
26
+ if (!options.appendOnly && (end.size !== s.size || end.mtimeNs !== s.mtimeNs || identityChanged)) throw new Error("experiment source changed");
27
+ if (options.appendOnly) {
28
+ if (end.size < s.size || identityChanged) throw new Error("experiment source changed");
29
+ const verify = Buffer.alloc(data.length); let checked = 0;
30
+ while (checked < verify.length) { const n = (await file.read(verify, checked, verify.length - checked, checked)).bytesRead; if (!n) throw new Error("short experiment read"); checked += n; }
31
+ if (!verify.equals(data)) throw new Error("experiment source changed");
32
+ }
25
33
  return data;
26
34
  }, [{ label: "experiment read close failed", run: () => file.close() }]);
27
35
  }
@@ -59,7 +67,7 @@ export function experimentStore(value: ExperimentBinding) {
59
67
  if (String(root.dev) !== b.device || String(root.ino) !== b.inode || String(file.dev) !== b.journalDevice || String(file.ino) !== b.journalInode) throw new Error("experiment identity replaced");
60
68
  };
61
69
  const read = async () => {
62
- await check(); const bytes = await readExperimentFile(path, 256 * 1024); await check();
70
+ await check(); const bytes = await readExperimentFile(path, 256 * 1024, { appendOnly: true }); await check();
63
71
  const text = new TextDecoder("utf8", { fatal: true }).decode(bytes); if (!text.endsWith("\n")) throw new Error("torn experiment journal");
64
72
  const lines = text.slice(0, -1).split("\n"); if (lines.length > 260 || experimentHash(parseWorkJson(lines[0])) !== experimentHash(b)) throw new Error("experiment header/bound mismatch");
65
73
  let previous = experimentHash(b); const events: Record<string, unknown>[] = [];
@@ -0,0 +1,179 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
3
+ import { dirname, isAbsolute, join, resolve } from "node:path";
4
+ import {
5
+ appendWorkLedgerEventOnce,
6
+ buildWorkOccurrenceEvent,
7
+ buildWorkRevisionEvent,
8
+ buildWorkSnapshotEvent,
9
+ parseWorkLedgerText,
10
+ projectWorkLedger,
11
+ type EventRef,
12
+ type RevisionRef,
13
+ type WorkFrozen,
14
+ type WorkOccurrencePayload,
15
+ type WorkProjectionContext,
16
+ type WorkRevision,
17
+ } from "./work-ledger.ts";
18
+ import { withFileLock } from "./file-lock.ts";
19
+
20
+ const ID = /^[A-Za-z0-9][A-Za-z0-9._:/@+\-]{0,127}$/;
21
+ const DIGEST = /^[a-f0-9]{64}$/;
22
+ const sha256 = (value: string): string => createHash("sha256").update(value).digest("hex");
23
+ const eventRef = (event: { eventId: string; digest: string }): EventRef => ({ eventId: event.eventId, digest: event.digest });
24
+ const revisionRef = (event: { payload: { revision: { kind: WorkRevision["kind"]; id: string; revision: number; digest: string } } }): RevisionRef => {
25
+ const { kind, id, revision, digest } = event.payload.revision;
26
+ return { kind, id, revision, digest };
27
+ };
28
+
29
+ export interface DeclaredWorkState {
30
+ readonly version: "pi-daddy-declared-work-v1";
31
+ readonly id: string;
32
+ readonly outcomeDigest: string;
33
+ readonly ledgerPath: string;
34
+ readonly statePath: string;
35
+ readonly grantLedgerPath: string | null;
36
+ readonly selectedSnapshot: NonNullable<WorkProjectionContext["selectedSnapshot"]>;
37
+ readonly scope: RevisionRef;
38
+ readonly intent: RevisionRef;
39
+ readonly obligation: RevisionRef;
40
+ readonly policy: RevisionRef;
41
+ }
42
+
43
+ interface StoredDeclaredWorkState extends Omit<DeclaredWorkState, "statePath"> {}
44
+
45
+ export interface DeclareWorkInput {
46
+ cwd: string;
47
+ id: string;
48
+ outcome: string;
49
+ }
50
+
51
+ function paths(input: DeclareWorkInput): { ledgerPath: string; statePath: string; grantLedgerPath: null } {
52
+ const root = resolve(input.cwd);
53
+ return { ledgerPath: join(root, ".pi", "work.jsonl"), statePath: join(root, ".pi", "work-current.json"), grantLedgerPath: null };
54
+ }
55
+
56
+ const exactKeys = (value: object, expected: readonly string[]): boolean => {
57
+ const actual = Object.keys(value).sort();
58
+ return actual.length === expected.length && actual.every((key, index) => key === [...expected].sort()[index]);
59
+ };
60
+ const validRef = (value: unknown, kind: WorkRevision["kind"]): value is RevisionRef => {
61
+ if (!value || typeof value !== "object" || !exactKeys(value, ["kind", "id", "revision", "digest"])) return false;
62
+ const ref = value as RevisionRef;
63
+ return ref.kind === kind && ID.test(ref.id) && Number.isSafeInteger(ref.revision) && ref.revision > 0 && DIGEST.test(ref.digest);
64
+ };
65
+
66
+ export async function loadDeclaredWork(statePath: string): Promise<WorkFrozen<DeclaredWorkState> | null> {
67
+ let text: string;
68
+ try { text = await readFile(statePath, "utf8"); }
69
+ catch (error) { if ((error as { code?: string }).code === "ENOENT") return null; throw error; }
70
+ try {
71
+ const value = JSON.parse(text) as StoredDeclaredWorkState;
72
+ if (!value || typeof value !== "object" || !exactKeys(value, [
73
+ "version", "id", "outcomeDigest", "ledgerPath", "grantLedgerPath", "selectedSnapshot", "scope", "intent", "obligation", "policy",
74
+ ]) || value.version !== "pi-daddy-declared-work-v1" || !ID.test(value.id) || !DIGEST.test(value.outcomeDigest) ||
75
+ typeof value.ledgerPath !== "string" || !isAbsolute(value.ledgerPath) ||
76
+ (value.grantLedgerPath !== null && (typeof value.grantLedgerPath !== "string" || !isAbsolute(value.grantLedgerPath))) ||
77
+ !validRef(value.scope, "scope") || !validRef(value.intent, "goal") || !validRef(value.obligation, "obligation") || !validRef(value.policy, "policy") ||
78
+ !value.selectedSnapshot || typeof value.selectedSnapshot !== "object" ||
79
+ !exactKeys(value.selectedSnapshot, ["snapshot", "event"]) ||
80
+ !exactKeys(value.selectedSnapshot.snapshot, ["id", "digest"]) || !ID.test(value.selectedSnapshot.snapshot.id) || !DIGEST.test(value.selectedSnapshot.snapshot.digest) ||
81
+ !exactKeys(value.selectedSnapshot.event, ["eventId", "digest"]) || !ID.test(value.selectedSnapshot.event.eventId) || !DIGEST.test(value.selectedSnapshot.event.digest)) {
82
+ throw new Error("shape");
83
+ }
84
+ const expectedLedger = resolve(dirname(statePath), "work.jsonl");
85
+ if (resolve(value.ledgerPath) !== expectedLedger) throw new Error("relocated");
86
+ const ledger = await readFile(value.ledgerPath, "utf8");
87
+ const ingestion = parseWorkLedgerText(ledger);
88
+ const projection = projectWorkLedger(ledger, { selectedSnapshot: value.selectedSnapshot, authority: null });
89
+ const obligation = projection.obligations.find(item => item.binding.obligation.digest === value.obligation.digest);
90
+ if (!ingestion.complete || projection.scopeState !== "valid" || !obligation || projection.selectedSnapshot?.digest !== value.selectedSnapshot.snapshot.digest) throw new Error("unbound");
91
+ return Object.freeze({ ...value, statePath: resolve(statePath) });
92
+ } catch {
93
+ throw new Error("invalid declared work state; no task was selected");
94
+ }
95
+ }
96
+
97
+ export async function declareWork(input: DeclareWorkInput): Promise<WorkFrozen<DeclaredWorkState>> {
98
+ if (!input || typeof input !== "object" || Object.keys(input).some(key => !["cwd", "id", "outcome"].includes(key))) {
99
+ throw new TypeError("unsupported work declaration field");
100
+ }
101
+ if (!ID.test(input.id) || typeof input.outcome !== "string" || !input.outcome.trim()) throw new TypeError("work declaration requires an identifier and non-empty outcome");
102
+ const resolved = paths(input), outcomeDigest = sha256(input.outcome.trim());
103
+ await mkdir(dirname(resolved.ledgerPath), { recursive: true });
104
+ await mkdir(dirname(resolved.statePath), { recursive: true });
105
+ return withFileLock(resolved.statePath, "declared work", async () => {
106
+ const existing = await loadDeclaredWork(resolved.statePath);
107
+ if (existing) {
108
+ if (existing.id === input.id && existing.outcomeDigest === outcomeDigest) return existing;
109
+ throw new Error(`declared work id ${input.id} already names a different outcome`);
110
+ }
111
+ const pendingPath = `${resolved.statePath}.pending`;
112
+ let prepared: { version: "pi-daddy-work-preparation-v1"; id: string; outcomeDigest: string; createdAt: string };
113
+ try {
114
+ prepared = JSON.parse(await readFile(pendingPath, "utf8"));
115
+ if (!prepared || Object.keys(prepared).sort().join() !== "createdAt,id,outcomeDigest,version" || prepared.version !== "pi-daddy-work-preparation-v1" || prepared.id !== input.id || prepared.outcomeDigest !== outcomeDigest || Number.isNaN(Date.parse(prepared.createdAt))) throw new Error("invalid");
116
+ } catch (error) {
117
+ if ((error as { code?: string }).code !== "ENOENT") throw new Error("another or invalid work declaration is pending; no events were replaced");
118
+ prepared = { version: "pi-daddy-work-preparation-v1", id: input.id, outcomeDigest, createdAt: new Date().toISOString() };
119
+ await writeFile(pendingPath, `${JSON.stringify(prepared)}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" });
120
+ }
121
+ const now = new Date(prepared.createdAt);
122
+ const revision = (kind: WorkRevision["kind"], name: string, patch: Partial<Omit<WorkRevision, "digest">> = {}) =>
123
+ buildWorkRevisionEvent({ eventId: `work:${input.id}:${name}:1`, now, revision: {
124
+ kind, id: `work:${input.id}:${name}`, revision: 1, scopeId: `work:${input.id}:scope`, predecessor: null,
125
+ contentDigest: sha256(`${name}\0${outcomeDigest}`), parent: null, dependencies: [], ownerId: "local-operator",
126
+ permittedEffects: [], policy: null, ...patch,
127
+ } });
128
+ const scope = revision("scope", "scope"), policy = revision("policy", "policy");
129
+ const intent = revision("goal", "goal", { parent: revisionRef(scope) });
130
+ const obligation = revision("obligation", "obligation", { parent: revisionRef(intent), policy: revisionRef(policy) });
131
+ const binding = { intent: revisionRef(intent), obligation: revisionRef(obligation), artifact: null, policy: revisionRef(policy) };
132
+ const snapshot = buildWorkSnapshotEvent({ eventId: `work:${input.id}:snapshot:1`, now, snapshot: {
133
+ snapshotId: `work:${input.id}:selected`, scope: revisionRef(scope), revisions: [revisionRef(policy), revisionRef(intent), revisionRef(obligation)], bindings: [binding],
134
+ } });
135
+ for (const event of [scope, policy, intent, obligation, snapshot]) await appendWorkLedgerEventOnce({ path: resolved.ledgerPath, grantLedgerPath: resolved.grantLedgerPath }, event);
136
+ const stored: StoredDeclaredWorkState = {
137
+ version: "pi-daddy-declared-work-v1", id: input.id, outcomeDigest, ledgerPath: resolved.ledgerPath, grantLedgerPath: resolved.grantLedgerPath,
138
+ selectedSnapshot: { snapshot: { id: snapshot.payload.snapshot.snapshotId, digest: snapshot.payload.snapshot.digest }, event: eventRef(snapshot) },
139
+ scope: revisionRef(scope), intent: revisionRef(intent), obligation: revisionRef(obligation), policy: revisionRef(policy),
140
+ };
141
+ const temporary = `${resolved.statePath}.${process.pid}.${randomUUID()}.tmp`;
142
+ try {
143
+ await writeFile(temporary, `${JSON.stringify(stored, null, 2)}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" });
144
+ await rename(temporary, resolved.statePath);
145
+ } finally { await rm(temporary, { force: true }); }
146
+ await rm(pendingPath);
147
+ return Object.freeze({ ...stored, statePath: resolved.statePath });
148
+ });
149
+ }
150
+
151
+ export type DeclaredOccurrenceIdentity = Pick<WorkOccurrencePayload["labels"], "toolCallId" | "taskId" | "workspaceId" | "definitionDigest" | "configurationDigest" | "modelId" | "effortId"> & {
152
+ executionId: string;
153
+ parentExecutionId: string | null;
154
+ childId: string | null;
155
+ variantId: string | null;
156
+ now: Date;
157
+ };
158
+
159
+ export async function appendDeclaredWorkOccurrence(state: WorkFrozen<DeclaredWorkState>, identity: DeclaredOccurrenceIdentity,
160
+ occurrenceState: WorkOccurrencePayload["state"]): Promise<void> {
161
+ const { executionId, parentExecutionId, childId, variantId, now, ...rawLabels } = identity;
162
+ const label = (name: string, value: string | null): string | null => value === null ? null : ID.test(value) ? value : `${name}:${sha256(value)}`;
163
+ const labels = {
164
+ toolCallId: label("toolcall", rawLabels.toolCallId), taskId: label("task", rawLabels.taskId),
165
+ workspaceId: label("workspace", rawLabels.workspaceId), definitionDigest: rawLabels.definitionDigest,
166
+ configurationDigest: rawLabels.configurationDigest, modelId: label("model", rawLabels.modelId), effortId: label("effort", rawLabels.effortId),
167
+ };
168
+ const event = buildWorkOccurrenceEvent({
169
+ eventId: `work:${state.id}:attempt:${identity.executionId}:${occurrenceState}`,
170
+ now,
171
+ payload: {
172
+ scope: state.scope, obligation: state.obligation, executionId,
173
+ parentExecutionId, childId, variantId: label("variant", variantId), artifact: null,
174
+ provenance: "observed", state: occurrenceState,
175
+ labels: { sessionId: null, branchLeafId: null, ...labels },
176
+ },
177
+ });
178
+ await appendWorkLedgerEventOnce({ path: state.ledgerPath, grantLedgerPath: state.grantLedgerPath }, event);
179
+ }