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.
Files changed (48) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/PRODUCT-GUIDE.md +22 -3
  3. package/README.md +9 -9
  4. package/REQUIREMENTS.md +1 -1
  5. package/dist/activity-timeline.d.ts +109 -0
  6. package/dist/activity-timeline.d.ts.map +1 -0
  7. package/dist/activity-timeline.js +208 -0
  8. package/dist/activity-timeline.js.map +1 -0
  9. package/dist/dashboard-cli.d.ts +6 -0
  10. package/dist/dashboard-cli.d.ts.map +1 -1
  11. package/dist/dashboard-cli.js +41 -5
  12. package/dist/dashboard-cli.js.map +1 -1
  13. package/dist/dashboard-display-controls.d.ts +5 -0
  14. package/dist/dashboard-display-controls.d.ts.map +1 -1
  15. package/dist/dashboard-display-controls.js +12 -3
  16. package/dist/dashboard-display-controls.js.map +1 -1
  17. package/dist/dashboard-handshake.d.ts +6 -1
  18. package/dist/dashboard-handshake.d.ts.map +1 -1
  19. package/dist/dashboard-handshake.js +21 -10
  20. package/dist/dashboard-handshake.js.map +1 -1
  21. package/dist/delegate-types.d.ts +8 -0
  22. package/dist/delegate-types.d.ts.map +1 -1
  23. package/dist/delegate-types.js.map +1 -1
  24. package/dist/delegate.d.ts.map +1 -1
  25. package/dist/delegate.js +14 -6
  26. package/dist/delegate.js.map +1 -1
  27. package/dist/herdr-name.d.ts +1 -35
  28. package/dist/herdr-name.d.ts.map +1 -1
  29. package/dist/herdr-name.js +9 -48
  30. package/dist/herdr-name.js.map +1 -1
  31. package/dist/measured-session.d.ts +1 -1
  32. package/extensions/activity-timeline.ts +90 -0
  33. package/extensions/execute-child.ts +6 -6
  34. package/extensions/grants-command.ts +1 -1
  35. package/extensions/grants-connected-command.ts +2 -2
  36. package/extensions/grants.ts +7 -6
  37. package/extensions/reload-environment.ts +1 -1
  38. package/extensions/session-environment.ts +6 -3
  39. package/extensions/session-report.ts +1 -0
  40. package/extensions/session.ts +21 -35
  41. package/package.json +2 -2
  42. package/src/activity-timeline.ts +133 -0
  43. package/src/dashboard-cli.ts +29 -4
  44. package/src/dashboard-display-controls.ts +6 -3
  45. package/src/dashboard-handshake.ts +18 -13
  46. package/src/delegate-types.ts +4 -0
  47. package/src/delegate.ts +13 -6
  48. package/src/herdr-name.ts +9 -49
@@ -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
- if (!options.ledgerPath) return setupFrame(options.cwd);
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, options.ledgerPath);
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
- const rendered = renderDashboard(parseDashboardLedger(text, { now: options.now }), {
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(ledgerPath && !connected && !debrief && !cli.debriefJson && !dailyView));
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 key = line.trim().toLowerCase();
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, then Enter: `
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
- if (plugin.state === "compatible") return "already-installed";
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 async function openDashboardCommand(input: DashboardCommandInput): Promise<DashboardOpenResult> {
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
- // Order is deliberate: a server elsewhere must not make an outside-pi command look hosted.
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) throw new Error(host.diagnostic);
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
  }
@@ -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
- // A child may only delegate further if it was granted the capability AND has the extension to do it.
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
- if (canSubDelegate && ctx.extensionPath) {
292
- // `-e` loads even under `--no-extensions`, which planSpawn sets — that is precisely why the
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
- * Naming a herdr agent: the grammar herdr enforces, and uniqueness it does not.
3
- *
4
- * Split out of `src/run-herdr.ts` at the 400-line ceiling, and a real seam: both rules below come from **herdr's
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
- spawnSeq += 1;
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, "-") // dots from a child id, and anything else outside the grammar
15
+ .replace(/[^a-z0-9_-]+/g, "-")
54
16
  .replace(/-{2,}/g, "-")
55
- .replace(/^[^a-z]+/, ""); // must START with a lowercase letter, so a leading digit or dash goes
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 `${head}${suffix}`;
20
+ return head + suffix;
61
21
  }