stageflow 0.1.0 → 0.3.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 (132) hide show
  1. package/README.md +190 -13
  2. package/dist/agent/activity.d.ts +10 -1
  3. package/dist/agent/activity.js +37 -2
  4. package/dist/agent/activityObserver.d.ts +5 -1
  5. package/dist/agent/activityObserver.js +174 -11
  6. package/dist/agent/fakeAgent.js +4 -0
  7. package/dist/agent/piAdapter.d.ts +16 -0
  8. package/dist/agent/piAdapter.js +88 -21
  9. package/dist/agent/port.d.ts +2 -0
  10. package/dist/agent/providerAuth.js +12 -7
  11. package/dist/catalog/displayCatalogPath.d.ts +12 -0
  12. package/dist/catalog/displayCatalogPath.js +28 -0
  13. package/dist/cli/ciIdentity.d.ts +14 -0
  14. package/dist/cli/ciIdentity.js +52 -0
  15. package/dist/cli/initCommand.d.ts +9 -0
  16. package/dist/cli/initCommand.js +71 -0
  17. package/dist/cli/initTemplates.d.ts +3 -0
  18. package/dist/cli/initTemplates.js +19 -0
  19. package/dist/cli/operatorCatalog.d.ts +10 -0
  20. package/dist/cli/operatorCatalog.js +16 -0
  21. package/dist/cli/runCommand.d.ts +26 -0
  22. package/dist/cli/runCommand.js +209 -0
  23. package/dist/cli/runOutput.d.ts +19 -0
  24. package/dist/cli/runOutput.js +125 -0
  25. package/dist/cli/validateCommand.d.ts +1 -1
  26. package/dist/cli/validateCommand.js +20 -3
  27. package/dist/cli.d.ts +12 -1
  28. package/dist/cli.js +82 -66
  29. package/dist/config/browseCatalog.d.ts +45 -0
  30. package/dist/config/browseCatalog.js +170 -0
  31. package/dist/config/createPipeline.d.ts +12 -3
  32. package/dist/config/createPipeline.js +292 -100
  33. package/dist/config/createStage.d.ts +10 -4
  34. package/dist/config/createStage.js +44 -14
  35. package/dist/config/listConfig.d.ts +14 -19
  36. package/dist/config/listConfig.js +26 -191
  37. package/dist/config/listModelsFromManifest.d.ts +2 -0
  38. package/dist/config/listModelsFromManifest.js +4 -0
  39. package/dist/config/loadOutcome.d.ts +4 -0
  40. package/dist/config/loadPipeline.d.ts +1 -15
  41. package/dist/config/loadPipeline.js +84 -142
  42. package/dist/config/loadStage.d.ts +4 -0
  43. package/dist/config/loadStage.js +60 -32
  44. package/dist/config/loadStageflowManifest.d.ts +5 -0
  45. package/dist/config/loadStageflowManifest.js +157 -0
  46. package/dist/config/mergePipelineIncludes.d.ts +9 -0
  47. package/dist/config/mergePipelineIncludes.js +141 -0
  48. package/dist/config/normalizePipelineStageEntry.d.ts +16 -0
  49. package/dist/config/normalizePipelineStageEntry.js +186 -0
  50. package/dist/config/pipelineStageKeys.d.ts +4 -0
  51. package/dist/config/pipelineStageKeys.js +14 -0
  52. package/dist/config/resolveCatalogContext.d.ts +13 -0
  53. package/dist/config/resolveCatalogContext.js +13 -0
  54. package/dist/config/resolveForkEmitContext.d.ts +3 -0
  55. package/dist/config/resolveForkEmitContext.js +12 -0
  56. package/dist/config/resolvePipelineDag.d.ts +5 -1
  57. package/dist/config/resolvePipelineDag.js +54 -36
  58. package/dist/config/scanCatalogPaths.d.ts +4 -0
  59. package/dist/config/scanCatalogPaths.js +83 -0
  60. package/dist/config/validateCatalog.d.ts +22 -4
  61. package/dist/config/validateCatalog.js +169 -132
  62. package/dist/envelope/check.js +7 -0
  63. package/dist/envelope/forkChoice.d.ts +3 -0
  64. package/dist/envelope/forkChoice.js +47 -0
  65. package/dist/index.d.ts +4 -0
  66. package/dist/index.js +4 -0
  67. package/dist/mcp/projectRun.d.ts +3 -0
  68. package/dist/mcp/projectRun.js +7 -0
  69. package/dist/mcp/server.js +1 -1
  70. package/dist/mcp/tools.js +28 -11
  71. package/dist/project/findProjectRoot.d.ts +2 -0
  72. package/dist/project/findProjectRoot.js +51 -0
  73. package/dist/project/globalHome.d.ts +2 -0
  74. package/dist/project/globalHome.js +19 -0
  75. package/dist/project/resolveProjectContext.d.ts +9 -0
  76. package/dist/project/resolveProjectContext.js +38 -0
  77. package/dist/project/resolveStageflowContext.d.ts +11 -0
  78. package/dist/project/resolveStageflowContext.js +66 -0
  79. package/dist/runstore/normalizeCatalogPath.d.ts +1 -0
  80. package/dist/runstore/normalizeCatalogPath.js +4 -0
  81. package/dist/runstore/port.d.ts +15 -0
  82. package/dist/runstore/runProjection.js +11 -1
  83. package/dist/runstore/sqlite/SqliteRunStore.js +46 -4
  84. package/dist/runstore/sqlite/schema.d.ts +1 -1
  85. package/dist/runstore/sqlite/schema.js +7 -1
  86. package/dist/runtime/credentialBinding.d.ts +8 -6
  87. package/dist/runtime/credentialBinding.js +18 -24
  88. package/dist/runtime/pipelineRunner.d.ts +11 -0
  89. package/dist/runtime/pipelineRunner.js +25 -1
  90. package/dist/runtime/pipelineScheduler.d.ts +3 -0
  91. package/dist/runtime/pipelineScheduler.js +70 -14
  92. package/dist/runtime/reloadRunCatalog.d.ts +5 -0
  93. package/dist/runtime/reloadRunCatalog.js +44 -0
  94. package/dist/runtime/resumeReconstruct.d.ts +1 -0
  95. package/dist/runtime/resumeReconstruct.js +27 -10
  96. package/dist/runtime/runManager.d.ts +8 -0
  97. package/dist/runtime/runManager.js +49 -18
  98. package/dist/runtime/settingsFile.d.ts +12 -0
  99. package/dist/runtime/settingsFile.js +87 -6
  100. package/dist/runtime/stageAttemptBootstrap.js +3 -0
  101. package/dist/runtime/stageProcessLauncher.d.ts +1 -0
  102. package/dist/runtime/stageProcessLauncher.js +6 -0
  103. package/dist/runtime/stageRecovery.js +3 -1
  104. package/dist/runtime/stageRunner.d.ts +2 -0
  105. package/dist/runtime/stageRunner.js +9 -2
  106. package/dist/runtime/stageWorker.js +4 -6
  107. package/dist/runtime/stageWorkerProtocol.d.ts +1 -0
  108. package/dist/server/http.js +33 -9
  109. package/dist/tools/emitStageEnvelope.d.ts +3 -1
  110. package/dist/tools/emitStageEnvelope.js +11 -1
  111. package/dist/types/envelope.d.ts +1 -0
  112. package/dist/types/forkChoice.d.ts +8 -0
  113. package/dist/types/forkChoice.js +1 -0
  114. package/dist/types/pipeline.d.ts +51 -2
  115. package/dist/types/stageflowManifest.d.ts +23 -0
  116. package/dist/types/stageflowManifest.js +1 -0
  117. package/dist/ui/assets/index-CFSzDZje.js +118 -0
  118. package/dist/ui/assets/{index-DCsDopak.css → index-DefBlEvN.css} +1 -1
  119. package/dist/ui/index.html +3 -2
  120. package/dist/ui/stageflow-icon.svg +12 -0
  121. package/package.json +7 -5
  122. package/dist/agent/cursorExtension.d.ts +0 -12
  123. package/dist/agent/cursorExtension.js +0 -88
  124. package/dist/runstore/catalog.d.ts +0 -8
  125. package/dist/runstore/catalog.js +0 -16
  126. package/dist/runstore/disk/DiskRunStore.d.ts +0 -30
  127. package/dist/runstore/disk/DiskRunStore.js +0 -238
  128. package/dist/runstore/layout.d.ts +0 -22
  129. package/dist/runstore/layout.js +0 -58
  130. package/dist/runtime/hitlSeams.d.ts +0 -38
  131. package/dist/runtime/hitlSeams.js +0 -2
  132. package/dist/ui/assets/index-Cry0Tpfx.js +0 -118
package/README.md CHANGED
@@ -1,43 +1,216 @@
1
- # Stageflow
1
+ # <img src="ui/public/stageflow-icon.svg" alt="" width="44" height="44" valign="middle"> Stageflow
2
2
 
3
- CLI pipeline runtime for configurable SDLC stages on [Pi](https://github.com/badlogic/pi-mono), plus a local operator console.
3
+ CLI pipeline runtime for **configurable stages** on [Pi](https://github.com/badlogic/pi-mono), with a local operator console.
4
4
 
5
- Author YAML pipelines, stages, and tasks in a project directory. Stageflow runs each stage in a fresh Pi agent session, writes a structured handoff envelope, and keeps run state under `.stageflow/`. Bins: **`sf`** and **`stageflow`**.
5
+ [![npm version](https://img.shields.io/npm/v/stageflow)](https://www.npmjs.com/package/stageflow)
6
+ [![npm downloads](https://img.shields.io/npm/dm/stageflow)](https://www.npmjs.com/package/stageflow)
7
+ ![Node >=20](https://img.shields.io/badge/node-%3E%3D20-339933?logo=node.js&logoColor=white)
8
+ [![MIT license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
9
+ [![GitHub issues](https://img.shields.io/github/issues/tejasghutukade/stageflow)](https://github.com/tejasghutukade/stageflow/issues)
10
+ [![CI](https://img.shields.io/github/actions/workflow/status/tejasghutukade/stageflow/ci.yml?branch=main)](https://github.com/tejasghutukade/stageflow/actions/workflows/ci.yml)
11
+
12
+ Author pipeline-owned YAML in your project. Stageflow runs each stage in a fresh Pi agent session, writes a structured handoff envelope, and keeps run state under `.stageflow/`. Bins: **`sf`** and **`stageflow`**.
13
+
14
+ ## Why Stageflow?
15
+
16
+ Multi-step agent work breaks down when every handoff is ad hoc — a shell script here, a chat transcript there, no shared contract between steps. You end up re-explaining context, losing artifacts, and unable to run the same flow locally and in CI.
17
+
18
+ Stageflow treats **stages as the unit of composition**. You author pipeline-owned YAML — `*.pipeline.yaml`, `*.task.yaml`, optional `stageflow.yaml` manifest — and define whatever workflow fits your domain: release automation, research pipelines, content review, SDLC, ops runbooks, or something entirely custom. Each stage runs in a **fresh Pi session**, emits a typed **envelope** for the next stage, and can pause on **human-in-the-loop (HITL)** gates when you need an operator in the loop.
19
+
20
+ The same pipeline runs three ways without rewriting anything:
21
+
22
+ - **Locally** — `sf ui` for triage, provider setup, and gate replies
23
+ - **Headless / CI** — `sf validate` and `sf run --json` with predictable exit codes
24
+ - **Via MCP** — Streamable HTTP tools when the console is running
25
+
26
+ **Stageflow is not an SDLC tool.** Software delivery is a popular pattern in fixtures and dogfood flows, but stages are user-authored and domain-agnostic. If you can express a multi-step workflow in YAML, Stageflow can run it on Pi.
27
+
28
+ ## Features
29
+
30
+ - **Pipeline-owned YAML** — `*.pipeline.yaml` with inline stages or `uses:` refs; separate `*.task.yaml` files; optional repo-root `stageflow.yaml` manifest
31
+ - **Path-based CLI** — `--pipeline` and `--task` take filesystem paths (no bare-id lookup)
32
+ - **Pi-native** — runs on `@earendil-works/pi-coding-agent`; reuse an existing Pi login (`pi_home`) or store credentials in `~/.stageflow/agent/auth.json` (`sf_owned`)
33
+ - **Envelope handoffs** — typed stage payloads and artifacts via `write_stage_artifact` / `emit_stage_envelope`
34
+ - **HITL gates** — operator questions in the console; CI exits `2` when a run is waiting
35
+ - **Operator console** — triage runs, connect providers, answer gates, inspect transcripts at `http://127.0.0.1:3847`
36
+ - **MCP endpoint** — Streamable HTTP at `/mcp` when `sf ui` is running
37
+ - **CI / headless** — `sf validate --strict --json`, `sf run --json` with exit codes `0` / `1` / `2`
38
+ - **Parallel stages** — pipeline DAG with fan-out and join (see [YAML catalog](docs/yaml-catalog.md))
39
+ - **SQLite run store** — `<git-root>/.stageflow/` state plus per-run workspaces under `.stageflow/runs/`
40
+
41
+ ## Installation
6
42
 
7
43
  Requires **Node.js ≥ 20**.
8
44
 
9
- ## Install
45
+ **Quick install (macOS / Linux):**
46
+
47
+ ```bash
48
+ curl -fsSL https://raw.githubusercontent.com/tejasghutukade/stageflow/main/install.sh | bash
49
+ ```
50
+
51
+ **npm:**
10
52
 
11
53
  ```bash
12
54
  npm i -g stageflow
13
55
  # or
14
56
  npx stageflow
57
+ # or, from a packed tarball
58
+ npm i -g ./stageflow-*.tgz
15
59
  ```
16
60
 
17
61
  `better-sqlite3` ships prebuilds for common platforms. `--ignore-scripts` is fine when a prebuild exists. Benign `node-gyp` warnings during install can be ignored if `require("better-sqlite3")` works.
18
62
 
19
63
  ## Quick start
20
64
 
21
- In a project directory, author:
65
+ In a project directory (preferably a git repo):
66
+
67
+ ```bash
68
+ sf init
69
+ ```
70
+
71
+ This scaffolds `stageflow.yaml`, `pipelines/hello.pipeline.yaml` (inline stage), and `tasks/hello.task.yaml`.
22
72
 
23
- - `pipelines/`ordered stage lists
24
- - `stages/` — stage definitions (instructions, model, optional human gates)
25
- - `tasks/` — work items (goal, pipeline, optional checkout)
73
+ Run (after connecting a provider see below):
26
74
 
27
75
  ```bash
28
76
  sf ui # operator console at http://127.0.0.1:3847
29
- sf run --task tasks/foo.yaml --pipeline <pipeline-id>
77
+ sf run --pipeline pipelines/hello.pipeline.yaml --task tasks/hello.task.yaml
78
+ ```
79
+
80
+ Expanded walkthrough: [docs/quickstart.md](docs/quickstart.md)
81
+
82
+ ## Connect a model provider
83
+
84
+ Each stage sets a **`model`** id in YAML (e.g. `anthropic/claude-sonnet-4-5`). The matching provider must be authenticated before runs succeed — `sf validate` does not check auth.
85
+
86
+ **Operator console** (easiest for local setup):
87
+
88
+ ```bash
89
+ sf ui
90
+ ```
91
+
92
+ Open **Settings → Providers** (or **Connect** from the rail when nothing is configured) and sign in with API key or OAuth.
93
+
94
+ **CLI** (works headless and in CI):
95
+
96
+ ```bash
97
+ sf providers list
98
+ sf providers login anthropic --type api_key
99
+ sf providers login anthropic --type api_key --api-key-env ANTHROPIC_API_KEY
100
+ ```
101
+
102
+ Use `sf providers list` to see provider ids and supported auth types (`api_key`, `oauth`).
103
+
104
+ **Credential storage:** reuse Pi's shared auth file (`pi_home`) or keep credentials in Stageflow's global store (`sf_owned`):
105
+
106
+ ```bash
107
+ sf providers detect
108
+ sf providers source set pi_home # or sf_owned
30
109
  ```
31
110
 
32
- Connect model providers in the console (Settings → Providers) or via `sf providers …`. Stageflow is a thin Pi shell: reuse an existing Pi login (`pi_home`) or store credentials in an SF-owned file (`sf_owned`). You do not need Pi CLI `/login` as a hard prerequisite.
111
+ Stageflow is a thin Pi shell you do not need Pi CLI `/login` as a hard prerequisite if you configure providers via the console or `sf providers`.
112
+
113
+ Full reference: [docs/providers.md](docs/providers.md)
114
+
115
+ ## Operator console
116
+
117
+ Start the console with `sf ui` (default `http://127.0.0.1:3847`).
118
+
119
+ - **Runs** — active and recent pipeline runs, capacity, and status at a glance
120
+ - **Run detail** — stage timeline, transcripts, envelope payloads, and artifact paths
121
+ - **HITL reply** — answer operator gates (`ask_operator`) without leaving the browser
122
+ - **Pipelines** — browse manifest-declared pipeline definitions
123
+ - **Settings → Providers** — connect model providers (`pi_home` or `sf_owned` credential storage)
124
+
125
+ *Screenshot coming soon — capture after console polish lands (see `docs/img/`).*
126
+
127
+ ## Headless / CI
128
+
129
+ The guest actor is the CLI (`sf` / `stageflow`). `sf ui` and MCP are not required in the job.
130
+
131
+ ```bash
132
+ sf validate --strict --json
133
+ ```
134
+
135
+ Validate exits `0` or `1` only (no waiting / `2`). It checks pipeline and stage YAML only; it does not prove provider auth, Task, or checkout.
136
+
137
+ ```bash
138
+ sf providers login <providerId> --api-key-env <VAR>
139
+ ```
140
+
141
+ If the provider also supports OAuth, pass `--type api_key`.
142
+
143
+ ```bash
144
+ sf run --pipeline pipelines/hello.pipeline.yaml --task tasks/hello.task.yaml --json
145
+ ```
146
+
147
+ The process exits `0` when the Run succeeded, `1` when it failed (including a busy start), and `2` when waiting. `sf run --json` prints one stdout document. `ok` is true only for `succeeded`. Busy has no `runId`.
148
+
149
+ | outcome | ok | runId | exit |
150
+ |---|---|---|---|
151
+ | `succeeded` | true | present | `0` |
152
+ | `failed` | false | present after start; omit when start never created a run | `1` |
153
+ | `waiting` | false | present | `2` |
154
+ | `busy` | false | omit | `1` |
155
+
156
+ On a mixed Pipeline, default wait parks the Run (exit `2`). `--skip-gates` fails the Stage (exit `1`). A Pipeline with no HITL does not need the flag.
33
157
 
34
158
  ## State
35
159
 
36
- Runtime state lives in **`.stageflow/`** (SQLite + per-run workspaces under `.stageflow/runs/`). If `.stageflow` is missing and `.software-factory` exists from an older install, the next store open renames it to `.stageflow` once.
160
+ Runtime state lives in **`<git-root>/.stageflow/`** when inside a git repository (SQLite + per-run workspaces under `.stageflow/runs/`). Global config and `sf_owned` auth live under **`~/.stageflow/`**. If `.stageflow` is missing and `.software-factory` exists from an older install, the next store open renames it to `.stageflow` once.
37
161
 
38
162
  ## MCP
39
163
 
40
- `sf ui` also serves a Streamable HTTP MCP endpoint at `http://127.0.0.1:3847/mcp` (URL printed on boot). Point a Cursor (or other) MCP client at that URL for tools like `list_pipelines`, `start_run`, and `get_run`.
164
+ `sf ui` also serves a Streamable HTTP MCP endpoint at `http://127.0.0.1:3847/mcp` (URL printed on boot). Point a Cursor (or other) MCP client at that URL.
165
+
166
+ Available tools: `list_pipelines`, `list_tasks`, `list_runs`, `get_health`, `start_run`, `get_run`, `read_artifact`.
167
+
168
+ Full reference: [docs/mcp.md](docs/mcp.md)
169
+
170
+ ## Stageflow vs Conductor
171
+
172
+ Both projects address multi-step agent workflows. They differ in orchestration model and runtime.
173
+
174
+ | | **Stageflow** | **Conductor** |
175
+ |---|---------------|---------------|
176
+ | **Model** | Configurable **stages** on Pi; pipeline-owned YAML (any domain) | Multi-**agent** workflow graph |
177
+ | **Orchestration** | Pipeline DAG + stage worker | Jinja routing, no LLM in router |
178
+ | **Unit of work** | Task → Pipeline → Stage attempts | Workflow → Agents |
179
+ | **Handoff** | Typed **envelope** + artifacts | Agent output → context |
180
+ | **Human gates** | Operator console + MCP | Dashboard + TUI fleet |
181
+ | **Runtime** | Node.js, Pi coding agent | Python, Copilot/Claude SDKs |
182
+ | **Best for** | Personal/team **multi-stage Pi workflows** you define (releases, research, SDLC, …) | Enterprise multi-agent workflows |
183
+
184
+ If you want deterministic YAML routing across many agents, look at [Conductor](https://github.com/microsoft/conductor). If you want stage-bound Pi runs with reviewable envelopes and an operator console for **workflows you author**, use Stageflow.
185
+
186
+ ## Examples
187
+
188
+ | Example | Description |
189
+ |---------|-------------|
190
+ | [hello-world](examples/hello-world/) | Single stage, domain-neutral |
191
+ | [plan-review](examples/plan-review/) | Multi-stage with operator gate — SDLC-style **example** |
192
+ | [github-release](examples/github-release/) | Dogfood: draft + publish GitHub Release |
193
+ | [ci-validate](examples/ci-validate/) | Strict validate in CI |
194
+
195
+ Index: [examples/README.md](examples/README.md)
196
+
197
+ ## Documentation
198
+
199
+ Full docs: **[tejasghutukade.github.io/stageflow](https://tejasghutukade.github.io/stageflow/)** (GitHub Pages — live after merge to `main` and Pages enabled). Source in [`docs/`](docs/).
200
+
201
+ | Doc | Description |
202
+ |-----|-------------|
203
+ | [docs/README.md](docs/README.md) | Documentation index |
204
+ | [docs/quickstart.md](docs/quickstart.md) | Expanded quick start |
205
+ | [docs/yaml-catalog.md](docs/yaml-catalog.md) | Pipelines, stages, tasks schema |
206
+ | [docs/cli-reference.md](docs/cli-reference.md) | `sf run`, `sf validate`, `sf ui`, `sf providers` |
207
+ | [docs/envelopes.md](docs/envelopes.md) | Handoff envelope contract |
208
+ | [docs/hitl.md](docs/hitl.md) | Gate kinds, `--skip-gates`, exit code `2` |
209
+ | [docs/ci.md](docs/ci.md) | `--json`, env vars, GitHub Actions |
210
+ | [docs/mcp.md](docs/mcp.md) | MCP tool reference |
211
+ | [docs/providers.md](docs/providers.md) | Pi providers, `sf providers` |
212
+ | [docs/operator-console.md](docs/operator-console.md) | Console IA and settings |
213
+ | [docs/compare-conductor.md](docs/compare-conductor.md) | Positioning deep dive |
41
214
 
42
215
  ## Develop from source
43
216
 
@@ -49,8 +222,12 @@ npm run build && npm run ui:build # ui:build builds the UI and copies assets i
49
222
  sf ui
50
223
  ```
51
224
 
225
+ Run tests: `npm test` and `npm run ui:test`. Typecheck: `npm run typecheck`.
226
+
52
227
  ## License
53
228
 
54
229
  MIT © Tejas G
55
230
 
56
- Issues: https://github.com/tejasghutukade/stageflow/issues
231
+ **Support:** [GitHub Issues](https://github.com/tejasghutukade/stageflow/issues) · [SUPPORT.md](SUPPORT.md)
232
+
233
+ **Contributing:** [CONTRIBUTING.md](CONTRIBUTING.md)
@@ -11,7 +11,11 @@
11
11
  * those Q&A bodies.
12
12
  */
13
13
  import type { AskOperatorAnswer, AskOperatorPrompt } from "../tools/askOperator.js";
14
- export declare const ACTIVITY_TEXT_LIMIT = 500;
14
+ export declare const ACTIVITY_TEXT_LIMIT = 2000;
15
+ export declare const ACTIVITY_TEXT_LIMIT_ENV = "STAGEFLOW_ACTIVITY_TEXT_LIMIT";
16
+ export declare const ACTIVITY_VERBOSE_ENV = "STAGEFLOW_ACTIVITY_VERBOSE";
17
+ export declare function readActivityTextLimit(env?: Record<string, string | undefined>, override?: number): number;
18
+ export declare function readActivityVerbose(env?: Record<string, string | undefined>, override?: boolean): boolean;
15
19
  export type StageActivityEvent = {
16
20
  event: "agent_start";
17
21
  } | {
@@ -29,6 +33,11 @@ export type StageActivityEvent = {
29
33
  toolCallId?: string;
30
34
  isError?: boolean;
31
35
  resultPreview?: string;
36
+ } | {
37
+ event: "tool_progress";
38
+ toolName: string;
39
+ toolCallId?: string;
40
+ textPreview?: string;
32
41
  } | {
33
42
  event: "message";
34
43
  role: string;
@@ -10,8 +10,43 @@
10
10
  * ACTIVITY_TEXT_LIMIT applies only to tool/message *previews* — never to
11
11
  * those Q&A bodies.
12
12
  */
13
- export const ACTIVITY_TEXT_LIMIT = 500;
14
- export function truncateActivityText(value, limit = ACTIVITY_TEXT_LIMIT) {
13
+ export const ACTIVITY_TEXT_LIMIT = 2000;
14
+ export const ACTIVITY_TEXT_LIMIT_ENV = "STAGEFLOW_ACTIVITY_TEXT_LIMIT";
15
+ export const ACTIVITY_VERBOSE_ENV = "STAGEFLOW_ACTIVITY_VERBOSE";
16
+ export function readActivityTextLimit(env = process.env, override) {
17
+ if (override !== undefined) {
18
+ if (Number.isFinite(override) && override >= 1)
19
+ return Math.floor(override);
20
+ return ACTIVITY_TEXT_LIMIT;
21
+ }
22
+ const raw = env[ACTIVITY_TEXT_LIMIT_ENV];
23
+ if (raw === undefined || raw.trim() === "") {
24
+ return ACTIVITY_TEXT_LIMIT;
25
+ }
26
+ const n = Number.parseInt(raw, 10);
27
+ if (!Number.isFinite(n) || n < 1) {
28
+ return ACTIVITY_TEXT_LIMIT;
29
+ }
30
+ return n;
31
+ }
32
+ export function readActivityVerbose(env = process.env, override) {
33
+ if (override !== undefined) {
34
+ return override;
35
+ }
36
+ const raw = env[ACTIVITY_VERBOSE_ENV];
37
+ if (raw === undefined || raw.trim() === "") {
38
+ return false;
39
+ }
40
+ const normalized = raw.trim().toLowerCase();
41
+ if (normalized === "0" ||
42
+ normalized === "false" ||
43
+ normalized === "off" ||
44
+ normalized === "no") {
45
+ return false;
46
+ }
47
+ return true;
48
+ }
49
+ export function truncateActivityText(value, limit = readActivityTextLimit()) {
15
50
  if (value === undefined || value === null)
16
51
  return undefined;
17
52
  let text;
@@ -1,6 +1,9 @@
1
- import type { StageActivityEvent } from "./activity.js";
1
+ import { type StageActivityEvent } from "./activity.js";
2
+ export declare const TOOL_PROGRESS_THROTTLE_MS = 300;
2
3
  export type StageActivityObserver = {
3
4
  onAssistantTextDelta(delta: string): void;
5
+ onThinkingDelta(delta: string): void;
6
+ onToolPartialResult(accumulatedText: string): void;
4
7
  onStreamBoundary(): void;
5
8
  onActivity(activity: StageActivityEvent): void;
6
9
  dispose(): void;
@@ -8,6 +11,7 @@ export type StageActivityObserver = {
8
11
  export type CreateStageActivityObserverOptions = {
9
12
  onActivity?: (event: StageActivityEvent) => void;
10
13
  writeStderr?: boolean;
14
+ toolProgressThrottleMs?: number;
11
15
  };
12
16
  /**
13
17
  * Observe StageActivityEvent milestones and optional assistant text deltas.
@@ -1,4 +1,11 @@
1
- function writeActivityStderr(activity) {
1
+ import { truncateActivityText, } from "./activity.js";
2
+ export const TOOL_PROGRESS_THROTTLE_MS = 300;
3
+ function writeIndentedPreview(preview) {
4
+ for (const line of preview.split("\n")) {
5
+ process.stderr.write(` ${line}\n`);
6
+ }
7
+ }
8
+ function writeActivityStderr(activity, skipAssistantMessage) {
2
9
  switch (activity.event) {
3
10
  case "agent_start":
4
11
  process.stderr.write(" … agent started\n");
@@ -11,13 +18,33 @@ function writeActivityStderr(activity) {
11
18
  return;
12
19
  case "tool_start":
13
20
  process.stderr.write(` → ${activity.toolName}\n`);
21
+ if (activity.argsPreview) {
22
+ writeIndentedPreview(activity.argsPreview);
23
+ }
14
24
  return;
15
25
  case "tool_end":
16
26
  if (activity.isError) {
17
27
  process.stderr.write(` ✕ ${activity.toolName}\n`);
18
28
  }
29
+ else if (activity.resultPreview) {
30
+ process.stderr.write(` ← ${activity.toolName}\n`);
31
+ }
32
+ if (activity.resultPreview) {
33
+ writeIndentedPreview(activity.resultPreview);
34
+ }
35
+ return;
36
+ case "tool_progress":
19
37
  return;
20
38
  case "message":
39
+ if (activity.role === "assistant" && skipAssistantMessage) {
40
+ return;
41
+ }
42
+ if (activity.role === "thinking") {
43
+ return;
44
+ }
45
+ if (activity.text) {
46
+ writeIndentedPreview(activity.text);
47
+ }
21
48
  return;
22
49
  }
23
50
  }
@@ -28,39 +55,175 @@ function writeActivityStderr(activity) {
28
55
  */
29
56
  export function createStageActivityObserver(options = {}) {
30
57
  const writeStderr = options.writeStderr !== false;
31
- let inAssistantText = false;
32
- const endTextLine = () => {
33
- if (!inAssistantText) {
58
+ const toolProgressThrottleMs = options.toolProgressThrottleMs ?? TOOL_PROGRESS_THROTTLE_MS;
59
+ let openStream = "none";
60
+ let assistantStreamedForMessage = false;
61
+ let lastToolPartial = "";
62
+ let thinkingBuffer = "";
63
+ let openTool = null;
64
+ let toolProgressAccum = "";
65
+ let toolProgressPending = false;
66
+ let toolProgressTimer = null;
67
+ const endOpenStream = () => {
68
+ if (openStream === "none") {
34
69
  return;
35
70
  }
36
71
  if (writeStderr) {
37
72
  process.stderr.write("\n");
38
73
  }
39
- inAssistantText = false;
74
+ openStream = "none";
75
+ };
76
+ const flushThinking = () => {
77
+ const raw = thinkingBuffer;
78
+ thinkingBuffer = "";
79
+ if (!raw.trim()) {
80
+ return;
81
+ }
82
+ options.onActivity?.({
83
+ event: "message",
84
+ role: "thinking",
85
+ text: truncateActivityText(raw),
86
+ });
87
+ };
88
+ const emitToolProgress = () => {
89
+ if (!toolProgressPending || !openTool) {
90
+ toolProgressPending = false;
91
+ return;
92
+ }
93
+ const textPreview = truncateActivityText(toolProgressAccum);
94
+ toolProgressPending = false;
95
+ if (!textPreview?.trim()) {
96
+ return;
97
+ }
98
+ options.onActivity?.({
99
+ event: "tool_progress",
100
+ toolName: openTool.toolName,
101
+ toolCallId: openTool.toolCallId,
102
+ textPreview,
103
+ });
104
+ };
105
+ const cancelToolProgressTimer = () => {
106
+ if (toolProgressTimer !== null) {
107
+ clearTimeout(toolProgressTimer);
108
+ toolProgressTimer = null;
109
+ }
110
+ };
111
+ const flushToolProgress = () => {
112
+ cancelToolProgressTimer();
113
+ emitToolProgress();
114
+ };
115
+ const scheduleToolProgress = () => {
116
+ if (!openTool || !toolProgressAccum.trim()) {
117
+ return;
118
+ }
119
+ toolProgressPending = true;
120
+ if (toolProgressTimer !== null) {
121
+ return;
122
+ }
123
+ toolProgressTimer = setTimeout(() => {
124
+ toolProgressTimer = null;
125
+ emitToolProgress();
126
+ }, toolProgressThrottleMs);
40
127
  };
41
128
  return {
42
129
  onAssistantTextDelta(delta) {
43
130
  if (!writeStderr || !delta) {
44
131
  return;
45
132
  }
46
- if (!inAssistantText) {
133
+ assistantStreamedForMessage = true;
134
+ if (openStream !== "assistant") {
135
+ endOpenStream();
47
136
  process.stderr.write(" ");
48
- inAssistantText = true;
137
+ openStream = "assistant";
138
+ }
139
+ process.stderr.write(delta);
140
+ },
141
+ onThinkingDelta(delta) {
142
+ if (!delta) {
143
+ return;
144
+ }
145
+ thinkingBuffer += delta;
146
+ if (!writeStderr) {
147
+ return;
148
+ }
149
+ if (openStream !== "thinking") {
150
+ endOpenStream();
151
+ process.stderr.write(" ∴ ");
152
+ openStream = "thinking";
153
+ }
154
+ process.stderr.write(delta);
155
+ },
156
+ onToolPartialResult(accumulatedText) {
157
+ if (!accumulatedText) {
158
+ return;
159
+ }
160
+ toolProgressAccum = accumulatedText;
161
+ scheduleToolProgress();
162
+ if (!writeStderr) {
163
+ return;
164
+ }
165
+ const delta = accumulatedText.startsWith(lastToolPartial)
166
+ ? accumulatedText.slice(lastToolPartial.length)
167
+ : accumulatedText;
168
+ lastToolPartial = accumulatedText;
169
+ if (!delta) {
170
+ return;
171
+ }
172
+ if (openStream !== "tool") {
173
+ endOpenStream();
174
+ process.stderr.write(" │ ");
175
+ openStream = "tool";
49
176
  }
50
177
  process.stderr.write(delta);
51
178
  },
52
179
  onStreamBoundary() {
53
- endTextLine();
180
+ endOpenStream();
181
+ flushThinking();
182
+ flushToolProgress();
183
+ lastToolPartial = "";
54
184
  },
55
185
  onActivity(activity) {
56
- endTextLine();
186
+ flushThinking();
187
+ if (activity.event === "tool_end") {
188
+ flushToolProgress();
189
+ }
190
+ endOpenStream();
191
+ lastToolPartial = "";
192
+ if (activity.event === "turn_start") {
193
+ assistantStreamedForMessage = false;
194
+ }
195
+ if (activity.event === "tool_start") {
196
+ openTool = {
197
+ toolName: activity.toolName,
198
+ toolCallId: activity.toolCallId,
199
+ };
200
+ toolProgressAccum = "";
201
+ toolProgressPending = false;
202
+ cancelToolProgressTimer();
203
+ }
204
+ else if (activity.event === "tool_end") {
205
+ openTool = null;
206
+ toolProgressAccum = "";
207
+ toolProgressPending = false;
208
+ cancelToolProgressTimer();
209
+ }
210
+ const skipAssistantMessage = activity.event === "message" &&
211
+ activity.role === "assistant" &&
212
+ assistantStreamedForMessage;
213
+ if (activity.event === "message" &&
214
+ activity.role === "assistant") {
215
+ assistantStreamedForMessage = false;
216
+ }
57
217
  options.onActivity?.(activity);
58
218
  if (writeStderr) {
59
- writeActivityStderr(activity);
219
+ writeActivityStderr(activity, skipAssistantMessage);
60
220
  }
61
221
  },
62
222
  dispose() {
63
- endTextLine();
223
+ endOpenStream();
224
+ flushThinking();
225
+ flushToolProgress();
226
+ cancelToolProgressTimer();
64
227
  },
65
228
  };
66
229
  }
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "
2
2
  import path from "node:path";
3
3
  import { assertRequiredEnvelope, isAdvancingEnvelope, } from "../envelope/check.js";
4
4
  import { assertEnvelopePayload } from "../envelope/payloadSchema.js";
5
+ import { assertForkEnvelope } from "../envelope/forkChoice.js";
5
6
  import { assertAnswerMatchesPrompt, parseAskOperatorAnswer, } from "../tools/askOperator.js";
6
7
  import { tryParsePendingPrompt } from "../runtime/stageHitl.js";
7
8
  import { runStageViaOpen } from "./port.js";
@@ -135,6 +136,9 @@ export class FakeAgent {
135
136
  : undefined;
136
137
  try {
137
138
  const envelope = assertRequiredEnvelope(envelopeValue);
139
+ if (input.forkEmitContext !== undefined) {
140
+ assertForkEnvelope(envelope, input.forkEmitContext);
141
+ }
138
142
  assertEnvelopePayload(envelope, input.stage.payload_schema);
139
143
  if (!isAdvancingEnvelope(envelope)) {
140
144
  input.onActivity?.({ event: "agent_end" });
@@ -2,6 +2,7 @@ import { type AgentSession, DefaultResourceLoader, SessionManager, SettingsManag
2
2
  import type { StageRoots } from "../runtime/stageRoots.js";
3
3
  import { type AskOperatorPrompt, type AskOperatorWaitBridge } from "../tools/askOperator.js";
4
4
  import "./cursorProvider.js";
5
+ import { type StageActivityObserver } from "./activityObserver.js";
5
6
  import type { AgentPort, OpaqueAnswer, StageHandle, StageHandleCloseOptions, StageRunInput, StageRunResult } from "./port.js";
6
7
  /**
7
8
  * Stage tool allowlist for sealed Pi sessions.
@@ -45,6 +46,21 @@ export type ConnectedAskWaitStageHandleOptions = {
45
46
  * runtime never imports the channel.
46
47
  */
47
48
  export declare function createConnectedAskWaitStageHandle(options: ConnectedAskWaitStageHandleOptions): StageHandle;
49
+ /**
50
+ * Extract display text from a Pi tool_execution_update partialResult.
51
+ * Prefers content[].text blocks; otherwise JSON-stringifies.
52
+ */
53
+ export declare function extractPartialResultText(partialResult: unknown): string;
54
+ export type RouteSessionEventToProgressOptions = {
55
+ observer: StageActivityObserver;
56
+ verbose: boolean;
57
+ };
58
+ /**
59
+ * Adapter-edge routing: stream deltas to the observer, map milestones to
60
+ * StageActivityEvent. Verbose thinking / live tool partials go to observer
61
+ * hooks (stderr streams + coalesced/throttled persist via onActivity).
62
+ */
63
+ export declare function routeSessionEventToProgress(event: Record<string, unknown>, options: RouteSessionEventToProgressOptions): void;
48
64
  export declare function composeStageUserPrompt(input: StageRunInput, emitToolName: string, emitHintOverride?: string, artifactToolName?: string): string;
49
65
  /**
50
66
  * DefaultResourceLoader with host/global discovery turned off.