@danypops/pi-papyrus 0.57.10 → 0.59.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/extension/src/index.ts
CHANGED
|
@@ -50,7 +50,7 @@ import {
|
|
|
50
50
|
automaticPauseReason,
|
|
51
51
|
shouldResumeFocusOnHumanInput,
|
|
52
52
|
} from "./task/active-task-continuation.ts";
|
|
53
|
-
import { emitTaskFocusEvent, setTaskFocusEventBus } from "./task/task-focus-events.ts";
|
|
53
|
+
import { emitTaskFocusEvent, extractDeclaredEffort, setTaskFocusEventBus } from "./task/task-focus-events.ts";
|
|
54
54
|
import { TASK_STATUS_PRESENTATION, taskTreeConnector } from "./task/task-presentation.ts";
|
|
55
55
|
import { buildTaskWidgetProjection, type TaskWidgetProjection } from "./task/task-widget.ts";
|
|
56
56
|
import { measure as artifactCardMeasure } from "./tool-rendering/artifact-card.ts";
|
|
@@ -492,7 +492,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
492
492
|
session_id: sessionId,
|
|
493
493
|
...sessionSecretField(sessionId),
|
|
494
494
|
});
|
|
495
|
-
emitTaskFocusEvent({ taskId: paused.artifact.id, sessionId, status: "paused" });
|
|
495
|
+
emitTaskFocusEvent({ taskId: paused.artifact.id, sessionId, status: "paused", effort: extractDeclaredEffort(paused.artifact.extra) });
|
|
496
496
|
if (ctx.hasUI) ctx.ui.notify(`Papyrus task driving paused: ${decision.reason}. Human input resumes it automatically.`, "warning");
|
|
497
497
|
}
|
|
498
498
|
} catch {
|
|
@@ -896,7 +896,12 @@ export default async function (pi: ExtensionAPI) {
|
|
|
896
896
|
session_id: sessionId,
|
|
897
897
|
...sessionSecretField(sessionId),
|
|
898
898
|
});
|
|
899
|
-
emitTaskFocusEvent({
|
|
899
|
+
emitTaskFocusEvent({
|
|
900
|
+
taskId: focus.artifact.id,
|
|
901
|
+
sessionId,
|
|
902
|
+
status: "unpaused",
|
|
903
|
+
effort: extractDeclaredEffort(focus.artifact.extra),
|
|
904
|
+
});
|
|
900
905
|
}
|
|
901
906
|
} catch {
|
|
902
907
|
// The daemon may be unavailable during startup, reload, or shutdown.
|
|
@@ -3,12 +3,31 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
3
3
|
|
|
4
4
|
export type TaskFocusStatus = "focused" | "paused" | "unpaused" | "cleared";
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* A Task's own declared effort convention (`extra.effort`) -- Papyrus's core schema and
|
|
8
|
+
* validation know nothing about this value; it is genuine free-form `extra` JSON, the same
|
|
9
|
+
* mechanism `gates`/`checklist` already use. This module only reads it, opaquely, to relay it
|
|
10
|
+
* on the task-focus broadcast when present; Papyrus never assigns or requires it.
|
|
11
|
+
*/
|
|
12
|
+
export const TASK_DECLARED_EFFORT_LEVELS = ["low", "medium", "high"] as const;
|
|
13
|
+
export type TaskDeclaredEffort = (typeof TASK_DECLARED_EFFORT_LEVELS)[number];
|
|
14
|
+
|
|
15
|
+
/** Returns the task's own declared `extra.effort` when it is one of the recognized values, or undefined otherwise -- an absent or malformed value is silently omitted, never guessed or defaulted. */
|
|
16
|
+
export function extractDeclaredEffort(extra: Record<string, unknown> | undefined): TaskDeclaredEffort | undefined {
|
|
17
|
+
const value = extra?.effort;
|
|
18
|
+
return typeof value === "string" && (TASK_DECLARED_EFFORT_LEVELS as readonly string[]).includes(value)
|
|
19
|
+
? (value as TaskDeclaredEffort)
|
|
20
|
+
: undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
6
23
|
export interface TaskFocusEvent {
|
|
7
24
|
schema: typeof PAPYRUS_TASK_FOCUS_SCHEMA;
|
|
8
25
|
taskId: string | null;
|
|
9
26
|
sessionId?: string;
|
|
10
27
|
status: TaskFocusStatus;
|
|
11
28
|
observedAt: number;
|
|
29
|
+
/** The focused task's own declared `extra.effort`, when it has one -- omitted (never null) otherwise, matching this payload's existing minimalism. */
|
|
30
|
+
effort?: TaskDeclaredEffort;
|
|
12
31
|
}
|
|
13
32
|
|
|
14
33
|
export interface TaskFocusEventInput {
|
|
@@ -16,14 +35,16 @@ export interface TaskFocusEventInput {
|
|
|
16
35
|
sessionId?: string;
|
|
17
36
|
status: TaskFocusStatus;
|
|
18
37
|
observedAt?: number;
|
|
38
|
+
effort?: TaskDeclaredEffort;
|
|
19
39
|
}
|
|
20
40
|
|
|
21
41
|
/**
|
|
22
42
|
* Pure event builder, mirroring buildContextInjection's shape: no task title, body, or any other
|
|
23
|
-
* artifact content -- only the id, session, lifecycle status,
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* token-cost router can correlate their
|
|
43
|
+
* artifact content -- only the id, session, lifecycle status, timestamp, and (when the task
|
|
44
|
+
* declares one) its own bounded effort enum, which are already public metadata a caller with the
|
|
45
|
+
* id could look up directly. This is the payload emitted on papyrus.task-focus.v1, the analogue
|
|
46
|
+
* of papyrus.context-injection.v1, so extensions such as a token-cost router can correlate their
|
|
47
|
+
* own telemetry with the currently focused task -- and now its declared effort -- without
|
|
27
48
|
* Papyrus depending on them.
|
|
28
49
|
*/
|
|
29
50
|
export function buildTaskFocusEvent(input: TaskFocusEventInput): TaskFocusEvent {
|
|
@@ -35,6 +56,7 @@ export function buildTaskFocusEvent(input: TaskFocusEventInput): TaskFocusEvent
|
|
|
35
56
|
status: input.status,
|
|
36
57
|
observedAt: input.observedAt ?? Date.now(),
|
|
37
58
|
...(input.sessionId === undefined ? {} : { sessionId: input.sessionId }),
|
|
59
|
+
...(input.effort === undefined ? {} : { effort: input.effort }),
|
|
38
60
|
};
|
|
39
61
|
}
|
|
40
62
|
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
import { callService } from "../service-client.ts";
|
|
26
26
|
import { sessionSecretField } from "../session-identity.ts";
|
|
27
27
|
import { showTaskDetails } from "./task-detail-view.ts";
|
|
28
|
-
import { emitTaskFocusEvent } from "./task-focus-events.ts";
|
|
28
|
+
import { emitTaskFocusEvent, extractDeclaredEffort } from "./task-focus-events.ts";
|
|
29
29
|
import { showTaskGraph } from "./task-graph.ts";
|
|
30
30
|
|
|
31
31
|
export { taskDetailsText } from "./task-detail-format.ts";
|
|
@@ -313,7 +313,7 @@ export async function showTasks(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
313
313
|
session_id: sessionId,
|
|
314
314
|
...sessionSecretField(sessionId),
|
|
315
315
|
});
|
|
316
|
-
emitTaskFocusEvent({ taskId: focused.id, sessionId, status: "focused" });
|
|
316
|
+
emitTaskFocusEvent({ taskId: focused.id, sessionId, status: "focused", effort: extractDeclaredEffort(focused.extra) });
|
|
317
317
|
ctx.ui.notify(`Active: ${action.row.title}`, "info");
|
|
318
318
|
} catch (error) {
|
|
319
319
|
ctx.ui.notify(`Focus failed: ${error instanceof Error ? error.message : error}`, "error");
|
|
@@ -336,7 +336,12 @@ export async function showTasks(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
336
336
|
session_id: sessionId,
|
|
337
337
|
...sessionSecretField(sessionId),
|
|
338
338
|
});
|
|
339
|
-
emitTaskFocusEvent({
|
|
339
|
+
emitTaskFocusEvent({
|
|
340
|
+
taskId: result.artifact.id,
|
|
341
|
+
sessionId,
|
|
342
|
+
status: choice === "Pause focus" ? "paused" : "unpaused",
|
|
343
|
+
effort: extractDeclaredEffort(result.artifact.extra),
|
|
344
|
+
});
|
|
340
345
|
}
|
|
341
346
|
ctx.ui.notify(choice === "Clear focus" ? "Task focus cleared" : choice, "info");
|
|
342
347
|
} catch (error) {
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
* had died.
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
+
import { createAgentNotifier } from "@danypops/vehicle-client-pi/agent-poll-ticker";
|
|
26
27
|
import { createReconnectingVehicleClient, daemonInstanceIdentity } from "@danypops/vehicle-client/daemon-client";
|
|
27
28
|
import { RemoteVehicleClient } from "@danypops/vehicle-client/http";
|
|
28
29
|
import {
|
|
@@ -31,11 +32,12 @@ import {
|
|
|
31
32
|
registerVehicleToolsWhenReady,
|
|
32
33
|
type VehicleReadyEvent,
|
|
33
34
|
} from "@danypops/vehicle-client-pi";
|
|
35
|
+
import { VehicleApprovalOutcomePoll } from "@danypops/vehicle-client-pi/vehicle-approval-outcome-poll";
|
|
34
36
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
35
37
|
import { discussLiveFollowUp } from "../discuss/discuss-live-follow-up.ts";
|
|
36
38
|
import { currentVehicleClientTarget } from "../service-client.ts";
|
|
37
39
|
import { sessionSecretField } from "../session-identity.ts";
|
|
38
|
-
import { emitTaskFocusEvent } from "../task/task-focus-events.ts";
|
|
40
|
+
import { emitTaskFocusEvent, extractDeclaredEffort } from "../task/task-focus-events.ts";
|
|
39
41
|
import { recordRenderDiagnostic, shapeFingerprint } from "./render-diagnostics.ts";
|
|
40
42
|
import { papyrusVehiclePresentations, papyrusVehicleRenderers } from "./vehicle-artifact-renderers.ts";
|
|
41
43
|
|
|
@@ -61,6 +63,13 @@ export const PAPYRUS_VEHICLE_PERMISSIONS = [
|
|
|
61
63
|
/** Task Focus's own internal write needs a real, per-session secret -- see below. Every other tasks.* operation reads session_id purely for read-scoping and needs no secret. */
|
|
62
64
|
const FOCUS_MUTATION_OPERATIONS = new Set(["tasks.focus", "tasks.pause", "tasks.unpause", "tasks.clear_focus"]);
|
|
63
65
|
|
|
66
|
+
/** Matches pi-tickets' own watch-events poll cadence -- no established Papyrus-specific reason
|
|
67
|
+
* to differ, and this Vehicle doesn't call configureApprovals() on anything today, so nothing yet
|
|
68
|
+
* actually exercises this path; it exists so a future gated operation (e.g. a genuinely
|
|
69
|
+
* destructive artifact mutation) gets outcome-visibility for free rather than needing this same
|
|
70
|
+
* wiring added later. */
|
|
71
|
+
const APPROVAL_OUTCOME_POLL_INTERVAL_MS = 30_000;
|
|
72
|
+
|
|
64
73
|
/**
|
|
65
74
|
* Vehicle Shell's core set (see @danypops/vehicle-client-pi's registerVehicleTools `shell`
|
|
66
75
|
* option): the handful of operations used in nearly every session, active from turn one with no
|
|
@@ -134,6 +143,22 @@ export function registerNotesVehicle(pi: ExtensionAPI): Promise<RegisteredPiVehi
|
|
|
134
143
|
connectRetry: true,
|
|
135
144
|
},
|
|
136
145
|
);
|
|
146
|
+
// Push half of the Approval Gate's outcome-visibility story -- see
|
|
147
|
+
// @danypops/vehicle-client-pi's own vehicle-approval-outcome-poll.ts doc comment (the Papyrus
|
|
148
|
+
// Discussion parallel this mirrors) and pi-tickets' identical wiring in vehicle-client.ts.
|
|
149
|
+
const approvalOutcomePoll = new VehicleApprovalOutcomePoll(client, createAgentNotifier(pi));
|
|
150
|
+
let approvalOutcomePollTimer: ReturnType<typeof setInterval> | undefined;
|
|
151
|
+
pi.on("session_start", (_event, ctx) => {
|
|
152
|
+
if (!ctx.hasUI) return;
|
|
153
|
+
if (approvalOutcomePollTimer) return;
|
|
154
|
+
approvalOutcomePollTimer = setInterval(() => void approvalOutcomePoll.poll(), APPROVAL_OUTCOME_POLL_INTERVAL_MS);
|
|
155
|
+
void approvalOutcomePoll.poll(); // an outcome may already be resolved before this session ever started polling
|
|
156
|
+
});
|
|
157
|
+
pi.on("session_shutdown", () => {
|
|
158
|
+
if (!approvalOutcomePollTimer) return;
|
|
159
|
+
clearInterval(approvalOutcomePollTimer);
|
|
160
|
+
approvalOutcomePollTimer = undefined;
|
|
161
|
+
});
|
|
137
162
|
return registerVehicleToolsWhenReady(pi, () => Promise.resolve(currentVehicleClientTarget() ? client : undefined), {
|
|
138
163
|
// registerVehicleMetricsOperations(..., "papyrus") in @danypops/papyrus's own daemon.ts uses
|
|
139
164
|
// the default "metrics" prefix -- without this, its metrics.query/metrics.recordClientEvent
|
|
@@ -205,19 +230,24 @@ export function registerNotesVehicle(pi: ExtensionAPI): Promise<RegisteredPiVehi
|
|
|
205
230
|
// couldn't batch a live ask alongside other tool calls in the same turn and
|
|
206
231
|
// let those run before the human sees the prompt -- same reasoning here.
|
|
207
232
|
executionMode: (descriptor) => (descriptor.name === "discuss.open" || descriptor.name === "discuss.reply" ? "sequential" : undefined),
|
|
233
|
+
onApprovalPending: (requestId, descriptor) => approvalOutcomePoll.record(requestId, descriptor.name),
|
|
208
234
|
onInvoked: ({ descriptor }, output) => {
|
|
209
235
|
// See vehicle-notes-client.ts's log wiring above -- correlates a real invocation's
|
|
210
236
|
// timestamp against when registration actually completed.
|
|
211
237
|
recordRenderDiagnostic({ event: "invoked", operation: descriptor.name, output: shapeFingerprint(output) });
|
|
212
238
|
if (descriptor.name === "tasks.focus") {
|
|
213
|
-
const artifact = output as { id: string } | undefined;
|
|
214
|
-
if (artifact?.id) emitTaskFocusEvent({ taskId: artifact.id, status: "focused" });
|
|
239
|
+
const artifact = output as { id: string; extra?: Record<string, unknown> } | undefined;
|
|
240
|
+
if (artifact?.id) emitTaskFocusEvent({ taskId: artifact.id, status: "focused", effort: extractDeclaredEffort(artifact.extra) });
|
|
215
241
|
return;
|
|
216
242
|
}
|
|
217
243
|
if (descriptor.name === "tasks.pause" || descriptor.name === "tasks.unpause") {
|
|
218
|
-
const focus = output as { artifact: { id: string } } | undefined;
|
|
244
|
+
const focus = output as { artifact: { id: string; extra?: Record<string, unknown> } } | undefined;
|
|
219
245
|
if (focus?.artifact?.id)
|
|
220
|
-
emitTaskFocusEvent({
|
|
246
|
+
emitTaskFocusEvent({
|
|
247
|
+
taskId: focus.artifact.id,
|
|
248
|
+
status: descriptor.name === "tasks.pause" ? "paused" : "unpaused",
|
|
249
|
+
effort: extractDeclaredEffort(focus.artifact.extra),
|
|
250
|
+
});
|
|
221
251
|
return;
|
|
222
252
|
}
|
|
223
253
|
if (descriptor.name === "tasks.clear_focus") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/pi-papyrus",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.59.0",
|
|
4
4
|
"description": "Pi host extension for Papyrus: native tools, TUI panels, and context injection over the daemon-backed graph store",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
14
14
|
},
|
|
15
15
|
"peerDependencies": {
|
|
16
|
-
"@danypops/vehicle-client-pi": "^0.
|
|
16
|
+
"@danypops/vehicle-client-pi": "^0.46.0",
|
|
17
17
|
"@earendil-works/pi-coding-agent": "*",
|
|
18
18
|
"@earendil-works/pi-tui": "*",
|
|
19
19
|
"typebox": "*"
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@danypops/pi-tui-harness": "^0.0.2",
|
|
32
|
-
"@danypops/vehicle-client-pi": "^0.
|
|
32
|
+
"@danypops/vehicle-client-pi": "^0.46.0",
|
|
33
33
|
"@danypops/vehicle-conformance": "^0.3.0",
|
|
34
34
|
"@earendil-works/pi-coding-agent": "^0.80.10",
|
|
35
35
|
"bun-types": "latest",
|