pi-daddy 0.43.1 → 0.44.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 +93 -31
- package/README.md +15 -0
- package/THIRD_PARTY_LICENSES/musl-COPYRIGHT.txt +193 -0
- package/THIRD_PARTY_LICENSES/zig-MIT.txt +21 -0
- package/THIRD_PARTY_NOTICES.md +9 -0
- package/dist/cli.d.ts +2 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +10 -5
- package/dist/cli.js.map +1 -1
- package/dist/executors/activity-session.js +116 -65
- package/dist/executors/activity-session.js.map +1 -1
- package/dist/executors/captured-execution.d.ts +13 -0
- package/dist/executors/captured-execution.d.ts.map +1 -0
- package/dist/executors/captured-execution.js +53 -0
- package/dist/executors/captured-execution.js.map +1 -0
- package/dist/executors/child-final.d.ts +31 -0
- package/dist/executors/child-final.d.ts.map +1 -0
- package/dist/executors/child-final.js +197 -0
- package/dist/executors/child-final.js.map +1 -0
- package/dist/executors/executor.d.ts +2 -1
- package/dist/executors/executor.d.ts.map +1 -1
- package/dist/executors/executor.js +12 -0
- package/dist/executors/executor.js.map +1 -1
- package/dist/executors/owned-worker.d.ts +18 -0
- package/dist/executors/owned-worker.d.ts.map +1 -0
- package/dist/executors/owned-worker.js +295 -0
- package/dist/executors/owned-worker.js.map +1 -0
- package/dist/executors/worker-artifact.d.ts +3 -0
- package/dist/executors/worker-artifact.d.ts.map +1 -0
- package/dist/executors/worker-artifact.js +3 -0
- package/dist/executors/worker-artifact.js.map +1 -0
- package/dist/governance/approval-prompt.d.ts +4 -3
- package/dist/governance/approval-prompt.d.ts.map +1 -1
- package/dist/governance/approval-prompt.js +13 -6
- package/dist/governance/approval-prompt.js.map +1 -1
- package/dist/governance/captured-worker-record.d.ts +9 -0
- package/dist/governance/captured-worker-record.d.ts.map +1 -0
- package/dist/governance/captured-worker-record.js +123 -0
- package/dist/governance/captured-worker-record.js.map +1 -0
- package/dist/governance/lease-record.d.ts +4 -0
- package/dist/governance/lease-record.d.ts.map +1 -1
- package/dist/governance/lease-record.js +19 -2
- package/dist/governance/lease-record.js.map +1 -1
- package/dist/governance/ledger-events.d.ts +5 -5
- package/dist/governance/ledger-events.d.ts.map +1 -1
- package/dist/governance/ledger-v3-validation.js +1 -1
- package/dist/governance/ledger-v3-validation.js.map +1 -1
- package/dist/governance/workspace-acceptance.d.ts +3 -1
- package/dist/governance/workspace-acceptance.d.ts.map +1 -1
- package/dist/governance/workspace-acceptance.js +21 -5
- package/dist/governance/workspace-acceptance.js.map +1 -1
- package/dist/governance/workspace-lease.d.ts.map +1 -1
- package/dist/governance/workspace-lease.js +44 -1
- package/dist/governance/workspace-lease.js.map +1 -1
- package/dist/kernel/authority-text.d.ts +3 -0
- package/dist/kernel/authority-text.d.ts.map +1 -0
- package/dist/kernel/authority-text.js +22 -0
- package/dist/kernel/authority-text.js.map +1 -0
- package/dist/kernel/bounded-read-failures.d.ts +9 -0
- package/dist/kernel/bounded-read-failures.d.ts.map +1 -0
- package/dist/kernel/bounded-read-failures.js +33 -0
- package/dist/kernel/bounded-read-failures.js.map +1 -0
- package/dist/kernel/bounded-read-sync.d.ts +24 -0
- package/dist/kernel/bounded-read-sync.d.ts.map +1 -0
- package/dist/kernel/bounded-read-sync.js +75 -0
- package/dist/kernel/bounded-read-sync.js.map +1 -0
- package/dist/kernel/bounded-read.d.ts +29 -2
- package/dist/kernel/bounded-read.d.ts.map +1 -1
- package/dist/kernel/bounded-read.js +98 -9
- package/dist/kernel/bounded-read.js.map +1 -1
- package/dist/kernel/capacity.d.ts +36 -0
- package/dist/kernel/capacity.d.ts.map +1 -0
- package/dist/kernel/capacity.js +94 -0
- package/dist/kernel/capacity.js.map +1 -0
- package/dist/kernel/captured-worker-contract.d.ts +38 -0
- package/dist/kernel/captured-worker-contract.d.ts.map +1 -0
- package/dist/kernel/captured-worker-contract.js +2 -0
- package/dist/kernel/captured-worker-contract.js.map +1 -0
- package/dist/kernel/catalog.d.ts +12 -3
- package/dist/kernel/catalog.d.ts.map +1 -1
- package/dist/kernel/catalog.js +28 -8
- package/dist/kernel/catalog.js.map +1 -1
- package/dist/kernel/chain.d.ts +1 -0
- package/dist/kernel/chain.d.ts.map +1 -1
- package/dist/kernel/chain.js.map +1 -1
- package/dist/kernel/definitions.d.ts +11 -1
- package/dist/kernel/definitions.d.ts.map +1 -1
- package/dist/kernel/definitions.js +6 -3
- package/dist/kernel/definitions.js.map +1 -1
- package/dist/kernel/fanout.d.ts +5 -25
- package/dist/kernel/fanout.d.ts.map +1 -1
- package/dist/kernel/fanout.js +15 -30
- package/dist/kernel/fanout.js.map +1 -1
- package/dist/kernel/grant-env.d.ts +0 -2
- package/dist/kernel/grant-env.d.ts.map +1 -1
- package/dist/kernel/grant-env.js +0 -1
- package/dist/kernel/grant-env.js.map +1 -1
- package/dist/kernel/model-preflight.d.ts +8 -0
- package/dist/kernel/model-preflight.d.ts.map +1 -1
- package/dist/kernel/model-preflight.js +23 -0
- package/dist/kernel/model-preflight.js.map +1 -1
- package/dist/kernel/selected-definitions.d.ts +15 -0
- package/dist/kernel/selected-definitions.d.ts.map +1 -0
- package/dist/kernel/selected-definitions.js +146 -0
- package/dist/kernel/selected-definitions.js.map +1 -0
- package/dist/kernel/skill-packages.d.ts +4 -2
- package/dist/kernel/skill-packages.d.ts.map +1 -1
- package/dist/kernel/skill-packages.js +34 -41
- package/dist/kernel/skill-packages.js.map +1 -1
- package/dist/kernel/skill-resources.d.ts.map +1 -1
- package/dist/kernel/skill-resources.js +8 -11
- package/dist/kernel/skill-resources.js.map +1 -1
- package/dist/kernel/workspace.d.ts +6 -3
- package/dist/kernel/workspace.d.ts.map +1 -1
- package/dist/kernel/workspace.js +10 -6
- package/dist/kernel/workspace.js.map +1 -1
- package/dist/products/dashboard-cli.js +1 -1
- package/dist/products/dashboard-cli.js.map +1 -1
- package/dist/products/dashboard-render.d.ts +1 -2
- package/dist/products/dashboard-render.d.ts.map +1 -1
- package/dist/products/dashboard-render.js +3 -1
- package/dist/products/dashboard-render.js.map +1 -1
- package/dist/products/dashboard-session-client.d.ts +1 -2
- package/dist/products/dashboard-session-client.d.ts.map +1 -1
- package/dist/products/dashboard-session-client.js.map +1 -1
- package/dist/products/episode-report.d.ts +12 -8
- package/dist/products/episode-report.d.ts.map +1 -1
- package/dist/products/episode-report.js +61 -12
- package/dist/products/episode-report.js.map +1 -1
- package/extensions/approvals.ts +1 -1
- package/extensions/chain-plan.ts +9 -3
- package/extensions/context-staging.ts +30 -35
- package/extensions/dashboard-session-server.ts +1 -2
- package/extensions/definition-describe.ts +44 -0
- package/extensions/definition-runtime.ts +89 -26
- package/extensions/delegate-chain.ts +273 -256
- package/extensions/delegation-activation.ts +1 -1
- package/extensions/delegation.ts +41 -53
- package/extensions/execute-child.ts +167 -134
- package/extensions/fanout-outcome.ts +1 -9
- package/extensions/grants-command.ts +2 -9
- package/extensions/grants.ts +106 -38
- package/extensions/init-command.ts +52 -12
- package/extensions/reload-environment.ts +21 -2
- package/extensions/retired-inputs.ts +7 -0
- package/extensions/run-delegation.ts +212 -265
- package/extensions/session-capacity.ts +53 -0
- package/extensions/session-environment.ts +5 -2
- package/extensions/session-model-prompt.ts +33 -32
- package/extensions/session-report.ts +28 -11
- package/extensions/session.ts +200 -157
- package/extensions/workspace-runtime.ts +2 -1
- package/native/README.md +25 -0
- package/native/linux-x64/worker +0 -0
- package/native/linux-x64/worker.sha256 +1 -0
- package/native/provenance.json +42 -0
- package/native/toolchain.json +18 -0
- package/native/worker.c +209 -0
- package/package.json +15 -7
- package/src/cli.ts +16 -6
- package/src/executors/activity-session.ts +120 -66
- package/src/executors/captured-execution.ts +61 -0
- package/src/executors/child-final.ts +190 -0
- package/src/executors/executor.ts +14 -1
- package/src/executors/owned-worker.ts +322 -0
- package/src/executors/worker-artifact.ts +2 -0
- package/src/governance/approval-prompt.ts +18 -8
- package/src/governance/captured-worker-record.ts +138 -0
- package/src/governance/lease-record.ts +21 -2
- package/src/governance/ledger-events.ts +5 -5
- package/src/governance/ledger-v3-validation.ts +1 -1
- package/src/governance/workspace-acceptance.ts +20 -4
- package/src/governance/workspace-lease.ts +59 -1
- package/src/kernel/authority-text.ts +17 -0
- package/src/kernel/bounded-read-failures.ts +42 -0
- package/src/kernel/bounded-read-sync.ts +87 -0
- package/src/kernel/bounded-read.ts +128 -11
- package/src/kernel/capacity.ts +123 -0
- package/src/kernel/captured-worker-contract.ts +29 -0
- package/src/kernel/catalog.ts +45 -16
- package/src/kernel/chain.ts +1 -0
- package/src/kernel/definitions.ts +11 -2
- package/src/kernel/fanout.ts +17 -32
- package/src/kernel/grant-env.ts +0 -3
- package/src/kernel/model-preflight.ts +29 -0
- package/src/kernel/selected-definitions.ts +156 -0
- package/src/kernel/skill-packages.ts +40 -38
- package/src/kernel/skill-resources.ts +8 -9
- package/src/kernel/workspace.ts +12 -5
- package/src/products/dashboard-cli.ts +1 -1
- package/src/products/dashboard-render.ts +6 -2
- package/src/products/dashboard-session-client.ts +1 -2
- package/src/products/episode-report.ts +80 -17
- package/dist/advisors/advisor.d.ts +0 -90
- package/dist/advisors/advisor.d.ts.map +0 -1
- package/dist/advisors/advisor.js +0 -154
- package/dist/advisors/advisor.js.map +0 -1
- package/dist/advisors/decider.d.ts +0 -88
- package/dist/advisors/decider.d.ts.map +0 -1
- package/dist/advisors/decider.js +0 -29
- package/dist/advisors/decider.js.map +0 -1
- package/dist/advisors/jev.d.ts +0 -41
- package/dist/advisors/jev.d.ts.map +0 -1
- package/dist/advisors/jev.js +0 -139
- package/dist/advisors/jev.js.map +0 -1
- package/dist/advisors/settings.d.ts +0 -49
- package/dist/advisors/settings.d.ts.map +0 -1
- package/dist/advisors/settings.js +0 -99
- package/dist/advisors/settings.js.map +0 -1
- package/dist/governance/episode-cost-gate.d.ts +0 -31
- package/dist/governance/episode-cost-gate.d.ts.map +0 -1
- package/dist/governance/episode-cost-gate.js +0 -112
- package/dist/governance/episode-cost-gate.js.map +0 -1
- package/extensions/advisor-session.ts +0 -67
- package/extensions/effort-advice.ts +0 -112
- package/extensions/pruning-advice.ts +0 -133
- package/src/advisors/advisor.ts +0 -246
- package/src/advisors/decider.ts +0 -74
- package/src/advisors/jev.ts +0 -163
- package/src/advisors/settings.ts +0 -115
- package/src/governance/episode-cost-gate.ts +0 -128
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/** Selected Pi 1.0.4 resources, frozen once per extension instance. No independent resource scan. */
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { dirname, join, resolve as resolvePath } from "node:path";
|
|
4
|
+
import { lstat, realpath } from "node:fs/promises";
|
|
5
|
+
import { BoundedReadCleanupError, readBoundedBytes } from "./bounded-read.ts";
|
|
6
|
+
import { ceilingForDefinition, parseSkillDefinition, type SkillDefinition } from "./definitions.ts";
|
|
7
|
+
import { resolve } from "./resolve.ts";
|
|
8
|
+
import type { CatalogEntry } from "./catalog.ts";
|
|
9
|
+
export interface SelectedCommand {
|
|
10
|
+
source?: string;
|
|
11
|
+
name: string;
|
|
12
|
+
sourceInfo?: { path?: string };
|
|
13
|
+
}
|
|
14
|
+
const phases = ["plan", "build", "review", "debug", "investigate"];
|
|
15
|
+
const hash = (value: string) => createHash("sha256").update(value, "utf8").digest("hex");
|
|
16
|
+
const object = (x: unknown): x is Record<string, unknown> => !!x && typeof x === "object" && !Array.isArray(x);
|
|
17
|
+
const exact = (x: Record<string, unknown>, keys: string[]) =>
|
|
18
|
+
Object.keys(x).sort().join(",") === [...keys].sort().join(",");
|
|
19
|
+
async function read(path: string): Promise<string> {
|
|
20
|
+
if (!(await lstat(path)).isFile()) throw Error(`nonregular selected resource: ${path}`);
|
|
21
|
+
const result = await readBoundedBytes(path, { maxBytes: 1024 * 1024, timeoutMs: 3000 });
|
|
22
|
+
if (!result.ok) throw Error(result.detail);
|
|
23
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(result.bytes);
|
|
24
|
+
}
|
|
25
|
+
async function principalBinding(path: string, inline: SkillDefinition, skillText: string): Promise<SkillDefinition> {
|
|
26
|
+
const phase = inline.name;
|
|
27
|
+
const marked = inline.metadata?.["principal-package"] === "principal-pi-skills";
|
|
28
|
+
if (!phases.includes(phase) || path !== join(dirname(dirname(path)), phase, "SKILL.md")) {
|
|
29
|
+
if (marked) throw Error("marked Principal skill has a noncanonical phase or path");
|
|
30
|
+
return inline;
|
|
31
|
+
}
|
|
32
|
+
const root = dirname(dirname(path));
|
|
33
|
+
let packageText: string;
|
|
34
|
+
try {
|
|
35
|
+
packageText = await read(join(root, "package.json"));
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT" && !marked) return inline;
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
const pkg: unknown = JSON.parse(packageText);
|
|
41
|
+
if (!object(pkg) || pkg.name !== "principal-pi-skills") {
|
|
42
|
+
if (marked) throw Error("marked Principal skill has missing or wrong package identity");
|
|
43
|
+
return inline;
|
|
44
|
+
}
|
|
45
|
+
const manifest: unknown = JSON.parse(await read(join(root, "principal-agents.json")));
|
|
46
|
+
if (
|
|
47
|
+
!object(manifest) ||
|
|
48
|
+
!exact(manifest, ["version", "package", "bindings"]) ||
|
|
49
|
+
manifest.version !== 1 ||
|
|
50
|
+
manifest.package !== "principal-pi-skills" ||
|
|
51
|
+
!object(manifest.bindings) ||
|
|
52
|
+
!exact(manifest.bindings, phases)
|
|
53
|
+
)
|
|
54
|
+
throw Error("invalid Principal binding manifest");
|
|
55
|
+
for (const name of phases) {
|
|
56
|
+
const row = manifest.bindings[name];
|
|
57
|
+
if (
|
|
58
|
+
!object(row) ||
|
|
59
|
+
!exact(row, ["skill", "agent", "skillSha256", "agentSha256"]) ||
|
|
60
|
+
row.skill !== `${name}/SKILL.md` ||
|
|
61
|
+
row.agent !== `agents/principal-${name}.md` ||
|
|
62
|
+
typeof row.skillSha256 !== "string" ||
|
|
63
|
+
!/^[a-f0-9]{64}$/.test(row.skillSha256) ||
|
|
64
|
+
typeof row.agentSha256 !== "string" ||
|
|
65
|
+
!/^[a-f0-9]{64}$/.test(row.agentSha256)
|
|
66
|
+
)
|
|
67
|
+
throw Error("invalid Principal binding row");
|
|
68
|
+
}
|
|
69
|
+
const row = manifest.bindings[phase] as Record<string, string>;
|
|
70
|
+
const agentPath = join(root, row.agent);
|
|
71
|
+
if (
|
|
72
|
+
(await realpath(agentPath)) !== join(await realpath(root), row.agent) ||
|
|
73
|
+
(await realpath(path)) !== join(await realpath(root), row.skill)
|
|
74
|
+
)
|
|
75
|
+
throw Error("Principal binding path escaped its selected package");
|
|
76
|
+
const agentText = await read(agentPath);
|
|
77
|
+
if (hash(skillText) !== row.skillSha256 || hash(agentText) !== row.agentSha256)
|
|
78
|
+
throw Error("Principal binding hash mismatch");
|
|
79
|
+
const agent = parseSkillDefinition(agentPath, agentText);
|
|
80
|
+
if (!agent) throw Error("Principal delegated definition is malformed");
|
|
81
|
+
const inlineCeiling = ceilingForDefinition(inline),
|
|
82
|
+
delegatedCeiling = ceilingForDefinition(agent);
|
|
83
|
+
if (
|
|
84
|
+
inlineCeiling.undeclared ||
|
|
85
|
+
delegatedCeiling.undeclared ||
|
|
86
|
+
inlineCeiling.patterns.length ||
|
|
87
|
+
delegatedCeiling.patterns.length
|
|
88
|
+
)
|
|
89
|
+
throw Error("Principal binding needs two declared pattern-free ceilings");
|
|
90
|
+
const ceiling = resolve({
|
|
91
|
+
requested: delegatedCeiling.capabilities,
|
|
92
|
+
parentGrant: inlineCeiling.capabilities,
|
|
93
|
+
subsumption: false,
|
|
94
|
+
}).effective;
|
|
95
|
+
const header = /^---\r?\n[\s\S]*?\r?\n---\r?\n?/.exec(agentText)!;
|
|
96
|
+
return {
|
|
97
|
+
...agent,
|
|
98
|
+
name: phase,
|
|
99
|
+
source: agentPath,
|
|
100
|
+
sourceHash: hash(agentText),
|
|
101
|
+
selectedSkillHash: hash(skillText),
|
|
102
|
+
body: agentText.slice(header[0].length),
|
|
103
|
+
allowedTools: ceiling.join(","),
|
|
104
|
+
runtimePreferences: agent.runtimePreferences ?? inline.runtimePreferences,
|
|
105
|
+
...(typeof pkg.version === "string" ? { packageVersion: pkg.version } : {}),
|
|
106
|
+
binding: Object.freeze({ package: "principal-pi-skills", phase }),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
export async function selectedDefinitions(commands: readonly SelectedCommand[]): Promise<{
|
|
110
|
+
definitions: Map<string, SkillDefinition>;
|
|
111
|
+
skills: CatalogEntry[];
|
|
112
|
+
skips: string[];
|
|
113
|
+
}> {
|
|
114
|
+
const definitions = new Map<string, SkillDefinition>();
|
|
115
|
+
const skills: CatalogEntry[] = [],
|
|
116
|
+
skips: string[] = [],
|
|
117
|
+
seen = new Set<string>();
|
|
118
|
+
for (const command of commands) {
|
|
119
|
+
if (command.source !== "skill" || !command.name.startsWith("skill:")) continue;
|
|
120
|
+
const name = command.name.slice(6);
|
|
121
|
+
if (seen.has(name)) {
|
|
122
|
+
definitions.delete(name);
|
|
123
|
+
skips.push(`${name}: duplicate selected resource`);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
seen.add(name);
|
|
127
|
+
try {
|
|
128
|
+
if (!command.sourceInfo?.path) throw Error("selected resource lacks a public source path");
|
|
129
|
+
const path = resolvePath(command.sourceInfo.path);
|
|
130
|
+
const text = await read(path);
|
|
131
|
+
const parsed = parseSkillDefinition(path, text);
|
|
132
|
+
if (!parsed || parsed.name !== name) throw Error("selected name/path does not match definition");
|
|
133
|
+
const bound = await principalBinding(path, parsed, text);
|
|
134
|
+
const definitionId = hash(
|
|
135
|
+
JSON.stringify({
|
|
136
|
+
name,
|
|
137
|
+
source: bound.source,
|
|
138
|
+
sourceHash: bound.sourceHash,
|
|
139
|
+
selectedSkillHash: bound.selectedSkillHash,
|
|
140
|
+
body: bound.body,
|
|
141
|
+
allowedTools: bound.allowedTools,
|
|
142
|
+
runtimePreferences: bound.runtimePreferences,
|
|
143
|
+
binding: bound.binding,
|
|
144
|
+
packageVersion: bound.packageVersion,
|
|
145
|
+
}),
|
|
146
|
+
);
|
|
147
|
+
if (bound.metadata) Object.freeze(bound.metadata);
|
|
148
|
+
definitions.set(name, Object.freeze({ ...bound, definitionId }));
|
|
149
|
+
skills.push({ capability: `skill:${name}`, kind: "skill", source: path });
|
|
150
|
+
} catch (error) {
|
|
151
|
+
if (error instanceof BoundedReadCleanupError) throw error;
|
|
152
|
+
skips.push(`${name}: ${String(error)}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return { definitions, skills: skills.filter((s) => definitions.has(s.capability.slice(6))), skips };
|
|
156
|
+
}
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* Names, ceilings and bytes are checked before they can enter the generated shell grant.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { readdir,
|
|
8
|
+
import { readdir, realpath } from "node:fs/promises";
|
|
9
|
+
import { readOptionalAuthorityText } from "./authority-text.ts";
|
|
9
10
|
import { readBoundedBytes } from "./bounded-read.ts";
|
|
10
11
|
import { homedir } from "node:os";
|
|
11
12
|
import { join, resolve, sep } from "node:path";
|
|
@@ -143,7 +144,11 @@ function refusalFor(skill: DiscoveredSkill): RefusedSkill | null {
|
|
|
143
144
|
}
|
|
144
145
|
|
|
145
146
|
/** One `pi.skills` entry: a directory holding `SKILL.md`, or a `.md` file — the same two shapes pi allows. */
|
|
146
|
-
async function readSkill(
|
|
147
|
+
async function readSkill(
|
|
148
|
+
packageDir: string,
|
|
149
|
+
entry: string,
|
|
150
|
+
reader: typeof readBoundedBytes,
|
|
151
|
+
): Promise<DiscoveredSkill | "not-utf8" | null> {
|
|
147
152
|
const target = resolve(packageDir, entry);
|
|
148
153
|
// A manifest is data from another package, so an entry escaping its own directory is refused rather than
|
|
149
154
|
// followed. **`realpath`, not a lexical prefix test** (R-80): `resolve()` normalises `..` and knows
|
|
@@ -158,7 +163,7 @@ async function readSkill(packageDir: string, entry: string): Promise<DiscoveredS
|
|
|
158
163
|
// `loadDefinitions` and accepted here, so `pi-daddy init` wrote `agent:advice` into the operator's grant
|
|
159
164
|
// for a definition the session could never load, and the eventual `delegate` said only
|
|
160
165
|
// `unknown agent "advice"`. An unreadable entry lands in `unreadable`, which `init` already prints.
|
|
161
|
-
const read = await
|
|
166
|
+
const read = await reader(path, {
|
|
162
167
|
maxBytes: DEFINITION_MAX_BYTES,
|
|
163
168
|
timeoutMs: DEFINITION_READ_TIMEOUT_MS,
|
|
164
169
|
});
|
|
@@ -180,13 +185,14 @@ async function readSkill(packageDir: string, entry: string): Promise<DiscoveredS
|
|
|
180
185
|
}
|
|
181
186
|
|
|
182
187
|
/** Read one installed package, if it declares skills. `null` means "not a skill package", not an error. */
|
|
183
|
-
export async function readSkillPackage(
|
|
188
|
+
export async function readSkillPackage(
|
|
189
|
+
packageDir: string,
|
|
190
|
+
reader: typeof readBoundedBytes = readBoundedBytes,
|
|
191
|
+
): Promise<SkillPackage | null> {
|
|
184
192
|
let manifest: { name?: string; version?: string; pi?: { skills?: unknown } };
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
return null;
|
|
189
|
-
}
|
|
193
|
+
const text = await readOptionalAuthorityText(join(packageDir, "package.json"));
|
|
194
|
+
if (text === undefined) return null;
|
|
195
|
+
manifest = JSON.parse(text);
|
|
190
196
|
const declared = manifest.pi?.skills;
|
|
191
197
|
if (!Array.isArray(declared) || declared.length === 0) return null;
|
|
192
198
|
|
|
@@ -195,7 +201,7 @@ export async function readSkillPackage(packageDir: string): Promise<SkillPackage
|
|
|
195
201
|
const refused: RefusedSkill[] = [];
|
|
196
202
|
for (const entry of declared) {
|
|
197
203
|
if (typeof entry !== "string") continue;
|
|
198
|
-
const skill = await readSkill(packageDir, entry);
|
|
204
|
+
const skill = await readSkill(packageDir, entry, reader);
|
|
199
205
|
if (skill === null) {
|
|
200
206
|
unreadable.push(entry);
|
|
201
207
|
} else if (skill === "not-utf8") {
|
|
@@ -243,7 +249,11 @@ export function skillPackageRoots(cwd: string): string[] {
|
|
|
243
249
|
return [join(cwd, "node_modules"), join(agentDir, "npm", "node_modules")];
|
|
244
250
|
}
|
|
245
251
|
|
|
246
|
-
|
|
252
|
+
/** The optional reader is a trusted per-loader dependency, never package metadata or requester input. */
|
|
253
|
+
export async function discoverSkillPackages(
|
|
254
|
+
cwd: string,
|
|
255
|
+
reader: typeof readBoundedBytes = readBoundedBytes,
|
|
256
|
+
): Promise<SkillPackage[]> {
|
|
247
257
|
const resolved = await resolveSkillResources(cwd);
|
|
248
258
|
const packages: SkillPackage[] = [];
|
|
249
259
|
const seenSkills = new Set<string>();
|
|
@@ -260,15 +270,12 @@ export async function discoverSkillPackages(cwd: string): Promise<SkillPackage[]
|
|
|
260
270
|
}),
|
|
261
271
|
);
|
|
262
272
|
for (const root of configuredRoots) {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
} catch {
|
|
266
|
-
/* absent */
|
|
267
|
-
}
|
|
273
|
+
const text = await readOptionalAuthorityText(join(root, "package.json"));
|
|
274
|
+
if (text !== undefined) configuredNames.add(JSON.parse(text).name);
|
|
268
275
|
}
|
|
269
276
|
for (const resource of resolved.skills) {
|
|
270
277
|
// Same bound, same reason: this is the third `SKILL.md` reader and it feeds `planInit`.
|
|
271
|
-
const resourceRead = await
|
|
278
|
+
const resourceRead = await reader(resource.path, {
|
|
272
279
|
maxBytes: DEFINITION_MAX_BYTES,
|
|
273
280
|
timeoutMs: DEFINITION_READ_TIMEOUT_MS,
|
|
274
281
|
});
|
|
@@ -281,12 +288,11 @@ export async function discoverSkillPackages(cwd: string): Promise<SkillPackage[]
|
|
|
281
288
|
let name = resource.metadata.source;
|
|
282
289
|
let version = "local";
|
|
283
290
|
if (resource.metadata.origin === "package" && resource.metadata.baseDir) {
|
|
284
|
-
|
|
285
|
-
|
|
291
|
+
const text = await readOptionalAuthorityText(join(resource.metadata.baseDir, "package.json"));
|
|
292
|
+
if (text !== undefined) {
|
|
293
|
+
const manifest = JSON.parse(text);
|
|
286
294
|
name = manifest.name ?? name;
|
|
287
295
|
version = manifest.version ?? version;
|
|
288
|
-
} catch {
|
|
289
|
-
/* the resource can be used without optional display metadata */
|
|
290
296
|
}
|
|
291
297
|
} else name = `${resource.metadata.scope} skills`;
|
|
292
298
|
let pkg = packages.find((p) => p.name === name && p.version === version);
|
|
@@ -313,7 +319,7 @@ export async function discoverSkillPackages(cwd: string): Promise<SkillPackage[]
|
|
|
313
319
|
const seenNames = new Set(packages.map((p) => p.name));
|
|
314
320
|
for (const dir of dirs) {
|
|
315
321
|
if (configuredRoots.has(resolve(dir))) continue;
|
|
316
|
-
const found = await readSkillPackage(dir);
|
|
322
|
+
const found = await readSkillPackage(dir, reader);
|
|
317
323
|
if (!found || configuredNames.has(found.name) || seenNames.has(found.name)) continue;
|
|
318
324
|
seenNames.add(found.name);
|
|
319
325
|
found.skills = found.skills.filter((skill) => !runtimeNames.has(skill.definition.name));
|
|
@@ -324,23 +330,19 @@ export async function discoverSkillPackages(cwd: string): Promise<SkillPackage[]
|
|
|
324
330
|
|
|
325
331
|
/** Append every package directory under one `node_modules`, scoped packages included. */
|
|
326
332
|
async function collectFrom(root: string, dirs: string[]): Promise<void> {
|
|
327
|
-
let entries
|
|
333
|
+
let entries;
|
|
328
334
|
try {
|
|
329
|
-
entries = await readdir(root);
|
|
330
|
-
} catch {
|
|
331
|
-
|
|
335
|
+
entries = await readdir(root, { withFileTypes: true });
|
|
336
|
+
} catch (error) {
|
|
337
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
|
|
338
|
+
throw error;
|
|
332
339
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
if (entry.startsWith("
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
continue;
|
|
341
|
-
}
|
|
342
|
-
} else {
|
|
343
|
-
dirs.push(join(root, entry));
|
|
344
|
-
}
|
|
340
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
341
|
+
if (entry.name.startsWith(".") || (!entry.isDirectory() && !entry.isSymbolicLink())) continue;
|
|
342
|
+
if (entry.name.startsWith("@")) {
|
|
343
|
+
const scoped = await readdir(join(root, entry.name), { withFileTypes: true });
|
|
344
|
+
for (const child of scoped.sort((a, b) => a.name.localeCompare(b.name)))
|
|
345
|
+
if (child.isDirectory() || child.isSymbolicLink()) dirs.push(join(root, entry.name, child.name));
|
|
346
|
+
} else dirs.push(join(root, entry.name));
|
|
345
347
|
}
|
|
346
348
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Read Pi's configured resource surface without locks, installs, extension execution or model calls. */
|
|
2
|
-
import {
|
|
2
|
+
import { statSync } from "node:fs";
|
|
3
|
+
import { readOptionalAuthorityText } from "./authority-text.ts";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
import { DefaultPackageManager, getAgentDir, SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
5
6
|
import { piProjectDir } from "./project-paths.ts";
|
|
@@ -21,17 +22,15 @@ export function skillResourceName(path: string): string {
|
|
|
21
22
|
|
|
22
23
|
export async function resolveSkillResources(cwd: string): Promise<SkillResources> {
|
|
23
24
|
const agentDir = getAgentDir();
|
|
25
|
+
// Read before Pi's synchronous storage callback: only known absence receives its default.
|
|
26
|
+
const texts = {
|
|
27
|
+
global: await readOptionalAuthorityText(join(agentDir, "settings.json")),
|
|
28
|
+
project: await readOptionalAuthorityText(join(piProjectDir(cwd), "settings.json")),
|
|
29
|
+
};
|
|
24
30
|
const settingsManager = SettingsManager.fromStorage({
|
|
25
31
|
withLock(scope, read) {
|
|
26
|
-
const path = scope === "global" ? join(agentDir, "settings.json") : join(piProjectDir(cwd), "settings.json");
|
|
27
|
-
let text: string | undefined;
|
|
28
|
-
try {
|
|
29
|
-
text = readFileSync(path, "utf8");
|
|
30
|
-
} catch (error) {
|
|
31
|
-
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
32
|
-
}
|
|
33
32
|
// This storage is intentionally read-only: no lock files or migrated settings are persisted.
|
|
34
|
-
read(
|
|
33
|
+
read(texts[scope]);
|
|
35
34
|
},
|
|
36
35
|
});
|
|
37
36
|
const errors = settingsManager.drainErrors();
|
package/src/kernel/workspace.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
2
2
|
import { execFile } from "node:child_process";
|
|
3
3
|
import { once } from "node:events";
|
|
4
4
|
import { mkdir, readFile, realpath, rename, stat, writeFile } from "node:fs/promises";
|
|
5
|
-
import { readBoundedFile } from "./bounded-read.ts";
|
|
5
|
+
import { readBoundedFile, BoundedReadCleanupError } from "./bounded-read.ts";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import { isAbsolute, join } from "node:path";
|
|
8
8
|
import { promisify } from "node:util";
|
|
@@ -59,13 +59,17 @@ const REGISTRY_READ_TIMEOUT_MS = 2_000;
|
|
|
59
59
|
/** A registry is an operator-authored JSON file; anything approaching this is not one. */
|
|
60
60
|
const REGISTRY_MAX_BYTES = 1 << 20;
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
/** The optional reader is a trusted loader dependency, never registry/request input. */
|
|
63
|
+
export async function loadWorkspaceRegistry(
|
|
64
|
+
path: string,
|
|
65
|
+
readFile: typeof readBoundedFile = readBoundedFile,
|
|
66
|
+
): Promise<WorkspaceRegistryFile> {
|
|
63
67
|
// The guards this call carries — non-blocking open, every check on the held descriptor, a deadline between
|
|
64
68
|
// chunks, the size bound checked twice — were worked out HERE and now live in `bounded-read.ts`, because a
|
|
65
69
|
// second session-start reader went on using a bare `readFile` rather than copying them. The mapping from a
|
|
66
70
|
// reason to a refusal stays here: only this caller knows that an unreadable registry is a governance
|
|
67
71
|
// refusal naming the file, and its messages are unchanged.
|
|
68
|
-
const read = await
|
|
72
|
+
const read = await readFile(path, {
|
|
69
73
|
maxBytes: REGISTRY_MAX_BYTES,
|
|
70
74
|
timeoutMs: REGISTRY_READ_TIMEOUT_MS,
|
|
71
75
|
});
|
|
@@ -151,7 +155,8 @@ export async function loadWorkspaceRegistry(path: string): Promise<WorkspaceRegi
|
|
|
151
155
|
}
|
|
152
156
|
|
|
153
157
|
/**
|
|
154
|
-
* The registered workspace ids, for `planInit` to scaffold and `/grants` to list.
|
|
158
|
+
* The registered workspace ids, for `planInit` to scaffold and `/grants` to list.
|
|
159
|
+
* No registry or malformed/unreadable data yields []; physical cleanup failure still rejects, with its owner.
|
|
155
160
|
*
|
|
156
161
|
* Fails SOFT, and only because nothing here is an authority: this decides which ids appear as COMMENTS in a
|
|
157
162
|
* generated file. `loadWorkspaceRegistry` throws a GovernanceRefusal naming the file, and that refusal is
|
|
@@ -172,11 +177,13 @@ export async function loadWorkspaceRegistry(path: string): Promise<WorkspaceRegi
|
|
|
172
177
|
export async function registeredWorkspaceIds(
|
|
173
178
|
registryPath = process.env[ENV_WORKSPACE_REGISTRY],
|
|
174
179
|
onRefusal?: (reason: string) => void,
|
|
180
|
+
loadRegistry: typeof loadWorkspaceRegistry = loadWorkspaceRegistry,
|
|
175
181
|
): Promise<string[]> {
|
|
176
182
|
if (!registryPath) return [];
|
|
177
183
|
try {
|
|
178
|
-
return Object.keys((await
|
|
184
|
+
return Object.keys((await loadRegistry(registryPath)).workspaces).sort();
|
|
179
185
|
} catch (error) {
|
|
186
|
+
if (error instanceof BoundedReadCleanupError) throw error;
|
|
180
187
|
onRefusal?.(error instanceof Error ? error.message : String(error));
|
|
181
188
|
return [];
|
|
182
189
|
}
|
|
@@ -144,7 +144,7 @@ export async function dashboardFrame(options: DashboardFrameOptions): Promise<st
|
|
|
144
144
|
details: options.details,
|
|
145
145
|
history: options.history,
|
|
146
146
|
modelRows: options.session?.rows,
|
|
147
|
-
episodeCost: options.session ? { cost: options.session.cost
|
|
147
|
+
episodeCost: options.session ? { cost: options.session.cost } : undefined,
|
|
148
148
|
});
|
|
149
149
|
return waiting ? `${rendered}\n\nwaiting for ledger ${ledgerPath}` : rendered;
|
|
150
150
|
}
|
|
@@ -9,7 +9,7 @@ export interface DashboardRenderOptions {
|
|
|
9
9
|
completedRoots?: number;
|
|
10
10
|
completedChildren?: number;
|
|
11
11
|
modelRows?: Array<{ definition: string; model: string; thinking: string; source: string }>;
|
|
12
|
-
episodeCost?: { cost: number
|
|
12
|
+
episodeCost?: { cost: number | null };
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
const ACTIVE = new Set<DashboardState>(["authorised", "starting", "running"]);
|
|
@@ -217,7 +217,11 @@ export function renderDashboard(projection: DashboardProjection, options: Dashbo
|
|
|
217
217
|
lines.push(`${clean(row.definition)} ${clean(row.model)} ${clean(row.thinking)} ${clean(row.source)}`);
|
|
218
218
|
lines.push("edit: m <definition> <provider:model> <thinking> | m all <provider:model> <thinking>");
|
|
219
219
|
if (options.episodeCost)
|
|
220
|
-
lines.push(
|
|
220
|
+
lines.push(
|
|
221
|
+
options.episodeCost.cost === null
|
|
222
|
+
? "episode cost unavailable (usage coverage partial)"
|
|
223
|
+
: `reported episode cost $${options.episodeCost.cost} (coverage partial)`,
|
|
224
|
+
);
|
|
221
225
|
lines.push("");
|
|
222
226
|
}
|
|
223
227
|
|
|
@@ -2,8 +2,7 @@ import { createConnection } from "node:net";
|
|
|
2
2
|
|
|
3
3
|
export interface DashboardSessionSnapshot {
|
|
4
4
|
rows: Array<{ definition: string; model: string; thinking: string; source: string }>;
|
|
5
|
-
cost: number;
|
|
6
|
-
ceiling: number;
|
|
5
|
+
cost: number | null;
|
|
7
6
|
}
|
|
8
7
|
|
|
9
8
|
export async function dashboardSessionRequest(
|
|
@@ -13,7 +13,13 @@ export interface EpisodeReportOptions {
|
|
|
13
13
|
model?: string;
|
|
14
14
|
groupBy?: EpisodeGroupBy;
|
|
15
15
|
}
|
|
16
|
-
export interface
|
|
16
|
+
export interface CostEvidence {
|
|
17
|
+
/** Null unless every recorded child has a terminal cost observation. Scope excludes unrecorded/root work. */
|
|
18
|
+
cost: number | null;
|
|
19
|
+
observedCost: number;
|
|
20
|
+
costCoverage: "complete" | "partial" | "unavailable";
|
|
21
|
+
}
|
|
22
|
+
export interface EpisodeReportRow extends CostEvidence {
|
|
17
23
|
episode: string;
|
|
18
24
|
started: string;
|
|
19
25
|
definition: string;
|
|
@@ -26,7 +32,6 @@ export interface EpisodeReportRow {
|
|
|
26
32
|
outputTokens: number;
|
|
27
33
|
cacheReadTokens: number;
|
|
28
34
|
reasoningTokens: number;
|
|
29
|
-
cost: number;
|
|
30
35
|
compactions: number;
|
|
31
36
|
turns: number;
|
|
32
37
|
durationMs: number;
|
|
@@ -34,20 +39,25 @@ export interface EpisodeReportRow {
|
|
|
34
39
|
commit: string;
|
|
35
40
|
outcome: "" | "positive" | "negative" | "unknown";
|
|
36
41
|
}
|
|
37
|
-
export interface EpisodeReportGroup {
|
|
42
|
+
export interface EpisodeReportGroup extends CostEvidence {
|
|
38
43
|
value: string;
|
|
39
44
|
count: number;
|
|
40
45
|
inputTokens: number;
|
|
41
46
|
outputTokens: number;
|
|
42
47
|
cacheReadTokens: number;
|
|
43
48
|
reasoningTokens: number;
|
|
44
|
-
cost: number;
|
|
45
49
|
}
|
|
46
50
|
export interface EpisodeReport {
|
|
51
|
+
costScope: "recorded-child-executions";
|
|
47
52
|
rows: EpisodeReportRow[];
|
|
48
53
|
groups?: EpisodeReportGroup[];
|
|
49
54
|
groupBy?: EpisodeGroupBy;
|
|
50
|
-
totals?:
|
|
55
|
+
totals?: CostEvidence & {
|
|
56
|
+
episodes: number;
|
|
57
|
+
tokens: number;
|
|
58
|
+
p50EpisodeCost: number | null;
|
|
59
|
+
p95EpisodeCost: number | null;
|
|
60
|
+
};
|
|
51
61
|
}
|
|
52
62
|
interface EpisodeAccumulator {
|
|
53
63
|
episode: string;
|
|
@@ -65,6 +75,7 @@ interface EpisodeAccumulator {
|
|
|
65
75
|
cacheReadTokens: number;
|
|
66
76
|
reasoningTokens: number;
|
|
67
77
|
cost: number;
|
|
78
|
+
reportedCosts: number;
|
|
68
79
|
compactions: number;
|
|
69
80
|
turns: number;
|
|
70
81
|
children: Set<string>;
|
|
@@ -103,6 +114,7 @@ function episodeOf(episodes: Map<string, EpisodeAccumulator>, id: string, at: nu
|
|
|
103
114
|
cacheReadTokens: 0,
|
|
104
115
|
reasoningTokens: 0,
|
|
105
116
|
cost: 0,
|
|
117
|
+
reportedCosts: 0,
|
|
106
118
|
compactions: 0,
|
|
107
119
|
turns: 0,
|
|
108
120
|
children: new Set(),
|
|
@@ -174,7 +186,11 @@ export function reportEpisodes(input: {
|
|
|
174
186
|
episode.cacheReadTokens += number(usage.cacheRead);
|
|
175
187
|
episode.reasoningTokens += number(usage.reasoning);
|
|
176
188
|
}
|
|
177
|
-
|
|
189
|
+
const charge = usage && object(usage.cost) ? usage.cost.total : undefined;
|
|
190
|
+
if (typeof charge === "number" && Number.isFinite(charge) && charge >= 0) {
|
|
191
|
+
episode.cost += charge;
|
|
192
|
+
episode.reportedCosts++;
|
|
193
|
+
}
|
|
178
194
|
episode.compactions += number(body.compactionCount);
|
|
179
195
|
}
|
|
180
196
|
const activity = readRecords(input.activityText);
|
|
@@ -203,7 +219,14 @@ export function reportEpisodes(input: {
|
|
|
203
219
|
outputTokens: episode.outputTokens,
|
|
204
220
|
cacheReadTokens: episode.cacheReadTokens,
|
|
205
221
|
reasoningTokens: episode.reasoningTokens,
|
|
206
|
-
cost: episode.cost,
|
|
222
|
+
cost: episode.children.size > 0 && episode.reportedCosts === episode.children.size ? episode.cost : null,
|
|
223
|
+
observedCost: episode.cost,
|
|
224
|
+
costCoverage:
|
|
225
|
+
episode.children.size > 0 && episode.reportedCosts === episode.children.size
|
|
226
|
+
? "complete"
|
|
227
|
+
: episode.reportedCosts > 0
|
|
228
|
+
? "partial"
|
|
229
|
+
: "unavailable",
|
|
207
230
|
compactions: episode.compactions,
|
|
208
231
|
turns: episode.turns,
|
|
209
232
|
durationMs: Math.max(0, episode.endedMs - episode.startedMs),
|
|
@@ -237,36 +260,58 @@ export function reportEpisodes(input: {
|
|
|
237
260
|
cacheReadTokens: 0,
|
|
238
261
|
reasoningTokens: 0,
|
|
239
262
|
cost: 0,
|
|
263
|
+
observedCost: 0,
|
|
264
|
+
costCoverage: "complete",
|
|
240
265
|
};
|
|
266
|
+
const summary = summarizeCosts(group.count ? [group, row] : [row]);
|
|
241
267
|
group.count += 1;
|
|
242
268
|
group.inputTokens += row.inputTokens;
|
|
243
269
|
group.outputTokens += row.outputTokens;
|
|
244
270
|
group.cacheReadTokens += row.cacheReadTokens;
|
|
245
271
|
group.reasoningTokens += row.reasoningTokens;
|
|
246
|
-
group
|
|
272
|
+
Object.assign(group, summary);
|
|
247
273
|
groups.set(value, group);
|
|
248
274
|
}
|
|
249
275
|
return {
|
|
276
|
+
costScope: "recorded-child-executions",
|
|
250
277
|
rows: [],
|
|
251
278
|
groups: [...groups.values()].sort((a, b) => a.value.localeCompare(b.value)),
|
|
252
279
|
groupBy: input.options.groupBy,
|
|
253
280
|
};
|
|
254
281
|
}
|
|
255
|
-
const
|
|
282
|
+
const summary = summarizeCosts(rows);
|
|
283
|
+
const costs = rows.flatMap((row) => (row.cost === null ? [] : [row.cost]));
|
|
256
284
|
return {
|
|
285
|
+
costScope: "recorded-child-executions",
|
|
257
286
|
rows,
|
|
258
287
|
totals: {
|
|
259
288
|
episodes: rows.length,
|
|
260
|
-
|
|
289
|
+
...summary,
|
|
261
290
|
tokens: rows.reduce((sum, row) => sum + row.inputTokens + row.outputTokens, 0),
|
|
262
|
-
p50EpisodeCost: percentile(costs, 0.5),
|
|
263
|
-
p95EpisodeCost: percentile(costs, 0.95),
|
|
291
|
+
p50EpisodeCost: summary.cost === null ? null : percentile(costs, 0.5),
|
|
292
|
+
p95EpisodeCost: summary.cost === null ? null : percentile(costs, 0.95),
|
|
264
293
|
},
|
|
265
294
|
};
|
|
266
295
|
}
|
|
267
296
|
|
|
297
|
+
/** Summaries never treat an unobserved charge as a measured zero. */
|
|
298
|
+
function summarizeCosts(values: readonly CostEvidence[]): CostEvidence {
|
|
299
|
+
const observedCost = values.reduce((sum, row) => sum + row.observedCost, 0);
|
|
300
|
+
const complete = values.length > 0 && values.every((row) => row.costCoverage === "complete");
|
|
301
|
+
return {
|
|
302
|
+
cost: complete ? observedCost : null,
|
|
303
|
+
observedCost,
|
|
304
|
+
costCoverage: complete
|
|
305
|
+
? "complete"
|
|
306
|
+
: values.some((row) => row.costCoverage !== "unavailable")
|
|
307
|
+
? "partial"
|
|
308
|
+
: "unavailable",
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
const costScope = "Cost coverage refers only to recorded child executions; root and unrecorded work are not measured.";
|
|
268
312
|
const escapeCell = (value: unknown): string => String(value).replaceAll("|", "\\|").replaceAll("\n", " ");
|
|
269
|
-
const cost = (value: number): string =>
|
|
313
|
+
const cost = (value: number | null): string =>
|
|
314
|
+
value === null ? "unavailable" : value.toFixed(6).replace(/\.?0+$/, "") || "0";
|
|
270
315
|
function duration(ms: number): string {
|
|
271
316
|
const seconds = Math.round(ms / 1000);
|
|
272
317
|
const minutes = Math.floor(seconds / 60);
|
|
@@ -290,6 +335,8 @@ export function renderEpisodeReport(report: EpisodeReport, json: boolean): strin
|
|
|
290
335
|
"cacheReadTokens",
|
|
291
336
|
"reasoningTokens",
|
|
292
337
|
"cost",
|
|
338
|
+
"observed cost",
|
|
339
|
+
"cost coverage",
|
|
293
340
|
];
|
|
294
341
|
return `${table(
|
|
295
342
|
headers,
|
|
@@ -301,8 +348,10 @@ export function renderEpisodeReport(report: EpisodeReport, json: boolean): strin
|
|
|
301
348
|
group.cacheReadTokens,
|
|
302
349
|
group.reasoningTokens,
|
|
303
350
|
cost(group.cost),
|
|
351
|
+
cost(group.observedCost),
|
|
352
|
+
group.costCoverage,
|
|
304
353
|
]),
|
|
305
|
-
)}\n`;
|
|
354
|
+
)}\n\n${costScope}\n`;
|
|
306
355
|
}
|
|
307
356
|
const headers = [
|
|
308
357
|
"episode",
|
|
@@ -318,6 +367,8 @@ export function renderEpisodeReport(report: EpisodeReport, json: boolean): strin
|
|
|
318
367
|
"cacheReadTokens",
|
|
319
368
|
"reasoningTokens",
|
|
320
369
|
"cost",
|
|
370
|
+
"observed cost",
|
|
371
|
+
"cost coverage",
|
|
321
372
|
"compactions",
|
|
322
373
|
"turns",
|
|
323
374
|
"duration",
|
|
@@ -339,6 +390,8 @@ export function renderEpisodeReport(report: EpisodeReport, json: boolean): strin
|
|
|
339
390
|
row.cacheReadTokens,
|
|
340
391
|
row.reasoningTokens,
|
|
341
392
|
cost(row.cost),
|
|
393
|
+
cost(row.observedCost),
|
|
394
|
+
row.costCoverage,
|
|
342
395
|
row.compactions,
|
|
343
396
|
row.turns,
|
|
344
397
|
duration(row.durationMs),
|
|
@@ -348,10 +401,20 @@ export function renderEpisodeReport(report: EpisodeReport, json: boolean): strin
|
|
|
348
401
|
]);
|
|
349
402
|
const totals = report.totals!;
|
|
350
403
|
const totalsTable = table(
|
|
351
|
-
["episodes", "cost", "tokens", "p50 episode cost", "p95 episode cost"],
|
|
352
|
-
[
|
|
404
|
+
["episodes", "cost", "tokens", "p50 episode cost", "p95 episode cost", "observed cost", "cost coverage"],
|
|
405
|
+
[
|
|
406
|
+
[
|
|
407
|
+
totals.episodes,
|
|
408
|
+
cost(totals.cost),
|
|
409
|
+
totals.tokens,
|
|
410
|
+
cost(totals.p50EpisodeCost),
|
|
411
|
+
cost(totals.p95EpisodeCost),
|
|
412
|
+
cost(totals.observedCost),
|
|
413
|
+
totals.costCoverage,
|
|
414
|
+
],
|
|
415
|
+
],
|
|
353
416
|
);
|
|
354
|
-
return `${table(headers, rows)}\n\n${totalsTable}\n`;
|
|
417
|
+
return `${table(headers, rows)}\n\n${totalsTable}\n\n${costScope}\n`;
|
|
355
418
|
}
|
|
356
419
|
|
|
357
420
|
export function commitsFromGitLog(log: string): Map<string, string> {
|