@agimon-ai/doompi-workflow 0.0.1-alpha.101 → 0.0.1-alpha.104

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/README.md CHANGED
@@ -64,9 +64,10 @@ Core tools are:
64
64
  - `list_workflows`: discover workflows.
65
65
  - `launch_workflow`: register and start an asynchronous run.
66
66
  - `workflow_run`: inspect or control a run through supported actions.
67
+ - `workflow_tools` (server/web sessions): read failed-run recovery evidence or resume the existing run.
67
68
 
68
- Launch returns after the run is registered, not after all jobs complete. Use status and follow
69
- controls for progress, and wait for a terminal notification.
69
+ Launch returns after the run is registered, not after all jobs complete. Use status for progress
70
+ and wait for a terminal notification. The TUI also supports follow controls.
70
71
 
71
72
  In the TUI, `SPC w l` lists the repository's workflows and launches the one under the cursor with
72
73
  `r`, `SPC w r` inspects this session's runs, `SPC w c` opens recovery, and `SPC w e`
@@ -96,10 +97,31 @@ adopts the eligible failure, validates the evidence, and transfers ownership to
96
97
  Live controls remain scoped to the owning session. Recovery does not launch a second copy beside a
97
98
  still-running job and does not guarantee that parent-session work survives shutdown.
98
99
 
99
- The package publishes `workflow-recovery` for active Workflow mode. While parent Help mode is
100
- active, it also contributes `doompi-author-workflow` for writing workflow definitions and
101
- `doompi-use-workflow` for launching, monitoring, and recovering runs. Deactivating Help hides both
102
- descriptors; cached files may remain.
100
+ The package publishes `workflow-recovery`, `doompi-author-workflow` for writing workflow
101
+ definitions, and `doompi-use-workflow` for launching, monitoring, and recovering runs while Workflow
102
+ mode is active. Deactivating Workflow mode hides them; cached files may remain.
103
+
104
+ ## Workflow sessions
105
+
106
+ In a server (web cockpit) session, a launch does not run in the session that asked for it. The
107
+ session creates a child **workflow session** (provenance `workflow-session`), nested under it in the
108
+ rail, and hands it the launch. The workflow session owns the run (`PI_SESSION_ID`), runs its step
109
+ sessions, and has Workflow mode on, so its agent can troubleshoot and recover the run. The run's
110
+ environment also carries `DOOMPI_WORKFLOW_LAUNCHER_SESSION_ID`, naming the launching session, which
111
+ lists the run in its Activity group, is told when it ends, and may read its status.
112
+
113
+ - The workflow session's rail row shows the run (`workflow · build › test · 4m`, or a failure or
114
+ pause marked like a pending question) through the host's per-session activity line.
115
+ - A failure or finish is posted into the workflow session's conversation as a notice, without
116
+ starting an agent turn. The owner agent also gets a live brief of its runs on every turn.
117
+ - After a success, a workflow session nobody interacted with asks its launcher to release it, and it
118
+ becomes `stopped · open to wake`. A session whose run failed or paused, or whose agent you
119
+ prompted, stays live until you remove it. Removing or restarting a live workflow session stops its
120
+ run.
121
+ - Recovery is refused from any session other than the owner while the owner is live.
122
+
123
+ Launches from inside a workflow session, and launches in hosts without a session service (the TUI
124
+ and CLI), run in place as before.
103
125
 
104
126
  Each Pi extension instance uses the runner's shared Doom lifecycle. Reload or shutdown stops retained
105
127
  callbacks, interrupts bounded inline work, removes UI and service registrations, and releases
@@ -125,10 +147,12 @@ Maintained by [Agimon](https://agimon.ai/about).
125
147
  ## Web cockpit plugin
126
148
 
127
149
  The `.web` routed files under `src/extensions/workspaces/sessions/(frontend)` are this package's
128
- DoomPi web cockpit plugin: the workflows panel the dock's workflows group opens in a temporary tab,
129
- its store, and its `workflow_runs` session channel, compiled into the cockpit bundle by
130
- `doompi-web`'s build. Each surface keeps its components in a colocated `_components` folder and its
131
- stores and helpers in `_lib`.
150
+ DoomPi web cockpit plugin: the `workflow` dock face beside Activity and Context (the owned run's
151
+ jobs, active steps and artifacts, each opening its own tab), the workflow catalog and launch
152
+ dialog in the overlay slot, the Activity group's run rows (a handed-off run opens its workflow
153
+ session), their stores, and the `workflow_runs` session channel, compiled into the cockpit bundle
154
+ by `doompi-web`'s build. Each surface keeps its components in a colocated `_components` folder and
155
+ its stores and helpers in `_lib`.
132
156
  The hub-side data source ships behind the `./web-hub` subpath and reads the workflow registry
133
157
  (run.json plus progress.ndjson) exactly as the engine writes it. Both halves are declared by the
134
158
  `doompiWeb` block in package.json.
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_workflowTerminal = require("../workflowTerminal-Bb4bLtLu.cjs");
5
+ const require_workflowTerminal = require("../workflowTerminal-DeJ9BBta.cjs");
6
6
  const require_workflowLaunchCommand = require("../workflowLaunchCommand-DJyDB96d.cjs");
7
7
  let _agimon_ai_doompi_core_piExtension = require("@agimon-ai/doompi-core/piExtension");
8
8
  let _agimon_ai_doompi_core_extensionFile = require("@agimon-ai/doompi-core/extensionFile");
@@ -23,76 +23,6 @@ let _agimon_ai_doompi_core_readiness = require("@agimon-ai/doompi-core/readiness
23
23
  let _agimon_ai_doompi_minor_mode = require("@agimon-ai/doompi-minor-mode");
24
24
  let _agimon_ai_doompi_telemetry = require("@agimon-ai/doompi-telemetry");
25
25
  let _earendil_works_pi_tui = require("@earendil-works/pi-tui");
26
- //#region src/schemas/workflowPi.ts
27
- const RUN_SELECTOR_FIELDS = {
28
- runKey: zod.z.string().min(1).describe("Run key of the workflow run."),
29
- workspace: zod.z.string().optional().describe("Workspace the run belongs to.")
30
- };
31
- const workflowRunInputSchema = zod.z.discriminatedUnion("action", [
32
- zod.z.object({
33
- action: zod.z.literal("status"),
34
- ...RUN_SELECTOR_FIELDS
35
- }).strict(),
36
- zod.z.object({
37
- action: zod.z.literal("tail"),
38
- ...RUN_SELECTOR_FIELDS
39
- }).strict(),
40
- zod.z.object({
41
- action: zod.z.literal("recovery-evidence"),
42
- ...RUN_SELECTOR_FIELDS
43
- }).strict(),
44
- zod.z.object({
45
- action: zod.z.literal("follow"),
46
- ...RUN_SELECTOR_FIELDS
47
- }).strict(),
48
- zod.z.object({
49
- action: zod.z.literal("open"),
50
- ...RUN_SELECTOR_FIELDS
51
- }).strict(),
52
- zod.z.object({
53
- action: zod.z.literal("pause"),
54
- ...RUN_SELECTOR_FIELDS,
55
- reason: zod.z.string().optional()
56
- }).strict(),
57
- zod.z.object({
58
- action: zod.z.literal("resume"),
59
- ...RUN_SELECTOR_FIELDS,
60
- reason: zod.z.string().optional()
61
- }).strict(),
62
- zod.z.object({
63
- action: zod.z.literal("stop"),
64
- ...RUN_SELECTOR_FIELDS,
65
- reason: zod.z.string().optional()
66
- }).strict(),
67
- zod.z.object({
68
- action: zod.z.literal("recover"),
69
- ...RUN_SELECTOR_FIELDS,
70
- dryRun: zod.z.boolean().optional(),
71
- job: zod.z.string().optional(),
72
- runner: zod.z.string().optional()
73
- }).strict()
74
- ]);
75
- const dispatchInputSchema = zod.z.object({
76
- description: zod.z.string().optional().describe("Description shown for the workflow input."),
77
- required: zod.z.boolean().optional().describe("Whether the workflow input must be provided."),
78
- default: zod.z.string().optional().describe("Default value for the workflow input."),
79
- type: zod.z.string().optional().describe("Declared workflow input type."),
80
- options: zod.z.array(zod.z.string()).optional().describe("Allowed values for the workflow input.")
81
- }).passthrough();
82
- const workflowDispatchInputsSchema = zod.z.object({ inputs: zod.z.record(zod.z.string(), dispatchInputSchema).optional().describe("Workflow dispatch input definitions.") }).passthrough();
83
- const workflowCatalogPageSchema = zod.z.object({
84
- directory: zod.z.string().min(1).describe("Directory searched for workflow definitions."),
85
- hasNextPage: zod.z.boolean().describe("Whether another catalog page exists."),
86
- page: zod.z.number().int().positive().describe("One-based catalog page number."),
87
- pageSize: zod.z.number().int().positive().describe("Catalog page size."),
88
- workflows: zod.z.array(zod.z.object({
89
- path: zod.z.string().min(1).describe("Workflow path relative to the catalog directory."),
90
- name: zod.z.string().describe("Workflow display name."),
91
- description: zod.z.string().describe("Workflow description."),
92
- tags: zod.z.array(zod.z.string()).describe("Workflow tags.")
93
- }).passthrough()).describe("Workflow definitions on this page.")
94
- }).passthrough();
95
- //#endregion
96
26
  //#region src/services/workflowTools/index.ts
97
27
  const PI_LAUNCH_FIELDS = {
98
28
  workflowPath: true,
@@ -175,11 +105,11 @@ function createWorkflowTools(dependencies, renderers = () => ({})) {
175
105
  "Use action recover only after loading the `workflow-recovery` skill and gathering its required evidence. Recovery may adopt a terminal failed run from an earlier Pi session; it never grants cross-session control of running work. Never edit issue.md or repair.json, and verify real process and registry progress before recovery.",
176
106
  "Never unlock or force-release an Agiflow claim from this tool."
177
107
  ],
178
- parameters: zod.z.toJSONSchema(workflowRunInputSchema),
108
+ parameters: zod.z.toJSONSchema(require_workflowTerminal.workflowRunInputSchema),
179
109
  renderShell: "self",
180
110
  ...renderers(require_workflowTerminal.WORKFLOW_RUN_TOOL_NAME),
181
111
  async execute(_toolCallId, rawParams, _signal, onUpdate, ctx) {
182
- const input = workflowRunInputSchema.parse(rawParams);
112
+ const input = require_workflowTerminal.workflowRunInputSchema.parse(rawParams);
183
113
  const sessionId = (0, _agimon_ai_doompi_core_childProcess.resolveRootSessionId)(ctx.sessionManager.getSessionId(), dependencies.environment);
184
114
  dependencies.observeSession?.(sessionId);
185
115
  const selector = {
@@ -887,7 +817,7 @@ function workflowTriggerBlock(workflow) {
887
817
  function dispatchInputs(workflow) {
888
818
  const dispatch = workflowTriggerBlock(workflow)[WORKFLOW_DISPATCH_TRIGGER];
889
819
  if (dispatch === null || dispatch === void 0) return {};
890
- const parsed = workflowDispatchInputsSchema.safeParse(dispatch);
820
+ const parsed = require_workflowTerminal.workflowDispatchInputsSchema.safeParse(dispatch);
891
821
  if (!parsed.success) throw new Error(`Invalid workflow_dispatch input definitions: ${parsed.error.message}`);
892
822
  return parsed.data.inputs ?? {};
893
823
  }
@@ -902,7 +832,7 @@ function parseWorkflowCatalogPage(result) {
902
832
  } catch (cause) {
903
833
  throw new Error("Workflow catalog returned invalid JSON.", { cause });
904
834
  }
905
- const parsed = workflowCatalogPageSchema.safeParse(value);
835
+ const parsed = require_workflowTerminal.workflowCatalogPageSchema.safeParse(value);
906
836
  if (!parsed.success) throw new Error(`Workflow catalog response was invalid: ${parsed.error.message}`);
907
837
  return {
908
838
  directory: parsed.data.directory,
@@ -3532,12 +3462,13 @@ function createWorkflowPiRuntime(pi, options) {
3532
3462
  if (!refreshIsCurrent()) return;
3533
3463
  const sessionId = (0, _agimon_ai_doompi_core_childProcess.resolveRootSessionId)(ctx.sessionManager.getSessionId(), options.environment);
3534
3464
  const rootProcess = isRootProcess(ctx, sessionId);
3535
- const sessionRecords = records.filter((record) => require_workflowTerminal.isSessionRun(record, sessionId));
3536
- const running = sessionRecords.filter((record) => record.stage === RUNNING_STATUS && !record.stale);
3537
- const failed = sessionRecords.filter((record) => record.stage === ERROR_STAGE);
3465
+ const ownRecords = records.filter((record) => require_workflowTerminal.isSessionRun(record, sessionId));
3466
+ const sessionRecords = records.filter((record) => require_workflowTerminal.isSessionRun(record, sessionId) || require_workflowTerminal.isLaunchedRun(record, sessionId));
3467
+ const running = ownRecords.filter((record) => record.stage === RUNNING_STATUS && !record.stale);
3468
+ const failed = ownRecords.filter((record) => record.stage === ERROR_STAGE);
3538
3469
  if (activeRevisionAtRead === activeRunRevision) replaceActiveRecords(running);
3539
3470
  activeTelemetry.recordEvent("doom_workflow.monitor_counts", {
3540
- "workflow.run_count": sessionRecords.length,
3471
+ "workflow.run_count": ownRecords.length,
3541
3472
  "workflow.running_count": running.length,
3542
3473
  "workflow.failed_count": failed.length,
3543
3474
  mode: workflowMode ? "on" : "off"
@@ -3552,11 +3483,12 @@ function createWorkflowPiRuntime(pi, options) {
3552
3483
  const previous = previousStages.get(identity);
3553
3484
  const startedAt = Date.parse(record.startedAt);
3554
3485
  const startedDuringSession = previous === void 0 && (sessionStartedAt === 0 || !Number.isFinite(startedAt) || startedAt >= sessionStartedAt);
3555
- if (lifecycleNarrationReady) narrateWorkflowTransition(narrationSink, record, previous === RUNNING_STATUS || previous === COMPLETED_STATUS || previous === ERROR_STAGE ? previous : void 0, startedDuringSession);
3486
+ if (lifecycleNarrationReady && require_workflowTerminal.isSessionRun(record, sessionId)) narrateWorkflowTransition(narrationSink, record, previous === RUNNING_STATUS || previous === COMPLETED_STATUS || previous === ERROR_STAGE ? previous : void 0, startedDuringSession);
3556
3487
  if (!notifyChanges || !rootProcess || record.stage === RUNNING_STATUS) continue;
3557
3488
  if (!deliveredTerminalRuns.has(identity) && (previous === RUNNING_STATUS || startedDuringSession)) pendingTerminalRuns.set(identity, record);
3558
3489
  }
3559
3490
  const retainedWork = new Map(mine.map((record) => [runIdentity(record), record]));
3491
+ for (const record of sessionRecords) if (require_workflowTerminal.isLaunchedRun(record, sessionId) && record.stage === RUNNING_STATUS && !record.stale) retainedWork.set(runIdentity(record), record);
3560
3492
  for (const [identity, record] of pendingTerminalRuns) retainedWork.set(identity, record);
3561
3493
  if (!refreshIsCurrent()) return;
3562
3494
  runProvider?.update([...retainedWork.values()].map((record) => ({
@@ -3575,28 +3507,39 @@ function createWorkflowPiRuntime(pi, options) {
3575
3507
  if (rootProcess && pendingTerminalRuns.size > 0) try {
3576
3508
  const terminalRecords = [...pendingTerminalRuns.values()];
3577
3509
  const summaries = await Promise.all(terminalRecords.map(async (record) => {
3578
- if (record.stage === COMPLETED_STATUS) return require_workflowTerminal.finishedRunSummary(record, []);
3510
+ if (record.stage === COMPLETED_STATUS) {
3511
+ const summary = require_workflowTerminal.finishedRunSummary(record, []);
3512
+ return require_workflowTerminal.isLaunchedRun(record, sessionId) ? `${summary}\n${require_workflowTerminal.launchedRunHint(record)}` : summary;
3513
+ }
3579
3514
  const identity = runIdentity(record);
3580
3515
  const events = runEvents.get(identity) ?? progressEventsForRun(record, await (0, _agimon_ai_workflow_mcp.readWorkflowProgress)(registry.runDirectoryFor(record)));
3581
- return require_workflowTerminal.finishedRunSummary(record, (0, _agimon_ai_workflow_mcp.summarizeWorkflowProgress)(events));
3516
+ const summary = require_workflowTerminal.finishedRunSummary(record, (0, _agimon_ai_workflow_mcp.summarizeWorkflowProgress)(events));
3517
+ return require_workflowTerminal.isLaunchedRun(record, sessionId) ? `${summary}\n${require_workflowTerminal.launchedRunHint(record)}` : summary;
3582
3518
  }));
3583
3519
  if (!refreshIsCurrent()) return;
3584
- pi.sendMessage({
3520
+ const message = (indexes) => ({
3585
3521
  customType: WORKFLOW_FINISHED_MESSAGE$1,
3586
- content: summaries.join("\n\n"),
3522
+ content: indexes.map((index) => summaries[index]).join("\n\n"),
3587
3523
  display: true,
3588
3524
  details: {
3589
- runIds: terminalRecords.map((record) => record.runId ?? runIdentity(record)),
3590
- runs: terminalRecords.map((record) => ({
3591
- runKey: record.runKey,
3592
- workspace: record.workspace,
3593
- stage: record.stage,
3594
- ...record.workflowId ? { workflowId: record.workflowId } : {},
3595
- ...record.failedJob ? { failedJob: record.failedJob } : {},
3596
- ...record.errorMessage ? { error: record.errorMessage } : {}
3597
- }))
3525
+ runIds: indexes.map((index) => terminalRecords[index].runId ?? runIdentity(terminalRecords[index])),
3526
+ runs: indexes.map((index) => {
3527
+ const record = terminalRecords[index];
3528
+ return {
3529
+ runKey: record.runKey,
3530
+ workspace: record.workspace,
3531
+ stage: record.stage,
3532
+ ...record.workflowId ? { workflowId: record.workflowId } : {},
3533
+ ...record.failedJob ? { failedJob: record.failedJob } : {},
3534
+ ...record.errorMessage ? { error: record.errorMessage } : {}
3535
+ };
3536
+ })
3598
3537
  }
3599
- }, {
3538
+ });
3539
+ const quiet = terminalRecords.flatMap((record, index) => require_workflowTerminal.isOwnedDelegatedRun(record, sessionId) ? [index] : []);
3540
+ const loud = terminalRecords.flatMap((record, index) => require_workflowTerminal.isOwnedDelegatedRun(record, sessionId) ? [] : [index]);
3541
+ if (quiet.length > 0) pi.sendMessage(message(quiet), { triggerTurn: false });
3542
+ if (loud.length > 0) pi.sendMessage(message(loud), {
3600
3543
  triggerTurn: true,
3601
3544
  deliverAs: "steer"
3602
3545
  });