@bastani/atomic 0.8.19 → 0.8.20-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 +13 -0
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/mcp/CHANGELOG.md +5 -0
- package/dist/builtin/mcp/package.json +2 -2
- package/dist/builtin/subagents/CHANGELOG.md +12 -2
- package/dist/builtin/subagents/agents/code-simplifier.md +1 -1
- package/dist/builtin/subagents/agents/codebase-analyzer.md +1 -1
- package/dist/builtin/subagents/agents/codebase-online-researcher.md +1 -1
- package/dist/builtin/subagents/agents/codebase-research-analyzer.md +1 -1
- package/dist/builtin/subagents/agents/debugger.md +1 -1
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/subagents/skills/subagent/SKILL.md +12 -12
- package/dist/builtin/subagents/src/agents/agent-management.ts +16 -11
- package/dist/builtin/subagents/src/agents/skills.ts +13 -1
- package/dist/builtin/subagents/src/extension/index.ts +14 -3
- package/dist/builtin/subagents/src/runs/background/async-execution.ts +8 -0
- package/dist/builtin/subagents/src/runs/background/run-status.ts +2 -3
- package/dist/builtin/subagents/src/runs/background/subagent-runner.ts +11 -1
- package/dist/builtin/subagents/src/runs/foreground/chain-clarify.ts +2 -2
- package/dist/builtin/subagents/src/runs/foreground/chain-execution.ts +31 -23
- package/dist/builtin/subagents/src/runs/foreground/execution.ts +13 -7
- package/dist/builtin/subagents/src/runs/foreground/subagent-executor.ts +160 -93
- package/dist/builtin/subagents/src/runs/shared/parallel-utils.ts +1 -0
- package/dist/builtin/subagents/src/runs/shared/run-history.ts +1 -1
- package/dist/builtin/subagents/src/shared/settings.ts +1 -0
- package/dist/builtin/subagents/src/shared/types.ts +78 -4
- package/dist/builtin/subagents/src/tui/render.ts +203 -19
- package/dist/builtin/web-access/CHANGELOG.md +5 -0
- package/dist/builtin/web-access/package.json +2 -2
- package/dist/builtin/workflows/CHANGELOG.md +19 -0
- package/dist/builtin/workflows/README.md +22 -3
- package/dist/builtin/workflows/builtin/deep-research-codebase.ts +1 -1
- package/dist/builtin/workflows/builtin/open-claude-design.ts +12 -4
- package/dist/builtin/workflows/builtin/ralph.ts +2 -2
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/src/extension/config-loader.ts +68 -0
- package/dist/builtin/workflows/src/extension/index.ts +246 -55
- package/dist/builtin/workflows/src/extension/lifecycle-notifications.ts +372 -0
- package/dist/builtin/workflows/src/extension/render-call.ts +1 -1
- package/dist/builtin/workflows/src/extension/wiring.ts +32 -3
- package/dist/builtin/workflows/src/runs/background/status.ts +14 -74
- package/dist/builtin/workflows/src/shared/persistence-restore.ts +5 -3
- package/dist/builtin/workflows/src/tui/chat-surface-message.ts +3 -13
- package/dist/builtin/workflows/src/tui/inline-form-overlay.ts +2 -10
- package/dist/builtin/workflows/src/tui/overlay-adapter.ts +5 -5
- package/dist/builtin/workflows/src/tui/session-confirm.ts +6 -7
- package/dist/builtin/workflows/src/tui/session-picker.ts +18 -14
- package/dist/builtin/workflows/src/tui/status-list.ts +2 -2
- package/dist/builtin/workflows/src/tui/store-widget-installer.ts +125 -30
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts +4 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +2 -1
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/atomic-guide-command.d.ts.map +1 -1
- package/dist/core/atomic-guide-command.js +3 -2
- package/dist/core/atomic-guide-command.js.map +1 -1
- package/dist/core/extensions/index.d.ts +1 -1
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +3 -2
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +6 -1
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +13 -0
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +63 -17
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/output-guard.d.ts.map +1 -1
- package/dist/core/output-guard.js +29 -0
- package/dist/core/output-guard.js.map +1 -1
- package/dist/core/sdk.d.ts +3 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +1 -0
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +1 -1
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +46 -13
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/utils/pi-user-agent.d.ts.map +1 -1
- package/dist/utils/pi-user-agent.js +2 -1
- package/dist/utils/pi-user-agent.js.map +1 -1
- package/dist/utils/syntax-highlight.d.ts.map +1 -1
- package/dist/utils/syntax-highlight.js +1 -1
- package/dist/utils/syntax-highlight.js.map +1 -1
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +3 -5
- package/dist/utils/tools-manager.js.map +1 -1
- package/docs/models.md +52 -52
- package/docs/quickstart.md +2 -2
- package/docs/workflows.md +22 -5
- package/package.json +9 -9
|
@@ -15,9 +15,8 @@ import { restoreOnSessionStart } from "../shared/persistence-restore.js";
|
|
|
15
15
|
import type { SessionManager } from "../shared/persistence-restore.js";
|
|
16
16
|
import { installCompactionHook } from "../shared/persistence-compaction-policy.js";
|
|
17
17
|
import {
|
|
18
|
+
killRun,
|
|
18
19
|
killAllRuns,
|
|
19
|
-
destroyRun,
|
|
20
|
-
destroyAllRuns,
|
|
21
20
|
resumeRun,
|
|
22
21
|
pauseRun,
|
|
23
22
|
pauseAllRuns,
|
|
@@ -65,6 +64,15 @@ import {
|
|
|
65
64
|
WORKFLOW_CONFIG_DEFAULTS,
|
|
66
65
|
withWorkflowDefaults,
|
|
67
66
|
} from "./config-loader.js";
|
|
67
|
+
import {
|
|
68
|
+
createWorkflowLifecycleNotificationState,
|
|
69
|
+
installWorkflowLifecycleNotifications,
|
|
70
|
+
registerLifecycleNoticeRenderer,
|
|
71
|
+
resetWorkflowLifecycleNotificationState,
|
|
72
|
+
seedWorkflowLifecycleNotificationState,
|
|
73
|
+
withWorkflowLifecycleNotificationsSuppressed,
|
|
74
|
+
} from "./lifecycle-notifications.js";
|
|
75
|
+
import type { WorkflowLifecycleNotificationConfig } from "./lifecycle-notifications.js";
|
|
68
76
|
import type { ConfigLoadResult } from "./config-loader.js";
|
|
69
77
|
import type {
|
|
70
78
|
WorkflowPersistencePort,
|
|
@@ -85,7 +93,7 @@ import type { StatusWriter } from "./status-writer.js";
|
|
|
85
93
|
import { setMcpScope, clearMcpScope } from "./mcp.js";
|
|
86
94
|
import type { PiMcpExtensionAPI, PiEventBus } from "./mcp.js";
|
|
87
95
|
import type { StageSessionRuntime } from "../runs/foreground/stage-runner.js";
|
|
88
|
-
import type
|
|
96
|
+
import { WORKFLOW_STAGE_SUBAGENT_GUARD_ENV, getEnvValue, type CreateAgentSessionOptions } from "@bastani/atomic";
|
|
89
97
|
|
|
90
98
|
// ---------------------------------------------------------------------------
|
|
91
99
|
// Minimal ExtensionAPI structural types
|
|
@@ -114,6 +122,13 @@ export interface PiRenderComponent {
|
|
|
114
122
|
includes(searchString: string): boolean;
|
|
115
123
|
}
|
|
116
124
|
|
|
125
|
+
export interface PiMessageRenderComponent {
|
|
126
|
+
render(width: number): string[];
|
|
127
|
+
invalidate?: () => void;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export type PiMessageRendererResult = string | PiMessageRenderComponent | undefined;
|
|
131
|
+
|
|
117
132
|
function textRenderComponent(text: string): PiRenderComponent {
|
|
118
133
|
return dynamicTextRenderComponent(() => text);
|
|
119
134
|
}
|
|
@@ -180,6 +195,11 @@ export interface PiCommandContext extends PiModelContext {
|
|
|
180
195
|
ui: {
|
|
181
196
|
notify: (message: string, type?: "info" | "warning" | "error") => void;
|
|
182
197
|
} & PiUISurface;
|
|
198
|
+
/**
|
|
199
|
+
* False when the host bound a no-op UI surface (print/JSON `-p` modes).
|
|
200
|
+
* Absent on older hosts and unit-test stubs; treat absence as interactive.
|
|
201
|
+
*/
|
|
202
|
+
hasUI?: boolean;
|
|
183
203
|
}
|
|
184
204
|
|
|
185
205
|
/** CLI flag registration options. Mirrors the inline options on `ExtensionAPI.registerFlag`. */
|
|
@@ -240,6 +260,7 @@ export interface PiExecuteContext extends PiModelContext {
|
|
|
240
260
|
sessionId?: string;
|
|
241
261
|
ui?: PiUISurface;
|
|
242
262
|
hasUI?: boolean;
|
|
263
|
+
orchestrationContext?: CreateAgentSessionOptions["orchestrationContext"];
|
|
243
264
|
sessionManager?: SessionManager & {
|
|
244
265
|
getSessionFile?: () => string | undefined;
|
|
245
266
|
};
|
|
@@ -273,7 +294,7 @@ export interface ExtensionAPI {
|
|
|
273
294
|
registerCommand?: (name: string, options: PiCommandOptions) => void;
|
|
274
295
|
registerMessageRenderer?: (
|
|
275
296
|
event: string,
|
|
276
|
-
renderer: (payload: unknown) =>
|
|
297
|
+
renderer: (payload: unknown) => PiMessageRendererResult,
|
|
277
298
|
) => void;
|
|
278
299
|
/**
|
|
279
300
|
* Inject a custom message into chat history. Used by inline workflow surfaces
|
|
@@ -306,6 +327,17 @@ export interface ExtensionAPI {
|
|
|
306
327
|
handler: (ctx?: PiCommandContext) => void | Promise<void>;
|
|
307
328
|
},
|
|
308
329
|
) => void;
|
|
330
|
+
/**
|
|
331
|
+
* Read the model's currently-active tool names. Present on pi's ExtensionAPI;
|
|
332
|
+
* absent on older runtimes.
|
|
333
|
+
*/
|
|
334
|
+
getActiveTools?: () => string[];
|
|
335
|
+
/**
|
|
336
|
+
* Replace the model's active tool set by name. Used to drop the `workflow`
|
|
337
|
+
* tool in non-interactive sessions. Present on pi's ExtensionAPI; absent on
|
|
338
|
+
* older runtimes.
|
|
339
|
+
*/
|
|
340
|
+
setActiveTools?: (toolNames: string[]) => void;
|
|
309
341
|
/**
|
|
310
342
|
* Sets the current session name. Present on pi's ExtensionAPI.
|
|
311
343
|
*/
|
|
@@ -896,6 +928,10 @@ function snapshotTranscriptEntries(
|
|
|
896
928
|
return sortTranscriptEntriesChronologically(entries);
|
|
897
929
|
}
|
|
898
930
|
|
|
931
|
+
function formatAlreadyEndedRetainedMessage(runId: string): string {
|
|
932
|
+
return `Run ${runId.slice(0, 8)} already ended; retained for inspection.`;
|
|
933
|
+
}
|
|
934
|
+
|
|
899
935
|
function stageFailureMessage(
|
|
900
936
|
runId: string,
|
|
901
937
|
resultReason: string,
|
|
@@ -937,6 +973,49 @@ function reloadFailureMessage(error: unknown): string {
|
|
|
937
973
|
return `Reload failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
938
974
|
}
|
|
939
975
|
|
|
976
|
+
function hasWorkflowStageSubagentGuardEnv(): boolean {
|
|
977
|
+
return getEnvValue(WORKFLOW_STAGE_SUBAGENT_GUARD_ENV) === "1";
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
function isWorkflowStageToolContext(ctx: PiExecuteContext): boolean {
|
|
981
|
+
return hasWorkflowStageSubagentGuardEnv() || ctx.orchestrationContext?.kind === "workflow-stage";
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
/** Tool name registered for workflow execution; shared by the de-advertise guard. */
|
|
985
|
+
const WORKFLOW_TOOL_NAME = "workflow";
|
|
986
|
+
|
|
987
|
+
/**
|
|
988
|
+
* User-facing message shown when the `/workflow` command is invoked in a
|
|
989
|
+
* non-interactive (`-p` / `--print` / `--mode json`) session. Such sessions
|
|
990
|
+
* bind a no-op UI surface (`ctx.hasUI === false`) and cannot drive workflow
|
|
991
|
+
* pickers, the graph overlay, or human-in-the-loop prompts.
|
|
992
|
+
*
|
|
993
|
+
* The `workflow` tool is removed from the model's tool set in these sessions
|
|
994
|
+
* (see `deAdvertiseWorkflowToolWhenHeadless`). The slash command has no
|
|
995
|
+
* advertise layer and is still reachable via `atomic -p "/workflow …"` (print
|
|
996
|
+
* mode dispatches leading-slash commands through `session.prompt`), so it is
|
|
997
|
+
* refused here instead.
|
|
998
|
+
*/
|
|
999
|
+
export const WORKFLOW_NON_INTERACTIVE_MESSAGE =
|
|
1000
|
+
"Workflows are disabled in non-interactive (-p) mode; run Atomic interactively to use workflows.";
|
|
1001
|
+
|
|
1002
|
+
/**
|
|
1003
|
+
* Remove the `workflow` tool from the model's active tool set in non-interactive
|
|
1004
|
+
* (`-p` / `--mode json`) sessions, which bind a no-op UI (`ctx.hasUI === false`)
|
|
1005
|
+
* and cannot drive workflow prompts or the graph overlay. Invoked from
|
|
1006
|
+
* `session_start`, which the host awaits before the first prompt, so the tool is
|
|
1007
|
+
* gone before the model's first turn. Interactive and RPC modes bind a real UI
|
|
1008
|
+
* context (`hasUI: true`) and keep the tool. No-ops on hosts that predate the
|
|
1009
|
+
* `getActiveTools`/`setActiveTools` extension API.
|
|
1010
|
+
*/
|
|
1011
|
+
function deAdvertiseWorkflowToolWhenHeadless(pi: ExtensionAPI, hasUI: boolean | undefined): void {
|
|
1012
|
+
if (hasUI !== false) return;
|
|
1013
|
+
if (typeof pi.getActiveTools !== "function" || typeof pi.setActiveTools !== "function") return;
|
|
1014
|
+
const active = pi.getActiveTools();
|
|
1015
|
+
if (!active.includes(WORKFLOW_TOOL_NAME)) return;
|
|
1016
|
+
pi.setActiveTools(active.filter((name) => name !== WORKFLOW_TOOL_NAME));
|
|
1017
|
+
}
|
|
1018
|
+
|
|
940
1019
|
// ---------------------------------------------------------------------------
|
|
941
1020
|
// Tool execute — dispatch with real registry for list/inputs/run (Phase E)
|
|
942
1021
|
// + real status/interrupt/resume (Phase D)
|
|
@@ -953,6 +1032,19 @@ export function makeExecuteWorkflowTool(
|
|
|
953
1032
|
): Promise<WorkflowToolResult> {
|
|
954
1033
|
const action = args.action ?? "run";
|
|
955
1034
|
const runId = args.runId ?? "";
|
|
1035
|
+
if (isWorkflowStageToolContext(ctx)) {
|
|
1036
|
+
// Workflow stages must not invoke the workflow tool at all, including
|
|
1037
|
+
// read-only inspection actions. The tool is normally excluded from stage
|
|
1038
|
+
// sessions; this guard is a defense-in-depth fallback for stale or
|
|
1039
|
+
// hand-crafted registrations.
|
|
1040
|
+
return {
|
|
1041
|
+
action: "run",
|
|
1042
|
+
runId,
|
|
1043
|
+
status: "failed",
|
|
1044
|
+
error: "workflows cannot invoke workflows from workflow stages",
|
|
1045
|
+
stages: [],
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
956
1048
|
const activeRuntime =
|
|
957
1049
|
typeof runtime === "function" ? runtime(ctx) : runtime;
|
|
958
1050
|
|
|
@@ -998,9 +1090,9 @@ export function makeExecuteWorkflowTool(
|
|
|
998
1090
|
error: `run not found: ${target}`,
|
|
999
1091
|
};
|
|
1000
1092
|
}
|
|
1001
|
-
// List mode — emit
|
|
1093
|
+
// List mode — emit all retained snapshots; the renderer produces the
|
|
1002
1094
|
// canonical band + card surface.
|
|
1003
|
-
const snapshots = store.runs()
|
|
1095
|
+
const snapshots = store.runs();
|
|
1004
1096
|
return {
|
|
1005
1097
|
action: "status",
|
|
1006
1098
|
snapshots: snapshots.map((snapshot) => structuredClone(snapshot)),
|
|
@@ -1319,7 +1411,7 @@ export function makeExecuteWorkflowTool(
|
|
|
1319
1411
|
message: allStageConflictMessage("kill"),
|
|
1320
1412
|
};
|
|
1321
1413
|
}
|
|
1322
|
-
const results =
|
|
1414
|
+
const results = killAllRuns({
|
|
1323
1415
|
cancellation: cancellationRegistry,
|
|
1324
1416
|
persistence: getPersistence(),
|
|
1325
1417
|
});
|
|
@@ -1330,7 +1422,7 @@ export function makeExecuteWorkflowTool(
|
|
|
1330
1422
|
status: killed > 0 ? "killed" : "noop",
|
|
1331
1423
|
message:
|
|
1332
1424
|
killed > 0
|
|
1333
|
-
? `Killed and
|
|
1425
|
+
? `Killed and retained ${killed} run(s) for inspection.`
|
|
1334
1426
|
: "No in-flight runs to kill.",
|
|
1335
1427
|
};
|
|
1336
1428
|
}
|
|
@@ -1340,7 +1432,7 @@ export function makeExecuteWorkflowTool(
|
|
|
1340
1432
|
if (target.kind === "not_found") {
|
|
1341
1433
|
return { action, runId: target.target, status: "noop", message: target.message };
|
|
1342
1434
|
}
|
|
1343
|
-
const result =
|
|
1435
|
+
const result = killRun(target.runId, {
|
|
1344
1436
|
cancellation: cancellationRegistry,
|
|
1345
1437
|
persistence: getPersistence(),
|
|
1346
1438
|
});
|
|
@@ -1349,14 +1441,17 @@ export function makeExecuteWorkflowTool(
|
|
|
1349
1441
|
action,
|
|
1350
1442
|
runId: result.runId,
|
|
1351
1443
|
status: "killed",
|
|
1352
|
-
message: `Run ${result.runId} killed and
|
|
1444
|
+
message: `Run ${result.runId} killed and retained for inspection (was ${result.previousStatus}).`,
|
|
1353
1445
|
};
|
|
1354
1446
|
}
|
|
1355
1447
|
return {
|
|
1356
1448
|
action,
|
|
1357
1449
|
runId: target.runId,
|
|
1358
1450
|
status: "noop",
|
|
1359
|
-
message:
|
|
1451
|
+
message: result.reason === "already_ended"
|
|
1452
|
+
? formatAlreadyEndedRetainedMessage(target.runId)
|
|
1453
|
+
// Defensive fallback: resolveRunTarget already found this run, and killRun no longer removes runs.
|
|
1454
|
+
: `Run not found: ${target.runId}`,
|
|
1360
1455
|
};
|
|
1361
1456
|
}
|
|
1362
1457
|
|
|
@@ -1908,7 +2003,7 @@ function factory(pi: ExtensionAPI): void {
|
|
|
1908
2003
|
const overlay: GraphOverlayPort = buildGraphOverlayAdapter(pi, store, {
|
|
1909
2004
|
onKillRun: (runId) => {
|
|
1910
2005
|
const run = store.runs().find((r) => r.id === runId);
|
|
1911
|
-
const result =
|
|
2006
|
+
const result = killRun(runId, {
|
|
1912
2007
|
cancellation: cancellationRegistry,
|
|
1913
2008
|
persistence: persistenceRef.current,
|
|
1914
2009
|
});
|
|
@@ -1917,7 +2012,6 @@ function factory(pi: ExtensionAPI): void {
|
|
|
1917
2012
|
kind: "killed",
|
|
1918
2013
|
run,
|
|
1919
2014
|
previousStatus: result.previousStatus,
|
|
1920
|
-
wasInFlight: result.wasInFlight,
|
|
1921
2015
|
});
|
|
1922
2016
|
}
|
|
1923
2017
|
},
|
|
@@ -1950,6 +2044,32 @@ function factory(pi: ExtensionAPI): void {
|
|
|
1950
2044
|
store,
|
|
1951
2045
|
runtimeConfigRef.current,
|
|
1952
2046
|
);
|
|
2047
|
+
let lifecycleNotificationsUnsubscribe: (() => void) | null = null;
|
|
2048
|
+
let lifecycleNotificationsActive = false;
|
|
2049
|
+
const lifecycleNotificationState = createWorkflowLifecycleNotificationState();
|
|
2050
|
+
const lifecycleNotificationConfigRef: { current: WorkflowLifecycleNotificationConfig } = {
|
|
2051
|
+
current: WORKFLOW_CONFIG_DEFAULTS.workflowNotifications,
|
|
2052
|
+
};
|
|
2053
|
+
registerLifecycleNoticeRenderer({
|
|
2054
|
+
rendererHost: pi,
|
|
2055
|
+
registerMessageRenderer: pi.registerMessageRenderer
|
|
2056
|
+
? (event, renderer) => pi.registerMessageRenderer?.(event, renderer)
|
|
2057
|
+
: undefined,
|
|
2058
|
+
});
|
|
2059
|
+
const reinstallLifecycleNotifications = (): void => {
|
|
2060
|
+
lifecycleNotificationsUnsubscribe?.();
|
|
2061
|
+
lifecycleNotificationsUnsubscribe = null;
|
|
2062
|
+
if (!lifecycleNotificationsActive) return;
|
|
2063
|
+
lifecycleNotificationsUnsubscribe = installWorkflowLifecycleNotifications({
|
|
2064
|
+
store,
|
|
2065
|
+
config: lifecycleNotificationConfigRef.current,
|
|
2066
|
+
state: lifecycleNotificationState,
|
|
2067
|
+
seedExisting: true,
|
|
2068
|
+
sendMessage: pi.sendMessage
|
|
2069
|
+
? (message, options) => pi.sendMessage?.(message, options)
|
|
2070
|
+
: undefined,
|
|
2071
|
+
});
|
|
2072
|
+
};
|
|
1953
2073
|
let intercomParentSession: string | null = null;
|
|
1954
2074
|
const intercomPort = {
|
|
1955
2075
|
emit:
|
|
@@ -2100,6 +2220,8 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2100
2220
|
statusFile: effectiveConfig.statusFile,
|
|
2101
2221
|
resumeInFlight: effectiveConfig.resumeInFlight,
|
|
2102
2222
|
};
|
|
2223
|
+
lifecycleNotificationConfigRef.current = effectiveConfig.workflowNotifications;
|
|
2224
|
+
reinstallLifecycleNotifications();
|
|
2103
2225
|
|
|
2104
2226
|
// Replace status writer with one that reflects the resolved config.
|
|
2105
2227
|
// Unsubscribe the prior (no-op) writer before creating the new one.
|
|
@@ -2182,7 +2304,7 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2182
2304
|
* connect [runId|prefix] no arg → picker overlay; arg → attach to graph
|
|
2183
2305
|
* attach [runId|prefix [stageId]] open the in-place attach pane on a stage
|
|
2184
2306
|
* interrupt [runId|prefix|--all] [-y] confirmation overlay unless -y
|
|
2185
|
-
* kill [runId|prefix|--all] [-y] kill and
|
|
2307
|
+
* kill [runId|prefix|--all] [-y] kill and retain for inspection
|
|
2186
2308
|
* pause [runId|prefix [stageId]] pause a run or specific stage
|
|
2187
2309
|
* resume [runId|prefix [stageId] …] resume paused work or reopen snapshot
|
|
2188
2310
|
*/
|
|
@@ -2201,7 +2323,7 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2201
2323
|
const ui = ctx.ui;
|
|
2202
2324
|
if (!ui || typeof ui.custom !== "function") {
|
|
2203
2325
|
print(
|
|
2204
|
-
`${renderSessionList(store.runs(), { theme, includeAll:
|
|
2326
|
+
`${renderSessionList(store.runs(), { theme, includeAll: true })}\n\nPicker requires a UI surface. Pass a runId: /workflow connect <id>`,
|
|
2205
2327
|
);
|
|
2206
2328
|
return true;
|
|
2207
2329
|
}
|
|
@@ -2217,6 +2339,10 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2217
2339
|
print(`Run not found: ${result.runId}`);
|
|
2218
2340
|
return true;
|
|
2219
2341
|
}
|
|
2342
|
+
if (run.endedAt !== undefined) {
|
|
2343
|
+
print(formatAlreadyEndedRetainedMessage(result.runId));
|
|
2344
|
+
return true;
|
|
2345
|
+
}
|
|
2220
2346
|
const confirmed = await openKillConfirm(ui, run, theme);
|
|
2221
2347
|
if (!confirmed) {
|
|
2222
2348
|
print(
|
|
@@ -2224,7 +2350,7 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2224
2350
|
);
|
|
2225
2351
|
return true;
|
|
2226
2352
|
}
|
|
2227
|
-
const killed =
|
|
2353
|
+
const killed = killRun(result.runId, {
|
|
2228
2354
|
cancellation: cancellationRegistry,
|
|
2229
2355
|
persistence: persistenceRef.current,
|
|
2230
2356
|
});
|
|
@@ -2233,13 +2359,14 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2233
2359
|
kind: "killed",
|
|
2234
2360
|
run,
|
|
2235
2361
|
previousStatus: killed.previousStatus,
|
|
2236
|
-
wasInFlight: killed.wasInFlight,
|
|
2237
2362
|
});
|
|
2238
2363
|
}
|
|
2239
2364
|
print(
|
|
2240
2365
|
killed.ok
|
|
2241
|
-
? `Run ${killed.runId.slice(0, 8)} killed and
|
|
2242
|
-
:
|
|
2366
|
+
? `Run ${killed.runId.slice(0, 8)} killed and retained for inspection.`
|
|
2367
|
+
: killed.reason === "already_ended"
|
|
2368
|
+
? formatAlreadyEndedRetainedMessage(killed.runId)
|
|
2369
|
+
: `Run not found: ${result.runId.slice(0, 8)}.`,
|
|
2243
2370
|
);
|
|
2244
2371
|
return true;
|
|
2245
2372
|
}
|
|
@@ -2367,7 +2494,7 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2367
2494
|
}
|
|
2368
2495
|
if (!yes && ctx.ui && typeof ctx.ui.confirm === "function") {
|
|
2369
2496
|
const ok = await ctx.ui.confirm(
|
|
2370
|
-
`Kill
|
|
2497
|
+
`Kill ${inFlight.length} in-flight workflow runs? Killed runs are retained for inspection.`,
|
|
2371
2498
|
`Aborts: ${inFlight.map((r) => `${r.name} (${r.id.slice(0, 8)})`).join(", ")}`,
|
|
2372
2499
|
);
|
|
2373
2500
|
if (!ok) {
|
|
@@ -2375,14 +2502,14 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2375
2502
|
return true;
|
|
2376
2503
|
}
|
|
2377
2504
|
}
|
|
2378
|
-
const results =
|
|
2505
|
+
const results = killAllRuns({
|
|
2379
2506
|
cancellation: cancellationRegistry,
|
|
2380
2507
|
persistence: persistenceRef.current,
|
|
2381
2508
|
});
|
|
2382
2509
|
const killed = results.filter((r) => r.ok).length;
|
|
2383
2510
|
print(
|
|
2384
2511
|
killed > 0
|
|
2385
|
-
? `Killed and
|
|
2512
|
+
? `Killed and retained ${killed} run(s) for inspection.`
|
|
2386
2513
|
: "No in-flight runs to kill.",
|
|
2387
2514
|
);
|
|
2388
2515
|
return true;
|
|
@@ -2401,6 +2528,10 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2401
2528
|
return true;
|
|
2402
2529
|
}
|
|
2403
2530
|
const run = store.runs().find((r) => r.id === resolved.runId);
|
|
2531
|
+
if (run?.endedAt !== undefined) {
|
|
2532
|
+
print(formatAlreadyEndedRetainedMessage(resolved.runId));
|
|
2533
|
+
return true;
|
|
2534
|
+
}
|
|
2404
2535
|
if (!yes && run && ctx.ui) {
|
|
2405
2536
|
const confirmed = await openKillConfirm(ctx.ui, run, theme);
|
|
2406
2537
|
if (!confirmed) {
|
|
@@ -2410,7 +2541,7 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2410
2541
|
return true;
|
|
2411
2542
|
}
|
|
2412
2543
|
}
|
|
2413
|
-
const result =
|
|
2544
|
+
const result = killRun(resolved.runId, {
|
|
2414
2545
|
cancellation: cancellationRegistry,
|
|
2415
2546
|
persistence: persistenceRef.current,
|
|
2416
2547
|
});
|
|
@@ -2420,14 +2551,17 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2420
2551
|
kind: "killed",
|
|
2421
2552
|
run,
|
|
2422
2553
|
previousStatus: result.previousStatus,
|
|
2423
|
-
wasInFlight: result.wasInFlight,
|
|
2424
2554
|
});
|
|
2425
2555
|
}
|
|
2426
2556
|
print(
|
|
2427
|
-
`Run ${result.runId.slice(0, 8)} killed and
|
|
2557
|
+
`Run ${result.runId.slice(0, 8)} killed and retained for inspection (was ${result.previousStatus}).`,
|
|
2428
2558
|
);
|
|
2429
2559
|
} else {
|
|
2430
|
-
print(
|
|
2560
|
+
print(
|
|
2561
|
+
result.reason === "already_ended"
|
|
2562
|
+
? formatAlreadyEndedRetainedMessage(result.runId)
|
|
2563
|
+
: `Run not found: ${target}`,
|
|
2564
|
+
);
|
|
2431
2565
|
}
|
|
2432
2566
|
return true;
|
|
2433
2567
|
}
|
|
@@ -2440,7 +2574,7 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2440
2574
|
const ui = ctx.ui;
|
|
2441
2575
|
if (!ui || typeof ui.custom !== "function") {
|
|
2442
2576
|
print(
|
|
2443
|
-
`${renderSessionList(store.runs(), { theme, includeAll:
|
|
2577
|
+
`${renderSessionList(store.runs(), { theme, includeAll: true })}\n\nPicker requires a UI surface. Pass a runId: /workflow attach <id> [stageId]`,
|
|
2444
2578
|
);
|
|
2445
2579
|
return true;
|
|
2446
2580
|
}
|
|
@@ -2657,6 +2791,13 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2657
2791
|
description:
|
|
2658
2792
|
"Run or inspect pi workflows. Usage: /workflow <name> [key=value…] | /workflow [list|status|connect|attach|interrupt|kill|pause|resume|inputs|reload] [args]",
|
|
2659
2793
|
handler: async (args: string, ctx: PiCommandContext) => {
|
|
2794
|
+
// Print/JSON (`-p`) sessions cannot drive workflow pickers, the graph
|
|
2795
|
+
// overlay, or human-in-the-loop prompts. Refuse before parsing so the
|
|
2796
|
+
// command surface matches the disabled `workflow` tool.
|
|
2797
|
+
if (ctx.hasUI === false) {
|
|
2798
|
+
ctx.ui.notify(WORKFLOW_NON_INTERACTIVE_MESSAGE, "warning");
|
|
2799
|
+
return;
|
|
2800
|
+
}
|
|
2660
2801
|
const print = (msg: string): void => ctx.ui.notify(msg, "info");
|
|
2661
2802
|
// Quote-aware split so `prompt="map the codebase"` stays a single
|
|
2662
2803
|
// token. Plain `.split(/\s+/)` would mangle quoted multi-word values
|
|
@@ -2702,9 +2843,8 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2702
2843
|
|
|
2703
2844
|
// -----------------------------------------------------------------------
|
|
2704
2845
|
// status — band-header rich list, or per-run detail when an id is
|
|
2705
|
-
// supplied. `/workflow status` lists
|
|
2706
|
-
//
|
|
2707
|
-
// drills into a single run via the inspectRun detail block.
|
|
2846
|
+
// supplied. `/workflow status` lists all retained snapshots; `/workflow
|
|
2847
|
+
// status <id>` drills into a single run via the inspectRun detail block.
|
|
2708
2848
|
// -----------------------------------------------------------------------
|
|
2709
2849
|
if (subcommand === "status") {
|
|
2710
2850
|
const target = parts[1];
|
|
@@ -2730,13 +2870,12 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2730
2870
|
emitChatSurface(pi, { kind: "detail", detail: inspected.detail });
|
|
2731
2871
|
return;
|
|
2732
2872
|
}
|
|
2733
|
-
//
|
|
2734
|
-
//
|
|
2735
|
-
const includeAll = parts.includes("--all");
|
|
2873
|
+
// Status lists all retained snapshots by default; --all remains
|
|
2874
|
+
// accepted as a compatibility no-op.
|
|
2736
2875
|
const rows = selectRunsForPicker(
|
|
2737
2876
|
store.runs(),
|
|
2738
2877
|
"",
|
|
2739
|
-
|
|
2878
|
+
true,
|
|
2740
2879
|
Date.now(),
|
|
2741
2880
|
);
|
|
2742
2881
|
emitChatSurface(pi, { kind: "status", runs: rows.map((r) => r.run) });
|
|
@@ -2782,7 +2921,7 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2782
2921
|
}
|
|
2783
2922
|
|
|
2784
2923
|
// -----------------------------------------------------------------------
|
|
2785
|
-
// kill —
|
|
2924
|
+
// kill — abort in-flight work, mark killed, and retain for inspection.
|
|
2786
2925
|
// -----------------------------------------------------------------------
|
|
2787
2926
|
if (subcommand === "kill") {
|
|
2788
2927
|
const killArgs = parts.slice(1);
|
|
@@ -3061,7 +3200,7 @@ function factory(pi: ExtensionAPI): void {
|
|
|
3061
3200
|
{
|
|
3062
3201
|
value: "status ",
|
|
3063
3202
|
label: "status",
|
|
3064
|
-
description: "List
|
|
3203
|
+
description: "List current-session active and retained terminal runs",
|
|
3065
3204
|
},
|
|
3066
3205
|
{
|
|
3067
3206
|
value: "interrupt ",
|
|
@@ -3071,7 +3210,7 @@ function factory(pi: ExtensionAPI): void {
|
|
|
3071
3210
|
{
|
|
3072
3211
|
value: "kill ",
|
|
3073
3212
|
label: "kill",
|
|
3074
|
-
description: "Kill and
|
|
3213
|
+
description: "Kill and retain a run for inspection",
|
|
3075
3214
|
},
|
|
3076
3215
|
{
|
|
3077
3216
|
value: "pause ",
|
|
@@ -3109,14 +3248,7 @@ function factory(pi: ExtensionAPI): void {
|
|
|
3109
3248
|
}
|
|
3110
3249
|
|
|
3111
3250
|
if (subcommand === "status") {
|
|
3112
|
-
return completeToken(partial,
|
|
3113
|
-
{
|
|
3114
|
-
value: "--all ",
|
|
3115
|
-
label: "--all",
|
|
3116
|
-
description: "Include recently ended runs",
|
|
3117
|
-
},
|
|
3118
|
-
...runIdItems(),
|
|
3119
|
-
]);
|
|
3251
|
+
return completeToken(partial, runIdItems());
|
|
3120
3252
|
}
|
|
3121
3253
|
|
|
3122
3254
|
if (subcommand === "connect") {
|
|
@@ -3132,7 +3264,7 @@ function factory(pi: ExtensionAPI): void {
|
|
|
3132
3264
|
}
|
|
3133
3265
|
|
|
3134
3266
|
if (subcommand === "interrupt" || subcommand === "kill") {
|
|
3135
|
-
const verb = subcommand === "kill" ? "Kill and
|
|
3267
|
+
const verb = subcommand === "kill" ? "Kill and retain" : "Interrupt";
|
|
3136
3268
|
return completeToken(partial, [
|
|
3137
3269
|
{
|
|
3138
3270
|
value: "--all ",
|
|
@@ -3256,18 +3388,60 @@ function factory(pi: ExtensionAPI): void {
|
|
|
3256
3388
|
// 4. Persistence: session_start restore + session_before_compact hook (§5.6, Phase D)
|
|
3257
3389
|
// -------------------------------------------------------------------------
|
|
3258
3390
|
if (typeof pi.on === "function") {
|
|
3391
|
+
pi.on("session_before_switch", async (event, ctx) => {
|
|
3392
|
+
const reason = typeof event === "object" && event !== null && "reason" in event
|
|
3393
|
+
? (event as { readonly reason?: string }).reason
|
|
3394
|
+
: undefined;
|
|
3395
|
+
if (reason !== "new" && reason !== "resume") return undefined;
|
|
3396
|
+
|
|
3397
|
+
// "In-flight" intentionally includes paused runs: session_start kills any run
|
|
3398
|
+
// without endedAt, so warn for the same set that would be stopped by the switch.
|
|
3399
|
+
const inFlightWorkflowCount = inFlightRunCount();
|
|
3400
|
+
if (inFlightWorkflowCount === 0) return undefined;
|
|
3401
|
+
|
|
3402
|
+
const confirmSessionSwitch = ctx?.ui?.confirm;
|
|
3403
|
+
// Headless/non-interactive callers intentionally fail open so automation cannot wedge.
|
|
3404
|
+
if (typeof confirmSessionSwitch !== "function") return undefined;
|
|
3405
|
+
|
|
3406
|
+
const workflowNoun = inFlightWorkflowCount === 1 ? "workflow" : "workflows";
|
|
3407
|
+
const actionLabel = reason === "new" ? "Start a new session" : "Resume another session";
|
|
3408
|
+
const messageLabel = reason === "new" ? "Starting a new session" : "Resuming another session";
|
|
3409
|
+
const promptTitle = `${actionLabel} and stop ${inFlightWorkflowCount} in-flight ${workflowNoun}?`;
|
|
3410
|
+
const promptMessage =
|
|
3411
|
+
`${messageLabel} will stop/kill ${inFlightWorkflowCount} in-flight ${workflowNoun} and clear workflow history tied to the current session.`;
|
|
3412
|
+
let shouldSwitchSession: boolean | undefined;
|
|
3413
|
+
try {
|
|
3414
|
+
shouldSwitchSession = await confirmSessionSwitch(promptTitle, promptMessage);
|
|
3415
|
+
} catch {
|
|
3416
|
+
// Keep headless/failed UI behavior fail-open so session automation cannot wedge.
|
|
3417
|
+
return undefined;
|
|
3418
|
+
}
|
|
3419
|
+
if (shouldSwitchSession) return undefined;
|
|
3420
|
+
|
|
3421
|
+
const cancelledLabel = reason === "new" ? "New session" : "Resume";
|
|
3422
|
+
ctx?.ui?.notify?.(`${cancelledLabel} cancelled; in-flight workflows were left unchanged.`, "info");
|
|
3423
|
+
return { cancel: true };
|
|
3424
|
+
});
|
|
3425
|
+
|
|
3259
3426
|
pi.on("session_start", async (_event, ctx) => {
|
|
3427
|
+
// Non-interactive (`-p` / `--mode json`) sessions cannot drive workflow
|
|
3428
|
+
// prompts or the graph overlay; drop the `workflow` tool from the model's
|
|
3429
|
+
// tool set before the first turn. The host awaits this handler during
|
|
3430
|
+
// bindExtensions, ahead of the initial prompt.
|
|
3431
|
+
deAdvertiseWorkflowToolWhenHeadless(pi, ctx?.hasUI);
|
|
3432
|
+
|
|
3260
3433
|
// Workflow lifecycle is scoped to the originating chat session.
|
|
3261
|
-
// A new session inherits a clean store; any leftover runs from a
|
|
3434
|
+
// A new session inherits a clean store; any leftover live runs from a
|
|
3262
3435
|
// previous session in the same pi process are killed (subprocess
|
|
3263
|
-
// aborted)
|
|
3264
|
-
// *this* session's persisted runs from disk.
|
|
3436
|
+
// aborted), then all stale snapshots are cleared. `restoreOnSessionStart`
|
|
3437
|
+
// below loads *this* session's persisted runs from disk.
|
|
3265
3438
|
killAllRuns({
|
|
3266
3439
|
store,
|
|
3267
3440
|
cancellation: cancellationRegistry,
|
|
3268
3441
|
persistence: persistenceRef.current,
|
|
3269
3442
|
});
|
|
3270
3443
|
store.clear();
|
|
3444
|
+
resetWorkflowLifecycleNotificationState(lifecycleNotificationState);
|
|
3271
3445
|
stageControlRegistry.clear();
|
|
3272
3446
|
|
|
3273
3447
|
// pi-intercom session naming lives here so we don't trip the
|
|
@@ -3278,6 +3452,8 @@ function factory(pi: ExtensionAPI): void {
|
|
|
3278
3452
|
// Ensure config+discovery are ready before restoring in-flight runs —
|
|
3279
3453
|
// tunables must be resolved first.
|
|
3280
3454
|
await discoveryPromise;
|
|
3455
|
+
lifecycleNotificationsActive = true;
|
|
3456
|
+
reinstallLifecycleNotifications();
|
|
3281
3457
|
if (ctx?.ui) {
|
|
3282
3458
|
const diagnostics = formatStartupDiagnostics(configLoadRef.current, discoveryRef.current);
|
|
3283
3459
|
if (diagnostics !== null) {
|
|
@@ -3290,13 +3466,25 @@ function factory(pi: ExtensionAPI): void {
|
|
|
3290
3466
|
const sessionManager = ctx?.sessionManager ?? pi.sessionManager;
|
|
3291
3467
|
if (sessionManager) {
|
|
3292
3468
|
const cfg = configLoadRef.current?.config;
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
{
|
|
3296
|
-
|
|
3297
|
-
|
|
3469
|
+
withWorkflowLifecycleNotificationsSuppressed(
|
|
3470
|
+
lifecycleNotificationState,
|
|
3471
|
+
() => {
|
|
3472
|
+
restoreOnSessionStart(
|
|
3473
|
+
sessionManager,
|
|
3474
|
+
{
|
|
3475
|
+
resumeInFlight: cfg?.resumeInFlight ?? "ask",
|
|
3476
|
+
persistRuns: cfg?.persistRuns ?? true,
|
|
3477
|
+
},
|
|
3478
|
+
store,
|
|
3479
|
+
);
|
|
3480
|
+
// The suppressed subscriber observes restore replay and marks matching
|
|
3481
|
+
// notices delivered. Seed explicitly as a defensive backstop for
|
|
3482
|
+
// runtimes without a lifecycle-notification subscriber installed.
|
|
3483
|
+
seedWorkflowLifecycleNotificationState(
|
|
3484
|
+
lifecycleNotificationState,
|
|
3485
|
+
store.snapshot(),
|
|
3486
|
+
);
|
|
3298
3487
|
},
|
|
3299
|
-
store,
|
|
3300
3488
|
);
|
|
3301
3489
|
}
|
|
3302
3490
|
});
|
|
@@ -3320,6 +3508,9 @@ function factory(pi: ExtensionAPI): void {
|
|
|
3320
3508
|
}
|
|
3321
3509
|
storeWidgetUnsubscribe?.();
|
|
3322
3510
|
storeWidgetUnsubscribe = null;
|
|
3511
|
+
lifecycleNotificationsActive = false;
|
|
3512
|
+
lifecycleNotificationsUnsubscribe?.();
|
|
3513
|
+
lifecycleNotificationsUnsubscribe = null;
|
|
3323
3514
|
});
|
|
3324
3515
|
}
|
|
3325
3516
|
|