refactor(monorepo)!: restructure workspace with @bastani/workflows - #936
Conversation
Two sequenced specs for the full v1 rewrite from an empty repo: - specs/2026-05-11-pi-workflows-extension.md (Spec 1): the publishable pi-workflows npm package - sibling of pi-subagents/pi-mcp-adapter/pi-intercom. Mirrors pi-subagents' file layout; reauthors the v0.x Atomic SDK's TUI graph engine as a shared overlay/widget under pi-tui. - specs/2026-05-11-atomic-pi-coding-agent-rewrite.md (Spec 2): the Atomic pi-coding-agent rebrand. Fork is package.json-only; bundled skills, MCP server configs, sub-agents, prompts, themes ship inside the atomic binary. Both specs anchor to a single git wipe (tag v0.x-archive on prior HEAD; wipe on branch rewrite/clean-slate) and use git-history cross-references only. Includes the research notes that informed the design.
- Add store-types.ts: RunStatus, StageStatus, ToolEvent, StageSnapshot, RunSnapshot, StoreSnapshot types - Add store.ts: mutable singleton store with subscribe/version counter, createStore() factory - Add runs/shared/graph-inference.ts: GraphFrontierTracker for inferring DAG parents from JS execution order (sequential, parallel, fan-in patterns) - Add runs/sync/stage-runner.ts: StageContext factory with prompt/complete/subagent adapters - Add runs/sync/executor.ts: main run() executor with input resolution, lifecycle callbacks, and DAG tracking - Add tests for graph-inference and executor (154 tests total pass) - Export all new public APIs from index.ts
- src/persistence/session-entries.ts: appendRunStart, appendStageStart, appendStageProgress, appendStageEnd, appendRunEnd helpers with PersistenceAPI structural type; setLabel for wf:<name>:<short-id> labels - src/persistence/restore.ts: scanInFlightRuns (pure scan), restoreOnSessionStart with ask/auto/never modes, stage snapshot rebuild from session entries - src/persistence/compaction-policy.ts: installCompactionHook for session_before_compact; re-appends run.start + pending stage.start entries - src/runs/detach/status.ts: statusRuns, killRun, killAllRuns, resumeRun helpers - extension/index.ts: ExtensionAPI gains appendEntry/setLabel/appendCustomMessageEntry/ on/sessionManager; factory wires session_start restore + compaction hook; /workflow status/kill/resume slash commands and tool actions use real helpers - 78 new unit tests across 4 test files; 306 total pass, 0 fail cross-ref: spec §5.6, §5.13, §8.1 Phase D
Create all tui module files for the overlay graph component: - layout.ts: DAG layout engine with BFS column assignment - connectors.ts: box-drawing connector helpers (buildConnector, buildMergeConnector) - status-helpers.ts: statusColor, statusIcon, fmtDuration pure helpers - color-utils.ts: lerpColor for hex color interpolation - graph-theme.ts: deriveGraphTheme from generic theme tokens - node-card.ts: multi-line stage card string renderer with ANSI colors - edge.ts: connector edge renderer between layout nodes - header.ts: header band with run name, status counts, elapsed time - switcher.ts: '/' popup stage jump list with filtering - toast.ts: nvim-style notification toast manager and renderer - graph-view.ts: GraphView class with overlay/widget modes, keyboard nav - renderers.ts: re-exports integration point for extension renderers Add 42 unit tests covering: - computeLayout (single node, empty, linear chain, parallel branch, coordinates) - buildConnector (basic, reversed, equal positions) - buildMergeConnector (single source, multi-source, empty) - statusColor and statusIcon for all statuses - fmtDuration (0ms, 45s, 1m24s, 3h2m, 1m, 1h) - GraphView keyboard navigation (j/k/gg/q/Escape/ArrowKeys/switcher) All 42 tests pass, typecheck clean.
…primitive unavailable fallback - Add WorkflowUIAdapter type alias to shared/types.ts (compatible with WorkflowUIContext) - Add ui?: WorkflowUIAdapter field to RunOpts - Replace generic makeUIContext() stub with makeUnavailableUIContext() producing precise per-primitive error messages: 'pi-workflows: HIL ctx.ui.<primitive> is unavailable because pi runtime did not provide a UI adapter' - Executor wires ctx.ui = opts.ui ?? makeUnavailableUIContext() - Add 9 HIL tests: delegate (input/confirm/select/editor) + fallback rejection per primitive + no-HIL regression
…sion/index.js with verifier
Add src/extension/discovery.ts with discoverBundledWorkflows():
- Statically imports bundled manifest (deep-research-codebase, ralph,
open-claude-design) — no risky runtime TS loader
- Validates each definition: object, __piWorkflow true, name non-empty,
normalizedName present, run function
- First-seen-wins duplicate policy; emits DUPLICATE_NAME warn diagnostic
- Invalid exports emit INVALID_DEFINITION error diagnostic
- Returns DiscoveryResult { registry, sources, errors }
- DiscoverySource: { id, kind: 'bundled', name }
Add test/unit/discovery.test.ts — 19 tests covering:
- Happy path: all 3 builtins registered, no errors
- sources array shape, uniqueness, kind='bundled'
- registry.get / registry.all / registry.names integrity
- DiscoveryDiagnostic type shape for both error codes
- Immutability contract (register returns new registry)
Cover: - package.json manifest field contract (main, types, exports import/types, pi.extensions) - dist/index.js dynamic import → defineWorkflow and createRegistry are functions - createRegistry() returns object with register/get - defineWorkflow() returns builder with description/input/run/compile - dist/extension/index.js default export is function (extension factory) - extension factory callable with pi-like stub without throwing All 446 tests pass (14 new in artifact-import-smoke.test.ts).
- Add src/extension/dispatcher.ts: WorkflowDispatcher dispatches list/inputs/run through registry + executor. No broad catch; input validation errors propagate; not-found run returns structured failed result (not throw) so tool consumers get honest status:'failed'. - Add src/extension/runtime.ts: ExtensionRuntime facade owns registry + dispatcher. Accepts external registry (discovery worker seam) or definitions array via ExtensionRuntimeOpts. - Update render-result.ts: WorkflowToolResult run variant now carries name?, result?, error?, stages? + legacy message? for backward compat. renderResult updated to handle new fields gracefully. - Wire extension/index.ts: factory creates ExtensionRuntime; makeExecuteWorkflowTool closure delegates list/inputs/run to runtime.dispatch(); slash /workflow list reads runtime.registry.names(); doctor reports real registry count. - Add runtime.test.ts: 18 tests covering list/inputs/run dispatch, structured not-found, execution failure, input validation propagation, renderResult rendering, createExtensionRuntime seeding.
…discoverBundledWorkflows - Add doctor.ts with buildDoctorReport(discovery, siblings) pure function Reports: registry count, bundled sources with kind/id/name, discovery diagnostics (INVALID_DEFINITION / DUPLICATE_NAME), sibling availability (pi-subagents via pi.subagents, pi-mcp-adapter via pi['mcpAdapter'], pi-intercom via pi.setSessionName presence) - Wire /workflows-doctor execute handler in index.ts to call discoverBundledWorkflows() and buildDoctorReport() — removes all stubs - Remove hardcoded stub lines: 'availability check not yet wired', 'Executor: wired (Phase C DAG executor)', 'Config: defaults in effect' - Add test/integration/doctor.test.ts: 27 focused tests covering header structure, registry count, bundled sources, diagnostics, sibling flags, and end-to-end execute via mock ExtensionAPI All 476 tests pass. Zero type errors.
…aliases, completions, inputs - Add parseWorkflowArgs(): parses key=value pairs and JSON object tokens - Add ADMIN_SUBCOMMANDS set; non-admin first token resolved as workflow name - /workflow <name> [key=value...] dispatches runtime.dispatch run action - /workflow inputs <name> dispatches inputs action; shows schema or not-found + available - Unknown workflow prints 'Workflow not found: <name>' + available names - getArgumentCompletions includes both admin subcommands and workflow names from registry - Register /workflow:<name> alias per discovered workflow (deep-research-codebase, ralph, open-claude-design) - Export parseWorkflowArgs for testability - 20 new slash-dispatch tests: parseWorkflowArgs, alias registration, completions, dispatch paths - All 496 tests pass; typecheck clean
…un and slash dispatch - tool list: assert bundled names (deep-research-codebase, ralph, open-claude-design) returned - tool inputs: assert deep-research-codebase schema has prompt (required text) and max_partitions (number, default 4) - tool run: assert non-placeholder runId (real UUID), terminal status, stages array; honest failed+error when adapters missing, no stub text - slash aliases: workflow:deep-research-codebase, workflow:ralph, workflow:open-claude-design registered with descriptions - completions: include all admin subcommands + bundled workflow names; filter by partial prefix - /workflow deep-research-codebase prompt=test dispatches run not unknown-subcommand - /workflows-doctor: real count >=3, no 'Phase B stub'/'Executor: not yet implemented', names all three bundled workflows 511 tests pass, 0 fail
…, registerWorkflowCliFlags, runWorkflowFromCliFlags
- Parse --workflow=<name> and --workflow <name> (space-sep)
- Parse --workflow-input-<key>=<value> and --workflow-input-<key> <value>
- JSON-parse values: numbers, booleans, objects; fallback to string
- Standalone --workflow-input-<key> (no value) → true
- Returns { handled: false } when --workflow absent
- Dispatches action:run via ExtensionRuntime.dispatch
- Returns { handled: true, status: completed|failed, result?, error? }
- Real errors from dispatch propagate as status:failed (no silent swallow)
- 21 tests passing, typecheck clean
…CONFIG_INVALID diagnostics
- Add config-loader.ts helper under src/extension/
- Reads project-local (.pi/extensions/workflow/config.json, .pi/agent/extensions/workflow/config.json) and global (~/.pi/agent/extensions/workflow/config.json) config files
- Parses optional workflows: { [name]: { path } } map
- Invalid JSON or invalid shape produces CONFIG_INVALID diagnostic (not silent success)
- Missing files silently skipped; no broad catch swallowing errors
- Project-local overrides global on merge; workflows map merged key-by-key
- 25 unit tests covering all branches (missing, valid, invalid JSON, invalid shape, merge, priority)
…fy-artifact Unit test suite now passes from clean checkout without bun run build. Removed three tests from 'verify-artifact integration — actual dist' describe block that imported real dist files (all package.json paths present, public API exports, extension factory). These required a built dist and caused bun test to fail on a clean checkout. Real dist verification is already covered by scripts/verify-artifact.ts which is invoked as step 5 of scripts/build.ts. A comment in the test file documents this division. 6 fixture-based unit tests remain, all using temp directories.
…/options inputs, camelCase keys, dispatch action:run payload verification
… discovery
Changes to src/extension/discovery.ts:
- scanWorkflowDir: add .mjs and .cjs support (was only .ts/.js)
- importWorkflowFile: collect default export AND named exports (was OR logic)
— default checked first; named exports always traversed regardless
— enables multi-workflow files and preserves RFC §5.12 default-first order
- loadFromPaths: accept string[] | Record<string,string> so settings entries
can carry a configuredName (named-map → configuredName populated in source)
- DiscoverySource: add optional configuredName field for settings-named entries
- DiscoveryConfig: widen projectWorkflows/globalWorkflows to string[] | Record<string,string>
- validateConfig: validate both array and named-map shapes
- discoverWorkflows: fix precedence per RFC §5.12
settings-project > project-local > settings-global > user-global > bundled
(was: project-local > settings-project > user-global > settings-global)
- discoverWorkflows: user-global scans ~/.pi/agent/workflows/ (RFC canonical)
(was: ~/.pi/workflows/)
- discoverWorkflows: guard settings loading when CONFIG_INVALID (prevent crash on bad config)
- All diagnostics preserved: IMPORT_FAILED, INVALID_DEFINITION, PATH_NOT_FOUND, CONFIG_INVALID
New test/unit/discovery-module-imports.test.ts (28 tests, all pass):
- Extension coverage: .js, .mjs, .cjs, unsupported extension filtering
- Default+named export collection and default-wins-on-conflict behavior
- IMPORT_FAILED on syntax error, non-blocking for sibling files
- PATH_NOT_FOUND for missing config paths, non-blocking for other paths
- configuredName populated/absent per source kind
- filePath set for fs-loaded, undefined for bundled
- Precedence tiers verified with conflict scenarios
- User-global path at ~/.pi/agent/workflows/, missing dir silent
588 tests pass, 0 fail. tsc --noEmit clean.
Cover all discovery sources and edge cases: - project-local: .pi/workflows/ scanned, kind=project-local, filePath set - user-global: homeDir/.pi/agent/workflows/, kind=user-global - configured projectWorkflows: string array (no configuredName) and named map (configuredName set) - configured globalWorkflows: string array and named map, kind=settings-global - invalid exports: null default → INVALID_DEFINITION, missing __piWorkflow sentinel - PATH_NOT_FOUND for missing configured path - CONFIG_INVALID for bad config structure - DUPLICATE_NAME precedence: settings-project > project-local > settings-global > user-global > bundled - includeBundled flag: true loads builtins, false excludes them 47 tests total (28 new + 19 existing), 0 failures
…d discovery
- Replace discoverBundledWorkflowsSync-only startup with mutable runtimeRef + runtimeProxy pattern
- Start discoverWorkflows() async immediately in factory; swap runtimeRef.current on resolve
- Proxy delegates all registry/dispatch calls to runtimeRef.current — all closures stay current without re-registration
- Bundled aliases registered synchronously (preserves backward compat); project-local/user-global aliases registered after async discovery
- Replace manual pi.registerFlag block with registerWorkflowCliFlags(pi) from cli-flags.ts
- Wire runWorkflowFromCliFlags via pi.on('session_start') startup hook; fallback to discoveryPromise.then() when pi.on absent
- Fix /workflows-doctor to use discoveryRef.result (unified registry) when available, fallback to discoverBundledWorkflows
- Preserve ExtensionAPI compatibility; all 616 tests pass
…red registry across tool, slash commands, doctor, CLI - discoverWorkflows with temp project-local + user-global dirs yields registry containing both custom and bundled workflows - ExtensionRuntime.dispatch action=list/inputs/run sees custom workflow names - buildDoctorReport shows [project-local] and [user-global] sources from discovery - runWorkflowFromCliFlags dispatches custom workflow via same runtime - /workflow slash command list + completions reflect shared runtimeProxy.registry - /workflow:<name> alias execute routes through same dispatch path as tool - end-to-end invariant: tool list count, doctor registry count, and CLI dispatch all reflect the same registry object (36 new tests, 0 fail)
…to dist/workflows, update package metadata - workflows/*.ts: import from 'pi-workflows' instead of '../src/index.js' - scripts/build.ts: add bun build step for workflows → dist/workflows/ with --external pi-workflows - tsconfig.build.json: add paths alias pi-workflows → ./src/index.ts for tsc declaration emit - package.json: files ['dist','README.md','LICENSE'], pi.workflows ['./dist/workflows'] - scripts/verify-artifact.ts: verify pi.workflows directories in artifact check - build + 652 tests pass
…discovery - Import loadWorkflowConfig + ConfigLoadResult from config-loader.ts - Chain loadWorkflowConfig() → discoverWorkflows() so config.workflows paths are passed as DiscoveryConfig.projectWorkflows (settings-project) - Store ConfigLoadResult in configLoadRef for future doctor-config task - Apply config-driven defaults for persistRuns and resumeInFlight in restoreOnSessionStart (was hardcoded); await discoveryPromise in session_start handler so tunables are resolved before restore runs - defaultConcurrency, maxDepth, statusFile retained in config for future consumer wiring (config-translation task)
…tensionConfig.workflows to DiscoveryConfig.projectWorkflows
- Add pure exported toDiscoveryConfig(config: WorkflowExtensionConfig): DiscoveryConfig
to config-loader.ts; maps {[name]: {path}} → {projectWorkflows: {[name]: path}};
returns {} when workflows absent/empty
- Import DiscoveryConfig type from discovery.ts in config-loader.ts
- Replace inline translation block in extension/index.ts factory with toDiscoveryConfig call
- Add config-loader.test.ts with 7 unit tests covering: empty config, empty workflows,
single entry, multiple entries, projectWorkflows-only output, field isolation, type shape
… tunables, and configured workflow entries
… workflow imports - verify-artifact.ts: add check 2 — scan all dist/workflows/*.js for ../src/ or /src/index.js forbidden patterns; exit 1 if found - verify-artifact.ts: add check 3 — extract all import/require specifiers from each workflow JS and flag any relative path that escapes dist/ or resolves into a src/ directory tree - package.json: add verify-artifact script so verifier is runnable standalone - verify-artifact.test.ts: add 22 new unit tests covering scanWorkflowForSrcImports, extractImportSpecifiers, findLeakyRelativeImports (pure helpers inlined to keep test suite build-state-independent) - All 702 existing tests pass; verifier exits 0 against current dist
… pi.exec surface
- Create src/extension/wiring.ts with buildRuntimeAdapters(pi: RuntimeWiringSurface): StageAdapters
- Adapters spawn `pi --mode json -p <text> --no-session` via pi.exec() and parse NDJSON
- extractAssistantText: scans backward through NDJSON for last message_end with role=assistant
- complete adapter: forwards CompleteStageOpts.model as --model flag
- subagent adapter: prefixes agent name + context into task prompt
- Graceful degradation: returns {} when pi.exec absent (stage-runner errors still fire)
- Add exec? to ExtensionAPI structural interface
- Pass adapters into both createExtensionRuntime() calls (initial bundled + async discovered)
- Preserve runtimeProxy swap behavior — adapters captured once, stable across registry swaps
- 25 new wiring tests covering all adapter paths, error handling, arg construction
- All 727 tests passing
…9 unit tests for config defaults - Add WORKFLOW_CONFIG_DEFAULTS export (maxDepth:4, concurrency:4, persistRuns:true, statusFile:false, resumeInFlight:ask) - Add WorkflowEffectiveConfig interface — all tunable fields concrete - Implement withWorkflowDefaults(config) — fills absent fields with RFC defaults, passes workflows through, does not mutate - Update config-loader.test.ts imports to include withWorkflowDefaults, WORKFLOW_CONFIG_DEFAULTS - Add 22 new tests across 5 describe blocks: empty config applies all defaults, explicit values preserved, partial config, immutability, constants validation - Total: 749 pass 0 fail (up from 702)
…ce, imports, src-leak scan, installed package simulation
…nvoked through dispatch path - Mock ExtensionAPI with exec surface → exec spy called during deep-research-codebase dispatch - Assert no 'prompt adapter not configured' failure (adapters take priority over test stub) - Pre-discovery: createExtensionRuntime with sync bundled registry + adapters → prompt/complete called - Post-discovery: swapped runtime (discovered registry, same adapters) → adapters still invoked - Cross-runtime: same adapters object works in both initial and swapped runtimes - No exec surface → test-env stub fires, exec NOT called (degraded runtime baseline) - E2e: factory workflow tool dispatch (initial runtime) → exec called immediately - 18 tests, 40 assertions, 0 failures
Code ReviewThanks for the substantial restructure. The new package layout, executor/runtime separation, and per-package test split feel coherent, and the test coverage (1197 unit + 260 integration) is very solid. The notes below mostly target rough edges introduced during the move rather than new architectural concerns. Highest-impact items
Bugs / correctness
Style / consistency
Security
Test coverage
Nits
Overall this is a clean, well-tested refactor. The functional surface (workflow authoring + DAG executor + extension wiring) is in good shape; most of the items above are housekeeping around the move rather than blockers. |
c77abe3 to
cae958c
Compare
PR Review —
|
Move ask_user_question and todo out of extension registration and into the coding-agent core tool registry. Remove direct built-in extension loading while preserving whimsical working messages in interactive mode. Assistant-model: ChatGPT
Code Review —
|
Run the prepare hook through the local @j178/prek binary via bunx so bun install does not depend on a globally installed prek. Document that prek.toml is the hook configuration and that default_install_hook_types controls the installed hook shims. Assistant-model: OpenAI ChatGPT
Include ask_user_question and todo in the default active tool set and visible system-prompt tool list for SDK-created sessions. Assistant-model: OpenAI ChatGPT
Increase the deep research partition ceiling and refresh bundled workflow prompts/model fallbacks while preserving the existing staged execution structure. Assistant-model: OpenAI ChatGPT
Render attached stage chat entries with coding-agent message, tool, and editor components; delegate shared text helpers to pi-tui; and register workflow renderers per live extension host. Guard inline form teardown so stale sessions do not restore editors into replacement hosts. Assistant-model: OpenAI ChatGPT
|
Code Review — PR #936 (part 1 of 4) — Massive refactor (2108 files, +266k/-225k). I focused review on the new @bastani/workflows package since it is the core of the change. Builtin workflows, TUI surfaces, and coding-agent were spot-checked. Summary: Architecture and public authoring API (defineWorkflow, runTask/runParallel/runChain, createStore, GraphFrontierTracker, CancellationRegistry) read well. Test coverage in packages/workflows is strong (executor 65 tests, discovery 47, cancellation 24). Findings cluster around concurrency lifecycle and type-safety violations of CLAUDE.md. BLOCKING/HIGH-PRIORITY: 1) Cancellation listeners and aborted controllers leak — packages/workflows/src/runs/background/cancellation-registry.ts:41-50 — register() silently replaces a prior entry without aborting/cleaning the old controller. The executor (runs/foreground/executor.ts:927) adds a permanent abort listener to callerSignal with no symmetric detachment when opts.signal is long-lived. abort() does not unregister, so the map keeps already-aborted entries. abortAll (:79-85) iterates _runs.keys() while handlers may call unregister() — concurrent mutation during iteration; snapshot the keys first. 2) Orphan stage handles when ctx.stage(name) returns without a tracked call — packages/workflows/src/runs/foreground/executor.ts:1078-1196 — ctx.stage() synchronously creates an inner context, registers a stage-control handle, subscribes for input. Disposal (disposeInnerContext, unregisterStageHandle, tracker.onSettle) only fires inside runTrackedStageCall. If the body calls ctx.stage("x") then throws (or never invokes prompt/complete/subagent), all those resources leak for the entire run. Track spawned stage cleanups and dispose them in the outer finally. 3) loadFromPaths will import() arbitrary specifiers — packages/workflows/src/extension/discovery.ts:328-355 — scanWorkflowDir filters .ts/.js/.mjs/.cjs (:244-246), but importWorkflowFile does a bare await import(filePath). Bun import() accepts URL specifiers, so an http:// or data: URI in globalWorkflows executes arbitrary code at extension boot. isAbsolute(rawPath) also allows escape from projectRoot via ../. Add an extension allowlist + reject URL-like specifiers + verify the path is under an expected root. 4) validateDefinition is too permissive — packages/workflows/src/extension/discovery.ts:146-165 — checks __piWorkflow and typeof run but not the inputs schema shape, so dispatcher.ts:137 resolveInputs() crashes at dispatch time on malformed definitions. Tighten the check so failures surface at discovery. |
|
Code Review — PR 936 (part 2 of 4) — Concurrency/correctness. 5) snapshot() deep-JSON-clones on every store mutation — packages/workflows/src/shared/store.ts:162-173 — every recordStageStart/recordToolStart|End/recordStageNotice calls notify() which deep-clones the entire store via JSON.parse(JSON.stringify(...)). For long runs with streaming tool events this is quadratic and produces noticeable GC pressure. It also silently strips functions/Date/Map. Consider lazy snapshots or structural-share/shallow-immutable snapshots. 6) GraphFrontierTracker is timing-dependent — packages/workflows/src/runs/shared/graph-inference.ts:26-51 — onSpawn/onSettle mutate frontier without locking and assume a synchronous spawn->await->settle ordering. If a workflow spawns a sibling stage inside a .then(), the inferred parent depends on event-loop order rather than the true DAG. Document the supported authoring patterns or capture the frontier at promise-construction time. 7) runDirectAsync fire-and-forget is unkillable — packages/workflows/src/extension/runtime.ts:289-335 — void background.then(...) (:311) never registers the run with cancellationRegistry. killAllRuns (index.ts:2064) only kills registered runs, so async direct runs leak past session shutdown and continue burning subagent tokens. Wire runId into the registry before returning. 8) awaitPendingPrompt has a TOCTOU window that can hang forever — packages/workflows/src/shared/store.ts:354-372 — the resolver is registered after recordPendingPrompt returns; if resolvePendingPrompt is called between the two, the response is dropped and the await hangs. Atomically construct the promise inside recordPendingPrompt. 9) setMcpScope has no try/finally guarantee — packages/workflows/src/extension/mcp.ts:73-83 — the comment claims "always clear scope" but the contract lives in the caller. If a stage between setMcpScope and clearMcpScope crashes, the scope leaks across stages. Move the cleanup into a finally or wrap with a disposable. |
|
Code Review — PR 936 (part 3 of 4) — Type safety, violates CLAUDE.md ("Avoid any and unknown"): (a) packages/workflows/src/extension/workflow-schema.ts:37,39,41,60 — Type.Any() on DirectTaskSchema.model, tools, noTools, WorkflowParametersSchema.inputs. LLM tool calls accept arbitrary payloads. At minimum inputs should be Type.Record(Type.String(), Type.Union([Type.String(), Type.Number(), Type.Boolean(), Type.Null()])). (b) packages/workflows/src/runs/foreground/executor.ts:178-192,611-614,696,740,829 — repeated (x as Record<string, unknown>) and as WorkflowArtifact[] / as WorkflowTaskResult[] casts. The WorkflowDefinition result type could be generic. (c) packages/workflows/src/runs/foreground/executor.ts:533-554 — isRunOpts structural sniff is fragile. Use a tagged discriminator. (d) packages/workflows/src/runs/foreground/stage-runner.ts:79-83 — Object.create(null) as ... casts; createStubAgentSession in workflow-runner.ts:55-98 returns an empty object cast to agent — first method call throws. Replace with a Proxy that throws a friendly error. (e) packages/workflows/src/shared/types.ts:283,327 — Record<string, unknown> on WorkflowPersistencePort.appendEntry and SubagentStageOpts.config. (f) packages/workflows/src/extension/subagents.ts:97,107 — payload as unknown as Record<string, unknown> is any-laundering; fix PiEventBus.emit signature instead. SMALLER FINDINGS: packages/workflows/src/extension/index.ts:730-739 — installInputInterceptor catch handler calls commandCtx.ui.notify unconditionally; if ctx.ui is undefined the catch itself throws into pi input pipeline. Guard ctx?.ui?.notify. Same file: commandCtx = ctx as PiCommandContext is an unchecked cast. packages/workflows/src/extension/config-loader.ts:115-123 — tryReadFile only swallows ENOENT; an EACCES on the global config (read-protected home dir) crashes extension boot. Convert to a diagnostic. packages/workflows/src/extension/subagents.ts:51 — injectWorkflowEnv does not sanitize runId/stageId before placing them in env vars. If those ever come from user input, newlines or shell metacharacters could flow to child processes. packages/workflows/src/runs/foreground/executor.ts:1067-1071 — rejectReleaseBarriers runs once on abort; barriers added after abort do not reject. packages/workflows/src/workflows/registry.ts:77 — remove returns this when key not present but register/merge always allocate a new wrapper. Inconsistent; add a comment or normalize. |
|
Code Review — PR 936 (part 4 of 4) — TEST COVERAGE: test/unit has 75 files (PR body says 76) covering executor, discovery, cancellation, store, dispatcher, persistence, TUI surfaces — strong for packages/workflows. test/integration has 7 files (PR body says 8): mock-extension-api, custom-registry, overlay-entrypoints, runtime-wiring, runtime-tunables, input-interceptor, mcp-entrypoint. packages/coding-agent has 123 in-package tests but uses vitest.config.ts, and the root bun test scripts only run test/unit and test/integration — CI gap: confirm coding-agent tests are actually executed in CI under the new scripts; bun run test:all from the root will not touch them. Untested areas: packages/web-access (25 files, no tests), most of packages/mcp OAuth (mcp-auth*.ts, mcp-oauth-provider.ts, mcp-callback-server.ts), runs/shared/worktree.ts, runs/background/job-tracker.ts, extension/render-call.ts/render-result.ts/renderers.ts, tui/graph-canvas.ts, tui/inline-form-editor.ts. The PR body count mismatch (76->75, 8->7) suggests files moved late in the PR; worth a final reconciliation. STYLE NOTES (non-blocking): package.json:13 — "lint": "tsc --noEmit" is identical to typecheck. With oxlint removed there is no actual linter wired up; document if intentional. CLAUDE.md is unchanged in this PR but its claims about the tech stack (OpenTUI / figlet / @clack/prompts) no longer match the new world (pi-coding-agent + pi-tui). Update before merge. OVERALL: the abstractions are sound and the test coverage on the core orchestration layer is impressive. Before merge I would want at least items 1-4 addressed and CI verified to actually run the in-package coding-agent suite. |
Code reviewThis PR is a structural rewrite — 2,094 files, 263k insertions, 222k deletions, six new packages and a runtime migration. A coherent end-to-end human review at this size isn't really feasible; if you can split it into landing PRs (e.g. workspace skeleton → `@bastani/workflows` core → extension surface → moved tests), each one becomes reviewable on its own and bisecting future regressions stops being a nightmare. Below is what I caught from spot reads of the runtime, extension, and packaging. Packaging / versioning inconsistencies
Workspace / tooling
Runtime correctness / behavior
SecurityThe git operations in `packages/workflows/src/runs/shared/worktree.ts` use `spawnSync(..., { shell: false })` consistently and `safePatchAgentName` sanitises the agent name before joining it into a path — no shell injection or path injection found there. One thing worth calling out in docs (not a bug): `runWorktreeSetupHook` happily executes any path the config points at — the threat model rightly assumes the config author is trusted, but readers of the README should be reminded that enabling setup hooks for an untrusted repo runs that repo's code. Tests144 test files moved/added under repo-root `test/`. Coverage looks comparable to what was removed from `tests/sdk/` and `tests/services/`, but a few cases I'd like to see explicitly:
Nits
Happy to dive into any of these in a follow-up. |
Remove the stage subagent helper and pi.callTool adapter surface so workflows rely on prompt/complete sessions and direct orchestration helpers. Assistant-model: ChatGPT
Review —
|
Forward host theme, tool expansion state, and editor factories into attached workflow stage chats so stage sessions match the parent Pi UI. Use Atomic's SDK resource loader path for stage sessions and cover inherited UI helpers with unit tests. Assistant-model: OpenAI Codex
|
Code Review — refactor(monorepo)!: restructure workspace with @bastani/workflows Reviewed at a depth proportional to the parts of the diff most likely to bite in production. Skipped surface-level commentary on the 2k+ deleted skill files. The new Overall this is a large, well-structured rewrite with strong test discipline (1457 passing tests, real-implementation fakes, deterministic race tests). The findings below are issues worth resolving before/shortly after merge — none invalidate the architectural direction. Security — High Priority 1. Path traversal -> arbitrary code execution via workflow discovery config
Mitigations to consider: normalize paths and require them to be under the project root or a per-user allowed-roots list; refuse paths containing 2. Stage subagent privilege model
3. Persistence may capture secrets
(continued in replies — correctness, code quality, performance, tests, CI, and strengths) |
|
Correctness — Concrete Bugs 4. AbortController registration is skipped for foreground runs that pass a signal — The guard 5. SDK errors silently swallowed by pause/resume retry loop — The catch block reads 6. Worktree cleanup leak on early abort —
7. Concurrency limiter waiters never reject on abort —
8. Extension lifecycle subscription leaks —
9. HIL form required check bypassed for non-string typed inputs — The empty-required guard only checks 10. Slash command single-quote values are not unwrapped —
11. delivery === "result" is dropped by shouldEmitControl —
12. pauseRun snapshot timing —
13. disposeCurrentSession race window — Sets 14. Minor — known DAG diamond-merge bug not pinned by a failing/skipped test The PR description acknowledges |
|
Code Quality / CLAUDE.md Compliance CLAUDE.md forbids
Duplicated truncation helpers — Unbounded form-state map — @bastani/atomic import in wiring — Error swallowing in diffWorktrees — Performance — TUI Hot Paths
|
|
Test Coverage Generally strong — 1197 unit + 260 integration tests, real
Workspace / CI / Config
Notable Strengths Worth Highlighting
Happy to dive deeper on any individual finding. The path-traversal in workflow discovery (Security #1), the AbortController guard bug (Correctness #4), the SDK error swallow (Correctness #5), and the worktree leak on early abort (Correctness #6) are the four items I would most want fixed before this lands as the new default. |
Validate Atomic release and prerelease versions consistently across the bump script and publish workflow, and ensure workspace packages remain lockstep/private as expected. Assistant-model: ChatGPT
Review — refactor(monorepo)!: restructure workspace with @bastani/workflowsThanks for the huge effort here. The new package split, Blockers (please address before merge)
Code quality
Performance
Correctness / sharp edges
Test coverage
Nice things
Happy to dig deeper on any of these. |
) * docs(specs): add pi-workflows extension + Atomic rebrand specs Two sequenced specs for the full v1 rewrite from an empty repo: - specs/2026-05-11-pi-workflows-extension.md (Spec 1): the publishable pi-workflows npm package - sibling of pi-subagents/pi-mcp-adapter/pi-intercom. Mirrors pi-subagents' file layout; reauthors the v0.x Atomic SDK's TUI graph engine as a shared overlay/widget under pi-tui. - specs/2026-05-11-atomic-pi-coding-agent-rewrite.md (Spec 2): the Atomic pi-coding-agent rebrand. Fork is package.json-only; bundled skills, MCP server configs, sub-agents, prompts, themes ship inside the atomic binary. Both specs anchor to a single git wipe (tag v0.x-archive on prior HEAD; wipe on branch rewrite/clean-slate) and use git-history cross-references only. Includes the research notes that informed the design. * feat(pi-workflows): implement Phase C DAG executor - Add store-types.ts: RunStatus, StageStatus, ToolEvent, StageSnapshot, RunSnapshot, StoreSnapshot types - Add store.ts: mutable singleton store with subscribe/version counter, createStore() factory - Add runs/shared/graph-inference.ts: GraphFrontierTracker for inferring DAG parents from JS execution order (sequential, parallel, fan-in patterns) - Add runs/sync/stage-runner.ts: StageContext factory with prompt/complete/subagent adapters - Add runs/sync/executor.ts: main run() executor with input resolution, lifecycle callbacks, and DAG tracking - Add tests for graph-inference and executor (154 tests total pass) - Export all new public APIs from index.ts * feat(pi-workflows): implement Phase D persistence + restore - src/persistence/session-entries.ts: appendRunStart, appendStageStart, appendStageProgress, appendStageEnd, appendRunEnd helpers with PersistenceAPI structural type; setLabel for wf:<name>:<short-id> labels - src/persistence/restore.ts: scanInFlightRuns (pure scan), restoreOnSessionStart with ask/auto/never modes, stage snapshot rebuild from session entries - src/persistence/compaction-policy.ts: installCompactionHook for session_before_compact; re-appends run.start + pending stage.start entries - src/runs/detach/status.ts: statusRuns, killRun, killAllRuns, resumeRun helpers - extension/index.ts: ExtensionAPI gains appendEntry/setLabel/appendCustomMessageEntry/ on/sessionManager; factory wires session_start restore + compaction hook; /workflow status/kill/resume slash commands and tool actions use real helpers - 78 new unit tests across 4 test files; 306 total pass, 0 fail cross-ref: spec §5.6, §5.13, §8.1 Phase D * feat(pi-workflows): implement overlay graph TUI module Create all tui module files for the overlay graph component: - layout.ts: DAG layout engine with BFS column assignment - connectors.ts: box-drawing connector helpers (buildConnector, buildMergeConnector) - status-helpers.ts: statusColor, statusIcon, fmtDuration pure helpers - color-utils.ts: lerpColor for hex color interpolation - graph-theme.ts: deriveGraphTheme from generic theme tokens - node-card.ts: multi-line stage card string renderer with ANSI colors - edge.ts: connector edge renderer between layout nodes - header.ts: header band with run name, status counts, elapsed time - switcher.ts: '/' popup stage jump list with filtering - toast.ts: nvim-style notification toast manager and renderer - graph-view.ts: GraphView class with overlay/widget modes, keyboard nav - renderers.ts: re-exports integration point for extension renderers Add 42 unit tests covering: - computeLayout (single node, empty, linear chain, parallel branch, coordinates) - buildConnector (basic, reversed, equal positions) - buildMergeConnector (single source, multi-source, empty) - statusColor and statusIcon for all statuses - fmtDuration (0ms, 45s, 1m24s, 3h2m, 1m, 1h) - GraphView keyboard navigation (j/k/gg/q/Escape/ArrowKeys/switcher) All 42 tests pass, typecheck clean. * feat(pi-workflows): inject WorkflowUIAdapter via RunOpts.ui with per-primitive unavailable fallback - Add WorkflowUIAdapter type alias to shared/types.ts (compatible with WorkflowUIContext) - Add ui?: WorkflowUIAdapter field to RunOpts - Replace generic makeUIContext() stub with makeUnavailableUIContext() producing precise per-primitive error messages: 'pi-workflows: HIL ctx.ui.<primitive> is unavailable because pi runtime did not provide a UI adapter' - Executor wires ctx.ui = opts.ui ?? makeUnavailableUIContext() - Add 9 HIL tests: delegate (input/confirm/select/editor) + fallback rejection per primitive + no-HIL regression * feat(pi-workflows): artifact build — emit index.js, index.d.ts, extension/index.js with verifier * feat(pi-workflows): implement workflow discovery module Add src/extension/discovery.ts with discoverBundledWorkflows(): - Statically imports bundled manifest (deep-research-codebase, ralph, open-claude-design) — no risky runtime TS loader - Validates each definition: object, __piWorkflow true, name non-empty, normalizedName present, run function - First-seen-wins duplicate policy; emits DUPLICATE_NAME warn diagnostic - Invalid exports emit INVALID_DEFINITION error diagnostic - Returns DiscoveryResult { registry, sources, errors } - DiscoverySource: { id, kind: 'bundled', name } Add test/unit/discovery.test.ts — 19 tests covering: - Happy path: all 3 builtins registered, no errors - sources array shape, uniqueness, kind='bundled' - registry.get / registry.all / registry.names integrity - DiscoveryDiagnostic type shape for both error codes - Immutability contract (register returns new registry) * test(pi-workflows): add artifact import smoke tests for dist contract Cover: - package.json manifest field contract (main, types, exports import/types, pi.extensions) - dist/index.js dynamic import → defineWorkflow and createRegistry are functions - createRegistry() returns object with register/get - defineWorkflow() returns builder with description/input/run/compile - dist/extension/index.js default export is function (extension factory) - extension factory callable with pi-like stub without throwing All 446 tests pass (14 new in artifact-import-smoke.test.ts). * feat(pi-workflows): implement extension runtime dispatcher - Add src/extension/dispatcher.ts: WorkflowDispatcher dispatches list/inputs/run through registry + executor. No broad catch; input validation errors propagate; not-found run returns structured failed result (not throw) so tool consumers get honest status:'failed'. - Add src/extension/runtime.ts: ExtensionRuntime facade owns registry + dispatcher. Accepts external registry (discovery worker seam) or definitions array via ExtensionRuntimeOpts. - Update render-result.ts: WorkflowToolResult run variant now carries name?, result?, error?, stages? + legacy message? for backward compat. renderResult updated to handle new fields gracefully. - Wire extension/index.ts: factory creates ExtensionRuntime; makeExecuteWorkflowTool closure delegates list/inputs/run to runtime.dispatch(); slash /workflow list reads runtime.registry.names(); doctor reports real registry count. - Add runtime.test.ts: 18 tests covering list/inputs/run dispatch, structured not-found, execution failure, input validation propagation, renderResult rendering, createExtensionRuntime seeding. * feat(pi-workflows): implement real /workflows-doctor diagnostics via discoverBundledWorkflows - Add doctor.ts with buildDoctorReport(discovery, siblings) pure function Reports: registry count, bundled sources with kind/id/name, discovery diagnostics (INVALID_DEFINITION / DUPLICATE_NAME), sibling availability (pi-subagents via pi.subagents, pi-mcp-adapter via pi['mcpAdapter'], pi-intercom via pi.setSessionName presence) - Wire /workflows-doctor execute handler in index.ts to call discoverBundledWorkflows() and buildDoctorReport() — removes all stubs - Remove hardcoded stub lines: 'availability check not yet wired', 'Executor: wired (Phase C DAG executor)', 'Config: defaults in effect' - Add test/integration/doctor.test.ts: 27 focused tests covering header structure, registry count, bundled sources, diagnostics, sibling flags, and end-to-end execute via mock ExtensionAPI All 476 tests pass. Zero type errors. * feat(pi-workflows): implement slash dispatch — /workflow <name> run, aliases, completions, inputs - Add parseWorkflowArgs(): parses key=value pairs and JSON object tokens - Add ADMIN_SUBCOMMANDS set; non-admin first token resolved as workflow name - /workflow <name> [key=value...] dispatches runtime.dispatch run action - /workflow inputs <name> dispatches inputs action; shows schema or not-found + available - Unknown workflow prints 'Workflow not found: <name>' + available names - getArgumentCompletions includes both admin subcommands and workflow names from registry - Register /workflow:<name> alias per discovered workflow (deep-research-codebase, ralph, open-claude-design) - Export parseWorkflowArgs for testability - 20 new slash-dispatch tests: parseWorkflowArgs, alias registration, completions, dispatch paths - All 496 tests pass; typecheck clean * test(pi-workflows): add runtime behavior tests for tool list/inputs/run and slash dispatch - tool list: assert bundled names (deep-research-codebase, ralph, open-claude-design) returned - tool inputs: assert deep-research-codebase schema has prompt (required text) and max_partitions (number, default 4) - tool run: assert non-placeholder runId (real UUID), terminal status, stages array; honest failed+error when adapters missing, no stub text - slash aliases: workflow:deep-research-codebase, workflow:ralph, workflow:open-claude-design registered with descriptions - completions: include all admin subcommands + bundled workflow names; filter by partial prefix - /workflow deep-research-codebase prompt=test dispatches run not unknown-subcommand - /workflows-doctor: real count >=3, no 'Phase B stub'/'Executor: not yet implemented', names all three bundled workflows 511 tests pass, 0 fail * feat(pi-workflows): implement workflow CLI flags — parseWorkflowFlags, registerWorkflowCliFlags, runWorkflowFromCliFlags - Parse --workflow=<name> and --workflow <name> (space-sep) - Parse --workflow-input-<key>=<value> and --workflow-input-<key> <value> - JSON-parse values: numbers, booleans, objects; fallback to string - Standalone --workflow-input-<key> (no value) → true - Returns { handled: false } when --workflow absent - Dispatches action:run via ExtensionRuntime.dispatch - Returns { handled: true, status: completed|failed, result?, error? } - Real errors from dispatch propagate as status:failed (no silent swallow) - 21 tests passing, typecheck clean * feat(pi-workflows): implement workflow extension config loading with CONFIG_INVALID diagnostics - Add config-loader.ts helper under src/extension/ - Reads project-local (.pi/extensions/workflow/config.json, .pi/agent/extensions/workflow/config.json) and global (~/.pi/agent/extensions/workflow/config.json) config files - Parses optional workflows: { [name]: { path } } map - Invalid JSON or invalid shape produces CONFIG_INVALID diagnostic (not silent success) - Missing files silently skipped; no broad catch swallowing errors - Project-local overrides global on merge; workflows map merged key-by-key - 25 unit tests covering all branches (missing, valid, invalid JSON, invalid shape, merge, priority) * test(pi-workflows): remove dist-dependent integration tests from verify-artifact Unit test suite now passes from clean checkout without bun run build. Removed three tests from 'verify-artifact integration — actual dist' describe block that imported real dist files (all package.json paths present, public API exports, extension factory). These required a built dist and caused bun test to fail on a clean checkout. Real dist verification is already covered by scripts/verify-artifact.ts which is invoked as step 5 of scripts/build.ts. A comment in the test file documents this division. 6 fixture-based unit tests remain, all using temp directories. * test(pi-workflows): add CLI flag regression tests — prompt/max/dryRun/options inputs, camelCase keys, dispatch action:run payload verification * feat(pi-workflows): implement workflow module imports — RFC-compliant discovery Changes to src/extension/discovery.ts: - scanWorkflowDir: add .mjs and .cjs support (was only .ts/.js) - importWorkflowFile: collect default export AND named exports (was OR logic) — default checked first; named exports always traversed regardless — enables multi-workflow files and preserves RFC §5.12 default-first order - loadFromPaths: accept string[] | Record<string,string> so settings entries can carry a configuredName (named-map → configuredName populated in source) - DiscoverySource: add optional configuredName field for settings-named entries - DiscoveryConfig: widen projectWorkflows/globalWorkflows to string[] | Record<string,string> - validateConfig: validate both array and named-map shapes - discoverWorkflows: fix precedence per RFC §5.12 settings-project > project-local > settings-global > user-global > bundled (was: project-local > settings-project > user-global > settings-global) - discoverWorkflows: user-global scans ~/.pi/agent/workflows/ (RFC canonical) (was: ~/.pi/workflows/) - discoverWorkflows: guard settings loading when CONFIG_INVALID (prevent crash on bad config) - All diagnostics preserved: IMPORT_FAILED, INVALID_DEFINITION, PATH_NOT_FOUND, CONFIG_INVALID New test/unit/discovery-module-imports.test.ts (28 tests, all pass): - Extension coverage: .js, .mjs, .cjs, unsupported extension filtering - Default+named export collection and default-wins-on-conflict behavior - IMPORT_FAILED on syntax error, non-blocking for sibling files - PATH_NOT_FOUND for missing config paths, non-blocking for other paths - configuredName populated/absent per source kind - filePath set for fs-loaded, undefined for bundled - Precedence tiers verified with conflict scenarios - User-global path at ~/.pi/agent/workflows/, missing dir silent 588 tests pass, 0 fail. tsc --noEmit clean. * test(pi-workflows): add discovery regression tests for discoverWorkflows Cover all discovery sources and edge cases: - project-local: .pi/workflows/ scanned, kind=project-local, filePath set - user-global: homeDir/.pi/agent/workflows/, kind=user-global - configured projectWorkflows: string array (no configuredName) and named map (configuredName set) - configured globalWorkflows: string array and named map, kind=settings-global - invalid exports: null default → INVALID_DEFINITION, missing __piWorkflow sentinel - PATH_NOT_FOUND for missing configured path - CONFIG_INVALID for bad config structure - DUPLICATE_NAME precedence: settings-project > project-local > settings-global > user-global > bundled - includeBundled flag: true loads builtins, false excludes them 47 tests total (28 new + 19 existing), 0 failures * feat(pi-workflows): wire extension registry startup with async unified discovery - Replace discoverBundledWorkflowsSync-only startup with mutable runtimeRef + runtimeProxy pattern - Start discoverWorkflows() async immediately in factory; swap runtimeRef.current on resolve - Proxy delegates all registry/dispatch calls to runtimeRef.current — all closures stay current without re-registration - Bundled aliases registered synchronously (preserves backward compat); project-local/user-global aliases registered after async discovery - Replace manual pi.registerFlag block with registerWorkflowCliFlags(pi) from cli-flags.ts - Wire runWorkflowFromCliFlags via pi.on('session_start') startup hook; fallback to discoveryPromise.then() when pi.on absent - Fix /workflows-doctor to use discoveryRef.result (unified registry) when available, fallback to discoverBundledWorkflows - Preserve ExtensionAPI compatibility; all 616 tests pass * test(pi-workflows): add custom registry integration tests — prove shared registry across tool, slash commands, doctor, CLI - discoverWorkflows with temp project-local + user-global dirs yields registry containing both custom and bundled workflows - ExtensionRuntime.dispatch action=list/inputs/run sees custom workflow names - buildDoctorReport shows [project-local] and [user-global] sources from discovery - runWorkflowFromCliFlags dispatches custom workflow via same runtime - /workflow slash command list + completions reflect shared runtimeProxy.registry - /workflow:<name> alias execute routes through same dispatch path as tool - end-to-end invariant: tool list count, doctor registry count, and CLI dispatch all reflect the same registry object (36 new tests, 0 fail) * feat(pi-workflows): emit dist/workflows — fix builtin imports, build to dist/workflows, update package metadata - workflows/*.ts: import from 'pi-workflows' instead of '../src/index.js' - scripts/build.ts: add bun build step for workflows → dist/workflows/ with --external pi-workflows - tsconfig.build.json: add paths alias pi-workflows → ./src/index.ts for tsc declaration emit - package.json: files ['dist','README.md','LICENSE'], pi.workflows ['./dist/workflows'] - scripts/verify-artifact.ts: verify pi.workflows directories in artifact check - build + 652 tests pass * feat(pi-workflows): wire config loader into extension factory before discovery - Import loadWorkflowConfig + ConfigLoadResult from config-loader.ts - Chain loadWorkflowConfig() → discoverWorkflows() so config.workflows paths are passed as DiscoveryConfig.projectWorkflows (settings-project) - Store ConfigLoadResult in configLoadRef for future doctor-config task - Apply config-driven defaults for persistRuns and resumeInFlight in restoreOnSessionStart (was hardcoded); await discoveryPromise in session_start handler so tunables are resolved before restore runs - defaultConcurrency, maxDepth, statusFile retained in config for future consumer wiring (config-translation task) * feat(pi-workflows): extract toDiscoveryConfig helper — map WorkflowExtensionConfig.workflows to DiscoveryConfig.projectWorkflows - Add pure exported toDiscoveryConfig(config: WorkflowExtensionConfig): DiscoveryConfig to config-loader.ts; maps {[name]: {path}} → {projectWorkflows: {[name]: path}}; returns {} when workflows absent/empty - Import DiscoveryConfig type from discovery.ts in config-loader.ts - Replace inline translation block in extension/index.ts factory with toDiscoveryConfig call - Add config-loader.test.ts with 7 unit tests covering: empty config, empty workflows, single entry, multiple entries, projectWorkflows-only output, field isolation, type shape * feat(pi-workflows): extend /workflows-doctor with config diagnostics, tunables, and configured workflow entries * feat(pi-workflows): strengthen artifact verifier — reject src-leaking workflow imports - verify-artifact.ts: add check 2 — scan all dist/workflows/*.js for ../src/ or /src/index.js forbidden patterns; exit 1 if found - verify-artifact.ts: add check 3 — extract all import/require specifiers from each workflow JS and flag any relative path that escapes dist/ or resolves into a src/ directory tree - package.json: add verify-artifact script so verifier is runnable standalone - verify-artifact.test.ts: add 22 new unit tests covering scanWorkflowForSrcImports, extractImportSpecifiers, findLeakyRelativeImports (pure helpers inlined to keep test suite build-state-independent) - All 702 existing tests pass; verifier exits 0 against current dist * feat(pi-workflows): wire runtime adapters — buildRuntimeAdapters from pi.exec surface - Create src/extension/wiring.ts with buildRuntimeAdapters(pi: RuntimeWiringSurface): StageAdapters - Adapters spawn `pi --mode json -p <text> --no-session` via pi.exec() and parse NDJSON - extractAssistantText: scans backward through NDJSON for last message_end with role=assistant - complete adapter: forwards CompleteStageOpts.model as --model flag - subagent adapter: prefixes agent name + context into task prompt - Graceful degradation: returns {} when pi.exec absent (stage-runner errors still fire) - Add exec? to ExtensionAPI structural interface - Pass adapters into both createExtensionRuntime() calls (initial bundled + async discovered) - Preserve runtimeProxy swap behavior — adapters captured once, stable across registry swaps - 25 new wiring tests covering all adapter paths, error handling, arg construction - All 727 tests passing * test(pi-workflows): add withWorkflowDefaults — implement helper and 29 unit tests for config defaults - Add WORKFLOW_CONFIG_DEFAULTS export (maxDepth:4, concurrency:4, persistRuns:true, statusFile:false, resumeInFlight:ask) - Add WorkflowEffectiveConfig interface — all tunable fields concrete - Implement withWorkflowDefaults(config) — fills absent fields with RFC defaults, passes workflows through, does not mutate - Update config-loader.test.ts imports to include withWorkflowDefaults, WORKFLOW_CONFIG_DEFAULTS - Add 22 new tests across 5 describe blocks: empty config applies all defaults, explicit values preserved, partial config, immutability, constants validation - Total: 749 pass 0 fail (up from 702) * test(pi-workflows): add artifact shape tests — dist/workflows existence, imports, src-leak scan, installed package simulation * test(pi-workflows): add runtime-wiring integration tests — adapters invoked through dispatch path - Mock ExtensionAPI with exec surface → exec spy called during deep-research-codebase dispatch - Assert no 'prompt adapter not configured' failure (adapters take priority over test stub) - Pre-discovery: createExtensionRuntime with sync bundled registry + adapters → prompt/complete called - Post-discovery: swapped runtime (discovered registry, same adapters) → adapters still invoked - Cross-runtime: same adapters object works in both initial and swapped runtimes - No exec surface → test-env stub fires, exec NOT called (degraded runtime baseline) - E2e: factory workflow tool dispatch (initial runtime) → exec called immediately - 18 tests, 40 assertions, 0 failures * feat(pi-workflows): thread ui adapter through dispatcher — add ui?: WorkflowUIAdapter to DispatcherOpts, forward into run() call * feat(pi-workflows): add WorkflowUIAdapter option to ExtensionRuntimeOpts — forward ui through dispatch to executor * test(runtime): cover confirm/select/editor UI primitives through createExtensionRuntime dispatch Add 6 tests to WorkflowUIAdapter runtime forwarding suite: - confirm primitive forwarded via runtime dispatch, captures message, returns value - confirm fallback: fails with 'ui.confirm is unavailable' when no ui provided - select primitive forwarded via runtime dispatch, captures message + options, returns pick - select fallback: fails with 'ui.select is unavailable' when no ui provided - editor primitive forwarded via runtime dispatch, captures initial content, returns result - editor fallback: fails with 'ui.editor is unavailable' when no ui provided All 27 tests pass (was 21). No production code changes. * feat(pi-workflows): build WorkflowUIAdapter from pi ctx.ui extension surface - Add PiUIDialogOptions, PiUISurface, UIWiringSurface structural types to wiring.ts - Add buildUIAdapter(pi) that maps pi.ui.input/confirm/select/editor to WorkflowUIAdapter; returns undefined when pi.ui absent (executor fallback intact) - Extend ExtensionAPI.ui in index.ts with PiUISurface (intersection type, optional) - Call buildUIAdapter(pi) in factory; thread ui into both sync initial and async discovery createExtensionRuntime calls - 18 unit tests covering absent/degraded surface, all four dialog methods, dismissed fallbacks, and full-surface integration sequence * feat(pi-workflows): report HIL adapter availability in /workflows-doctor - Add hil: boolean to DoctorSiblingStatus interface - Render 'hil — available/unavailable' in buildDoctorReport siblings section - Set hil: pi.ui !== undefined in index.ts doctor command handler - Add focused doctor.test.ts (7 unit tests) - Fix existing integration test fixtures to include hil field * test(pi-workflows): cover extension entrypoints with mocked pi.ui — tool, slash, alias, CLI flag paths all proved HIL-capable * feat(pi-workflows): add CancellationRegistry — register/registerChild/abort/abortAll/unregister/isAborted with 24 tests * feat(pi-workflows): harden terminal store state — terminal guard, boolean return, error param, WorkflowNotice APIs - recordRunEnd now returns boolean: true if state changed, false if run not found or already in terminal state (completed|failed|killed) - Terminal guard: completed/failed/killed statuses cannot be overwritten - result stored only for completed; error stored only for failed/killed - Add WorkflowNotice model to store-types: id, runId?, stageId?, level, message, createdAt, requiresAck?, ackedAt? - Add notices() accessor, recordNotice(notice), ackNotice(id): boolean to Store - StoreSnapshot now includes notices field - Propagate errorMessage through recordRunEnd in executor (failed path) - Fix overlay-graph and widget-rendering test mocks for updated Store interface - Add 30 focused tests in store-terminal-guard.test.ts (all passing) * feat(pi-workflows): add shared runtime ports — StageOptions, WorkflowMcpPort, WorkflowPersistencePort, WorkflowOverlayAdapter, RunOpts port fields - StageOptions + StageMcpOptions in shared/types.ts: per-stage MCP allow/deny - WorkflowRunContext.stage(name, options?) backward-compat optional param - WorkflowMcpPort: abstract setScope/clearScope adapter (no hard dep on integrations/mcp) - WorkflowPersistencePort: abstract appendEntry/setLabel/appendCustomMessageEntry port - WorkflowOverlayAdapter in store-types.ts: show(notice)/hide() backed by existing WorkflowNotice - RunOpts extended: persistence, mcp, cancellation (CancellationRegistry), overlay, signal (AbortSignal) - Executor wires StageOptions.mcp → WorkflowMcpPort.setScope/clearScope around stage execution - Fix pre-existing TS2540: WorkflowNotice.message readonly → mutable (consistent with ackedAt) - 14 new unit tests covering all new ports and MCP wiring paths * feat(pi-workflows): apply MCP stage scoping — mcpScope on StageSnapshot, set/clear order, focused tests * feat(pi-workflows): wire lifecycle persistence — appendEntry calls in executor for run.start/stage.start/stage.end/run.end with terminal guard * feat(pi-workflows): route intercom decisions — buildIntercomCallbacks wires need_decision confirm+emit+ack, notify store notice, unknown warning; 16 tests * feat(pi-workflows): report sibling, UI, persistence, abort capabilities in /workflows-doctor Extend DoctorSiblingStatus with 6 new fields: - subagentsCallable: pi.subagents has at least one callable method - mcpScopeEvents: pi.events.emit present (mcp.scope.set dispatchable) - uiCustom: pi.ui.custom is a function (custom overlay UI available) - shortcut: pi.registerShortcut is a function (keyboard shortcuts available) - execAbortable: pi.exec is a function (abortable subprocess execution) - persistenceAppendEntry: pi.appendEntry is a function buildDoctorReport now renders 'Capabilities:' section instead of 'Siblings:': - pi-subagents: available (callable) / available / not detected - pi-mcp-adapter: available / not detected - mcp scope evts: known / unknown - pi-intercom: present / not detected - hil: available / unavailable - ui.custom: available / unavailable - shortcut: available / unavailable - exec abortable: yes / unavailable - persistence: appendEntry available / unavailable ExtensionAPI updated: - exec opts param added (signal, timeout) for AbortSignal support - registerShortcut added - ui.custom added Tests: 20 unit tests in doctor.test.ts, integration tests updated. All 971 pi-workflows tests pass. * feat(pi-workflows): replace subagent exec fallback with pi-subagents delegation buildRuntimeAdapters().subagent() now delegates via pi-subagents public surface: - Primary: pi.subagents.run({ agent, task, context, env, signal }) - Secondary: pi.callTool('subagent', { action: 'run', ... }) - Missing both surfaces throws exact error: 'pi-workflows: subagent delegation requires pi-subagents — install npm:pi-subagents and restart pi.' - Never falls back to pi --mode json exec subprocess for subagent() - Workflow env vars (PI_WORKFLOW_RUN_ID, PI_WORKFLOW_STAGE_ID) injected into env - assertSubagentsPresent error message updated to exact RFC text - Tests: 11 new tests prove no exec fallback, delegation priority, exact error * feat(pi-workflows): expose graph overlay — F2 shortcut, /workflow resume, WorkflowGraphOverlayAdapter - Add PiCustomOverlayHandle + PiCustomOverlayOpts to wiring.ts - Add registerShortcut (already existed; wire F2 in factory with correct opts shape) - Build tui/overlay-adapter.ts: buildGraphOverlayAdapter using GraphView + pi.ui.custom - Factory: build overlay, register F2 → overlay.open(activeRunId), update ui.custom type - /workflow resume: call overlay.open(runId) after successful resumeRun() - Expand DoctorSiblingStatus: subagentsCallable, mcpScopeEvents, uiCustom, shortcut, execAbortable, persistenceAppendEntry - Fix pre-existing type errors: WorkflowToolArgs optional fields, remove FallbackResult from union, RuntimeWiringSurface.subagents: unknown, dispatcher name/inputs normalization - 20 new tests in test/integration/overlay-entrypoints.test.ts; 998 total pass * test(executor): add abort wiring tests - abort signal aborts in-flight stage, run finishes as aborted - later resolution does not overwrite terminal status * test(overlay): add /workflow resume happy-path integration tests — RFC regression gate * feat(pi-workflows): wire persistence through runtime and dispatcher layers - Add persistence?: WorkflowPersistencePort to ExtensionRuntimeOpts - Add persistence?: WorkflowPersistencePort to DispatcherOpts - createExtensionRuntime carries persistence into dispatch call - dispatch run action passes persistence to executor run() - Preserves existing behavior when undefined * feat(pi-workflows): persist kill controls — append workflow.run.end on killRun/killAllRuns * test(pi-workflows): no-duplicate workflow.run.end when external killRun races executor abort path * test(pi-workflows): cover persistence forwarding through runtime dispatch - runtime.test.ts: add 'WorkflowPersistencePort — runtime persistence forwarding' describe block with 4 tests verifying createExtensionRuntime({ persistence }) forwards port through dispatch → executor; asserts full lifecycle order (run.start → stage.start → stage.end → run.end), run.start payload shape, and graceful no-op when persistence omitted - dispatcher.test.ts: add 'dispatch run forwards persistence' describe block with 4 tests verifying dispatch() passes persistence into run(); asserts appendEntry called for lifecycle events, full ordered sequence, no-crash when omitted, and DispatcherOpts type accepts persistence field - 43 focused tests pass; 1024/1024 suite clean * feat(pi-workflows): adapt extension persistence — config-gated WorkflowPersistencePort in factory - Add makePersistencePort(pi, persistRuns): returns undefined when persistRuns false or pi.appendEntry absent; binds appendEntry, optional setLabel, optional appendCustomMessageEntry - Wire into initial bundled createExtensionRuntime() using WORKFLOW_CONFIG_DEFAULTS.persistRuns - Wire into async discovered createExtensionRuntime() using resolved config.persistRuns (avoids stale default) - Import WORKFLOW_CONFIG_DEFAULTS and WorkflowPersistencePort - Export makePersistencePort for testability - 12 new unit tests in persistence-port.test.ts covering all gates and slot bindings * test(pi-workflows): assert tsconfig.json and tsconfig.build.json path mappings for pi-workflows * feat(pi-workflows): add declaration and external-import guardrails to artifact verifier - verify-artifact.ts: add isMissingTypesDeclaration — fails when main is declared but types is absent (dist/index.d.ts required for TS consumers) - verify-artifact.ts: add findBundledMainImports — flags workflow JS files that import pi-workflows via relative path (../index.js) instead of bare 'pi-workflows' specifier, catching missing --external pi-workflows during bundling - verify-artifact.ts: wire both checks into runtime (check 0b + check 4) with clear diagnostics - test/unit/verify-artifact.test.ts: 17 new focused tests across isMissingTypesDeclaration, findBundledMainImports, and end-to-end simulation suites (48 total, 0 fail) * fix(pi-workflows): make workflowParameters name and inputs optional Align TypeBox schema with WorkflowToolArgs interface. Allows { action: 'list' } and { action: 'status' } without name or inputs. * feat(pi-workflows): wire WorkflowMcpPort through ExtensionRuntimeOpts and DispatcherOpts to executor * feat(pi-workflows): build WorkflowMcpPort from ExtensionAPI events in extension factory - Add makeMcpPort(pi) — guards typeof pi.events?.emit !== 'function' → undefined (no-op) - Adapts ExtensionAPI to PiMcpExtensionAPI, delegates setScope → setMcpScope, clearScope → clearMcpScope - Import WorkflowMcpPort from shared/types.ts - Pass mcpPort to both createExtensionRuntime calls (sync bundled + async discovery swap) * test(pi-workflows): assert tool, slash, CLI entrypoints emit mcp.scope.set events - Export makeExecuteWorkflowTool from extension/index.ts for test access - Add test/integration/mcp-entrypoint.test.ts with 9 tests across 3 describe blocks: - Tool entrypoint: makeExecuteWorkflowTool execute emits set+clear mcp.scope.set - Slash entrypoint: runtime.dispatch (what /workflow handler calls) emits set+clear - CLI entrypoint: runWorkflowFromCliFlags emits set+clear via mcpPort - Uses makeMcpPort(pi) with mock pi.events.emit recorder - Defines mcp-restricted fixture workflow with ctx.stage('restricted', { mcp: { allow: ['github'], deny: ['filesystem'] } }) - Asserts clear event has allow:null deny:null per integrations/mcp.ts clearMcpScope * feat(pi-workflows): inject explicit workflow metadata into subagent adapter env - Add SubagentStageMeta { runId, stageId, signal } to stage-runner.ts - Extend SubagentAdapter.subagent(opts, meta?) with optional metadata param - Add runId? and signal? to StageRunnerOpts; stage-runner passes meta to adapter - workflowEnvRecord(meta?) merges explicit meta over ambient process.env fallback without mutating process.env - Forward meta.signal to pi.subagents.run when surface supports it - executor.ts passes runId + ownController.signal to createStageContext per stage - 14 new tests covering both pi.subagents.run and pi.callTool paths * feat(pi-workflows): thread stage execution metadata through StageAdapters - Add stageName field to SubagentStageMeta (runId, stageId, stageName, signal) - Make runId and stageId required fields in SubagentStageMeta - Add runId (required) and signal (optional) to StageRunnerOpts - createStageContext builds SubagentStageMeta and passes to subagent adapter - Export SubagentStageMeta from public index for adapter implementors - wiring.ts: inject PI_WORKFLOW_STAGE_NAME into subagent env from meta.stageName - Update wiring tests to include stageName in SubagentStageMeta fixtures - Preserve public StageContext API (workflow authors unaffected) * test(pi-workflows): regression test for post-stage abort race Add deterministic test covering the window between final stage settling and workflow body returning. Uses a holdWorkflow gate so the abort signal fires exactly after the stage resolves but before def.run(ctx) returns. Asserts: - result.status === 'killed' - store snapshot status === 'killed' - onRunEnd receives 'killed' - persistence appends exactly one workflow.run.end with status 'killed' - no 'completed' workflow.run.end entry exists * test(pi-workflows): add executor-level regression tests for subagent env metadata propagation Covers RFC requirements: - ctx.stage(...).subagent(...) propagates executor-owned runId/stageId into subagent env for both pi.subagents.run and pi.callTool fallback paths - Explicit executor metadata overrides conflicting process.env values - Parallel stages receive same PI_WORKFLOW_RUN_ID and distinct PI_WORKFLOW_STAGE_ID 14 new tests across 4 describe blocks (spy-adapter, subagents.run, callTool, parallel). * feat(pi-workflows): add StageExecutionMeta and signal-aware adapter contracts - Add StageExecutionMeta interface to shared/types.ts (runId, stageId, stageName, signal?) - Update PromptAdapter.prompt(text, meta?) and CompleteAdapter.complete(text, opts?, meta?) - SubagentAdapter.subagent now typed against StageExecutionMeta (was SubagentStageMeta) - SubagentStageMeta kept as deprecated type alias for backward compat - createStageContext builds meta once and threads it into all three adapter calls - wiring.ts: prompt/complete impls accept _meta (ignored, foundation); subagent uses StageExecutionMeta - Export StageExecutionMeta from types.ts and via shared/types wildcard in index.ts * feat(pi-workflows): decouple subagent adapter from pi.exec, add PiExecOpts signal passthrough - Add PiExecOpts interface with signal and timeout fields - Update exec signature: exec(command, args, opts?: PiExecOpts) - runPiJson passes { signal: meta?.signal } to exec when signal present - buildRuntimeAdapters: prompt/complete gated on pi.exec; subagent built independently when pi.subagents.run OR pi.callTool present - Returns {} only when no surfaces available (was: {} when pi.exec absent) * feat(pi-workflows): add runId seam, job tracker, and detached runner core * test(pi-workflows): add adapter propagation tests for stage-runner metadata and wiring buildRuntimeAdapters * feat(pi-workflows): report runtime adapter capabilities in /workflows-doctor Add promptAdapter, completeAdapter, subagentAdapterVia fields to DoctorSiblingStatus. Render new 'Runtime adapters' section in buildDoctorReport showing: - pi.exec: available/unavailable - prompt adapter: configured/unconfigured - complete adapter: configured/unconfigured - subagent adapter: configured via pi.subagents | callTool | unavailable Wire fields in /workflows-doctor handler using same surface checks as buildRuntimeAdapters (pi.exec, pi.subagents.run, pi.callTool). Update all three DoctorSiblingStatus fixtures across unit and integration tests. * feat(pi-workflows): wire --detach/--bg slash flags and detach tool field to runDetached() - WorkflowToolArgs: add detach?: boolean field - workflowParameters TypeBox schema: add detach optional boolean - dispatcher: import runDetached + JobTracker; add DispatcherOpts.jobs?; case 'run' routes to runDetached() when args.detach === true - render-result: add detached?: boolean to RunResult; renderResult case 'run' renders background start message - index.ts: export stripDetachFlags(); strip --detach/--bg from full token list before subcommand resolution in /workflow slash handler; registerWorkflowAlias strips flags and passes detach to dispatch - 18 new tests: stripDetachFlags unit, dispatcher detach routing, /workflow --detach slash integration, workflow:<name> alias --detach * test(pi-workflows): add detached workflow tests (RFC §2, §5, §6, §7) Cover missing RFC test requirements: - RFC §2: stripDetachFlags + parseWorkflowArgs compose for --bg prompt=test, inputs parsed as { prompt: 'test' }, --bg not in parsed inputs - RFC §5: statusRuns lists detached run while delayed stage active; completed run absent from default (in-flight) query; all:true includes it - RFC §6: killRun aborts delayed stage, store records killed terminal state, cancellation controller aborted, ok:false for unknown/already-ended runId, double-kill returns already_ended - RFC §7: throwing workflow rejection swallowed (voidPromise resolves), no unhandledRejection event, store records failed status, job tracker unregistered after settle New file: packages/pi-workflows/src/runs/detach/runner.test.ts (12 tests) Modified: packages/pi-workflows/src/extension/slash-dispatch.test.ts (+4 tests) * chore: add root verify-artifact fan-out script to workspace packages * feat(pi-workflows): update registerCommand to canonical (name, options) shape - Add PiCommandOptions interface with handler field (canonical pi >= 1.x shape) - Update ExtensionAPI.registerCommand to (name: string, options: PiCommandOptions) - Keep registerSlashCommand as explicit legacy compatibility path only - Update tryRegisterSlashCommand: canonical registerCommand call is primary; maps internal execute → handler; legacy registerSlashCommand is fallback - Update test mocks to accept new canonical signature; reconstruct PiSlashCommandOpts internally so existing .execute() call sites unchanged - All 239 tests pass; typecheck clean cross-ref: research/docs/2026-05-11-pi-coding-agent-reference.md §4.2 * feat(pi-workflows): wire intercom callbacks in extension factory Replace no-op stubs with buildIntercomCallbacks in extension/index.ts: - onNotify: records notice via store.recordNotice at payload.level - onNeedDecision: records requiresAck warning notice, surfaces pi.ui.confirm when available, emits intercom:response, acks notice - onUnknown: records warning notice with type in message - emit/confirm deps gated on runtime capability presence - No silent drops; callback errors surface explicitly * fix(pi-workflows): register workflow-input-<key> template name for dynamic input flag contract registerWorkflowCliFlags previously registered the literal name "workflow-input-key", implying pi users should pass --workflow-input-key=value. The actual parser contract is --workflow-input-<key>=<value> (dynamic prefix). Since pi.registerFlag does not support wildcards/prefix patterns, use the angle-bracket template notation "workflow-input-<key>" as the registered name — a standard CLI documentation convention that communicates the dynamic nature. Updated description to explicitly call out the template substitution and repeat usage. Parser (parseWorkflowFlags) unchanged; backward compat preserved. * test(pi-workflows): add end-to-end intercom routing integration tests Wire subscribeIntercomControl + buildIntercomCallbacks together to test store-level behaviour end-to-end for all three event kinds: - notify: records info/warning notice, no ack, no emit - need_decision (confirm unavailable): records requiresAck=true warning, emits accepted=false response, notice acked after response - unknown type: records warning with type name + message, no ack, no emit 28 pass (was 17) in integrations-intercom.test.ts RFC §5.10, §8.1 Phase G * feat(pi-workflows): preserve config scope provenance in toScopedDiscoveryConfig - Add globalConfig/projectConfig fields (optional) to ConfigLoadResult - Add toScopedDiscoveryConfig(globalConfig, projectConfig, opts): DiscoveryConfig - Global entries → globalWorkflows, relative paths resolved under <homeDir>/.pi/agent - Project entries → projectWorkflows, relative paths resolved under projectRoot - Absolute paths kept as-is; overlapping keys: project wins, global entry excluded - Fix discovery.ts: settings-global loadFromPaths uses homeDir not cwd as base - Update index.ts to use toScopedDiscoveryConfig with proper projectRoot/homeDir - Add 15 new tests for toScopedDiscoveryConfig; all 44 config-loader tests pass - Keep toDiscoveryConfig unchanged (deprecated) for backward compat * feat(pi-workflows): add WorkflowRuntimeConfig port and thread through runtime option seams - Add WorkflowRuntimeConfig interface to shared/types.ts with maxDepth, defaultConcurrency, persistRuns, statusFile, optional statusFilePath, resumeInFlight fields - Add config?: WorkflowRuntimeConfig to RunOpts (executor), DetachedRunOpts (detached runner inherits via Omit<RunOpts,...>), DispatcherOpts, and ExtensionRuntimeOpts - Thread config through: createExtensionRuntime -> dispatch -> run/runDetached - Composition root (factory/index.ts): seed runtimeConfigRef from WORKFLOW_CONFIG_DEFAULTS at startup, resolve via withWorkflowDefaults() after async config load, inject into both createExtensionRuntime() calls - Export WorkflowRuntimeConfig from public types.ts entry point - Fix doctor.test.ts ConfigLoadResult constructions to include globalConfig/ projectConfig (required by pre-existing WIP addition to ConfigLoadResult type) - Add runtime-config.test.ts: 10 tests covering type seams and runtime threading * test(pi-workflows): add config provenance regression tests - loadWorkflowConfig: verify globalConfig/projectConfig provenance fields populated from real config files (global/project candidate paths) - toScopedDiscoveryConfig: ./workflows/foo.ts in globalConfig resolves under <homeDir>/.pi/agent; project key override excludes global entry - discoverWorkflows: settings-project/settings-global source kinds distinguished correctly when fed scoped DiscoveryConfig - End-to-end: loadWorkflowConfig → toScopedDiscoveryConfig → discoverWorkflows with override semantics verified (project scope wins on conflict) * feat(pi-workflows): add ConcurrencyLimiter and wire per-run defaultConcurrency into stage executor - Add packages/pi-workflows/src/runs/shared/concurrency.ts: ConcurrencyLimiter semaphore (acquire/release/run) + createRunLimiter factory - Wire limiter into executor.ts wrapMethod: acquire slot before marking stage running, release in finally after tracker.onSettle - createRunLimiter(opts.config?.defaultConcurrency) defaults to 4 when no config - Add concurrency.test.ts: 13 unit tests covering limit enforcement, queue drain, release-on-throw, serialization, and factory defaults - Add 4 executor integration tests: limit=1 serializes, limit=2 caps, default ≤4, slot release on stage failure - All 114 runs/ tests pass; no new typecheck errors * test(pi-workflows): add maxDepth enforcement tests for executor.run - 10 tests covering: depth >= maxDepth fails, depth < maxDepth passes, no config = no limit (backward compat), exact boundary (maxDepth-1 passes, maxDepth fails), pre-allocated runId preserved in failed result, error message includes configured max value - All 10 pass; no new typecheck errors in depth-enforcement.test.ts - executor.ts depth?: number field and guard already present in HEAD * test(pi-workflows): add runtime-tunables integration tests for maxDepth, concurrency, statusFile * feat(pi-workflows): align extension resume surfaces to new ResumeResult shape - Remove not_ended variant from ResumeResult union type - resumeRun returns ok:true snapshot for both active and ended runs; only unknown IDs return ok:false not_found - Tool action resume: success message says 'Snapshot available:' with stages count; failure always 'Run not found' - Slash /workflow resume: calls overlay.open for any ok result; prints 'Run not found' for unknown IDs only; removes 'still active — no resume needed' branch * fix(pi-workflows): resumeRun returns snapshot for active and ended runs Remove not_ended from ResumeResult. resumeRun is now a pure snapshot lookup — returns ok:true for any known runId (in-flight or ended), ok:false reason:not_found only for unknown IDs. Read-only; no store mutation. Deep-copy via JSON.parse(JSON.stringify) preserved. Update unit test: active run now asserts ok:true with running snapshot. Update integration test: /workflow resume on active runId now calls overlay.open (overlay reopen unblocked). * test(pi-workflows): add slash and tool resume regression tests - /workflow resume <runId> with active run: assert overlay.open called (pi.ui.custom invoked with overlay:true) - active resume output does not include 'still active — no resume needed' - makeExecuteWorkflowTool resume against in-flight run returns status:'ok' All 1331 pi-workflows tests pass. * refactor(pi-workflows): convert test files from bun:test to node:test + node:assert/strict Converts all 55 test files in test/unit/ and test/integration/ from bun:test to node:test + node:assert/strict to align with the pi-subagents extension which uses node:test. Changes per file: - Replace `import { ... } from "bun:test"` with `import { ... } from "node:test"` - Add `import assert from "node:assert/strict"` - Rename beforeAll -> before, afterAll -> after (node:test naming) - Replace mock() -> mock.fn() (node:test mock API) - Convert all expect(x).METHOD(y) patterns to assert equivalents - toMatchObject conversions use assert.deepEqual with TODO comments (13 instances) * refactor!: flatten pi-workflows into root oh-my-pi extension Migrate from the atomic monorepo layout (packages/pi-workflows/*) to a single-package repo published as @bastani/atomic-workflows, an oh-my-pi extension loaded directly as raw TypeScript. - Move sources, tests, workflows, examples, and scripts to the repo root; drop the inner package, its tsconfigs, and generated .d.ts artifacts. - Replace Bun tooling (bun.lock, bunfig.toml, packages/*/bunfig.toml) with npm + Node ≥ 22; switch tests to node:test + node:assert/strict driven by test/support/register-loader.mjs and --experimental-transform-types. - Update package.json to the oh-my-pi extension shape (omp.extensions, omp.workflows, raw .ts files in files[]), peer on @oh-my-pi/pi-coding-agent, and expose lint/typecheck/test scripts. - Wire prek-based pre-commit hooks (prek.toml + scripts/install-hooks.mjs) running builtin checks plus npm run lint / npm run test:unit. - Refresh tsconfig.json for the flat layout and rewrite CLAUDE.md / README.md / DESIGN.md / DEV_SETUP.md / PRODUCT.md to document the oh-my-pi integration path. - Add CI workflow (.github/workflows/test.yml), .omp/settings.json, and install.mjs bin entry for host-level extension linking. BREAKING CHANGE: the package previously lived at packages/pi-workflows and shipped a compiled dist/; @bastani/atomic-workflows now ships raw .ts files from the repo root and is loaded by oh-my-pi rather than consumed as a standalone library. Assistant-model: Claude Code * refactor!: migrate to Bun and reshape stage + dispatch APIs Move development, scripts, hooks, CI, and the test runner off Node's --experimental-transform-types loader and onto Bun >= 1.3.7. The `test/support/ts-loader.mjs` + `register-loader.mjs` shims are gone because Bun resolves the `.js` -> `.ts` ESM convention natively. `package-lock.json` is replaced by `bun.lock`, `bunfig.toml` is added, `tsconfig.json` types switch to `bun`, and `install.mjs` is rewritten to use `bunx` + argv-form spawnSync (no shell interpolation). Public surface changes: - `ctx.stage(name, options?)` is now synchronous. Stages register up front; work only starts when a stage method (`prompt`, `complete`, `subagent`, ...) is awaited. The bundled workflows are migrated. - `dispatch({ action: "list" })` returns `items` -- an array of `{ name, description, inputs }` -- instead of the prior `workflows: string[]`. One source of truth for the catalogue renderer and the new workflow-list TUI. - `package.json` declares `exports` for `.` and `./workflows/*` so consumers can import the bundled workflows directly, and adds `engines.bun >= 1.3.7` + `packageManager: bun@1.3.13`. UI + runtime additions: - TUI: new `chat-surface`, `stage-chat-view`, `dispatch-confirm`, `workflow-attach-pane`, `workflow-list`, `keybindings-adapter`, plus refreshed `node-card`, `graph-theme`, and overlay plumbing. - `runs/foreground/stage-control-registry` decouples per-stage controls from the executor. All ~80 affected test files swap `node:test` -> `bun:test`. 1140 unit tests pass under `bun test`; `tsc --noEmit` is clean. BREAKING CHANGE: `dispatch({ action: "list" })` returns `{ items: WorkflowListItem[] }` instead of `{ workflows: string[] }`. Read `result.items` and pull `name`, `description`, `inputs` from each entry. BREAKING CHANGE: development now requires Bun >= 1.3.7. The Node `--experimental-transform-types` test/run path and the `test/support/{register-loader,ts-loader}.mjs` shims are removed. Use `bun install`, `bun run test:unit`, `bun run test:integration`, and `bun run typecheck` instead of the npm equivalents. Assistant-model: Claude * ci: run prek pre-commit checks on push and PR Adds a GitHub Actions job that installs Bun and runs the prek hooks defined in prek.toml (check-* builtins, bun run lint, bun run test:unit) on every push and pull request. Assistant-model: Claude Code * docs(ui): add chat-surface and attach interaction mockups Static HTML mockups under ui/ illustrating the chat-surface layout and the two-step attach interaction, using the Catppuccin Mocha tokens documented in DESIGN.md. Reference material for upcoming TUI/graph theming work; not wired into any build or runtime. Assistant-model: Claude Code * chore(prek): exclude vendored skill JSON refs from check-json Vendored skill references under .agents/skills/ include JSONC-style samples (notably typescript-expert/references/tsconfig-strict.json, which carries comments because it mirrors a real tsconfig). Strict JSON validation rejects them, so scope the check-json hook away from that subtree while keeping it active for the rest of the repository. Assistant-model: Claude Code * chore(agents): vendor OMP skills and sub-agent definitions Add the agent harness assets that AGENTS.md documents but were not yet tracked: skill references under .agents/skills/ (bun, prek, tdd, playwright-cli, typescript-expert, typescript-advanced-types, prompt-engineer, research-codebase, gh-commit, gh-create-pr, create-spec) and sub-agent definitions under .omp/agents/ (code-simplifier, debugger, and the codebase-{analyzer,locator, pattern-finder,research-analyzer,research-locator,online-researcher} family). Each .agents/skills entry pins upstream provenance via frontmatter metadata (github-repo/ref/sha) so contributors load identical content. .omp/agents/ sits next to the already-tracked .omp/settings.json, completing the project-local agent harness config. Assistant-model: Claude Code * refactor!: migrate to pi (≥ 0.74), cascade pause, and structured doctor card Comprehensive migration off the legacy oh-my-pi host onto the renamed `pi` toolchain (npm scope `@earendil-works/pi-coding-agent` / `pi-tui`), plus the cross-cutting type changes that grew out of the same refactor. Host rebrand ------------ * `oh-my-pi` → `pi` (binary, docs, CLI flags, package names, scopes) * `.omp/` → `.pi/` (project- and home-scoped agent dirs, workflows paths, settings keys) * Peer dep moves from `@oh-my-pi/pi-coding-agent` to `@earendil-works/pi-coding-agent` + `@earendil-works/pi-tui`. Both declared as optional peer deps so the package installs cleanly alongside other pi extensions. * Drops the `bunx atomic-workflows` post-publish convenience CLI (`install.mjs`) and the `src/oh-my-pi-shim.d.ts` ambient module shim — superseded by pi's first-class `pi install npm:<pkg>`. * Removes the vendored `.omp/agents/*` sub-agent prompts that lived alongside the legacy oh-my-pi extension shape. pi SDK ≥ 0.74 migration ----------------------- * `createAgentSession` is no longer injected on `ExtensionAPI.pi`; it is now a top-level export from `@earendil-works/pi-coding-agent`. `buildRuntimeAdapters` reaches into the package directly via a lazy dynamic import (`createPiSdkAgentSession`) so the heavy SDK module is only loaded when a stage actually runs. * Stage-session option forwarding no longer pre-fills `disableExtensionDiscovery` / `skills` / `promptTemplates` / `slashCommands` — resource isolation is owned by pi's `SettingsManager` / `ResourceLoader` ctor args. * `installInputInterceptor` returns the new `InputEventResult` shape (`{ action: "handled" }`); the old `{ handled: true }` is silently ignored by pi's runner. * Subagent adapter now sends a schema-compliant `pi.callTool("subagent", args)` payload aligned with pi-subagents v0.24.2 `SubagentParams` — `action: "run"` and `env: …` were not part of the schema and have been removed. `SubagentStageOpts.context` is tightened from `string` to `"fresh" | "fork"`. * Drops the `extractAssistantText` NDJSON parser and `workflowEnvRecord` helpers used by the legacy task-tool fallback (no replacement needed — pi-subagents returns the assistant text directly). Cascade pause (`blocked` stage status) -------------------------------------- * New `StageStatus = "blocked"` plus `StageNotice` event type carried on stage snapshots, and `Store.recordStageBlocked` / `recordStageUnblocked` / `recordStageNotice` write methods. * Executor maintains per-stage release barriers; pausing an ancestor now cascade-pauses running descendants and blocks pending ones until the ancestor resumes (or fails, which propagates as failure). * TUI surfaces the new status: header counter, node-card stripe + `↑ blocked by <upstream>` badge, stage-chat banner copy, graph-view border palette, and a `↑` glyph in `status-helpers`. Structured `/workflows-doctor` chat-surface card ------------------------------------------------ * New `companions.ts` performs structural detection of first-party pi companion extensions (`pi-subagents`, `pi-mcp-adapter`, `pi-web-access`, `pi-intercom`) by inspecting pi's command + tool registries — no `require()` against companion modules, which pi loads in isolated module roots. * `doctor.ts` splits into a typed `buildDoctorPayload(...)` (sections / rows / hints) plus the existing `buildDoctorReport(...)` plain-text fallback (overloaded to preserve the old 3-arg signature for callers that pass `notify`-style strings). * New `src/tui/doctor-card.ts` renders the payload as a chat-surface card with stripes, bands, status glyphs, and `pi install` hint rows for missing companions. * `/workflows-doctor` prefers `pi.sendMessage(...)` when available (interactive runs) and falls back to `ctx.ui.notify(...)` text for RPC / headless harnesses. * Removes the legacy `"task tool"` `subagentAdapterVia` literal — the task tool no longer exists post-SDK-migration; the new values are `"pi-subagents tool"` / `"pi.callTool"` / `"unavailable"`. TUI redesign — stage chat surface --------------------------------- * `StageChatView` rebuilt to the ui/stage-chat-mockup.html visual contract (welcome panel, transcript, two-line footer, dashed hint strip, paused / blocked banners, notice timeline). * `GraphView` gains a 10 FPS animation tick (`requestRender`) so running-stage borders pulse and duration counters tick without a key press; `WorkflowAttachPane` forwards the host render tick and gates it on `graph` mode so the hidden view stays cheap. * `chat-surface.renderTaggedCard` adds an optional `titleSuffix` slot used by the dispatch-confirm redesign. * New mockup HTMLs: `ui/dispatch-mockup.html`, `ui/stage-chat-mockup.html`. Tests ----- * New: `cascade-pause`, `store`, `companions`, `doctor-card`, `executor-subagent-call-shape`, `stage-chat-render-snapshot` (manual visual snapshot). * Renamed: `executor-metadata-propagation` → `executor-subagent-call-shape` (the old name described a behaviour we no longer support). * Updated: `doctor`, `integration/doctor`, `wiring`, `wiring-adapters`, `stage-runner`, `executor`, `discovery`, `config-loader`, `stage-chat-view`, `node-card`, `overlay-graph`, `dispatch-confirm`, and all rebrand-touched suites. BREAKING CHANGE: peer dep renamed from `@oh-my-pi/pi-coding-agent` to `@earendil-works/pi-coding-agent` + `@earendil-works/pi-tui`. Project / user workflow directories move from `.omp/workflows/` to `.pi/workflows/` and config paths move from `.omp/extensions/workflow/config.json` to `.pi/extensions/workflow/config.json`. `SubagentStageOpts.context` is now `"fresh" | "fork"` instead of `string`. The legacy task-tool subagent bridge is gone — pi-subagents companion (or a `pi.callTool`-capable host) is required for stage delegation. Assistant-model: Claude Code * feat(tools): vendor ask_user_question HIL tool from rpiv-mono Adds the `ask_user_question` tool — a structured multi-question dialog with single / multi-select rows, "Type something" free-text fallback, "Chat about this" escape hatch, per-option markdown previews, and a trailing notes field. Headless flows return `{ error: "no_ui" }` cleanly so non-interactive runs do not deadlock. Ported from juicesharp/rpiv-mono (packages/rpiv-ask-user-question — MIT). See `src/extension/tools/ask-user-question/LICENSE.upstream` for the upstream copyright notice. Differences from upstream ------------------------- * i18n is removed entirely. The upstream `state/i18n-bridge.ts`, `locales/*.json`, and the optional `@juicesharp/rpiv-i18n` peer dep are gone. All UI copy is plain English string literals. * Package paths rewritten to live under `src/extension/tools/ask-user-question/` so the tool ships inside this extension rather than as a separate npm package. * No build step — raw TypeScript, consistent with the rest of the repo (Bun loads it directly). Wiring ------ * `src/extension/index.ts` registers the tool at extension factory time once the host exposes `registerTool` and `registerCommand`. * `test/integration/mock-extension-api.test.ts` now expects two registered tools (`workflow` + `ask_user_question`) with the workflow tool first so existing `mock.tools[0]!` shortcuts keep working. Assistant-model: Claude Code * chore(skills): vendor impeccable agent skill Adds the `impeccable` skill (Apache 2.0 — pbakaus/impeccable, based on Anthropic's frontend-design skill) under `.agents/skills/impeccable/`, joining the project-scoped skills already shipped in this repo (`bun`, `gh-commit`, `prek`, `tdd`, `typescript-expert`, etc.). The skill is referenced from `CLAUDE.md` § Tips as one of the recommended skills for working on this extension — it covers UX review, visual hierarchy, accessibility, typography, motion, and the TUI-redesign idioms used by the recent dispatch-confirm / stage-chat work. Assistant-model: Claude Code * chore: vendor `.pi/` project-scoped agent state Brings the project-local pi agent configuration back under version control after the `.omp/` → `.pi/` rename. Mirrors the legacy `.omp/agents/` and `.omp/settings.json` content the rebrand commit deleted, plus the local extension surface and theme set this repo's contributors use. * `.pi/agents/` — sub-agent prompts (`codebase-analyzer`, `codebase-locator`, `codebase-pattern-finder`, `codebase-research-analyzer`, `codebase-research-locator`, `codebase-online-researcher`, `code-simplifier`, `debugger`). * `.pi/extensions/` — project-scoped pi extensions in raw TypeScript (`btw`, `goal`, `multi-edit`, `review`, `todos`, `whimsical`); pi loads these directly from source via the `pi` extension manifest. * `.pi/themes/` — catppuccin-macchiato and catppuccin-mocha theme JSON files used by pi-tui at the project level. * `.pi/settings.json` — project-scoped pi settings overrides. Assistant-model: Claude Code * chore(extensions): remove project multi-edit extension Assistant-model: OpenAI GPT-5 Codex * feat(workflow): add direct execution sdk parity Add pi-subagents-style workflow execution modes, unified workflow details, intercom/result routing, worktree support, and a scriptable SDK entrypoint. Package workflow prompts/skills and include supporting research, specs, UI mockups, and test coverage for the rewritten workflow surface. Remove git-backed worktree tests to avoid nested git test side effects during hooks. Assistant-model: OpenAI ChatGPT * feat(hil): surface in-stage user input state Inject ask_user_question into foreground stage sessions with live pi UI bindings and lifecycle callbacks. Track awaiting_input in the workflow store and render waiting states in graph cards and headers. Assistant-model: OpenAI ChatGPT * test(workflows): add converted example workflow coverage Add project-local fixtures converted from upstream atomic examples and validate discovery plus non-interactive SDK execution for each workflow. Assistant-model: OpenAI ChatGPT * docs(specs): correct fallback models author * feat(workflows): add model fallback support Add ordered fallback model handling for workflow tasks and direct execution, refresh bundled workflow definitions, relocate packaged agents and skills, and render goal state in the footer. Assistant-model: GPT-5 Codex * chore(release): prepare atomic workflows package Finalize the raw TypeScript pi package shape for the initial npm release, including publish automation, bundled resources, release documentation, and the programmatic workflow runner API. Remove obsolete CLI flag entrypoint code and vendored agent reference docs now that workflows are exposed through pi package resources and in-process APIs. Assistant-model: OpenAI ChatGPT * chore: track promo asset with git lfs Store the restored full-resolution promo GIF as a Git LFS object while keeping the repository blob as a small pointer. Assistant-model: OpenAI ChatGPT * test(workflows): remove stale converted example coverage Assistant-model: OpenAI ChatGPT * chore: keep pi rewrite tree over main * fix(ci): address CodeQL workflow alerts * fix(ci): stabilize status writer on Windows * feat: restructure Atomic as bundled monorepo Introduce the Bun workspace layout with packages/coding-agent…
Summary
Restructures the Atomic monorepo from two packages (
packages/atomic+packages/atomic-sdk) into a focused six-package workspace and migrates the runtime requirement from Node.js ≥22 to Bun ≥1.3.14. Introduces@bastani/workflows(packages/workflows) as a pi extension delivering DAG-driven, multi-stage workflow authoring and execution — shipped as raw TypeScript with no compile step.Key Changes
Package & Repository Structure
@bastani/atomic-monorepo(private, Node ≥22) toatomic-monorepo(private, Bun ≥1.3.14)packages/*— examples and test fixtures no longer treated as workspacespackages/atomic+packages/atomic-sdkwith six focused workspace packages:packages/coding-agent→@bastani/atomic— Atomic-branded fork of pi's coding-agent CLIpackages/workflows→@bastani/workflows— pi extension for multi-stage workflow executionpackages/intercom,packages/mcp,packages/subagents,packages/web-access— supporting packages@bastani/workflowsships raw.tssources loaded by pi directly — nodist/build artifactsWorkflow Authoring & Runtime API (
packages/workflows)defineWorkflow,runTask,runParallel,runChain,resolveInputsauthoring primitivesWorkflowModelValidationError), input resolution, worktree support, and runtime config tunables (maxDepth,concurrency,statusFile)ConcurrencyLimiter— semaphore controlling per-run stage parallelismGraphFrontierTracker— infers DAG parent edges from JavaScript execution order for automatic stage dependency detectioncreateStore,store, compaction policy (re-emits in-flight runs across auto-compaction), and restore utilitiesCancellationRegistry— tracks and manages active background run AbortControllers (children aborted before primary)Pi Extension Entrypoint (
packages/workflows/src/extension/)/workflowand/workflows-doctorslash commands with argument completionkill,pause,resume,inspect)ask_user_questionHIL tool vendored from upstreamTUI Surfaces (
packages/workflows/src/tui/)GraphCanvasrenderer, stage chat, inline HIL input formsui/)Built-in Workflows (
packages/workflows/builtin/)deep-research-codebase— multi-wave parallel specialist pipeline (scout + research-history chain, locator/pattern-finder/analyzer/online-researcher waves, aggregator)open-claude-design— design-doc generation workflowralph— autonomous task runner workflowDependencies
@earendil-works/pi-coding-agentand@earendil-works/pi-tuias peer dependencies^0.3.13→^0.4.0CI / DX
test:unit,test:integration,test:alltest/unit/,test/integration/)Breaking Changes
packages/atomic,packages/atomic-sdkpackages/coding-agent,packages/workflows(+ 4 supporting)@bastani/atomic-sdk@bastani/workflowsdist/artifactsdefineWorkflow+ pi extension runtimeValidation
bun run typecheck✅bun run test:unit✅ — 1197 passing tests across 76 filesbun run test:integration✅ — 260 passing tests across 8 filesbun run test:all✅Known Issues / Follow-ups
packages/workflows/package.jsonhas"private": true— remove before publishingGraphFrontierTracker.onSettle()produces incomplete parent edges for diamond-merge DAG patterns (e.g. A→B→D, A→C→D: D gets[C]instead of[B, C])extension/index.ts(2143 lines) andexecutor.ts(1506 lines) should be split in a follow-uptsconfig.jsondroppednoUncheckedIndexedAccess— consider restoring