Skip to content

refactor(monorepo)!: restructure workspace with @bastani/workflows - #936

Merged
lavaman131 merged 121 commits into
mainfrom
refactor/pi-rewrite
May 15, 2026
Merged

refactor(monorepo)!: restructure workspace with @bastani/workflows#936
lavaman131 merged 121 commits into
mainfrom
refactor/pi-rewrite

Conversation

@lavaman131

@lavaman131 lavaman131 commented May 15, 2026

Copy link
Copy Markdown
Collaborator

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

  • Root package renamed from @bastani/atomic-monorepo (private, Node ≥22) to atomic-monorepo (private, Bun ≥1.3.14)
  • Workspace simplified to packages/* — examples and test fixtures no longer treated as workspaces
  • Replaced packages/atomic + packages/atomic-sdk with six focused workspace packages:
    • packages/coding-agent@bastani/atomic — Atomic-branded fork of pi's coding-agent CLI
    • packages/workflows@bastani/workflows — pi extension for multi-stage workflow execution
    • packages/intercom, packages/mcp, packages/subagents, packages/web-access — supporting packages
  • @bastani/workflows ships raw .ts sources loaded by pi directly — no dist/ build artifacts

Workflow Authoring & Runtime API (packages/workflows)

  • Added defineWorkflow, runTask, runParallel, runChain, resolveInputs authoring primitives
  • Implemented model fallback handling with pre-run validation (WorkflowModelValidationError), input resolution, worktree support, and runtime config tunables (maxDepth, concurrency, statusFile)
  • ConcurrencyLimiter — semaphore controlling per-run stage parallelism
  • GraphFrontierTracker — infers DAG parent edges from JavaScript execution order for automatic stage dependency detection
  • Structured persistence layer: createStore, store, compaction policy (re-emits in-flight runs across auto-compaction), and restore utilities
  • CancellationRegistry — tracks and manages active background run AbortControllers (children aborted before primary)

Pi Extension Entrypoint (packages/workflows/src/extension/)

  • Implemented /workflow and /workflows-doctor slash commands with argument completion
  • Workflow tool with discovery, config loading, persistence, and MCP scoping
  • Intercom routing (structural — no hard pi-intercom import), subagent delegation, and async/background run controls (kill, pause, resume, inspect)
  • Cascade pause support, structured doctor card output, and inline HIL input forms
  • ask_user_question HIL tool vendored from upstream

TUI Surfaces (packages/workflows/src/tui/)

  • Status widgets, DAG graph overlays with sparse GraphCanvas renderer, stage chat, inline HIL input forms
  • Workflow/session pickers, doctor cards, background UI adapter
  • UI mockups for attach, dispatch, HIL, and stage-chat surfaces (ui/)

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 workflow
  • ralph — autonomous task runner workflow

Dependencies

  • Replaced OpenTUI + React + Claude Agent SDK with @earendil-works/pi-coding-agent and @earendil-works/pi-tui as peer dependencies
  • Removed oxlint, ajv, bun-pty, yaml, and React-related dev deps
  • Updated prek ^0.3.13^0.4.0

CI / DX

  • Migrated pre-commit configuration to prek; added push/PR prek hook checks
  • Consolidated test scripts: test:unit, test:integration, test:all
  • Updated GitHub Actions workflows for new workspace structure and Bun runtime
  • Added Catppuccin theme variants (frappe, latte, macchiato, mocha)
  • 86 new test files covering unit and integration suites (test/unit/, test/integration/)

Breaking Changes

Area Before After
Workspace packages packages/atomic, packages/atomic-sdk packages/coding-agent, packages/workflows (+ 4 supporting)
Workflows package name @bastani/atomic-sdk @bastani/workflows
Runtime Node.js ≥22 Bun ≥1.3.14
Pi requirement pi ≥ 0.74
Distribution Built dist/ artifacts Raw TypeScript via pi
Workflow API Legacy CLI/SDK dispatch defineWorkflow + pi extension runtime

Migration: Consumers of @bastani/atomic or @bastani/atomic-sdk must migrate to @bastani/workflows imports and install via pi package management.

Validation

  • bun run typecheck
  • bun run test:unit ✅ — 1197 passing tests across 76 files
  • bun run test:integration ✅ — 260 passing tests across 8 files
  • bun run test:all

Known Issues / Follow-ups

  • packages/workflows/package.json has "private": true — remove before publishing
  • GraphFrontierTracker.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) and executor.ts (1506 lines) should be split in a follow-up
  • tsconfig.json dropped noUncheckedIndexedAccess — consider restoring

Comment thread .github/workflows/test.yml Fixed
flora131 added 29 commits May 15, 2026 02:52
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
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
… 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
Comment thread packages/mcp/index.ts Fixed
Comment thread packages/web-access/extract.ts Fixed
@claude

claude Bot commented May 15, 2026

Copy link
Copy Markdown

Code Review

Thanks 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

  • CLAUDE.md is stale and contradicts this PR. It still describes the project as "TUI built on OpenTUI" powered by "OpenCode SDK, Claude Agent SDK, and Copilot SDK," references .impeccable.md (deleted), packages/atomic/script/bump-version.ts (removed — actual path is scripts/bump-version.ts), and docs/opencode/, docs/copilot-cli/, docs/claude-code/ (all gone). Since CLAUDE.md is the entry-point doc for agents working in this repo, leaving it stale will misdirect every future contribution. Either update to describe the pi extension model, or delete the dead sections.

  • engines / packageManager mismatch in root package.json. packageManager pins bun@1.3.13 but engines.bun requires >=1.3.14. A fresh bun install on a machine without 1.3.14 will warn, and any tooling that honours packageManager will install a version that fails the engine check. Pick one.

  • packages/coding-agent/package.json still declares engines.node >= 20.6.0. Given the rest of the monorepo is Bun-only and CLAUDE.md says "do NOT use node," it's worth checking whether this is intentional (the fork still ships as a Node-runnable CLI?) or a leftover from the prior structure.

  • typescript: "^6.0.3" as a root devDependency. TS 6 is unreleased at the time of this PR's draft; if it's a typo for ~5.6.x or similar this will surface only at first bun install post-merge.

Bugs / correctness

  • packages/workflows/src/runs/shared/concurrency.ts:46-54release() has no guard against over-release. If a caller pairs an unbalanced release() (e.g. a bug in error handling) with no queued waiter, _running decrements unbounded and below zero, silently inflating subsequent capacity. The executor's finally blocks pair acquire/release correctly today, but this is a footgun worth defending against with an if (this._running > 0) this._running--; plus an assert in debug builds.

  • packages/workflows/src/runs/foreground/executor.ts:893-912maxDepth rejection allocates a runId but never records anything in the store/persistence. Callers polling the store by runId will see no record. The doc says "reject before any store/persistence side effects" — fine, but you may want to at least omit the runId (return empty string) so consumers don't try to look it up. As-is, the runId is essentially a dangling reference.

  • packages/workflows/src/runs/foreground/executor.ts:348-363truncateByBytes can cut UTF-8 mid-codepoint via text.slice(0, mid) (UTF-16 code units). Buffer.byteLength re-encodes the JS string to UTF-8, but slice cuts at a UTF-16 boundary, so a surrogate pair can be split, leaving a lone surrogate that re-encodes as U+FFFD. Either step mid to a valid codepoint boundary, or use a TextEncoder + byte-level slice + TextDecoder({ fatal:false }) round-trip to guarantee a clean cut.

  • packages/workflows/src/runs/shared/worktree.ts:155-156 — worktree paths are placed in os.tmpdir() keyed only by runId and index. Functionally fine (UUIDs collide negligibly), but if two host processes share runId (e.g. resumed run replaying), git worktree add will fail because the branch already exists. The cleanupWorktrees path is best-effort, so half-cleaned state from a crash will block re-runs until manual cleanup. Worth documenting or detecting on startup.

  • packages/workflows/src/runs/shared/worktree.ts:147safePatchAgentName strips characters to _ but does not enforce length bounds. Very long agent names produce very long filenames; on some filesystems (eCryptfs, certain FAT variants) the 255-byte path limit gets hit silently.

  • packages/workflows/src/extension/discovery.ts:261-294importWorkflowFile collects every named export as a candidate. If a workflow file legitimately exports helpers/types alongside its default, every helper is validated as a WorkflowDefinition and generates INVALID_DEFINITION diagnostics. Consider filtering on the __piWorkflow: true sentinel before passing into applyBatch, so the diagnostic stream isn't polluted by every non-workflow named export.

Style / consistency

  • Stale cross-ref comments. Many files contain cross-ref: v0.x packages/atomic-sdk/... comments pointing at code deleted in this PR (e.g. define-workflow.ts:8, registry.ts:7, several tui/*.ts files). These rot fast; either rewrite them to point at the new locations or remove them entirely — per CLAUDE.md, identifiers should document themselves.

  • packages/workflows/src/runs/shared/worktree.ts uses tabs while the rest of packages/workflows/src/** uses spaces. Minor but visible in mixed editors.

  • packages/workflows/src/extension/index.ts is 2163 lines. The other extension files (dispatcher, runtime, discovery, config-loader) are appropriately sized. Carving out tool registration, surface wiring, and lifecycle hooks into separate files would make future review markedly easier — this file is currently the long-pole for understanding the extension entrypoint.

  • run (executor.ts) is ~600 lines after // 5. Build WorkflowRunContext. The closure-heavy style (release barriers, cascade pause/resume, stage handle registration, runTrackedStageCall) makes each individual flow readable but the file as a whole hard to step through. The ctx.stage(...) factory in particular is large enough to warrant its own module.

Security

  • packages/workflows/src/runs/shared/worktree.ts:270-313runWorktreeSetupHook spawns a user-configured executable with the input piped on stdin. The path checks (must be absolute or contain a /) and the existence check are good. Two follow-ups:

    1. The hook receives the run's baseCommit and full repo paths via stdin JSON — this is fine for a trusted hook, but worth documenting that any setup hook can read/write the worktree, the original repo (via the node_modules symlink), and os.tmpdir().
    2. parseWorktreeSetupHookOutput doesn't bound stdout length; a runaway hook could OOM the parent. Adding a maxBuffer-equivalent guard would be cheap.
  • .mcp.json ships <your-org> placeholder for @azure-devops/mcp. Not security-critical but it will fail at startup if the user runs bunx -y against the literal <your-org> value. Either remove or note in docs.

Test coverage

  • 82 test files for ~8.7k lines of workflow source is genuinely strong, and the split between test/unit and test/integration reads well. Two gaps worth filling:
    1. No explicit test for the maxDepth rejection path returning a usable shape (current depth-enforcement.test.ts exercises depth but the no-store-recording side effect noted above isn't asserted on).
    2. truncateTaskOutput has no test covering multi-byte UTF-8 characters straddling the byte boundary.

Nits

  • prek.toml runs bun run test:unit as a pre-commit hook for every commit. With ~1200 unit tests this could be slow on large PRs; consider making it a pre-push hook instead and keeping pre-commit light (lint only).
  • packages/workflows/package.json peerDependencies.@bastani/atomic: "*" with peerDependenciesMeta.optional: true is unusual — most consumers can't satisfy * without explicit installs. Pin to the workspace version (or use workspace:* semantics if Bun supports it).

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.

@lavaman131
lavaman131 force-pushed the refactor/pi-rewrite branch from c77abe3 to cae958c Compare May 15, 2026 18:48
@claude

claude Bot commented May 15, 2026

Copy link
Copy Markdown

PR Review — refactor(monorepo)!: restructure workspace with @bastani/workflows

Massive but well-scoped refactor. The core workflow runtime (packages/workflows) is the strongest part: tight types (essentially zero any/as any in source — the lone match is inside a string literal), immutable builder, abort-aware release barriers, and spawnSync with shell: false plus path-escape validation in worktree code. The test suite is real (1197 unit / 260 integration) and the executor is unusually well-commented for code this dense. Comments below in rough order of impact.


Workspace / tooling

  1. Root packageManager vs engines contradicts itself. package.json sets "packageManager": "bun@1.3.13" while every package (including root) requires "bun": ">=1.3.14". Corepack-aware tooling will reject the declared pm against the engine constraint. Bump one or the other.

  2. Two TypeScript versions in the same workspace. Root pins "typescript": "^6.0.3", packages/coding-agent/package.json pins "^5.7.3". Workspaces resolve TS per package, so bun run typecheck at root and cd packages/coding-agent && tsc will use different compilers — different diagnostics, different lib types. Either align both or document that coding-agent is intentionally pinned to upstream's TS.

  3. bun run typecheck covers far less than the PR claims. Root tsconfig.json explicitly excludes packages/coding-agent, packages/subagents, packages/mcp, packages/web-access, and packages/intercom, leaving only packages/workflows, test/, and examples/ in the include set. The validation section's "✅" emoji glosses over this — five of the six workspace packages aren't being checked by the top-level script.

  4. bun run lint isn't a linter. Both lint and typecheck in root and packages/workflows run tsc --noEmit -p ../../tsconfig.json. The PR description says oxlint was removed; the script wasn't. The prek bun-lint hook on every commit (prek.toml:19) and every push therefore runs tsc twice (bun-lint + bun-test-unit both transitively check). Either drop the alias or wire an actual linter back in.

  5. Mixed indentation across packages/workflows/src. 35 of 114 .ts files use tabs (vendored ask-user-question/* and runs/shared/worktree.ts); the rest use 2-space. Split correlates with provenance, but the inconsistency reaches across sibling modules in runs/shared/. Running prettier once on the package would be cheap and keeps diffs clean going forward.

  6. packages/coding-agent uses Vitest, contradicting CLAUDE.md. CLAUDE.md mandates bun test over Vitest/Jest, but packages/coding-agent/package.json ships "test": "vitest --run" with "vitest": "^3.2.4" in devDeps. If the fork intentionally keeps its upstream test runner, call that out in CLAUDE.md so contributors don't trip on it. Same goes for engines.node: >=20.6.0 only on coding-agent — the rest of the repo is Bun-only.

  7. README TypeScript badge. README badges "TypeScript 6.x" but the published @bastani/atomic package is built against TS 5.7. Pick a story or two badges.


Runtime correctness

  1. CancellationRegistry.register silently replaces controllers. packages/workflows/src/runs/background/cancellation-registry.ts:43-51: re-registering with the same runId swaps the controller in place without aborting the previous one. Anyone holding the original AbortController then loses the wire — its signal will never fire from registry.abort(runId). The executor at packages/workflows/src/runs/foreground/executor.ts:948-950 already routes around this via a comment ("avoid overwriting it"), which is the tell that this API is sharp. Either throw on duplicate runId, or expose replaceController and make register strict.

  2. Caller-signal listener leak on the happy path. executor.ts:927: callerSignal.addEventListener("abort", ...) runs once-only, but the listener is never removed when the run completes normally (it's only { once: true }, which removes after firing). If a top-level workflow holds one AbortSignal and spawns many nested runs over its lifetime, each nested run accretes a listener that lives until either the parent aborts or is GC'd. Add a signal.removeEventListener(...) in the finally of run().

  3. resolveInputs validates presence, not types. executor.ts:125-144 honors required and default, but never inspects the declared type ("text" / "number" / etc.). A workflow declaring .input("max_partitions", { type: "number", default: 4 }) will happily accept "4" as a string and crash later inside the stage body. If type-validation is deferred by design, document it on WorkflowInputSchema; otherwise add a guard.

  4. Worktrees live under os.tmpdir(). worktree.ts:155-157: pi-worktree-<runId>-<index> goes in /tmp. On Linux that's typically tmpfs (RAM-backed, often half of system RAM) and crosses filesystems from the repo, so the node_modules symlink (linkNodeModulesIfPresent, line 177) will work but cross-filesystem git worktree operations can be flaky on some setups. Also, if the process is kill -9'd before cleanupWorktrees runs, the parent repo accumulates orphan worktree records. Consider git worktree prune on startup, or at minimum document recovery in the doctor card.

  5. prepareDirectWorktrees and runChain chain-step worktrees per step. Each chain step creates and tears down its own worktree (executor.ts:756, :779), so a 5-step chain with worktree isolation pays 5× the git worktree add/rm cost and forfeits any continuity between steps. If that's intentional ("steps are independent units"), worth a doc line — otherwise consider one worktree per chain.


Smaller things worth a look

  1. bun-test-unit runs the full 1197-test suite as a pre-commit hook. That will get slow fast and contributors will reach for --no-verify. Move it to pre-push, scope it to changed packages, or split out a "fast" subset.

  2. builtin/ralph.ts hard-codes model names (openai/gpt-5.5, github-copilot/claude-opus-4.7, etc.). These are likely to drift faster than the workflow file; consider pulling from WorkflowModelCatalogPort so the catalog is the single source of truth for available models.

  3. reviewApproved in builtin/ralph.ts uses substring matching on the LLM output ("patch is correct", "approved", "no actionable findings"). Easy to false-positive on phrases like "this is not yet approved." Either tighten the regex (anchored, with negative lookbehind) or have the reviewer emit a structured { status: "approve" | "request-changes" } payload.

  4. @bastani/workflows ships raw TypeScript and is published privately ("private": true) with peer deps marked optional. Worth a quick callout in the README about how downstream consumers are expected to invoke the loader — discoverability is currently low.


Net

The core authoring API and executor are solid. The biggest blocker is #3 (typecheck doesn't actually check most packages): the PR's "✅ typecheck" claim is technically true for one package, but consumers reading the Validation section will assume it covers the whole workspace. I'd ship after items 1–4 are addressed and would treat 8–11 as fast-follow.

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
@claude

claude Bot commented May 15, 2026

Copy link
Copy Markdown

Code Review — refactor(monorepo)!: restructure workspace with @bastani/workflows

Massive PR (+265k/-224k) — I focused on the new packages/workflows runtime, the persistence/store layer, and the Pi extension entrypoint, since the bulk of new behavior lives there. Skimmed the rest. Overall the architecture is well-factored: clean separation between authoring API (defineWorkflow), executor, store, and Pi-facing extension; ports for persistence/MCP/cancellation; tests cover the core paths. Below is a punch list, ordered roughly by severity.

🔴 Bugs / correctness

  1. runOptionsWithAdapters silently overrides caller-provided storepackages/workflows/src/runs/shared/workflow-runner.ts:48-53. The spread happens before store: createStore(), so any runOptions.store passed by an external caller of runWorkflow() is discarded and replaced by a fresh in-memory one. Either drop the override (and let ...options.runOptions win) or store: options.runOptions?.store ?? createStore(). As-is, the public SDK contract leaks.
  2. inspectRun ambiguous-prefix doc/code mismatchpackages/workflows/src/runs/background/status.ts:316-353. The JSDoc claims "ambiguous" is returned when a prefix matches multiple runs, and RunDetail's contract reads that way, but the implementation just find()s the first match silently. Either return { ok: false, reason: "ambiguous", matches: [...] } (mirroring resolveRunIdPrefix in extension/index.ts:755) or update the doc — the current state means a user typing a short prefix may attach to the wrong run with no warning.
  3. packageManagerengines.bun skew — root package.json pins bun@1.3.13 via packageManager while engines.bun requires >=1.3.14. Corepack-style tools will install 1.3.13 and then fail the engines gate. Bump one.
  4. pauseRun marks the run paused before pause() settlesstatus.ts:284,303,307. void handle.pause() is fire-and-forget, then the run is recorded as paused synchronously. If pause() rejects on one stage, the run snapshot says paused but the stage is still running. At minimum await Promise.allSettled(...) and skip recordRunPaused if no handle actually transitioned (mirror what resume already does via the recordRunResumed guard).
  5. Lint script is just typecheckbun lint runs the same tsc --noEmit as bun typecheck. CLAUDE.md tells contributors to run both. After dropping oxlint there's no actual linter; either rewire lint to a real one (oxlint, biome) or drop the script and update CLAUDE.md to stop promising a separate lint stage.

🟡 Performance / scalability

  1. Store snapshot is deep-cloned on every state changeshared/store.ts:169-173. notify() calls snapshot() which does JSON.parse(JSON.stringify(...)) over the entire run set. With one tool event per stage tick, this is O(M·N) for a long run with many tool events, and snapshots are pushed to every subscriber. For a multi-stage workflow with many tool events this becomes the dominant CPU cost. Consider:
    • lazy snapshot computation (compute on demand inside the listener)
    • structural sharing / immer-style updates
    • or version-bumped notifications where listeners pull on-demand.
      status.ts also uses JSON.parse(JSON.stringify(...)) 5× as a defensive copy boundary — same concern at lower frequency, but worth a structuredClone pass at minimum (faster than JSON, supports more types).
  2. unsubscribe on _listeners: Set<> mutated during iteration — if a listener inside notify() unsubscribes itself, the for (const fn of _listeners) loop is fine for Set (iteration tolerates deletion of the current element), but inserting during iteration would yield the new listener immediately. Worth a brief guard or doc note.

🟠 API / ergonomics

  1. runTask overload disambiguation is brittleexecutor.ts:533-554 isRunOpts enumerates 17 keys to tell WorkflowDirectOptions from RunOpts. A new RunOpts field requires editing this list; missing the edit causes silent misrouting of the second arg. Either:
    • require explicit calling style (drop the overload, take a single options object), or
    • use a sentinel/branded type so the discriminator can't drift.
  2. directRunId allocates UUIDs before validation can fail — when validateDirectModels rejects, failedDirectDetails returns a runId that was never recorded in the store. Anything that tries to look up that runId later (e.g. tracker UIs) will see a phantom. Either validate before allocating, or skip the runId in the failed case.
  3. makeUnavailableUIContext returns rejected promises whose handlers depend on caller awaitexecutor.ts:150-159. If a user accidentally calls ctx.ui.input(...) without await, the unhandled rejection crashes Bun's default unhandled-rejection mode. Consider returning a plain throw (or attaching a .catch(() => {})) and a clearer error type.
  4. @bastani/workflows ships raw .ts and depends on Bun-only loaders — fine for the documented pi runtime, but the engines.bun: ">=1.3.14" in packages/workflows/package.json doesn't gate the actual host. Worth a peerDependencies or runtime check that fails fast with a useful message if someone tries to load it under Node.

🛡️ Security

  1. discoverWorkflows dynamically imports any .ts/.js/.mjs/.cjs from <cwd>/.atomic/workflows/ and <homeDir>/.atomic/agent/workflows/discovery.ts:241-295. This is the documented design (it's how user workflows load), but worth being explicit in user-facing docs that opening a project in atomic executes any workflow file in that directory at startup. If you want defense in depth, consider:
  • a one-time trust prompt per project (similar to VS Code's workspace trust)
  • or restricting user-global discovery to files owned by the current user.
  1. parseWorkflowArgs JSON-parses user input — safe (no eval), just confirming this is JSON.parse not vm.runInNewContext. ✅
  2. No secret-scrubbing in persisted inputsappendRunStart writes the resolved inputs map into the session entry stream. If a workflow input ever contains an API key (e.g. a "token" field), it lands in the persistent transcript and any compaction snapshot. Consider redacting fields whose schema marks them sensitive, or document that workflow inputs MUST NOT contain secrets.

🧪 Tests

  1. Unit/integration coverage is solid (86 unit + 7 integration files for the workflows package alone) — good. Two gaps I'd suggest closing:
    • No reentrancy/deadlock test for ConcurrencyLimiter under nested workflow runs. Today nested run() calls each get their own limiter (createRunLimiter is per-run), which is correct, but a test asserting that nested runs at maxDepth - 1 complete without starvation would lock that behavior in.
    • No test for runOptionsWithAdapters honoring an externally-supplied store. Add one and watch it fail against bug add agent instructions #1 above.
  2. Manual tests in test/manual/ aren't wired into any CI script. If they're expected to be run by humans, document that in CLAUDE.md; otherwise consider deleting to avoid bitrot.

🧹 Style / nits

  1. Cross-ref comments to v0.x packages/atomic-sdk/... (define-workflow.ts:8) point at deleted code. Either remove or rewrite to point at the relevant test/spec.
  2. status.ts:330 runs.length > 0 ? runs.find(...) : undefined is redundant — Array.find on empty arrays already returns undefined.
  3. appendRunStart/appendStageStart calls in executor.ts are sync (no await) even though WorkflowPersistencePort.appendEntry returns synchronously today. If that ever becomes async, the executor will silently lose ordering. Worth a type-level guarantee that the port is sync-only or wiring await now.

Validation

PR description claims bun typecheck/test:unit/test:integration/test:all all pass. I trust the run; not re-running locally given the size.


Nice work — the executor's DAG/cascade-pause logic, the GraphFrontierTracker, and the per-run AbortController plumbing are particularly clean. The deepest concerns are #1 (silent store override), #2 (silent ambiguous-prefix attach), and #6 (JSON.parse(JSON.stringify(...)) in the hot path); the rest is polish.

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
@claude

claude Bot commented May 15, 2026

Copy link
Copy Markdown

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.

@claude

claude Bot commented May 15, 2026

Copy link
Copy Markdown

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.

@claude

claude Bot commented May 15, 2026

Copy link
Copy Markdown

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.

@claude

claude Bot commented May 15, 2026

Copy link
Copy Markdown

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.

@claude

claude Bot commented May 15, 2026

Copy link
Copy Markdown

Code review

This 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

  1. Bun version mismatch. Root `package.json` declares `"packageManager": "bun@1.3.13"` but `"engines": { "bun": ">=1.3.14" }`. The pinned package manager is below the declared minimum.
  2. Workflows version vs. changelog. `packages/workflows/package.json` is at `0.8.0`, but `packages/workflows/CHANGELOG.md` lists only `[0.0.1] — 2026-05-15` as the initial release. Pick one.
  3. `private: true` on a "publishable" package. `@bastani/workflows` is marked `private: true` yet the PR description and README call it a publishable pi extension. If publish is the goal, drop `private` (and add a `publishConfig` if needed); otherwise update the description.
  4. `@bastani/atomic` peer dependency is optional + `"*"`. `peerDependenciesMeta` marks it optional, but `packages/workflows/src/runs/foreground/executor.ts:7`, `extension/discovery.ts`, and `extension/config-loader.ts` all do `import { CONFIG_DIR_NAME } from "@bastani/atomic"` on the hot path. If a consumer installs `@bastani/workflows` without `@bastani/atomic`, those imports fail at load time. Either inline `CONFIG_DIR_NAME` (it's a string), or make the peer mandatory and pinned to a real version range, not `"*"`.
  5. TypeScript v6 in root, v5.7 in coding-agent. Root devDependency is `"typescript": "^6.0.3"`, but `packages/coding-agent/package.json` pins `"typescript": "^5.7.3"`. Two major TS versions in one workspace is asking for cross-package type drift; please align unless there's a concrete reason.
  6. vitest in coding-agent vs. "use bun test" in CLAUDE.md. `packages/coding-agent/package.json` keeps `"test": "vitest --run"` and `vitest: ^3.2.4`. Top-level `CLAUDE.md` says: "Use `bun test` instead of `jest` or `vitest`". Either migrate or carve an explicit exception in `CLAUDE.md` so the rule isn't half-true.

Workspace / tooling

  1. `typecheck` doesn't typecheck most of the repo. Root `tsconfig.json` `include` is `["packages/workflows//*", "test//", "examples/**/"]` and `exclude` lists every other workspace package (`coding-agent`, `subagents`, `mcp`, `web-access`, `intercom`). The root `"lint": "tsc --noEmit"` and `"typecheck": "tsc --noEmit"` scripts therefore only cover workflows and tests. Either include the other packages, or document that CI runs per-package typechecks separately.
  2. Workspace test script reaches outside the package. `packages/workflows/package.json` runs `"test:unit": "bun test ../../test/unit"`. The workspace's tests live at the repo root, not in the package. That's surprising for anyone running `bun run --filter @bastani/workflows test` and makes it harder to publish the package with its own tests. Consider moving tests under `packages/workflows/test/`.

Runtime correctness / behavior

  1. `store.ts:snapshot()` deep-clones via `JSON.parse(JSON.stringify(...))` on every notify (`packages/workflows/src/shared/store.ts:165`). Every `recordToolStart`/`recordToolEnd` fires `notify()` → full snapshot clone, even when no subscriber is listening. For streaming tool events on a long run this is wasteful and will show up on profiles. Cheap wins: lazy snapshot computed on demand, and/or version-keyed memoization so multiple subscribers in the same tick share one snapshot.
  2. `discovery.ts:importWorkflowFile` calls `await import(absPath)` with a raw filesystem path (`packages/workflows/src/extension/discovery.ts:267`). On Windows, ESM dynamic import requires a `file://` URL — passing a plain `C:...` path throws `ERR_INVALID_URL`. Use `pathToFileURL(absPath).href`.
  3. `ConcurrencyLimiter.release()` can drive `_running` negative if release is called more times than acquire (`packages/workflows/src/runs/shared/concurrency.ts:46`). Probably won't happen via the `run()` helper, but `acquire()`/`release()` is also exposed publicly and the executor uses the raw form. One stray `release()` in an error path and the counter is off forever. Either assert `_running > 0` before decrementing, or guard against the case.
  4. `cancellation-registry.ts:unregister()` doesn't clear children (`packages/workflows/src/runs/background/cancellation-registry.ts:90`). If a future caller unregisters a run that still has live child controllers, those become unreachable — no `abort(runId)` will hit them. Today the only call site is `executor.run()`'s `finally`, so it's safe, but defensive cleanup makes the API harder to misuse.
  5. `validateConfig` allows `maxDepth: 0` (`packages/workflows/src/extension/config-loader.ts:144`). The executor enforces `depth >= maxDepth` (`executor.ts:902`), so `maxDepth: 0` immediately rejects every top-level run. Either require `>= 1` in the validator, or document the semantic.
  6. `config-loader.ts:tryReadFile` dynamically imports `node:fs/promises` per call (`packages/workflows/src/extension/config-loader.ts:122`). The module is always loaded; the dynamic import adds latency on every read with no benefit. Hoist it to a top-level static import.
  7. `worktree.ts:linkNodeModulesIfPresent` symlinks the host `node_modules` into each parallel worktree (`packages/workflows/src/runs/shared/worktree.ts:177`). If two parallel tasks run a tool that mutates `node_modules` (a `bun install`, a postinstall script), they race on the shared tree. At minimum, document that workflows touching dependency installation must disable the symlink — or skip linking when worktree count > 1 and emit a notice.
  8. Large single-file modules. `runs/foreground/executor.ts` is 1506 lines; `extension/index.ts` is 2143 lines. Both bundle disparate responsibilities. Splitting by responsibility now will pay back during incident reviews.
  9. `executor.ts:descendantsOf` / `hasAncestor` are O(n²) over `runSnapshot.stages` (`executor.ts:983-1006`). Fine for small DAGs, but the executor calls `descendantsOf` per pause/resume cascade; a memoized children-adjacency map costs nothing and removes a foot-gun for large workflows.

Security

The 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.

Tests

144 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:

  • `ConcurrencyLimiter`: an extra `release()` does not corrupt state.
  • `CancellationRegistry`: `unregister(runId)` after `registerChild` does not orphan the child controller (or document that it does).
  • `awaitPendingPrompt` race: a run is killed between `recordPendingPrompt` and `awaitPendingPrompt`; the awaiter rejects cleanly.
  • Windows: `discoverWorkflows` importing from an absolute path on Win32 (likely fails today because of (10)).

Nits

  • README and package metadata use `@bastani/workflows`, but several internal error messages still say `"pi-workflows: ..."` (e.g. `executor.ts:925`, several spots in `validate-inputs.ts`). Prefer one identifier in user-facing strings.
  • `tsconfig.base.json` is present (Node16, `types: ["node"]`) but doesn't appear to be `extends`-ed anywhere I can see. If unused, delete; if it's for a package's tsconfig, wire the `extends` in.

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
@claude

claude Bot commented May 15, 2026

Copy link
Copy Markdown

Review — refactor(monorepo)!: restructure workspace with @bastani/workflows

Massive restructure — this is a great direction (focused @bastani/workflows extension, raw-TS distribution, Bun-only runtime). Type discipline is consistent (no any/unknown floating around, structural pi surfaces). Below are concrete items worth addressing before/after merge.

Bugs / correctness

  1. GraphFrontierTracker.onSettle() drops diamond-merge parents (PR self-flagged; no test exists).
    packages/workflows/src/runs/shared/graph-inference.ts:43-51onSettle removes parents of the settled stage from the frontier. For A→B→D, A→C→D the order spawn(A), settle(A), spawn(B), spawn(C), settle(B), settle(C), spawn(D) ends with the frontier containing only the last settler. test/unit/graph-frontier-tracker.test.ts covers sequential / parallel / fan-in but never builds a diamond that re-uses an ancestor across two branches that settle non-atomically. Please add a failing test now so the bug is captured in the suite, then fix in a follow-up. A simple fix is to track per-frontier-entry refcounts (number of unsettled descendants) or rebuild the frontier from nodes + stageParents at each onSpawn.

  2. GraphFrontierTracker.getParents typing lies.
    graph-inference.ts:33,59Object.freeze(parents) freezes the same array stored in stageParents, but getParents(): string[] advertises a mutable result. Either return readonly string[] or clone on read. Right now an inadvertent getParents(id).push(...) would silently no-op in strict mode and crash in non-strict.

  3. CancellationRegistry.register silently replaces the primary controller.
    runs/background/cancellation-registry.ts:41-50 — the old controller is detached without abort. Combined with executor.ts:927 which registers an abort listener on the caller signal, a re-registration leaks the old listener for the lifetime of the caller signal. Either reject re-registration (throw) or abort the old controller before replacing. Comment on line 42 acknowledges this but doesn't guard.

  4. store.recordStageAwaitingInput filter ignores awaiting=false for awaiting_input stage.
    shared/store.ts:438-447 — the if (stage.status === "completed" ...) return false guard rejects awaiting_input → running transition that is supposed to be the inverse case. Trace: recordStageAwaitingInput(awaiting=false) after the prompt resolves, against a stage in awaiting_input — falls through guard fine, but the symmetric guard for the upward transition (awaiting=true on a paused stage) silently returns false. Worth a unit test that exercises both directions across running ↔ awaiting_input ↔ paused.

  5. store.snapshot() deep-clones via JSON.parse(JSON.stringify(...)) on every notify().
    shared/store.ts:169-172 — fired from every record* method. For a typical run with ~50 stages and many ToolEvents, this is N² work per second. Switch to structuredClone() (Bun supports it natively, faster, handles Date/Map-shaped values if they ever appear) — or notify listeners with the mutable _runs/_notices/_version and let consumers clone when they need a frozen copy. Same pattern in runs/background/status.ts:230-337 is fine since it's per-call, not per-mutation.

Performance

  1. hasAncestor / descendantsOf / blockingAncestorFor are O(V+E) per call.
    executor.ts:984-1012, 1035-1057cascadeResumeFrom iterates descendantsOf and for each descendant calls blockingAncestorFor. For a wide DAG this is O(V²). Probably fine for typical workflow sizes (< 50 stages) but worth a cached descendants(stageId) memo keyed by tracker.version if the deep-research workflow keeps growing.

Code quality / maintainability

  1. packages/workflows/package.json is "private": true.
    PR self-flagged. Confirm the publish gate happens before the first cut of the new package (CI/release docs say private: true will silently no-op npm publish).

  2. extension/index.ts at 2143 lines and executor.ts at 1505 lines.
    PR self-flagged. The executor's main run() function is ~600 lines with a deeply nested ctx.stage closure — splitting runTrackedStageCall into its own file (taking tracker, limiter, releaseBarriers, etc. as args) would also let it be tested in isolation. Right now the only coverage is via the public run().

  3. tsconfig.json dropped noUncheckedIndexedAccess.
    PR self-flagged. There's heavy use of array[i]! non-null assertions across executor.ts (e.g. chain[index]!, prepared.tasks[0]!, results[index - 1]) — restoring the flag would catch the next regression. The current code is correct, but the safety net is off.

  4. Mixed indentation: runs/shared/worktree.ts uses tabs, the rest uses spaces.
    Worth running prek run --all-files (or whatever the project's prettier/biome config is) to normalize before the next release branch.

  5. Test asserts on void return.
    test/unit/cascade-pause.test.ts:43assert.equal(store.recordStageEnd(...), undefined). recordStageEnd is typed void in the Store interface; asserting undefined is asserting on JS-runtime behavior rather than the API contract. Drop the assertion or change recordStageEnd to return boolean like the other record* methods for consistency.

Security

  1. importWorkflowFile does dynamic import(filePath) from config-supplied paths.
    extension/discovery.ts:268 — by design (user is loading their own workflows). Worth a one-line note in the README / loadWorkflowConfig docstring that workflows from config.paths execute with full process privileges. Right now a misconfigured shared config file could load arbitrary code.

  2. Worktree setup hook execution is safely shelled.
    runs/shared/worktree.ts:274-280spawnSync(hookPath, [], { shell: false, ... }), hook path resolved against repoRoot and existence-checked. No injection vector. The syntheticPaths normalization in normalizeSyntheticPath correctly rejects .. escapes. Looks good.

Test coverage

  • 1197+260 tests is great. Targeted gaps:
    • graph-frontier-tracker.test.ts — add a diamond-merge case (issue add agent instructions #1 above) and a chained-fanin case (A→B, A→C, B→D, C→D, D→E) to catch any future frontier bug.
    • cancellation-registry.test.ts — add a "re-register same runId" case that asserts on the old controller's signal.
    • store.test.ts — round-trip awaiting_inputrunningpaused against recordStageAwaitingInput to lock the precondition guard.

Nits

  • executor.ts:69 — trailing import without newline (} from "../shared/types.js";import { buildRuntimeAdapters } — formatter slip in extension/index.ts:69).
  • executor.ts:60export interface ResolvedInputs extends Record<string, unknown> {} — empty extension. Just export type ResolvedInputs = Record<string, unknown>.

Overall: ship-worthy with the diamond-merge bug captured as a failing test before merge. Items 1–4 are correctness; the rest can land in follow-ups.

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
@claude

claude Bot commented May 15, 2026

Copy link
Copy Markdown

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 @bastani/workflows runtime, extension entrypoint, TUI surfaces, tests, and workspace plumbing were each analyzed in detail.

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

packages/workflows/src/extension/discovery.ts:329 resolves user-config-supplied paths via resolve(baseCwd, rawPath) and importWorkflowFile (discovery.ts:261-295) calls import(filePath) after only a stat check — no containment check that the path stays inside an expected directory. Combined with validateDefinition (discovery.ts:146-165) only gating on the __piWorkflow === true sentinel, anyone who can write .atomic/extensions/workflow/config.json (or the global equivalent) can inject a config entry pointing to ../../evil.ts and the extension will import() and execute it in-process with full tool/skill surface.

Mitigations to consider: normalize paths and require them to be under the project root or a per-user allowed-roots list; refuse paths containing .. after normalization; require workflow files to be inside a configured workflows/ directory.

2. Stage subagent privilege model

wiring.ts:255-287 documents that each stage session inherits the same themes/extensions/tools/skills as the orchestrator (wiring.ts:269-273). For an orchestrator framework this is a deliberate choice, but it deserves a SECURITY.md note: a workflow that imports a malicious subagent gets the same blast radius as the user.

3. Persistence may capture secrets

runs/foreground/executor.ts:958-961 persists resolvedInputs verbatim. If users pass secrets through workflow inputs (API keys etc.) they land in the session entries on disk. Worth either documenting (inputs are persisted, do not pass secrets) or adding a scrub layer keyed on input schema.

(continued in replies — correctness, code quality, performance, tests, CI, and strengths)

@claude

claude Bot commented May 15, 2026

Copy link
Copy Markdown

Correctness — Concrete Bugs

4. AbortController registration is skipped for foreground runs that pass a signalexecutor.ts:948-949

The guard if (!opts.signal) { opts.cancellation?.register(runId, ownController); } is intended to avoid overwriting a pre-registration from the detached runner. The condition is wrong: any foreground caller that passes opts.signal ends up unregistered, so cancellation.abort(runId) becomes a no-op for those runs. Fix: detect pre-registration explicitly (e.g. if (!cancellation.has(runId))) rather than gating on opts.signal.

5. SDK errors silently swallowed by pause/resume retry loopstage-runner.ts:481-484

The catch block reads if (pauseRequest) { ...continue; } throw err;. If a non-pause SDK error (e.g. transient network failure) throws while pauseRequest is non-null, the catch discards the error, awaits the resume deferred, and retries with the resume message. The original error is gone. Distinguish pause-triggered aborts from real errors before consuming them.

6. Worktree cleanup leak on early abortexecutor.ts:651-831 (runTask / runParallel / runChain)

createWorktrees runs synchronously before the workflow body executes; cleanup lives in try/finally inside the body. If ownController is already aborted when def.run(ctx) is invoked, the body never runs and worktrees + branches are leaked. Move cleanup outside the body or wrap creation in an outer try/finally.

7. Concurrency limiter waiters never reject on abortconcurrency.ts:38-41 and executor.ts:1222

limiter.acquire() is awaited unconditionally with no race against the run's abort signal. A stage queued for a concurrency slot at the moment of abort hangs until prior stages release — which only happens after their finally blocks. Race acquire against ownController.signal so aborted runs cancel queued stages immediately.

8. Extension lifecycle subscription leaksextension/index.ts:2074, 2115

installStoreWidget(pi, store) (line 2074) and subscribeIntercomControl (line 2115) run unconditionally at factory load and assign to storeWidgetUnsubscribe / intercomControlUnsubscribe. The session_start handler (lines 2039-2040) reinstalls the widget with session-scoped UI and overwrites the previous unsubscribe ref — the original subscription leaks. Same shape for the intercom subscription on re-factory scenarios. Either do not pre-install before session_start, or unsubscribe before re-assigning.

9. HIL form required check bypassed for non-string typed inputsindex.ts:1739-1745

The empty-required guard only checks value === "". If a user passes count=0 via CLI and parseWorkflowArgs coerces it to the number 0, the picker is skipped — the workflow receives 0 for what was a required field. Use the input schema's required declaration plus a per-type emptiness check, not a string-only literal.

10. Slash command single-quote values are not unwrappedindex.ts:800-877

tokenizeWorkflowArgs keeps quote characters in the buffer; for prompt='hello world', JSON.parse("'hello world'") throws and the fallback path delivers the literal 'hello world' (including quotes) to the workflow. Add single-quote unwrapping or document the requirement to use double quotes.

11. delivery === "result" is dropped by shouldEmitControlruntime.ts:264 and intercom/result-intercom.ts:125-127

shouldEmitControl only returns true for "notify" and "control-and-result". Passing delivery: "result" to emitWorkflowControlIntercom silently emits nothing. Either widen shouldEmitControl to include "result" or strip the call site so the dead path is not reached.

12. pauseRun snapshot timingruns/background/status.ts:284-307

void handle.pause() is fire-and-forget; the returned snapshot reflects pre-cascade state. TUI consumers can briefly show a stage as paused that has not yet received its abort. Worth awaiting the handle.

13. disposeCurrentSession race windowstage-runner.ts:458-465

Sets session = undefined and sessionPromise = undefined before awaiting current.dispose(). External callers (steer, followUp, compact) reaching ensureSession() during this window will construct a new session before the old one finishes disposing. Either await dispose first or guard ensureSession with the in-flight dispose.

14. Minor — known DAG diamond-merge bug not pinned by a failing/skipped test

The PR description acknowledges GraphFrontierTracker.onSettle() produces incomplete parent edges for diamond merges, but there is no test.skip("documents diamond-merge bug", ...) in graph-frontier-tracker.test.ts. Add one to prevent regression bookkeeping loss.

@claude

claude Bot commented May 15, 2026

Copy link
Copy Markdown

Code Quality / CLAUDE.md Compliance

CLAUDE.md forbids any and unknown. Most of the new code adheres; the violations worth fixing:

  • packages/workflows/src/shared/store.ts:69resolvePendingPrompt(response: unknown) and awaitPendingPrompt(): Promise<unknown> in the public Store interface, propagated to ResolverEntry at line 163. The widget store is a structural surface — type the prompt response payloads.
  • packages/workflows/src/extension/wiring.ts:423-424export type PiTheme = unknown; export type PiKeybindings = unknown. These are then cast at every callsite (e.g. stage-chat-view.ts:318) via as ConstructorParameters<typeof CustomEditor>[2]. Import the real types from @earendil-works/pi-tui or define structural interfaces.
  • packages/workflows/src/extension/workflow-schema.ts:37-60 — TypeBox schema uses Type.Any() for model, tools, noTools, inputs. Constrain these.
  • packages/workflows/src/extension/index.ts:91, 229stages?: unknown[] and [key: string]: unknown index signatures on the Pi*Context types.
  • tui/stage-chat-view.ts:378-490event as { field?: unknown } casts throughout _appendEvent. The SDK event union should already discriminate by type; eliminate the structural re-cast.
  • tui/graph-view.ts:648-649canvas as unknown as { rows: ... } reaches into GraphCanvas's private field. Expose a getCell(row, col) query instead.

Duplicated truncation helpersnormalizeMaxOutput, truncateByLines, truncateByBytes are duplicated identically in executor.ts:328-363 and stage-runner.ts:207-236 (~80 lines). Lift to a shared util.

Unbounded form-state mapinline-form-store.ts:46 retains every /workflow form state for the life of the module (acknowledged at line 14). Add a finalize hook that evicts after submission.

@bastani/atomic import in wiringwiring.ts:140 does await import("@bastani/atomic") but JSDoc throughout (wiring.ts:86-91, 253) names @earendil-works/pi-coding-agent as the canonical module. Reconcile to avoid future confusion.

Error swallowing in diffWorktreesruns/shared/worktree.ts:539-543 writes an empty patch on failure with no log/warning surface. Callers cannot distinguish 'genuinely no diff' from 'capture failed.'


Performance — TUI Hot Paths

  • stage-chat-view.ts:863-870_statusPill calls blendBg/lerpColor (six per render, each does parseInt x6) at the 80 ms animation cadence. Theme-derived values are constant; cache them in the constructor when this.theme is set.
  • stage-chat-view.ts:1069-1098 — A fresh ToolExecutionComponent is constructed inside _renderEntry on every frame; any internal state (subscriptions, image caches) is discarded each tick. Memoize per-toolCallId.
  • stage-chat-view.ts:971-983_renderComponentTail renders every component from the tail and only breaks after the render call; for long transcripts with a tight height budget, many off-screen components are rendered then discarded. Move the break above the .render(width) call.
  • graph-view.ts:487-490 and 800-801nodeByStageId/byId Maps are reconstructed every render. The layout itself is cached; cache the index alongside it.
  • stage-chat-view.ts:767-768render() calls _currentStage() and _isBlocked() (which internally calls _currentStage() again), so store.snapshot() is traversed twice per frame. Pass the first snapshot down.

@claude

claude Bot commented May 15, 2026

Copy link
Copy Markdown

Test Coverage

Generally strong — 1197 unit + 260 integration tests, real createStore not mocks, hand-rolled typed fakes rather than framework mocking. Specific gaps worth addressing:

  • extension.test.ts is 12 lines for a 2143-line entrypoint. It only asserts the factory is callable and does not throw. The dispatch logic at index.ts:408-660 and the orchestration inside makeExecuteWorkflowTool/registerWorkflowCommand are not directly exercised. Slash parsing is well-covered by slash-dispatch.test.ts but the wiring is not.
  • runs/shared/worktree.ts has no test file and is not imported in any test. Critical for the parallel workflows feature given it manages git branch + filesystem state outside the per-test temp dirs.
  • tui/stage-chat-view.ts tool-card transcript and compaction UI paths are untested in stage-chat-view.test.ts (which covers keyboard routing well).
  • overlay-graph.test.ts:47-78 stubs every store method as a no-op and asserts on _focusedIndex / _switcherOpen internals. Brittle; assert on rendered output where possible.
  • as never escape hatches in stage-chat-view.test.ts:139,188, inline-form.test.ts:445,497, runtime-wiring.test.ts:137 weaken the type-level contract on what those host interfaces require.

Workspace / CI / Config

  • Engine/packageManager mismatch — root package.json:5 pins packageManager: "bun@1.3.13" while engines.bun: ">=1.3.14" (line 7). Bump packageManager.
  • packages/coding-agent declares engines.node: ">=20.6.0" with no bun engine (line 103). Repo CLAUDE.md is unambiguously 'Bun >= 1.3.14'; either add the bun engine here or document why the coding-agent fork keeps Node.
  • vitest still present in packages/coding-agent/package.json (line 41 test script, line 85 devDep). Root CI uses bun test, so this is unused but adds install weight. Either drop or document why both runners coexist.
  • publish.yml npm publish --provenance — per .claude-pr/CLAUDE.md this is the documented exception (OIDC provenance lives in the npm CLI). No NPM_TOKEN / NODE_AUTH_TOKEN present, which is correct. Just verified, no action needed.
  • TypeScript major split — root uses typescript ^6.0.3, coding-agent uses typescript ^5.7.3 plus @typescript/native-preview 7.0.0-dev.20260511.1. Two majors across the monorepo will create subtle type-inference differences. Worth aligning, or documenting why coding-agent needs the older toolchain.
  • Root tsconfig.json include (lines 32-39) only typechecks packages/workflows, test, examples. The other four packages are not covered by bun run typecheck. Consider expanding or document the divided typecheck strategy.
  • noUncheckedIndexedAccess dropped (PR-acknowledged) — combined with the index signatures noted in the Code Quality section, this loses meaningful safety on the Pi*Context types. Restoring it would force fixes for cases the index signatures currently hide.
  • packages/workflows private: true is actively guarded by publish.yml:94-98, so the cleanup mentioned in Known Issues needs the guard updated too — otherwise the publish workflow will error when the flag is removed.

Notable Strengths Worth Highlighting

  • releaseBarriers / rejectReleaseBarriers (executor.ts:971-1071) — cascade-pause gating wired to the run's abort signal so no pending stage can hang on abort. Solid design.
  • recordRunEnd terminal-status guard (store.ts:276-278) — prevents completed runs from being overwritten by late finalizers and signals to callers via boolean return.
  • Worktree paths and branch names use unguessable UUIDs (worktree.ts:155-157) — concurrent runs cannot collide; runWorktreeSetupHook uses shell:false and JSON stdin (worktree.ts:274) — correctly hardened against shell injection.
  • Post-stage-settle abort race test (executor.test.ts:1361-1431) uses a holdWorkflow gate to make the race deterministic — a precise regression test rather than a setTimeout approximation.
  • runChain parallel-group handoff tests (executor.test.ts:792-831) exercise template interpolation end-to-end across mixed sequential/parallel steps.
  • Mutable-ref + stable proxy pattern in extension/index.ts:1012-1037 cleanly handles async registry swap-in without closure updates.

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
@lavaman131
lavaman131 merged commit c4cb45d into main May 15, 2026
8 checks passed
@lavaman131
lavaman131 deleted the refactor/pi-rewrite branch May 15, 2026 21:31
@claude

claude Bot commented May 15, 2026

Copy link
Copy Markdown

Review — refactor(monorepo)!: restructure workspace with @bastani/workflows

Thanks for the huge effort here. The new package split, defineWorkflow builder, frontier-based DAG inference, and pi extension entrypoints are clean and the test surface (1,457 tests across 81 files) is substantial. A few issues / suggestions, ordered roughly by importance.

Blockers (please address before merge)

  1. CI typecheck only validates packages/workflows/. Root tsconfig.json explicitly excludes packages/coding-agent, packages/subagents, packages/mcp, packages/web-access, packages/intercom, and four of those packages (intercom, mcp, subagents, web-access) define no scripts.typecheck of their own. The PR description's bun run typecheck ✅ is therefore narrower than it reads. Either add per-package typecheck scripts wired into CI, or expand the root include / drop the exclusions so tsc --noEmit covers the whole workspace. Otherwise changes to those four packages can silently break types.

  2. packageManager vs engines.bun mismatch in root package.json:

    "packageManager": "bun@1.3.13",
    "engines": { "bun": ">=1.3.14" }

    The pinned package manager fails its own engines constraint. Bump packageManager to bun@1.3.14 (or relax engines).

  3. packages/workflows/package.json is still "private": true. Already called out in the PR description as a follow-up, but worth flagging as a release-gate item since this package is the headline of the rename.

Code quality

  1. No tripwire for the documented diamond-DAG bug. GraphFrontierTracker.onSettle() is acknowledged to drop parent edges in A→B→D, A→C→D (D ends up with [C] not [B, C]), but test/unit/graph-frontier-tracker.test.ts and test/unit/graph-inference.test.ts have no test.todo / test.skip pinning this. Adding a skipped test that asserts the desired behavior would prevent silent regressions and act as an executable spec for whoever picks up the fix. (See packages/workflows/src/runs/shared/graph-inference.ts:43.)

  2. noUncheckedIndexedAccess dropped from tsconfig.json. Given how much new indexing-heavy code lands (executor.ts, worktree.ts, frontier tracker), restoring this flag would catch a real class of bugs. CLAUDE.md also says "avoid any/unknown" — noUncheckedIndexedAccess is the natural complement.

  3. Mega-files flagged for follow-up are large enough that splitting before merge would help review and git blame:

    • packages/workflows/src/extension/index.ts — 2,143 lines, mixes ExtensionAPI types, command handlers, tool registration, lifecycle, and HIL plumbing.
    • packages/workflows/src/runs/foreground/executor.ts — 1,505 lines.
  4. Minor formatting nit at packages/workflows/src/extension/index.ts:69 — two import statements joined on the same line: } from "../shared/types.js";import { buildRuntimeAdapters } from "./wiring.js";. Lint/format would catch it; worth tightening since the file is heavily-imported.

Performance

  1. Store.snapshot() deep-clones via JSON.parse(JSON.stringify(...)) on every read (packages/workflows/src/shared/store.ts:170). notify() calls it once, and every TUI consumer (graph-view, stage-chat-view, workflow-attach-pane, store-widget-installer, …) calls it again per re-render. With long-running multi-stage runs this becomes the hot path. Two cheap wins:

    • Replace with structuredClone() — native in Bun/Node, considerably faster than JSON round-trips and handles undefined/Date correctly.
    • Cache the snapshot per-version (the store already increments _version) so multiple subscribers in the same tick share one clone.

    Same pattern repeats in runs/background/status.ts at lines 230, 231, 286, 305, 337.

Correctness / sharp edges

  1. Singleton cancellationRegistry (packages/workflows/src/runs/background/cancellation-registry.ts:113) has no clear() and no test-time reset hook. Unit tests that import the singleton instead of calling createCancellationRegistry() will leak AbortControllers across cases. Worth either exporting a reset() method or adding a comment pointing tests at the factory.

  2. Windows browser open in packages/mcp/utils.ts:14 shells out via cmd /c start "" <browser> <target>. Even with proper arg quoting from pi.exec, cmd.exe's start builtin re-parses metacharacters (&, ^, |) in its arguments. URLs containing those characters can misbehave. Not new to this PR if the code is lifted from elsewhere, but worth confirming.

Test coverage

  • Strong unit coverage across executor, store, frontier tracker, persistence, MCP scoping, inline forms, slash dispatch.
  • Integration suite is thin relative to the surface area (7 files). Specifically missing: end-to-end coverage of kill / pause / resume cascading across nested runs, and worktree setup-hook timeout / cleanup behavior.
  • Examples (examples/parallel-fan-out.ts) reach into packages/workflows/src/shared/types.js directly rather than through @bastani/workflows — fine for the in-repo example, but a published consumer would have to expose those types from the public index.ts. Worth confirming the public type surface.

Nice things

  • defineWorkflow builder's immutable chain with WorkflowBuilder / CompletedWorkflowBuilder split (.compile() only available after .run()) is a tidy use of the type system.
  • worktree.ts uses spawnSync(..., { shell: false }) everywhere — no injection surface.
  • Atomic temp-file-then-rename status writer with deduped error notices avoids both torn reads and notification loops.
  • validateInputs deliberately refuses to coerce — surfacing user mistakes early is the right call.

Happy to dig deeper on any of these.

lavaman131 added a commit that referenced this pull request Jun 29, 2026
)

* 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…
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants