Skip to content

v0.4.0

Choose a tag to compare

@dzikowski dzikowski released this 21 Mar 09:07
· 772 commits to main since this release

Summary

  • Docker sandboxing as an opt-in (beta) feature: disabled by default, with explicit config required to enable. New dedicated documentation page consolidates setup and options.
  • Agent inbox (channels) added: workflows can send (channel <- echo "Message") and receive (channel -> workflow) messages via named channels for async event handling.
  • ensure [rule] recover now passes failed rule output as a parameter ($1) to the recover block, enabling context-aware recovery.
  • Logging system overhaul: log now outputs to stdout (for informational messages) while logerr is introduced to write errors/warnings to stderr.
  • Run artifact naming and persistence improved: sequence-prefixed filenames guarantee unique, ordered artifacts across subshells and loops.
  • E2E test suite greatly expanded: high-level helpers now assert on exact run artifacts and CLI output, improving coverage and test clarity.

Full Changelog: v0.3.0...v0.4.0

All changes

  • ensure ... recover now forwards failed ensure output to recover as $1 — In recover loops, Jaiph now captures the failed ensure invocation output (stdout + stderr) and temporarily binds it to positional arg $1 while executing the recover body. This enables patterns like ensure ci_passes recover { echo "$1"; ... } and allows recover logic to inspect failure details directly. Original workflow args are restored after each recover attempt. Existing bounded retry behavior and JAIPH_ENSURE_MAX_RETRIES handling remain unchanged.
  • Docker sandboxing is now opt-in (beta) — Docker sandbox is no longer enabled by default on local machines. runtime.docker_enabled defaults to false in all environments; set runtime.docker_enabled = true or JAIPH_DOCKER_ENABLED=true to enable it. The CI-specific default logic (CI=true → disabled) is removed — the default is simply false everywhere. Docker sandboxing documentation is moved from configuration.md to a new dedicated page: Sandboxing. All references in README, getting-started, configuration, and the homepage now link to the sandboxing page and mark the feature as beta.
  • Dockerfile-based runtime image with pre-installed agent backends — When no explicit docker_image is configured, the runtime checks for .jaiph/Dockerfile in the workspace root. If present, it runs docker build and tags the result as jaiph-runtime:latest, using it instead of the default ubuntu:24.04. A shipped .jaiph/Dockerfile is included with Node.js LTS, Claude Code CLI, cursor-agent, and standard utilities (bash, curl, git, ca-certificates) pre-installed. Agent authentication env vars (ANTHROPIC_API_KEY, CURSOR_*) are now forwarded into the Docker container alongside JAIPH_* variables. When an explicit image is set via JAIPH_DOCKER_IMAGE or runtime.docker_image, the Dockerfile is ignored. Without a .jaiph/Dockerfile, the runtime falls back to ubuntu:24.04 as before. New resolveImage() and buildImageFromDockerfile() functions in docker.ts. DockerRunConfig gains an imageExplicit field to distinguish default vs. configured images. New E2E test (73_docker_dockerfile_detection.sh) covers Dockerfile detection, explicit-image bypass, fallback, and env var forwarding.
  • log writes to stdout; new logerr keyword writes to stderr — log "message" now echoes to stdout (previously stderr), making its output capturable and separable from error output. A new keyword logerr "message" mirrors log but writes to stderr. Both emit events on fd 3 for the progress tree. In the CLI tree, log lines display with a dim ℹ (unchanged); logerr lines display with a red !. The runtime functions are jaiph::log (stdout + LOG event) and jaiph::logerr (stderr + LOGERR event). Parser, transpiler, types, and event handling updated. New E2E test (92_log_logerr.sh) validates stdout/stderr separation and .out/.err artifact content for both keywords.
  • Fix: Docker runs now persist artifacts on the host — When Docker mode was enabled, run artifacts (.out files, run_summary.jsonl) were written to a host-only absolute path inside the container filesystem and lost on docker run --rm. The root cause: JAIPH_WORKSPACE was forwarded unchanged into the container, so steps.sh computed run paths using the host absolute path (e.g. /Users/me/project/.jaiph/runs/) which doesn't exist inside the container. The fix: buildDockerArgs() now calls remapDockerEnv() before forwarding JAIPH_* variables. JAIPH_WORKSPACE is always overridden to /jaiph/workspace inside the container. JAIPH_RUNS_DIR is handled based on its value: relative paths pass through unchanged, absolute paths inside the host workspace are remapped to the equivalent container path under /jaiph/workspace, and absolute paths outside the host workspace cause a clear E_DOCKER_RUNS_DIR error (unsupported in container path mapping). Non-Docker behavior is unchanged. New E2E test (72_docker_run_artifacts.sh) covers the happy path, relative JAIPH_RUNS_DIR, absolute JAIPH_RUNS_DIR inside workspace, and absolute JAIPH_RUNS_DIR outside workspace (gated on Docker availability).
  • Fix: Prompt output displayed multiple times in CLI — When an LLM backend returned the same answer in multiple JSON event formats (e.g. a content_block_delta, then an assistant message, then a result summary), the stream parser wrote each copy to the progress tree, causing the prompt response to appear two or three times in the output. The parser (jaiph::stream_json_to_text in prompt.sh) now tracks whether a final-answer message has already been emitted (sawFinalMessage flag) and skips subsequent duplicates. The full response is still captured in the step's .out artifact file. Only the CLI display was affected — no data was lost.
  • Log messages use ℹ symbol in CLI progress tree — log lines in the run progress tree now display with the ℹ (information) symbol instead of the log keyword (e.g. ℹ saving impl.patch instead of log saving impl.patch). The symbol is rendered in dim/gray, matching the previous log styling.
  • Breaking: Channel identifier always on the left — Channel syntax is redesigned so the channel name is always on the left side of the operator. Send uses channel <- command (was command -> channel); route declarations use channel -> workflow (was on channel -> workflow). The on keyword is removed from route declarations. Standalone forwarding is now channel <- (was -> channel). The old right-sided send syntax is no longer supported. Parser, transpiler, CLI output, docs, E2E tests, and syntax highlighting are all updated. See Inbox & Dispatch.
  • Inbox: channel passed as named parameter to dispatched workflows — Dispatched workflows now receive the event channel as a named parameter (channel=<name>) instead of displaying it via custom rendering logic. The channel appears in the CLI progress tree through the standard key="value" parameter display pipeline (e.g. ▸ workflow analyst (channel="findings")), removing the need for dispatch-specific display code in run.ts. Internally, inbox.sh sets JAIPH_STEP_PARAM_KEYS='channel' on dispatch so jaiph::step_params_json() in events.sh emits the channel as a named param in JSONL events. JAIPH_DISPATCH_CHANNEL is still set for event metadata tagging ("dispatched":true,"channel":"…"). The custom formatStartLine branch that handled channel+message formatting is removed; all step kinds now use the same formatNamedParamsForDisplay path.
  • Fix: Sequence counter lost across subshells in looped run steps — When a workflow used run inside a loop (e.g. for item in a b c; do result = run sub "$item"; done), only the last iteration's artifact files were retained because every subshell started from the same in-memory JAIPH_STEP_SEQ value, producing identical file name prefixes that overwrote earlier iterations. The sequence counter is now persisted to a file ($JAIPH_RUN_DIR/.seq) that is created at run init and read/written atomically by each step. Subshells spawned by run read the current value from disk, increment it, and write it back, so every step across all iterations receives a unique, monotonically increasing sequence number. New E2E test (71_loop_run_artifacts.sh) verifies that a three-iteration loop produces three distinct prompt .out files with correct sequence prefixes.
  • Unified parameter display with whitespace normalization — The CLI progress tree now uses a single key="value" format for all step parameters everywhere. Positional arguments ($1, $2, argN) display as 1="value", 2="value", etc.; named arguments display as name="value". Line breaks, tabs, and consecutive spaces in parameter values are collapsed to a single space before display, fixing garbled multi-line output (e.g. when a prompt role or task body contained newlines). The $ prefix on positional keys is removed. Internal display functions are consolidated: formatNamedParamsForDisplay is now used uniformly and formatParamsForDisplay is no longer used in the run renderer. A new normalizeParamValue utility handles whitespace collapsing.
  • Prompt steps no longer show output in the CLI tree — When a prompt step completes, the tree shows only the step line and ✓ — no Command/Prompt/Reasoning/Final answer block. To display agent output in the tree, use log explicitly: response = prompt "..."; log "$response". The step's .out file in .jaiph/runs/ still contains the full agent transcript for debugging. This keeps the progress tree clean and predictable; output appears only when the user opts in via log.
  • Sequence-prefixed run artifact file names — Step .out and .err files in .jaiph/runs/ are now named with a zero-padded sequence prefix (000001-, 000002-, ...) reflecting step execution order, instead of the previous timestamp-based prefix. This makes file names predictable and monotonically ordered across runs, enabling exact-name assertions in tests without glob matching. The sequence number is derived from the step counter (JAIPH_STEP_SEQ) assigned at step start.
  • E2E: human-readable test helpers (part 2) — Extended E2E helper library with e2e::expect_run_file (assert content of a named artifact file by exact sequence-prefixed name), e2e::expect_run_file_at (same for custom run directories), e2e::expect_run_file_count / e2e::expect_run_file_count_at (assert total artifact file count), e2e::run_dir_at / e2e::latest_run_dir_at (locate run directories under custom base paths). Refactored tests in 20_rule_and_prompt, 40_nested_and_native_tests, 70_run_artifacts, 81_tty_progress_tree, and 85_infile_metadata to replace manual nullglob/glob/printf boilerplate with the new helpers. Inline multi-line expected values replace printf '%s\n%s\n...' patterns for readability.
  • E2E: human-readable test helpers — Replaced low-level boilerplate in all 20+ E2E test files with high-level helpers in e2e/lib/common.sh. Tests now follow an explicit Given / When / Then pattern using e2e::file (write workflow from heredoc), e2e::run (build + run), e2e::expect_stdout (full tree output match via heredoc), e2e::expect_out / e2e::expect_rule_out (artifact content match), e2e::expect_out_files (artifact count), e2e::expect_file / e2e::expect_no_file (glob-based artifact content and absence assertions for .err and other files), e2e::expect_fail (assert non-zero exit), e2e::git_init, and e2e::git_current_branch. The helpers encapsulate run-directory discovery, glob matching, ANSI stripping, and time normalization so tests read like specifications rather than shell scripts. No runtime or compiler changes.
  • E2E: full .out file content assertions for all run artifacts — Every E2E test that executes a workflow now asserts on the content of .out (and .err where applicable) files written to .jaiph/runs/, not only the CLI tree output. Tests with deterministic stdout compare the full .out file content via e2e::assert_equals; tests whose steps produce no stdout (redirected output, touch, test) assert that zero .out files exist. Covers 18 test files: 10_basic_workflows, 20_rule_and_prompt, 22_assign_capture, 30_filesystem_side_effects, 40_nested_and_native_tests, 60_ensure_conditionals, 61_ensure_recover, 65_fail_then_retry_pass, 70_run_artifacts, 81_tty_progress_tree, 82_sibling_parse_error, 85_infile_metadata, 86_metadata_scope_nested, 90_function_steps, 91_inbox_dispatch, 91_top_level_local. The runtime (events.sh, steps.sh) now recreates run directories with mkdir -p if they are removed mid-run (e.g. by test cleanup), preventing artifact-write failures.
  • Fix: prompt tree line shows named parameters instead of fake positional args — When a prompt step references shell variables (e.g. prompt "$role does $task"), the progress tree now displays named key="value" pairs instead of misleading positional arguments. Before: ▸ prompt "$role does $task" ("engineer", "Fix bugs") (implies $1, $2). After: ▸ prompt "$role does $task" (role="engineer", task="Fix bugs"). The transpiler (emit-workflow.ts) extracts $var / ${var} references from prompt text and emits them as explicit named arguments and JAIPH_STEP_PARAM_KEYS. The CLI (run.ts) detects named keys and delegates to a new formatNamedParamsForDisplay() formatter that renders key="value" pairs. Prompts without variable references and prompts using only positional $1-style args retain the existing comma-separated value display. E2E and unit test coverage added.
  • E2E: unset agent env overrides in test harness — e2e/lib/common.sh now unsets JAIPH_AGENT_MODEL, JAIPH_AGENT_COMMAND, JAIPH_AGENT_BACKEND, JAIPH_AGENT_TRUSTED_WORKSPACE, JAIPH_AGENT_CURSOR_FLAGS, and JAIPH_AGENT_CLAUDE_FLAGS during e2e::prepare_shared_context, preventing user/machine-level agent configuration from leaking into E2E runs and causing non-deterministic output.
  • E2E tests: full stdout assertions — Replaced partial e2e::assert_contains checks on stdout with exact e2e::assert_output_equals comparisons across seven test files (61_ensure_recover, 90_function_steps, 91_inbox_dispatch, 22_assign_capture, 20_rule_and_prompt, 50_cli_and_parse_guards, 82_sibling_parse_error). Each assertion now matches the complete normalized tree output, catching regressions in formatting, ordering, or extra/missing lines. e2e::normalize_output gained an agent-command normalizer (cursor-agent/printf %s lines → <agent-command>) so expected output remains deterministic across backends.
  • Unified runtime output reporting between Docker and non-Docker modes — The bash stdlib now always embeds out_content in STEP_END events (and err_content for failed steps), regardless of whether the step was inbox-dispatched. The CLI uses this embedded content exclusively for display — no more readFileSync(out_file) fallback in run.ts. In errors.ts, readFailedStepOutput() prefers embedded out_content/err_content from the summary and falls back to reading files only for older summaries that lack them. Embedded content is capped at 1 MB; output exceeding this limit is truncated with a [truncated] marker. The StepEvent type gains an err_content: string field. out_file/err_file artifacts remain on disk for debugging/archival. Docker TTY stream merging and line-based demuxing are unchanged and documented as a known limitation.
  • Fix: Dispatched step output now displays in Docker mode — When running inside the Docker sandbox, log and stdout output from inbox-dispatched steps was silently lost because the CLI tried to read out_file from the host filesystem, but the file lived inside the container. The runtime now embeds stdout content directly in the STEP_END event as out_content. The CLI prefers out_content when present and falls back to reading out_file for non-dispatched steps. The StepEvent type gains an out_content: string field. Non-Docker runs are unaffected.
  • Fix: Filter __JAIPH_EVENT__ lines from stdout in Docker mode — When Docker runs with -t (TTY passthrough), the container merges stderr into stdout, causing raw __JAIPH_EVENT__ {"type":"STEP_START",...} JSON lines to appear in terminal output. The CLI now buffers Docker stdout line-by-line and routes event lines through the same handler that processes stderr in non-Docker mode. Non-event lines pass through to the terminal unchanged. Any partial line remaining after the child process exits is flushed with the same filtering. Non-Docker runs are unaffected. The progress tree output is now identical regardless of whether Docker is enabled.
  • Fix: Dispatched inbox steps now render in the progress tree — The CLI progress output now correctly displays dispatched workflow steps with their channel name via the standard parameter display: ▸ workflow analyst (channel="findings"). Previously, dispatched STEP_START/STEP_END events were emitted by the runtime but the CLI did not parse the dispatched and channel fields, so inbox-triggered steps were invisible in the progress tree. The StepEvent type now includes dispatched: boolean and channel: string. The channel is passed as a named parameter so it flows through formatNamedParamsForDisplay like any other step parameter. New e2e test verifies channel names appear in CLI output.
  • Harden inbox runtime for bash 3.2 and subshell correctness — The inbox dispatch system (src/runtime/inbox.sh) no longer uses bash associative arrays (broken on bash 3.2 — reading a non-existent key can return the last inserted value). Routes are now stored as a newline-delimited list. The dispatch queue and sequence counter are file-backed (inbox/.queue, inbox/.seq) so that sends inside subshells (e.g. run_step pipelines) survive into the parent process. New jaiph::_lookup_route helper. No user-facing syntax changes.
  • E2E tests for inbox/dispatch — New e2e/tests/91_inbox_dispatch.sh covers four scenarios: basic send + route, multi-target route dispatch, silent drop on unregistered channel, and inbox file written. Runs as part of npm run test:e2e.
  • Homepage inbox sample tab — docs/index.html now has a fourth sample tab (inbox_pipeline.jh) showing a two-stage workflow pipeline: scanner sends findings, analyst summarizes, reviewer echoes the result. Demonstrates -> send, on route, and chained dispatch.
  • Syntax highlighting: local, on, ->, expectNotContain — The docs syntax highlighter (docs/assets/js/main.js) now recognises the local, on, and expectNotContain keywords and the -> arrow operator. local name = value highlights the variable name as a definition. on channel -> target1, target2 highlights on as a keyword, -> as an operator, and the workflow targets as identifiers. The arrow -> is tokenised as a two-character operator instead of two separate symbols.
  • Restructured .jaiph/runs/ directory layout — Run artifacts are now stored under <YYYY-MM-DD>/<HH-MM-SS>-<source-file>/ instead of the previous flat <timestamp>-<run-id>/ layout (e.g. .jaiph/runs/2026-03-18/07-03-28-say_hello.jh/). The date and time use local time. If the same file is run twice within one second, a collision suffix (-2, -3, etc.) is appended. JAIPH_RUN_ID is still generated and available in the runtime environment for internal tracking and JSONL events. JAIPH_RUNS_DIR override is still respected — the date/time subdirectory structure applies under the custom root as well. The CLI now exports JAIPH_SOURCE_FILE (basename of the input file) to the runtime environment.
  • Fix: Error message prefix jai: renamed to jaiph: — All runtime and compiler diagnostic messages now use the jaiph: prefix instead of the abbreviated jai:. Affects stderr messages from the stdlib (jaiph__die, jaiph__expect_contain, jaiph__expect_not_contain), prompt backend errors, mock dispatch errors, schema validation errors, and the generated bootstrap preamble (stdlib not found, incompatible runtime). No behavioral change — only the human-readable prefix is updated.
  • Fix: Docker enabled by default for local execution — resolveDockerConfig() now returns enabled: true when no env var or in-file config is set and CI is not true. Previously, Docker was never enabled unless explicitly configured because DEFAULTS.enabled was false. CI environments (CI=true) continue to default to Docker disabled. Explicit overrides via JAIPH_DOCKER_ENABLED env var or runtime.docker_enabled in-file config still take precedence. prepareGeneratedDir() now accepts an optional buildOutDir parameter and copies all *.sh files from the build output directory into the Docker generated mount, so multi-file workflows with imports work correctly inside the container. The inbox.sh runtime module is now included in the Docker generated directory.
  • Top-level local variable declarations — Modules can now declare variables at the top level with local name = value. Values may be double-quoted strings (multi-line, same quoting rules as prompt), single-quoted strings, or bare values. Variables are module-scoped and transpile to prefixed bash variables using __ as separator (e.g. local role in module entry becomes entry__role="..."). Inside each rule, function, and workflow body, a local shim is emitted so $role resolves to the prefixed variable. Variable names participate in the unified namespace — they cannot collide with rule, workflow, or function names (E_PARSE). Variables are not exportable; cross-module access is not supported. New EnvDeclDef type and envDecls field on jaiphModule. Parser: src/parse/env.ts. Grammar: env_decl production in Grammar.
  • Fix: single-file run no longer compiles sibling .jh files — jaiph run file.jh now compiles only the specified file and its transitive imports instead of every .jh file in the parent directory. A parse error in a sibling file no longer prevents execution of unrelated files. Directory mode (jaiph build ./) is unchanged and continues to compile all files. The fix introduces collectFileWithImports() in src/transpile/build.ts, which walks imports via the AST to build the minimal file set.
  • Inbox & dispatch: event passing between agent workflows — Workflows can now send messages to named channels with the -> send operator and declare routing rules with on <channel> -> <workflow>. The runtime dispatches messages sequentially via an in-memory queue — no filesystem watchers, no polling, no inotifywait/fswatch. echo "data" -> findings transpiles to jaiph::send 'findings' "$(echo "data")". Standalone -> channel forwards $1. on findings -> analyst registers a route; when a message arrives on findings, analyst is called with the message content as $1. Multi-target routes (on ch -> wf1, wf2) dispatch sequentially in declaration order; each target receives the same message. Routes are static declarations stored in WorkflowDef.routes, not executable steps. The dispatch queue drains after the orchestrator completes; invoked workflows may produce further sends. Max dispatch depth of 100 guards against circular sends (E_DISPATCH_DEPTH). Send to an unregistered channel is a silent drop (message still written to inbox for audit). Non-zero exit from a dispatched workflow halts the queue (fail-fast). Inbox files are written as NNN-<channel>.txt under .jaiph/runs/<run-id>/inbox/. name = cmd -> channel is a parse error (E_PARSE); use two steps instead. New runtime functions in src/runtime/inbox.sh: jaiph::inbox_init, jaiph::send, jaiph::register_route, jaiph::drain_queue. Progress tree shows on routes as nodes; dispatched calls appear as children with dispatched: true and channel metadata. See Inbox & Dispatch.
  • Docker sandbox runtime for workflow execution — New optional Docker sandbox isolates workflow execution in a disposable container. Docker is enabled by default on local machines; disable with runtime.docker_enabled = false or JAIPH_DOCKER_ENABLED=false. The container receives only the transpiled bash script and jaiph_stdlib.sh — no Jaiph source, TypeScript, or Node.js. New src/runtime/docker.ts module handles mount parsing/validation, image pull, UID/GID mapping (Linux), TTY passthrough, timeout enforcement (E_TIMEOUT), and Docker availability checks (E_DOCKER_NOT_FOUND). Mount strings support full form (host:container:mode) and shorthand (host:mode); exactly one mount must target /jaiph/workspace. JAIPH_STDLIB is set to /jaiph/generated/jaiph_stdlib.sh inside the container. CI=true disables Docker by default unless in-file override is set. Precedence: env vars (JAIPH_DOCKER_*) > in-file config > defaults. jaiph init now recommends adding .jaiph/ (not just .jaiph/runs/) to .gitignore.
  • Config parser: support integer and array value types — parseMetadataValue() now handles bare integer literals (regex /^[0-9]+$/, returned as number) and bracket-delimited arrays of quoted strings (returned as string[]). Multi-line arrays support trailing commas, inline # comments, and empty arrays (= []). A new runtime.* key namespace is added with five keys: runtime.docker_enabled (boolean, default true locally, false in CI), runtime.docker_image (string, default "ubuntu:24.04"), runtime.docker_network (string, default "default"), runtime.docker_timeout (integer, default 300), and runtime.workspace (string[], default [".:/jaiph/workspace:rw"]). Each key enforces its expected type at parse time (E_VALIDATE on mismatch). Unknown runtime.* keys produce E_PARSE. A new RuntimeConfig interface is added to src/types.ts and an optional runtime field to WorkflowMetadata.
  • Enforce calling conventions and unify symbol namespace — Breaking change. Rules, workflows, and functions now share a single namespace per module; declaring two items with the same name (e.g. a rule foo and a workflow foo) yields E_PARSE. The compiler enforces calling conventions at compile time: ensure must target a rule (E_VALIDATE if used on a workflow or function, with a message indicating the correct keyword), run must target a workflow (E_VALIDATE if used on a rule or function), and functions cannot be used with ensure or run. These checks apply to both local and imported references. Internally, the bash symbol format is flattened from <module>::rule::<name> / <module>::workflow::<name> / <module>::function::<name> to <module>::<name>, with the step kind passed as an explicit argument to jaiph::run_step and jaiph::run_step_passthrough. The resolveShellFunctionRefs pass is generalized to resolveShellRefs, resolving any alias.name in shell context to the flat symbol::name form. External scripts that call generated bash functions by their old triple-prefix names will need to update to the new <module>::<name> format.
  • Docs: Update samples to use log instead of cat — The say_hello.jh sample now uses log "$response" to display the agent's reply in the progress tree instead of writing to a file and using cat. The run command in docs is simplified from ./say_hello.jh Jakub && cat hello.txt to ./say_hello.jh Jakub. Syntax highlighting JS updated accordingly.
  • Add log keyword for workflow messages — Workflows can now use log "message" to display a message in the progress tree at the correct indentation level. At compile time (jaiph tree / --dry-run), log lines render as static tree nodes with the literal string (unexpanded variables shown as-is). At runtime, log emits a LOG event (not STEP_START/STEP_END) that the progress renderer displays inline at the correct depth — no spinner, no timing. Shell variable interpolation ($var, ${var}) works inside the string at runtime. The log keyword transpiles to jaiph::log "message", a runtime function that emits the event on fd 3 and echoes to stderr. New AST variant { type: "log", message: string, loc: SourceLoc } in WorkflowStepDef. Parse error on log without a double-quoted string argument.
  • Fix empty lines from prompt params in tree view — formatParamsForDisplay() now filters out params whose value is empty or whitespace-only after stripping the key= prefix. Previously, blank lines in a multiline prompt "..." arrived as empty-string params and rendered as empty quoted strings ("") or extra whitespace artifacts in the tree output. The function was also extracted from run.ts into its own format-params.ts module for testability.
  • Support positive if ensure ref args; then in workflow parser — The parser now supports the positive form if ensure <rule_ref> [args]; then ... fi in addition to the existing negated if ! ensure ... form. Both forms now accept optional arguments after the rule reference (e.g. if ensure check "$env"; then). Both forms support else branches (if [!] ensure ref; then ... else ... fi). The parser emits E_PARSE when ensure appears inside an if-ensure then/else branch or in an unrecognised shell context, preventing silent pass-through to bash.
  • Remove dead code: tree branch characters — The run progress tree no longer uses Unicode box-drawing characters (├── , └── , │ ). Nesting is shown with simple indentation only (matching the current ·-prefixed display). Removed the branch field from TreeRow, the RowState type, and unused exported functions: renderProgressTree, renderRunTree, RuntimeGraphStore, RuntimeNode, createRuntimeGraphStore, beginRuntimeNode, completeRuntimeNode, runtimeRunningLine, runtimeCompletedLine, runtimeRunningIndentLine, runtimeCompletedIndentLine, renderRuntimeIndentRows, renderRuntimeTreeRows. No user-visible change — the display already used the indent style.
  • Fix imported workflow metadata scope prefix — prefixForImportedWorkflowCall now resolves the module's emitted symbol (e.g. ensure_ci_passes) instead of the import alias (e.g. ci) when generating with_metadata_scope prefixes. This fixes incorrect scope prefixes for imported workflows whose alias differs from their filename symbol.
  • Tolerant JSON extraction for typed prompts — When a prompt "..." returns '{ ... }' step receives a response, the runtime now extracts JSON even when the agent returns text before the JSON object on the same line (e.g. Here is the result: {"field": "value"}). The parser tries multiple strategies in order: last non-empty line, fenced code blocks, a standalone {…} line, and embedded JSON within a line (text before { is stripped). The first candidate that parses as valid JSON wins. This makes typed prompts more robust against LLMs that add preamble text before their JSON output.
  • Business analyst review workflow (ba_review.jh) — New workflow that reviews the first task in QUEUE.md before development begins. An agent prompt evaluates the task for clarity, consistency with the codebase, absence of conflicts with other queued or documented features, and feasibility. If the task passes review, it is marked with a <!-- dev-ready --> comment; otherwise, the workflow exits with questions and recommendations that must be resolved first. The implementation workflow (implement_from_queue.jh) now gates on the dev-ready marker via a new first_task_is_dev_ready rule — tasks without the marker cannot proceed to implementation. The main entrypoint (main.jh) runs the BA review before the implementation step.
  • GitHub Pages template matching docs/index.html — Extracted CSS, JS, and layout from docs/index.html into a reusable Jekyll template (_layouts/default.html, assets/css/style.css, assets/js/main.js) with _config.yml. All doc pages (getting-started.md, cli.md, configuration.md, grammar.md, testing.md, hooks.md, jaiph-skill.md) now render through the shared template with consistent header, nav, card styling, code block copy buttons, and footer. docs/index.html uses layout: null to keep its custom landing-page layout. Clean permalink URLs (e.g. /getting-started instead of /getting-started.md) with redirect_from for backward compatibility. No duplicate CSS/JS — single source of truth in assets/.
  • Docs Jekyll theme: section-based layout — New _layouts/docs.html template renders markdown pages with the same visual style as docs/index.html: h1 and intro in a hero panel, h2 headers outside white card boxes, section content inside cards. Navigation links to all docs pages are provided by the template header — removed manual navigation blocks from individual markdown pages. Updated docs_parity.jh prompts to enforce this pattern.