pi-daddy 0.27.3 → 0.28.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 +16 -0
- package/PRODUCT-GUIDE.md +22 -3
- package/README.md +9 -9
- package/REQUIREMENTS.md +1 -1
- package/dist/activity-timeline.d.ts +109 -0
- package/dist/activity-timeline.d.ts.map +1 -0
- package/dist/activity-timeline.js +208 -0
- package/dist/activity-timeline.js.map +1 -0
- package/dist/dashboard-cli.d.ts +6 -0
- package/dist/dashboard-cli.d.ts.map +1 -1
- package/dist/dashboard-cli.js +41 -5
- package/dist/dashboard-cli.js.map +1 -1
- package/dist/dashboard-display-controls.d.ts +5 -0
- package/dist/dashboard-display-controls.d.ts.map +1 -1
- package/dist/dashboard-display-controls.js +12 -3
- package/dist/dashboard-display-controls.js.map +1 -1
- package/dist/dashboard-handshake.d.ts +6 -1
- package/dist/dashboard-handshake.d.ts.map +1 -1
- package/dist/dashboard-handshake.js +21 -10
- package/dist/dashboard-handshake.js.map +1 -1
- package/dist/delegate-types.d.ts +8 -0
- package/dist/delegate-types.d.ts.map +1 -1
- package/dist/delegate-types.js.map +1 -1
- package/dist/delegate.d.ts.map +1 -1
- package/dist/delegate.js +14 -6
- package/dist/delegate.js.map +1 -1
- package/dist/herdr-name.d.ts +1 -35
- package/dist/herdr-name.d.ts.map +1 -1
- package/dist/herdr-name.js +9 -48
- package/dist/herdr-name.js.map +1 -1
- package/dist/measured-session.d.ts +1 -1
- package/extensions/activity-timeline.ts +90 -0
- package/extensions/execute-child.ts +6 -6
- package/extensions/grants-command.ts +1 -1
- package/extensions/grants-connected-command.ts +2 -2
- package/extensions/grants.ts +7 -6
- package/extensions/reload-environment.ts +1 -1
- package/extensions/session-environment.ts +6 -3
- package/extensions/session-report.ts +1 -0
- package/extensions/session.ts +21 -35
- package/package.json +2 -2
- package/src/activity-timeline.ts +133 -0
- package/src/dashboard-cli.ts +29 -4
- package/src/dashboard-display-controls.ts +6 -3
- package/src/dashboard-handshake.ts +18 -13
- package/src/delegate-types.ts +4 -0
- package/src/delegate.ts +13 -6
- package/src/herdr-name.ts +9 -49
package/src/dashboard-cli.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { resolve } from "node:path";
|
|
|
8
8
|
import { pathToFileURL } from "node:url";
|
|
9
9
|
import { parseDashboardLedger } from "./dashboard-projection.ts";
|
|
10
10
|
import { renderDashboard } from "./dashboard-render.ts";
|
|
11
|
+
import { defaultActivityTimelinePath, detailForTimeline, parseActivityTimeline, renderActivityTimeline, type TimelineFilter } from "./activity-timeline.ts";
|
|
11
12
|
import { createDashboardDisplayControls } from "./dashboard-display-controls.ts";
|
|
12
13
|
import { createDailyViewReader, isDailyViewReader, readDailyView, type DailyViewOptions } from "./daily-view.ts";
|
|
13
14
|
import { renderDailyView } from "./daily-view-render.ts";
|
|
@@ -46,6 +47,8 @@ export interface DashboardFrameOptions {
|
|
|
46
47
|
width?: number;
|
|
47
48
|
details?: boolean;
|
|
48
49
|
history?: boolean;
|
|
50
|
+
filter?: TimelineFilter;
|
|
51
|
+
activityDetail?: { taskKey: string; field: "prompt" | "final" };
|
|
49
52
|
now?: Date;
|
|
50
53
|
dailyView?: DailyViewOptions;
|
|
51
54
|
dailyReader?: ReturnType<typeof createDailyViewReader>;
|
|
@@ -137,9 +140,24 @@ export async function dashboardFrame(options: DashboardFrameOptions): Promise<st
|
|
|
137
140
|
return "PI-DADDY — DAILY VIEW UNAVAILABLE\nInvalid or unavailable read-only input. No previous acceptance or continuity was substituted.";
|
|
138
141
|
}
|
|
139
142
|
}
|
|
140
|
-
|
|
143
|
+
// Timeline is the default local activity surface. Retain the existing setup frame until a real
|
|
144
|
+
// extension-owned timeline exists, so opening a plugin before Pi has run remains explanatory.
|
|
145
|
+
const configuredPath = options.ledgerPath ?? defaultActivityTimelinePath(options.cwd);
|
|
146
|
+
if (!options.ledgerPath) {
|
|
147
|
+
try {
|
|
148
|
+
const activityPath = resolve(options.cwd, configuredPath);
|
|
149
|
+
const activity = await readFile(activityPath, "utf8");
|
|
150
|
+
const timeline = parseActivityTimeline(activity);
|
|
151
|
+
let content: { taskKey: string; field: "prompt" | "final"; text: string } | undefined;
|
|
152
|
+
if (options.activityDetail) try { content = await detailForTimeline(activityPath, options.activityDetail.taskKey, options.activityDetail.field); } catch (error) { return `PI-DADDY — ACTIVITY DETAIL UNAVAILABLE\n${error instanceof Error ? error.message : String(error)}`; }
|
|
153
|
+
return renderActivityTimeline(timeline, { details: options.details, filter: options.filter, content });
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if ((error as { code?: string }).code === "ENOENT") return setupFrame(options.cwd);
|
|
156
|
+
return "PI-DADDY — ACTIVITY UNAVAILABLE\nTimeline could not be read; no substitute view was shown.";
|
|
157
|
+
}
|
|
158
|
+
}
|
|
141
159
|
|
|
142
|
-
const ledgerPath = resolve(options.cwd,
|
|
160
|
+
const ledgerPath = resolve(options.cwd, configuredPath);
|
|
143
161
|
let text = "";
|
|
144
162
|
let waiting = false;
|
|
145
163
|
try {
|
|
@@ -157,7 +175,14 @@ export async function dashboardFrame(options: DashboardFrameOptions): Promise<st
|
|
|
157
175
|
}
|
|
158
176
|
}
|
|
159
177
|
|
|
160
|
-
|
|
178
|
+
let timeline: string | undefined;
|
|
179
|
+
if (text.trimStart().startsWith('{"version":1')) {
|
|
180
|
+
const activity = parseActivityTimeline(text);
|
|
181
|
+
let content: { taskKey: string; field: "prompt" | "final"; text: string } | undefined;
|
|
182
|
+
if (options.activityDetail) try { content = await detailForTimeline(ledgerPath, options.activityDetail.taskKey, options.activityDetail.field); } catch (error) { return `PI-DADDY — ACTIVITY DETAIL UNAVAILABLE\n${error instanceof Error ? error.message : String(error)}`; }
|
|
183
|
+
timeline = renderActivityTimeline(activity, { details: options.details, filter: options.filter, content });
|
|
184
|
+
}
|
|
185
|
+
const rendered = timeline ?? renderDashboard(parseDashboardLedger(text, { now: options.now }), {
|
|
161
186
|
color: options.color,
|
|
162
187
|
width: options.width,
|
|
163
188
|
details: options.details,
|
|
@@ -233,7 +258,7 @@ export async function runDashboard(argv = process.argv.slice(2), env: NodeJS.Pro
|
|
|
233
258
|
}
|
|
234
259
|
const dailyReader = createDailyViewReader();
|
|
235
260
|
const actionMenu=createDashboardMenu();
|
|
236
|
-
const display = createDashboardDisplayControls(cli.details, Boolean(
|
|
261
|
+
const display = createDashboardDisplayControls(cli.details, Boolean(!connected && !debrief && !cli.debriefJson && !dailyView));
|
|
237
262
|
let previous = "";
|
|
238
263
|
let feedback = "";
|
|
239
264
|
let input: ReturnType<typeof createInterface> | null = null;
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
/** Local display controls; these never dispatch actions to an owner or change ledger state. */
|
|
2
2
|
export function createDashboardDisplayControls(details: boolean, ledger: boolean) {
|
|
3
|
-
const state = { details, history: false };
|
|
3
|
+
const state: { details: boolean; history: boolean; filter?: "everything" | "agents" | "skills" | "needs-you"; activityDetail?: { taskKey: string; field: "prompt" | "final" } } = { details, history: false, ...(ledger ? { filter: "everything" as const } : {}) };
|
|
4
4
|
return {
|
|
5
5
|
state,
|
|
6
6
|
input(line: string): boolean {
|
|
7
|
-
const
|
|
7
|
+
const input = line.trim(), key = input.toLowerCase();
|
|
8
8
|
if (key === "d") { state.details = !state.details; return true; }
|
|
9
9
|
if (ledger && key === "h") { state.history = !state.history; return true; }
|
|
10
|
+
if (ledger && ["everything", "agents", "skills", "needs-you"].includes(key)) { state.filter = key as NonNullable<typeof state.filter>; return true; }
|
|
11
|
+
const detail = /^(p|f)\s+([^\s]{1,1400})$/i.exec(input);
|
|
12
|
+
if (ledger && detail) { state.activityDetail = { field: detail[1]!.toLowerCase() === "p" ? "prompt" : "final", taskKey: detail[2]! }; return true; }
|
|
10
13
|
return false;
|
|
11
14
|
},
|
|
12
15
|
prompt(): string {
|
|
13
16
|
return ledger
|
|
14
|
-
? `d Details / h ${state.history ? "Collapse" : "Expand"} history
|
|
17
|
+
? `d Details / h ${state.history ? "Collapse" : "Expand"} history / Everything|Agents|Skills|Needs-you / p|f <root-id:task-id>, then Enter: `
|
|
15
18
|
: "Action number / d Details, then Enter: ";
|
|
16
19
|
},
|
|
17
20
|
};
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from "./dashboard-herdr.ts";
|
|
14
14
|
import { DASHBOARD_PROTOCOL_VERSION } from "./dashboard-cli.ts";
|
|
15
15
|
import type { HerdrExec } from "./herdr-cli.ts";
|
|
16
|
+
import { defaultActivityTimelinePath, parseActivityTimeline, renderActivityTimeline } from "./activity-timeline.ts";
|
|
16
17
|
|
|
17
18
|
export type DashboardPromptChoice = "not-now" | "never";
|
|
18
19
|
interface DashboardPreference {
|
|
@@ -87,7 +88,13 @@ export async function offerDashboardHandshake(input: DashboardHandshakeInput): P
|
|
|
87
88
|
const preference = await loadPreference(input.preferencePath);
|
|
88
89
|
if (preference) return "suppressed";
|
|
89
90
|
const plugin = await inspectDashboardPlugin(exec, input.pluginRoot);
|
|
90
|
-
|
|
91
|
+
// A compatible bundled panel is local software already chosen by the operator. Present it by default;
|
|
92
|
+
// pane identity makes this one panel per caller tab/session and --no-focus avoids focus theft.
|
|
93
|
+
if (plugin.state === "compatible") {
|
|
94
|
+
const opened = await openOrReuseDashboard({ exec, host: host.host, ledgerPath: input.ledgerPath ?? "", cwd: input.cwd, statePath: input.paneStatePath, pluginRoot: input.pluginRoot, allowInactive: true });
|
|
95
|
+
input.ui.notify(`pi-daddy dashboard ${opened.kind} in pane ${opened.paneId} without changing focus.`, "info");
|
|
96
|
+
return "already-installed";
|
|
97
|
+
}
|
|
91
98
|
const replacingDifferentPackage = plugin.state === "incompatible" && plugin.incompatibility === "package-root";
|
|
92
99
|
if (plugin.state !== "absent" && !replacingDifferentPackage) {
|
|
93
100
|
input.ui.notify(`pi-daddy dashboard: ${plugin.diagnostic}`, "error");
|
|
@@ -157,21 +164,19 @@ export interface DashboardCommandInput {
|
|
|
157
164
|
paneStatePath: string;
|
|
158
165
|
}
|
|
159
166
|
|
|
160
|
-
export
|
|
167
|
+
export type DashboardCommandResult = DashboardOpenResult | { kind: "fallback"; frame: string; visibleBesideCaller: false };
|
|
168
|
+
async function piFallback(cwd: string, reason: string): Promise<Extract<DashboardCommandResult, { kind: "fallback" }>> {
|
|
169
|
+
try { return { kind: "fallback", visibleBesideCaller: false, frame: `${reason}\n\n${renderActivityTimeline(parseActivityTimeline(await readFile(defaultActivityTimelinePath(cwd), "utf8")))}` }; }
|
|
170
|
+
catch { return { kind: "fallback", visibleBesideCaller: false, frame: `${reason}\n\nNo local activity recorded yet.` }; }
|
|
171
|
+
}
|
|
172
|
+
export async function openDashboardCommand(input: DashboardCommandInput): Promise<DashboardCommandResult> {
|
|
161
173
|
const exec = input.exec ?? dashboardHerdrExec;
|
|
162
|
-
//
|
|
174
|
+
// A panel can only target this verified process; otherwise /grants dashboard renders the local Pi fallback.
|
|
163
175
|
const host = await verifyHerdrHost({ env: input.env, pid: input.pid, exec });
|
|
164
|
-
if (!host.ok)
|
|
165
|
-
if (!input.ledgerPath?.trim())
|
|
166
|
-
throw new Error("no pi-daddy ledger is configured — set PI_GRANTS_LEDGER, then retry /grants dashboard");
|
|
167
|
-
}
|
|
176
|
+
if (!host.ok) return piFallback(input.cwd, `Herdr panel unavailable: ${host.diagnostic}`);
|
|
177
|
+
if (!input.ledgerPath?.trim()) return piFallback(input.cwd, "Herdr panel has no governance ledger; showing the local activity timeline.");
|
|
168
178
|
const plugin = await inspectDashboardPlugin(exec, input.pluginRoot);
|
|
169
|
-
if (plugin.state === "absent") {
|
|
170
|
-
throw new Error(
|
|
171
|
-
`the pi-daddy Herdr plugin is not installed. Nothing was installed automatically. Run: ` +
|
|
172
|
-
`herdr plugin link ${quote(resolve(input.pluginRoot))} --enabled`,
|
|
173
|
-
);
|
|
174
|
-
}
|
|
179
|
+
if (plugin.state === "absent") return piFallback(input.cwd, `Herdr panel is not linked. Run: herdr plugin link ${quote(resolve(input.pluginRoot))} --enabled`);
|
|
175
180
|
if (plugin.state === "disabled") {
|
|
176
181
|
throw new Error(`the pi-daddy Herdr plugin is disabled. Run: herdr plugin enable ${DASHBOARD_PLUGIN_ID}`);
|
|
177
182
|
}
|
package/src/delegate-types.ts
CHANGED
|
@@ -61,6 +61,10 @@ export interface DelegationContext {
|
|
|
61
61
|
ledgerPath?: string;
|
|
62
62
|
/** Path to this extension, so a child granted `tool:delegate` can delegate in turn. */
|
|
63
63
|
extensionPath?: string;
|
|
64
|
+
/** No-tool activity observer injected only for governed leaf children. */
|
|
65
|
+
observerExtensionPath?: string;
|
|
66
|
+
/** Parent-owned local activity identity for this exact turn. */
|
|
67
|
+
activity?: { rootId: string; path: string; taskId?: string };
|
|
64
68
|
/** Live capability catalog. When supplied, capabilities absent from it are refused as unknown. */
|
|
65
69
|
catalog?: Catalog;
|
|
66
70
|
/**
|
package/src/delegate.ts
CHANGED
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
import { resolveDelegationApproval } from "./delegation-approval.ts";
|
|
35
35
|
import type { Delegation, DelegationContext, DelegationRequest } from "./delegate-types.ts";
|
|
36
36
|
import { assertCapabilitiesArePropagatable } from "./capabilities.ts";
|
|
37
|
+
import { ENV_ACTIVITY_PARENT_TASK, ENV_ACTIVITY_PATH, ENV_ACTIVITY_ROOT, ENV_ACTIVITY_TASK } from "./activity-timeline.ts";
|
|
37
38
|
export type { Delegation, DelegationContext, DelegationRequest } from "./delegate-types.ts";
|
|
38
39
|
|
|
39
40
|
export function planDelegation(request: DelegationRequest, ctx: DelegationContext): Delegation {
|
|
@@ -286,13 +287,11 @@ export function planDelegation(request: DelegationRequest, ctx: DelegationContex
|
|
|
286
287
|
}, "DEFINITION_UNREADABLE");
|
|
287
288
|
}
|
|
288
289
|
|
|
289
|
-
//
|
|
290
|
+
// `-e` loads even under `--no-extensions`. A delegate-capable child loads the governance extension;
|
|
291
|
+
// a leaf gets only the no-tool observer. Neither path alters --tools or the computed grant.
|
|
290
292
|
const args = [...plan.args];
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
// extension is added explicitly here and nowhere else.
|
|
294
|
-
args.splice(args.length - 1, 0, "-e", ctx.extensionPath);
|
|
295
|
-
}
|
|
293
|
+
const observer = canSubDelegate ? ctx.extensionPath : ctx.observerExtensionPath;
|
|
294
|
+
if (observer) args.splice(args.length - 1, 0, "-e", observer);
|
|
296
295
|
|
|
297
296
|
// `inheritableGrant`, not `result.effective` directly: this is the path a DELEGATED child's grant
|
|
298
297
|
// actually travels, and the "held but never inherited" rule for `tool:*` and `workspace:*` was enforced
|
|
@@ -322,6 +321,14 @@ export function planDelegation(request: DelegationRequest, ctx: DelegationContex
|
|
|
322
321
|
// reason on the other path.
|
|
323
322
|
env[ENV_APPROVED] = inheritApprovals(ctx.approved ?? [], inheritable).join(",");
|
|
324
323
|
if (ctx.ledgerPath) env[ENV_LEDGER] = ctx.ledgerPath;
|
|
324
|
+
// These local observation values are per-child argv environment, never process-global grant state.
|
|
325
|
+
// A child cannot acquire a tool or a larger grant by receiving them.
|
|
326
|
+
if (ctx.activity?.taskId && ctx.childExecutionId) {
|
|
327
|
+
env[ENV_ACTIVITY_PATH] = ctx.activity.path;
|
|
328
|
+
env[ENV_ACTIVITY_ROOT] = ctx.activity.rootId;
|
|
329
|
+
env[ENV_ACTIVITY_TASK] = ctx.childExecutionId;
|
|
330
|
+
env[ENV_ACTIVITY_PARENT_TASK] = ctx.activity.taskId;
|
|
331
|
+
}
|
|
325
332
|
|
|
326
333
|
return {
|
|
327
334
|
ok: true,
|
package/src/herdr-name.ts
CHANGED
|
@@ -1,61 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* own validation and lifecycle**, not from anything this package decides. Two shipping defects lived here, and
|
|
6
|
-
* both were invisible to every test because the unit fake accepts whatever name it is handed and the integration
|
|
7
|
-
* suite never reaches a real herdr spawn. They surfaced from two real spawns against the live daemon.
|
|
2
|
+
* Herdr reserves agent names across all parents until their tabs close.
|
|
3
|
+
* A counter alone is process-local: parallel parents and module reloads repeat it.
|
|
4
|
+
* Keep the readable base separate from a fresh, 80-bit execution suffix.
|
|
8
5
|
*/
|
|
6
|
+
import { randomBytes } from "node:crypto";
|
|
9
7
|
|
|
10
|
-
/** Monotonic within this process. See `uniqueAgentName`. */
|
|
11
|
-
let spawnSeq = 0;
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* herdr's agent-name grammar, measured from its own rejection message.
|
|
15
|
-
*
|
|
16
|
-
* `agent name must start with a lowercase letter and contain only lowercase letters, digits, '-' or '_'
|
|
17
|
-
* (1-32 characters)`.
|
|
18
|
-
*/
|
|
19
8
|
const AGENT_NAME_MAX = 32;
|
|
20
9
|
|
|
21
|
-
/**
|
|
22
|
-
* Make a herdr agent name that is **valid** and cannot collide with a live one.
|
|
23
|
-
*
|
|
24
|
-
* **Validity is a separate, PRE-EXISTING defect, and it is the more serious half.** Callers build a name as
|
|
25
|
-
* `${definition}-${childId}`, and a ledger child id is hierarchical — `d0.1`, `d0.1.2` (ADR-0008/F8). Those dots
|
|
26
|
-
* are **not in herdr's grammar**, so `agent start review-d0.1 …` is rejected with `invalid_agent_name`. Every
|
|
27
|
-
* `delegate({agent})` on the herdr path has therefore failed at `agent start` since the executor was written.
|
|
28
|
-
*
|
|
29
|
-
* Nothing could see it. The unit fake accepts any name it is handed, and the integration suite never reaches a
|
|
30
|
-
* real herdr spawn — so both were green while the feature could not work. It surfaced only by running two real
|
|
31
|
-
* spawns against the live daemon, which is the argument for doing that at all.
|
|
32
|
-
*
|
|
33
|
-
* **Measured, and a shipping defect without it.** herdr binds an agent name to its **tab**, and only closing
|
|
34
|
-
* the tab frees the name: a second `agent start` with a name still held returns
|
|
35
|
-
* `agent_name_taken: agent <name> is already used; … tab_id=…`. `herdr agent stop` does not exist (see
|
|
36
|
-
* `cleanup`), so nothing else releases it.
|
|
37
|
-
*
|
|
38
|
-
* Callers build a name from the definition and the ledger child id — and for a plain blocking `delegate` that
|
|
39
|
-
* id is **constant** (`d0.1`, index 0 of the session), so every delegation in a session asked for the same
|
|
40
|
-
* name. That was harmless while the pane closed at the end of each call. Once ADR-0032 kept panes alive to
|
|
41
|
-
* `agent_settled`, the **first** delegation of a turn worked and every later one failed with
|
|
42
|
-
* `agent_name_taken`, on the executor ADR-0031 had just made the default.
|
|
43
|
-
*
|
|
44
|
-
* Uniquified HERE rather than at the call site, so no caller can forget: the constraint belongs to herdr, and
|
|
45
|
-
* this module is the only thing that talks to herdr. The suffix is a counter rather than a random token so a
|
|
46
|
-
* pane label stays readable and reproducible within a run.
|
|
47
|
-
*/
|
|
10
|
+
/** Valid Herdr identity independent of process counters, parent labels and reloads. */
|
|
48
11
|
export function uniqueAgentName(base: string): string {
|
|
49
|
-
|
|
50
|
-
const suffix = `-${spawnSeq}`;
|
|
12
|
+
const suffix = "-" + randomBytes(10).toString("hex");
|
|
51
13
|
const cleaned = base
|
|
52
14
|
.toLowerCase()
|
|
53
|
-
.replace(/[^a-z0-9_-]+/g, "-")
|
|
15
|
+
.replace(/[^a-z0-9_-]+/g, "-")
|
|
54
16
|
.replace(/-{2,}/g, "-")
|
|
55
|
-
.replace(/^[^a-z]+/, "");
|
|
56
|
-
// Truncated so the whole name fits, and trimmed of a trailing separator so the join stays readable. The
|
|
57
|
-
// fallback covers a base that sanitises to nothing at all (a definition named entirely in non-Latin script).
|
|
17
|
+
.replace(/^[^a-z]+/, "");
|
|
58
18
|
const room = AGENT_NAME_MAX - suffix.length;
|
|
59
19
|
const head = cleaned.slice(0, room).replace(/[-_]+$/, "") || "agent";
|
|
60
|
-
return
|
|
20
|
+
return head + suffix;
|
|
61
21
|
}
|