From 1e0c75699f9614d7958de0f2fd2013dc74ea661b Mon Sep 17 00:00:00 2001 From: Jakub Dzikowski Date: Wed, 13 May 2026 13:28:24 +0200 Subject: [PATCH 1/5] =?UTF-8?q?chore:=20release=200.9.4=20=E2=80=94=20vers?= =?UTF-8?q?ion=20bump=20and=20changelog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor Signed-off-by: Jakub Dzikowski --- .jaiph/docs_parity.jh | 138 ++++++++++++++++++++-------------------- CHANGELOG.md | 39 ++++++++++-- README.md | 2 +- docs/cli.md | 2 +- docs/index.html | 6 +- docs/install | 6 +- docs/setup.md | 4 +- package-lock.json | 4 +- package.json | 2 +- src/cli/commands/use.ts | 2 +- src/cli/index.ts | 2 +- 11 files changed, 119 insertions(+), 88 deletions(-) diff --git a/.jaiph/docs_parity.jh b/.jaiph/docs_parity.jh index 10209f1c..bfec7344 100755 --- a/.jaiph/docs_parity.jh +++ b/.jaiph/docs_parity.jh @@ -1,29 +1,29 @@ #!/usr/bin/env jaiph const role = """ - You are an expert technical writer for this project. - 1. You are fluent in Markdown and can read TypeScript code and Bash - 2. You write for a developer audience, focusing on clarity and practical - examples. - 3. You are concise, specific, and value dense - 4. Write so that a new developer to this codebase can understand your - writing, but don't assume your audience are experts in the topic/area you - are writing about. - 5. You are good in formulating generic context and describing the problem - starting from the generic part, leaving the specific details for the - last step, once the audience is aware of the generic context and the - problem. - 6. You write problem explanation and goals in a human approachable way, - while keeping details dense in separate sections, so both human and AI - 7. Source code and docs/architecture.md are the single source of truth. You don't - trust the existing documentation blindly. + You are an expert technical writer for this project. + 1. You are fluent in Markdown and can read TypeScript code and Bash + 2. You write for a developer audience, focusing on clarity and practical + examples. + 3. You are concise, specific, and value dense + 4. Write so that a new developer to this codebase can understand your + writing, but don't assume your audience are experts in the topic/area you + are writing about. + 5. You are good in formulating generic context and describing the problem + starting from the generic part, leaving the specific details for the + last step, once the audience is aware of the generic context and the + problem. + 6. You write problem explanation and goals in a human approachable way, + while keeping details dense in separate sections, so both human and AI + 7. Source code and docs/architecture.md are the single source of truth. You don't + trust the existing documentation blindly. """ script assert_newline_paths_are_files = ``` -while IFS= read -r f; do - [ -z "$f" ] && continue - test -f "$f" || return 1 -done <<< "$1" + while IFS= read -r f; do + [ -z "$f" ] && continue + test -f "$f" || return 1 + done <<< "$1" ``` rule docs_files_present(list) { @@ -31,20 +31,20 @@ rule docs_files_present(list) { } script assert_worktree_clean_for_docs = ``` -local current_changed_files -current_changed_files="$( - { - git diff --name-only --cached - git diff --name-only - git ls-files --others --exclude-standard - } | sort -u -)" -if [ -n "$current_changed_files" ]; then - echo "Refusing to run docs parity workflow on a dirty worktree." >&2 - echo "Please commit, stash, or discard these files first:" >&2 - echo "$current_changed_files" >&2 - return 1 -fi + local current_changed_files + current_changed_files="$( + { + git diff --name-only --cached + git diff --name-only + git ls-files --others --exclude-standard + } | sort -u + )" + if [ -n "$current_changed_files" ]; then + echo "Refusing to run docs parity workflow on a dirty worktree." >&2 + echo "Please commit, stash, or discard these files first:" >&2 + echo "$current_changed_files" >&2 + return 1 + fi ``` rule worktree_is_clean() { @@ -52,23 +52,23 @@ rule worktree_is_clean() { } script assert_only_allowed_changed = ``` -local allowed="$1" -local after_changed_files -after_changed_files="$( - { - git diff --name-only --cached - git diff --name-only - git ls-files --others --exclude-standard - } | sort -u -)" -while IFS= read -r changed_file; do - [ -z "$changed_file" ] && continue - if [[ $'\n'"$allowed"$'\n' == *$'\n'"$changed_file"$'\n'* ]]; then - continue - fi - echo "Unexpected file changed by docs prompt: $changed_file" >&2 - return 1 -done <<< "$after_changed_files" + local allowed="$1" + local after_changed_files + after_changed_files="$( + { + git diff --name-only --cached + git diff --name-only + git ls-files --others --exclude-standard + } | sort -u + )" + while IFS= read -r changed_file; do + [ -z "$changed_file" ] && continue + if [[ $'\n'"$allowed"$'\n' == *$'\n'"$changed_file"$'\n'* ]]; then + continue + fi + echo "Unexpected file changed by docs prompt: $changed_file" >&2 + return 1 + done <<< "$after_changed_files" ``` rule only_expected_docs_changed_after_prompt(allowed) { @@ -80,26 +80,26 @@ script first_line_str = `printf '%s\n' "$1" | head -n 1` script rest_lines_str = `printf '%s\n' "$1" | tail -n +2` script list_docs_md_paths = ``` -local out="" -local f -for f in docs/*.md; do - out="${out:+$out -}$f" -done -printf '%s\n' "$out" + local out="" + local f + for f in docs/*.md; do + out="${out:+$out + }$f" + done + printf '%s\n' "$out" ``` script build_allowed_paths_block = ``` -local out="README.md -docs/index.html -docs/_layouts/docs.html -src/cli/shared/usage.ts" -local f -for f in docs/*.md; do - out="$out -$f" -done -printf '%s\n' "$out" + local out="README.md + docs/index.html + docs/_layouts/docs.html + src/cli/shared/usage.ts" + local f + for f in docs/*.md; do + out="$out + $f" + done + printf '%s\n' "$out" ``` script join_newline_args = `printf '%s\n' "$@"` @@ -124,7 +124,7 @@ workflow update_from_task(taskDesc) { The task description is: ${taskDesc} -""" + """ } workflow docs_page(path) { diff --git a/CHANGELOG.md b/CHANGELOG.md index add83fb6..4e7987e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,46 @@ # Unreleased -- **Cleanup — remove `JAIPH_TEST_MODE` event suppression from production runtime code:** `RuntimeEventEmitter.emitStep` / `emitLog` no longer read `this.env.JAIPH_TEST_MODE` to decide whether to write `__JAIPH_EVENT__` lines to stderr. A construction-time `suppressLiveEvents?: boolean` option replaces the per-call env check: `NodeWorkflowRuntime` accepts it in its options and forwards it to `RuntimeEventEmitter`. `node-test-runner.ts` passes `suppressLiveEvents: true` when constructing the in-process runtime for `test_run_workflow` steps so `node --test` reporter output stays clean. `JAIPH_TEST_MODE: "1"` is still set in the test runner's env — but only for `prompt.ts`'s mock-mode selection, not event emission. No other production caller constructs `NodeWorkflowRuntime` directly, so the spawned `node-workflow-runner.js` child defaults to `suppressLiveEvents: false` and live events stream to stderr exactly as before. Durable `appendRunSummaryLine` writes to `run_summary.jsonl` are unchanged in either mode. Existing in-process unit tests under `node-workflow-runtime.artifacts.test.ts` pass the new option through their `NodeWorkflowRuntime` constructions. +# 0.9.4 + +## Summary +Maintenance and simplification: +- **Breaking:** Inbox dispatch is sequential only (parallel config/env removed). Stricter grammar: multiline `config` blocks only; no one-line braced workflows; no semicolon-separated statements in workflow/rule bodies. +- **Runtime:** Single-line shell steps run in the Node runtime (`sh -c`); script capture only on success; async `run` + `recover` return propagation fixed; mock prompts use JSON arm dispatch and an in-memory response queue; inbox artifact files are written only when a route consumes the channel. +- **CLI / install:** Failure footers use the **last** failed step in `run_summary.jsonl`; curl install ships `package.json` so stable installs resolve the correct default Docker image tag. +- **Language:** RHS bare identifiers and bare dotted identifiers are treated as interpolation sugar where applicable. +- **Library:** `artifacts.save(paths)` in single-argument form (path or newline-separated list); `git format-patch` workflows use `--stdout` so patch bytes are captured. +- **Repo:** `node-workflow-runtime` split into arg-parser, event-emitter, and mock modules; test directories consolidated under `integration/`, `test-fixtures/`, `test-infra/`; `JAIPH_TEST_MODE` no longer suppresses stderr events in runtime code (constructor option instead). +- **Docs / DX:** Agent-proxy design note; explicit parse error for `test` blocks outside `*.test.jh`; architecture/inbox corrections; getting-started shortened. + +## All changes + +- **Breaking — Language:** Inline one-line `config { k = v }` is removed — only the multiline `config {\n … \n}` form parses (matches documented grammar). The formatter no longer emits compact inline `config`, which would be invalid input. Examples such as `examples/async.jh` were migrated. +- **Breaking — Language:** Single-line `workflow name() { stmt }` braced form removed; workflow and rule bodies require one statement per line as in the grammar. +- **Breaking — Language:** Semicolons no longer separate statements in workflow/rule bodies (`splitStatementsOnSemicolons` remains for `match` arms). Multiple statements on one line joined by `;` must be split across lines. +- **Breaking — Inbox dispatch is always sequential** — The optional parallel inbox mode is removed: there is no `run.inbox_parallel` config key, no `JAIPH_INBOX_PARALLEL` environment variable (it is ignored), and no `JAIPH_INBOX_PARALLEL_LOCKED` shim. Route targets for a queued message always run **one after another** in declaration order on the `channel` line, inside `NodeWorkflowRuntime`’s `drainWorkflowQueue`. Using `run.inbox_parallel = …` in a `config { … }` block is `E_PARSE: unknown config key: run.inbox_parallel`. Docs and E2E now match sequential-only semantics; unit tests cover the unknown key and parity of dispatch event order with and without the old env var set. E2E harness clears inherited `JAIPH_*` noise so CI stays reliable in polluted agent environments. +- **Language / Runtime:** Single-line shell steps execute via `sh -c` with script working-directory semantics in the Node workflow runtime (replacing the removed bash-era path for these steps). `validateReferences` and related checks were extended for `send` arrow targets, managed `run` on bare names, and dotted references. +- **Fix — Interpolation:** RHS values treat bare identifiers and bare dotted identifiers as `${…}` interpolation sugar where the grammar allows, so dotted env-style names behave consistently with other binding references. +- **Fix — Runtime return capture:** `executeScript` / `executeShLine` / `executeMockShellBody` return captured stdout only when the subprocess exits with status 0 (failed commands no longer leak stdout as a workflow return value). +- **Fix — Async recover:** `run … recover(e) { … }` now propagates `recoverReturn` through the implicit async join site (parity with synchronous `ensure` / catch semantics). +- **Language:** Reject `return 0`, `return $?`, and bare integer `return N` in workflows/rules with a clear diagnostic instead of emitting a useless shell line. +- **Runtime — Mock prompts:** Mock arms are passed structurally as JSON via `JAIPH_MOCK_PROMPT_ARMS_JSON` with in-process dispatch in `mock.ts` (no bash dispatcher). Sequential mock responses use `JAIPH_MOCK_RESPONSES_JSON` and an in-memory queue (`consumeNextMockResponse`), removing per-step file churn. +- **Runtime — Inbox files:** Inbox files under the run directory are written only when a route consumes the channel (no “audit-only” files for unrouted sends). +- **Fix — Mock shell:** `executeMockShellBody` uses `bash -c` instead of a tempfile indirection; removes an ESM/`require` shadowing bug in the mock shell path. +- **Library:** `jaiphlang/artifacts` exposes a single `save(paths)` workflow: one filesystem path or a newline-separated list; destination relpaths are derived per source (leading `./` stripped; absolute sources use `basename` only). The bundled engineer workflow uses `git.commit` plus `git format-patch` with `--stdout` / `HEAD` so the patch **contents** are saved (without `--stdout`, `format-patch` only printed the filename on stdout). +- **Parser / UX:** A `test { … }` block in a file whose name does not end in `.test.jh` now fails with `E_PARSE` explaining that test blocks belong in `*.test.jh` (instead of falling through to a generic unsupported-statement error). +- **Repo — Compiler/runtime cleanup:** Removed a large amount of dead bash-era kernel code and legacy parse rejects; consolidated import parsers and config-key handling; stricter top-level dispatch in `parser.ts`. `.jaiph/git.jh` moves to `jaiphlang/git` with `import "jaiphlang/git" as git`. Collapsed duplicate parser/runtime paths from the audit series (`B1`, `B10`, `B11`, etc.). +- **Repo — AST clarity (no source keyword changes):** AST field names now align with keywords: the single-shot branch is `step.catch`, the repair-and-retry loop body is `step.recover`. TypeScript uses `catchDef` where `catch` is reserved. Workflow source still uses `run foo() recover(e) { … }` and `run foo() catch(e) { … }`. +- **Fix — Runtime config seed:** Restore `cpSync` seeding of Claude config into the workspace fallback when only session env is unwritable (auth preservation). +- **Docs:** Add `design/2026-05-12-agent-proxy.md` (Phantom Token / credential proxy design for sandboxed agents). Update `architecture.md` (drop stale `run-step-exec` / `seq-alloc` references). Update `inbox.md` (remove unused dispatch env vars; document inbox files only when consumed). Shorten `getting-started` overview. +- **Tests / QA:** E2E and txtar fixtures for `import script` (shell/Python, capture, missing file); extended parse/validate error fixtures; QA scripts (`read_txtar_*`) point at `test-fixtures/compiler-txtar/`. +- **Repo:** `AUDIT_PROGRESS.md` removed (remaining items tracked in `QUEUE.md`). `Gemfile.lock` records `ffi` platform gems for arm64-darwin and x86_64-linux where needed. +- **Cleanup — remove `JAIPH_TEST_MODE` event suppression from production runtime code:** `RuntimeEventEmitter.emitStep` / `emitLog` no longer read `this.env.JAIPH_TEST_MODE` to decide whether to write `__JAIPH_EVENT__` lines to stderr. A construction-time `suppressLiveEvents?: boolean` option replaces the per-call env check: `NodeWorkflowRuntime` accepts it in its options and forwards it to `RuntimeEventEmitter`. `node-test-runner.ts` passes `suppressLiveEvents: true` when constructing the in-process runtime for `test_run_workflow` steps so `node --test` reporter output stays clean. `JAIPH_TEST_MODE: "1"` is still set in the test runner's env — but only for `prompt.ts`'s mock-mode selection, not event emission. No other production caller constructs `NodeWorkflowRuntime` directly, so the spawned `node-workflow-runner.js` child defaults to `suppressLiveEvents: false` and live events stream to stderr exactly as before. Durable `appendRunSummaryLine` writes to `run_summary.jsonl` are unchanged in either mode. Existing in-process unit tests under `node-workflow-runtime.artifacts.test.ts` pass the new option through their `NodeWorkflowRuntime` constructions. - **Repo — `node-workflow-runtime.ts` split:** The 1915-LoC `src/runtime/kernel/node-workflow-runtime.ts` god file is split into the orchestrator plus three focused sibling modules under `src/runtime/kernel/`. No behavior changes — pure relocation; existing tests pass unchanged (helpers re-imported from their new location where needed). - **`runtime-arg-parser.ts`** — every stateless free helper that used to live above the `NodeWorkflowRuntime` class (`interpolate`, `parseInlineCaptureCall`, `commaArgsToInterpolated`, `parseArgsRaw`, `parseInlineScriptAt`, `parseManagedArgAt`, `parseArgTokens`, `stripOuterQuotes`, `parsePromptSchema`, `sanitizeName`, `nowIso`), the `BARE_IDENT_RE` / `MAX_EMBED` / `MAX_RECURSION_DEPTH` constants, and the `ParsedArgToken` / `PromptSchemaField` types. Direct unit tests added in `runtime-arg-parser.test.ts`. - **`runtime-event-emitter.ts`** — `RuntimeEventEmitter` owns `emitWorkflow`, `emitStep`, `emitPromptStepStart`, `emitPromptStepEnd`, `emitPromptEvent`, `emitLog`, plus the monotonic step and prompt sequence counters. Constructed with `{ runId, runDir, env, getFrameStack, getAsyncIndices }`. No more direct `process.stderr.write(__JAIPH_EVENT__ …)` scattered through the runtime. - **`runtime-mock.ts`** — `executeMockBodyDef` and `executeMockShellBody` move here as exported functions taking `{ ref, args, env, cwd, executeStepsBack }` (the last is a callback so steps-kind mocks dispatch back into the runtime). The `require("node:child_process")` call that shadowed ESM imports inside `executeMockShellBody` is gone — replaced by a top-of-file `import`. - The orchestrator (`node-workflow-runtime.ts`) keeps the `NodeWorkflowRuntime` class, workflow/step orchestration (`runDefault`, `runNamedWorkflow`, `executeSteps`, `executeStep`, `runRecoverBody`, `runPromptStep`, frame and scope management), async-handle bookkeeping (`getAsyncIndices`, `getFrameStack`), and heartbeat (`startHeartbeat`, `stopHeartbeat`, `writeHeartbeat`). Dependency direction is one-way (orchestrator → helpers/emitter/mock); no circular imports. - -- **Breaking — Inbox dispatch is always sequential** — The optional parallel inbox mode is removed: there is no `run.inbox_parallel` config key, no `JAIPH_INBOX_PARALLEL` environment variable (it is ignored), and no `JAIPH_INBOX_PARALLEL_LOCKED` shim. Route targets for a queued message always run **one after another** in declaration order on the `channel` line, inside `NodeWorkflowRuntime`’s `drainWorkflowQueue`. Using `run.inbox_parallel = …` in a `config { … }` block is `E_PARSE: unknown config key: run.inbox_parallel`. Docs and E2E now match sequential-only semantics; unit tests cover the unknown key and parity of dispatch event order with and without the old env var set. - - **Fix — CLI failure footer:** `Output of failed step` and the footer `out:` / `err:` paths now resolve from the **last** non-zero `STEP_END` in `run_summary.jsonl` (append order), not the first. The first failure line could be a recovered `catch`/`ensure` attempt, a stray record, or unrelated noise; the last failure matches the terminal step (the one the progress tree marks as failed). **`src/cli/shared/errors.test.ts`** covers multiple non-zero `STEP_END` lines. - **Fix — Docker default image tag:** `curl` / `docs/install` copied only `dist/src` into `~/.local/bin/.jaiph`, so the CLI could not read `package.json` and defaulted the sandbox image to `ghcr.io/jaiphlang/jaiph-runtime:nightly` even for stable installs. The installer now copies `package.json` beside `src/`, and `resolveDefaultDockerImageTag` checks both the installer layout and the npm `dist/src/runtime` layout. - **Repo — Test directory consolidation:** Consolidated the five-way test directory split (`src/**/*.test.ts`, `test/`, `tests/`, `compiler-tests/`, `golden-ast/`) into three test "places" plus two clearly named support directories. File moves: diff --git a/README.md b/README.md index a5bed42c..c113e01e 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ Or install from npm: npm install -g jaiph ``` -Verify: `jaiph --version`. Switch versions: `jaiph use nightly` or `jaiph use 0.9.3`. +Verify: `jaiph --version`. Switch versions: `jaiph use nightly` or `jaiph use 0.9.4`. ## Example diff --git a/docs/cli.md b/docs/cli.md index ddd7c6a0..a5258692 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -393,7 +393,7 @@ jaiph use ```bash jaiph use nightly -jaiph use 0.9.3 +jaiph use 0.9.4 ``` ## File extension diff --git a/docs/index.html b/docs/index.html index 8670e2d1..96a0985e 100644 --- a/docs/index.html +++ b/docs/index.html @@ -77,7 +77,7 @@

Try it out!

curl -fsSL https://jaiph.org/run | bash -s 'workflow default() {  const response = prompt "Say: Hello, I am [model name]!"  log response}'
-

Installs Jaiph v0.9.3 to ~/.local/bin (if not +

Installs Jaiph v0.9.4 to ~/.local/bin (if not already installed), and runs the sample workflow with Cursor CLI agent backend (the default one). @@ -89,14 +89,14 @@

Try it out!

Run the script below from the project directory:

curl -fsSL https://jaiph.org/init | bash
-

Installs Jaiph v0.9.3 to ~/.local/bin (if not +

Installs Jaiph v0.9.4 to ~/.local/bin (if not already installed), and runs jaiph init to initialize the Jaiph workspace in the current directory.

curl -fsSL https://jaiph.org/install | bash
-

The installer will install the version 0.9.3 of Jaiph to +

The installer will install the version 0.9.4 of Jaiph to ~/.local/bin. To switch versions, use jaiph use nightly or jaiph use <version> to switch.

diff --git a/docs/install b/docs/install index a6be8c1a..8c43b126 100755 --- a/docs/install +++ b/docs/install @@ -55,11 +55,11 @@ elif [ -n "${1+x}" ] && [ -d "${1}" ] && [ -f "${1}/package.json" ]; then JAIPH_REPO_URL="${REPO_URL}" fi REPO_URL="${REPO_URL:-${JAIPH_REPO_URL:-https://github.com/jaiphlang/jaiph.git}}" -# Version/ref: first argument only when not a local path, or JAIPH_REPO_REF env, or default tag v0.9.3. +# Version/ref: first argument only when not a local path, or JAIPH_REPO_REF env, or default tag v0.9.4. if [ -n "${JAIPH_FROM_LOCAL}" ]; then - REPO_REF="${JAIPH_REPO_REF:-v0.9.3}" + REPO_REF="${JAIPH_REPO_REF:-v0.9.4}" else - REPO_REF="${1:-${JAIPH_REPO_REF:-v0.9.3}}" + REPO_REF="${1:-${JAIPH_REPO_REF:-v0.9.4}}" fi BIN_DIR="${JAIPH_BIN_DIR:-$HOME/.local/bin}" LIB_DIR="${JAIPH_LIB_DIR:-${BIN_DIR}/.jaiph}" diff --git a/docs/setup.md b/docs/setup.md index 2a382b74..0eefa22a 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -31,11 +31,11 @@ jaiph --version If the command is not found, ensure `~/.local/bin` (installer) or the npm global bin directory is in your `PATH`. -Switch versions at any time (re-runs the install script with a Git ref: `nightly` or `v` such as `v0.9.3` when you pass `0.9.3`): +Switch versions at any time (re-runs the install script with a Git ref: `nightly` or `v` such as `v0.9.4` when you pass `0.9.4`): ```bash jaiph use nightly -jaiph use 0.9.3 +jaiph use 0.9.4 ``` The default install command is `curl -fsSL https://jaiph.org/install | bash`. Override it with `JAIPH_INSTALL_COMMAND` if you need a fork, air-gapped bundle, or local script. diff --git a/package-lock.json b/package-lock.json index 2dbbe150..393fb652 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "jaiph", - "version": "0.9.3", + "version": "0.9.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "jaiph", - "version": "0.9.3", + "version": "0.9.4", "bin": { "jaiph": "dist/src/cli.js" }, diff --git a/package.json b/package.json index def844bd..bd08315e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jaiph", - "version": "0.9.3", + "version": "0.9.4", "description": "jaiph compiler/transpiler", "repository": { "type": "git", diff --git a/src/cli/commands/use.ts b/src/cli/commands/use.ts index b1440327..f575fad4 100644 --- a/src/cli/commands/use.ts +++ b/src/cli/commands/use.ts @@ -14,7 +14,7 @@ function toInstallRef(version: string): string | undefined { export function runUse(rest: string[]): number { const version = rest[0]; if (!version) { - process.stderr.write("jaiph use requires a version (e.g. 0.9.3) or 'nightly'\n"); + process.stderr.write("jaiph use requires a version (e.g. 0.9.4) or 'nightly'\n"); return 1; } const ref = toInstallRef(version); diff --git a/src/cli/index.ts b/src/cli/index.ts index 2270770a..3248529e 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -16,7 +16,7 @@ export async function main(argv: string[]): Promise { return 0; } if (cmd === "--version" || cmd === "-v") { - process.stdout.write("jaiph 0.9.3\n"); + process.stdout.write("jaiph 0.9.4\n"); return 0; } try { From 64f6dbc576c9b5a51a6cd87bc8ff685804527041 Mon Sep 17 00:00:00 2001 From: Jakub Dzikowski Date: Wed, 13 May 2026 14:55:23 +0200 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20for=20=E2=80=A6=20in=20=E2=80=A6=20?= =?UTF-8?q?iteration=20over=20newline-delimited=20strings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add parse, validate, emit, and runtime support for `for in { … }` in workflows and rules. Split helper normalizes CRLF and drops a trailing empty segment after a final newline. Add string-lines unit tests and E2E 135. Document the change under # Unreleased in CHANGELOG.md. Co-authored-by: Cursor --- CHANGELOG.md | 3 + e2e/test_all.sh | 1 + e2e/tests/135_for_string_lines.sh | 99 +++++++++++++++++++++ src/format/emit.ts | 7 ++ src/parse/core.ts | 2 +- src/parse/workflow-brace.ts | 21 +++++ src/runtime/kernel/node-workflow-runtime.ts | 17 ++++ src/runtime/string-lines.test.ts | 25 ++++++ src/runtime/string-lines.ts | 13 +++ src/transpile/emit-script.ts | 4 + src/transpile/validate.ts | 66 +++++++++++--- src/types.ts | 8 ++ 12 files changed, 254 insertions(+), 12 deletions(-) create mode 100644 e2e/tests/135_for_string_lines.sh create mode 100644 src/runtime/string-lines.test.ts create mode 100644 src/runtime/string-lines.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e7987e8..be0622c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Unreleased +- **Language:** `for in { … }` in workflows and rules iterates newline-delimited lines of a string binding. Newlines normalize `\r\n` to `\n`; a single trailing empty segment from a final newline is omitted. Lines are not trimmed and empty interior lines are still iterated unless the body skips them (e.g. `if line != "" { … }`). Documented in `docs/language.md`. +- **Tests / QA:** Unit tests for string line splitting (`src/runtime/string-lines.test.ts`); E2E `e2e/tests/135_for_string_lines.sh`. + # 0.9.4 ## Summary diff --git a/e2e/test_all.sh b/e2e/test_all.sh index 114c6a7a..d99eb90e 100755 --- a/e2e/test_all.sh +++ b/e2e/test_all.sh @@ -88,6 +88,7 @@ TEST_SCRIPTS=( "e2e/tests/132_return_log_inline_script.sh" "e2e/tests/133_return_bare_identifier.sh" "e2e/tests/134_script_imports.sh" + "e2e/tests/135_for_string_lines.sh" ) PASS_COUNT=0 diff --git a/e2e/tests/135_for_string_lines.sh b/e2e/tests/135_for_string_lines.sh new file mode 100644 index 00000000..8f5f262a --- /dev/null +++ b/e2e/tests/135_for_string_lines.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +source "${ROOT_DIR}/e2e/lib/common.sh" +trap e2e::cleanup EXIT + +e2e::prepare_test_env "for_string_lines" +TEST_DIR="${JAIPH_E2E_TEST_DIR}" + +e2e::section "for line in string iterates lines" + +e2e::file "for_lines.jh" <<'EOF' +workflow default() { + const paths = """ +docs/a.md +docs/b.md +""" + for path in paths { + log "${path}" + } + log "done" +} +EOF + +out="$(e2e::run "for_lines.jh")" +grep -q "docs/a.md" <<<"${out}" || { + echo "${out}" >&2 + exit 1 +} +grep -q "docs/b.md" <<<"${out}" || { + echo "${out}" >&2 + exit 1 +} +grep -q "done" <<<"${out}" || { + echo "${out}" >&2 + exit 1 +} +e2e::pass "for … in … runs body per line" + +e2e::section "for line in string skips only trailing empty segment" + +e2e::file "for_lines_trim_nl.jh" <<'EOF' +workflow default() { + const paths = """ +one +two +""" + for line in paths { + log ">>${line}<<" + } +} +EOF + +out2="$(e2e::run "for_lines_trim_nl.jh")" +grep -q ">>one<<" <<<"${out2}" || exit 1 +grep -q ">>two<<" <<<"${out2}" || exit 1 +# No third empty iteration from final newline +if grep -q '>><<' <<<"${out2}"; then + echo "unexpected empty line iteration:${out2}" >&2 + exit 1 +fi +e2e::pass "final newline does not yield empty line" + +e2e::section "for … in … with empty line in middle" + +e2e::file "for_lines_interior_blank.jh" <<'EOF' +workflow default() { + const paths = """ +x + +y +""" + for line in paths { + if line == "" { + log "(empty)" + } + if line != "" { + log "${line}" + } + } +} +EOF + +out3="$(e2e::run "for_lines_interior_blank.jh")" +grep -q "ℹ x" <<<"${out3}" || { + echo "${out3}" >&2 + exit 1 +} +grep -q "ℹ (empty)" <<<"${out3}" || { + echo "${out3}" >&2 + exit 1 +} +grep -q "ℹ y" <<<"${out3}" || { + echo "${out3}" >&2 + exit 1 +} +e2e::pass "interior empty line is still iterated" diff --git a/src/format/emit.ts b/src/format/emit.ts index bbc70329..f1315f22 100644 --- a/src/format/emit.ts +++ b/src/format/emit.ts @@ -731,6 +731,13 @@ function emitStep(step: WorkflowStepDef, pad: string, currentIndent: string): st lines.push(`${ci}}`); break; } + + case "for_lines": { + lines.push(`${ci}for ${step.iterVar} in ${step.sourceVar} {`); + lines.push(...emitSteps(step.body, pad, ci + pad)); + lines.push(`${ci}}`); + break; + } } return lines; diff --git a/src/parse/core.ts b/src/parse/core.ts index c131c794..0cac7c10 100644 --- a/src/parse/core.ts +++ b/src/parse/core.ts @@ -92,7 +92,7 @@ const JAIPH_KEYWORDS = new Set([ "run", "ensure", "prompt", "return", "fail", "log", "logerr", "if", "else", "not", "const", "match", "import", "export", "workflow", "rule", "script", "channel", "config", "catch", "async", - "returns", "send", "true", "false", + "returns", "send", "true", "false", "for", "in", ]); /** Check if a token is a bare identifier (valid identifier, not a keyword). */ diff --git a/src/parse/workflow-brace.ts b/src/parse/workflow-brace.ts index cb12675f..485d1c10 100644 --- a/src/parse/workflow-brace.ts +++ b/src/parse/workflow-brace.ts @@ -160,6 +160,27 @@ export function parseBlockStatement( ); } + // for in { ... } + const forHead = inner.match(/^for\s+([A-Za-z_][A-Za-z0-9_]*)\s+in\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{\s*$/); + if (forHead) { + const iterVar = forHead[1]; + const sourceVar = forHead[2]; + const forLoc = { line: innerNo, col: innerRaw.indexOf("for") + 1 }; + const { steps: body, nextIdx } = parseBraceBlockBody(filePath, lines, idx + 1, innerNo, opts); + return { + step: { type: "for_lines", iterVar, sourceVar, body, loc: forLoc }, + nextIdx, + }; + } + if (/^for\s/.test(inner)) { + fail( + filePath, + 'invalid for syntax; expected: for in { ... }', + innerNo, + innerRaw.indexOf("for") + 1, + ); + } + const constMatch = inner.match(/^const\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.+)$/s); if (constMatch) { const name = constMatch[1]; diff --git a/src/runtime/kernel/node-workflow-runtime.ts b/src/runtime/kernel/node-workflow-runtime.ts index 97ff655e..d6c91545 100644 --- a/src/runtime/kernel/node-workflow-runtime.ts +++ b/src/runtime/kernel/node-workflow-runtime.ts @@ -31,6 +31,7 @@ import { } from "./runtime-arg-parser"; import { RuntimeEventEmitter, type Frame } from "./runtime-event-emitter"; import { executeMockBodyDef, type MockBodyDef, type StepResult } from "./runtime-mock"; +import { linesOfDelimitedString } from "../string-lines"; export type { MockBodyDef } from "./runtime-mock"; @@ -866,6 +867,22 @@ export class NodeWorkflowRuntime { } continue; } + if (step.type === "for_lines") { + const raw = + scope.vars.get(step.sourceVar) ?? + scope.env?.[step.sourceVar] ?? + ""; + for (const line of linesOfDelimitedString(raw)) { + scope.vars.set(step.iterVar, line); + const bodyResult = await this.executeSteps(scope, step.body, io); + if (bodyResult.status !== 0 || bodyResult.returnValue !== undefined) { + return this.mergeStepResult(accOut, accErr, bodyResult); + } + accOut += bodyResult.output; + accErr += bodyResult.error; + } + continue; + } if (step.type === "match") { const matchResult = await this.evaluateMatch(scope, step.expr); if (!matchResult.ok) return this.mergeStepResult(accOut, accErr, matchResult.result); diff --git a/src/runtime/string-lines.test.ts b/src/runtime/string-lines.test.ts new file mode 100644 index 00000000..e4b6dd44 --- /dev/null +++ b/src/runtime/string-lines.test.ts @@ -0,0 +1,25 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { linesOfDelimitedString } from "./string-lines"; + +test("linesOfDelimitedString: empty", () => { + assert.deepEqual(linesOfDelimitedString(""), []); +}); + +test("linesOfDelimitedString: no trailing newline", () => { + assert.deepEqual(linesOfDelimitedString("a"), ["a"]); + assert.deepEqual(linesOfDelimitedString("a\nb"), ["a", "b"]); +}); + +test("linesOfDelimitedString: trailing newline drops empty last segment", () => { + assert.deepEqual(linesOfDelimitedString("a\n"), ["a"]); + assert.deepEqual(linesOfDelimitedString("a\nb\n"), ["a", "b"]); +}); + +test("linesOfDelimitedString: normalizes CRLF", () => { + assert.deepEqual(linesOfDelimitedString("a\r\nb"), ["a", "b"]); +}); + +test("linesOfDelimitedString: preserves empty interior lines", () => { + assert.deepEqual(linesOfDelimitedString("a\n\nb"), ["a", "", "b"]); +}); diff --git a/src/runtime/string-lines.ts b/src/runtime/string-lines.ts new file mode 100644 index 00000000..ae097ccc --- /dev/null +++ b/src/runtime/string-lines.ts @@ -0,0 +1,13 @@ +/** + * Lines of a newline-delimited string for `for x in str`. + * Normalizes `\r\n` to `\n`. If the string ends with a final newline, the + * trailing empty segment is not yielded (so `"a\nb\n"` → `["a", "b"]`). + */ +export function linesOfDelimitedString(s: string): string[] { + const normalized = s.replace(/\r\n/g, "\n"); + const lines = normalized.split("\n"); + if (lines.length && lines[lines.length - 1] === "") { + lines.pop(); + } + return lines; +} diff --git a/src/transpile/emit-script.ts b/src/transpile/emit-script.ts index ca8c9185..5ccf8675 100644 --- a/src/transpile/emit-script.ts +++ b/src/transpile/emit-script.ts @@ -91,6 +91,10 @@ function collectInlineScripts( } else if ((s.type === "ensure" || s.type === "run") && s.catch) { const recoverSteps = "single" in s.catch ? [s.catch.single] : s.catch.block; collectInlineScripts(recoverSteps, seen, out); + } else if (s.type === "if") { + collectInlineScripts(s.body, seen, out); + } else if (s.type === "for_lines") { + collectInlineScripts(s.body, seen, out); } } } diff --git a/src/transpile/validate.ts b/src/transpile/validate.ts index b537a683..30627918 100644 --- a/src/transpile/validate.ts +++ b/src/transpile/validate.ts @@ -206,6 +206,10 @@ function collectKnownVars(steps: WorkflowStepDef[], envDecls?: { name: string }[ if (s.type === "if") { walk(s.body); } + if (s.type === "for_lines") { + vars.add(s.iterVar); + walk(s.body); + } } }; walk(steps); @@ -227,8 +231,8 @@ function validateImmutableBindings( bound.set(p, { kind: "parameter", line: declLoc.line }); } - const check = (name: string, kind: string, loc: { line: number; col: number }): void => { - const prev = bound.get(name); + const check = (name: string, kind: string, loc: { line: number; col: number }, b: Map): void => { + const prev = b.get(name); if (prev) { throw jaiphError( filePath, @@ -247,33 +251,47 @@ function validateImmutableBindings( `cannot rebind immutable name "${name}"; already bound as script in this module`, ); } - bound.set(name, { kind, line: loc.line }); + b.set(name, { kind, line: loc.line }); }; - const walk = (ss: WorkflowStepDef[]): void => { + const walk = (ss: WorkflowStepDef[], b: Map): void => { for (const s of ss) { if (s.type === "const") { - check(s.name, "const", s.loc); + check(s.name, "const", s.loc, b); } if (s.type === "ensure" && s.captureName) { - check(s.captureName, "capture", s.ref.loc); + check(s.captureName, "capture", s.ref.loc, b); } if (s.type === "run" && s.captureName) { - check(s.captureName, "capture", s.workflow.loc); + check(s.captureName, "capture", s.workflow.loc, b); } if ((s.type === "prompt" || s.type === "run_inline_script") && s.captureName) { - check(s.captureName, "capture", s.loc); + check(s.captureName, "capture", s.loc, b); } if ((s.type === "ensure" || s.type === "run") && s.catch) { const recoverSteps = "single" in s.catch ? [s.catch.single] : s.catch.block; - walk(recoverSteps); + walk(recoverSteps, b); } if (s.type === "if") { - walk(s.body); + walk(s.body, b); + } + if (s.type === "for_lines") { + if (b.has(s.iterVar)) { + throw jaiphError( + filePath, + s.loc.line, + s.loc.col, + "E_VALIDATE", + `for loop iterator "${s.iterVar}" conflicts with an existing binding`, + ); + } + const inner = new Map(b); + inner.set(s.iterVar, { kind: "loop_iterator", line: s.loc.line }); + walk(s.body, inner); } } }; - walk(steps); + walk(steps, bound); } /** Count the number of call arguments from a space-separated args string (respects quotes). */ @@ -882,6 +900,19 @@ export function validateReferences(ast: jaiphModule, ctx: ValidateContext): void for (const bodyStep of s.body) validateRuleStep(bodyStep); return; } + if (s.type === "for_lines") { + if (!ruleKnownVars.has(s.sourceVar)) { + throw jaiphError( + ast.filePath, + s.loc.line, + s.loc.col, + "E_VALIDATE", + `for ... in : "${s.sourceVar}" is not a known variable in this scope`, + ); + } + for (const bodyStep of s.body) validateRuleStep(bodyStep); + return; + } if (s.type === "run_inline_script") { return; } @@ -1270,6 +1301,19 @@ export function validateReferences(ast: jaiphModule, ctx: ValidateContext): void for (const bodyStep of s.body) validateStep(bodyStep, recoverBindings); return; } + if (s.type === "for_lines") { + if (!wfKnownVars.has(s.sourceVar)) { + throw jaiphError( + ast.filePath, + s.loc.line, + s.loc.col, + "E_VALIDATE", + `for ... in : "${s.sourceVar}" is not a known variable in this scope`, + ); + } + for (const bodyStep of s.body) validateStep(bodyStep, recoverBindings); + return; + } if (s.type === "run_inline_script") { return; } diff --git a/src/types.ts b/src/types.ts index ae4b4d98..61e6abff 100644 --- a/src/types.ts +++ b/src/types.ts @@ -252,6 +252,14 @@ export type WorkflowStepDef = body: WorkflowStepDef[]; loc: SourceLoc; } + | { + /** `for line in paths { ... }` — iterate lines of a string variable (newline-delimited). */ + type: "for_lines"; + iterVar: string; + sourceVar: string; + body: WorkflowStepDef[]; + loc: SourceLoc; + } | { /** Preserved intentional blank line between steps (formatter only). */ type: "blank_line"; From 91d40868cc929371b105948cc90857ea229a7c1d Mon Sep 17 00:00:00 2001 From: Jakub Dzikowski Date: Wed, 13 May 2026 14:55:25 +0200 Subject: [PATCH 3/5] docs: document for-line iteration; use it in docs_parity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add language.md subsection for `for … in …`. Queue follow-up on optional trim/skip-empty sugar. Replace head/tail recursion in .jaiph/docs_parity.jh with a loop over the docs path list. Co-authored-by: Cursor --- .jaiph/docs_parity.jh | 53 +++++++++++++------------------------------ QUEUE.md | 17 ++++++++++++++ docs/language.md | 36 ++++++++++++++++++++++++----- 3 files changed, 63 insertions(+), 43 deletions(-) diff --git a/.jaiph/docs_parity.jh b/.jaiph/docs_parity.jh index bfec7344..30bf62cc 100755 --- a/.jaiph/docs_parity.jh +++ b/.jaiph/docs_parity.jh @@ -21,6 +21,8 @@ const role = """ script assert_newline_paths_are_files = ``` while IFS= read -r f; do + f="${f#"${f%%[![:space:]]*}"}" + f="${f%"${f##*[![:space:]]}"}" [ -z "$f" ] && continue test -f "$f" || return 1 done <<< "$1" @@ -75,35 +77,27 @@ rule only_expected_docs_changed_after_prompt(allowed) { run assert_only_allowed_changed(allowed) } -script first_line_str = `printf '%s\n' "$1" | head -n 1` - -script rest_lines_str = `printf '%s\n' "$1" | tail -n +2` - script list_docs_md_paths = ``` - local out="" - local f + local out="" f for f in docs/*.md; do - out="${out:+$out - }$f" + if [ -z "$out" ]; then + out="$f" + else + out="$out"$'\n'"$f" + fi done printf '%s\n' "$out" ``` script build_allowed_paths_block = ``` - local out="README.md - docs/index.html - docs/_layouts/docs.html - src/cli/shared/usage.ts" - local f + local out f + out="$(printf '%s\n' README.md docs/index.html docs/_layouts/docs.html src/cli/shared/usage.ts)" for f in docs/*.md; do - out="$out - $f" + out="$out"$'\n'"$f" done printf '%s\n' "$out" ``` -script join_newline_args = `printf '%s\n' "$@"` - workflow update_from_task(taskDesc) { prompt """ @@ -206,31 +200,16 @@ workflow docs_overview(docPaths) { """ } -workflow process_docs_md_recursive(file, remaining) { - run docs_page(file) - if remaining == "" { - return - } - const next = run first_line_str(remaining) - const rest = run rest_lines_str(remaining) - run process_docs_md_recursive(next, rest) -} - -workflow maybe_process_docs_md(first_doc, rest_docs) { - if first_doc == "" { - return - } - run process_docs_md_recursive(first_doc, rest_docs) -} - workflow default() { ensure worktree_is_clean() const allowed_list = run build_allowed_paths_block() ensure docs_files_present(allowed_list) const docs_md_list = run list_docs_md_paths() - const first_doc = run first_line_str(docs_md_list) - const rest_docs = run rest_lines_str(docs_md_list) - run maybe_process_docs_md(first_doc, rest_docs) + for path in docs_md_list { + if path != "" { + run docs_page(path) + } + } run docs_overview(docs_md_list) ensure only_expected_docs_changed_after_prompt(allowed_list) } diff --git a/QUEUE.md b/QUEUE.md index 72264987..e077f988 100644 --- a/QUEUE.md +++ b/QUEUE.md @@ -48,3 +48,20 @@ When starting workflows (e.g. `jaiph run` / first step), users observe a 2–4 s * `npm test` passes. *** + +## `for … in …` — optional built-in trim / skip-empty #dev-ready + +**Goal** +Evaluate optional sugar (keywords, modifiers, or a small stdlib helper) for trimming each iterated line or skipping empty lines, if authors repeatedly need shell-style normalization beyond an explicit `if line != "" { … }`. + +**Scope** + +* Baseline behavior and splitting rules are documented in `docs/language.md`. +* If a language change is chosen, add tests and note migration for workflows that would adopt it. + +**Acceptance criteria** + +* Decision recorded in `docs/language.md` (new subsection or changelog in that doc). +* `npm test` passes. + +*** diff --git a/docs/language.md b/docs/language.md index 9fae1d15..228ed583 100644 --- a/docs/language.md +++ b/docs/language.md @@ -164,7 +164,7 @@ Routes (`->`) declare which workflows receive messages sent to the channel. See ### Config -Optional block setting agent and run options. Allowed at module level and inside individual workflow bodies. +Optional `config { … }` block. At **module** level it may set `agent.*`, `run.*`, `runtime.*`, and `module.*` keys (see `src/parse/metadata.ts`). A **workflow** may contain **at most one** nested `config { … }`, it must appear **before** the first step, and only **`agent.*`** and **`run.*`** are allowed there — `runtime.*` and `module.*` are rejected with `E_PARSE`. ```jaiph config { @@ -182,6 +182,8 @@ See [Configuration](configuration.md) for all available keys and precedence rule Named sequences of orchestration steps. Workflows can call other workflows, scripts, prompts, and channels. Parentheses are required on definitions, even when parameterless. +`jaiph run` only executes the workflow named **`default`** in the entry `.jh` file (the runner’s argv hard-codes that name today). Other workflows are reachable from steps inside the module or its imports. See the `jaiph run` sequence in [Architecture](architecture.md). + ```jaiph workflow default() { ensure check_deps() @@ -198,6 +200,14 @@ workflow deploy(env, version) { Workflows support all step types: `run`, `ensure`, `prompt`, `const`, `log`, `logerr`, `fail`, `return`, `send`, `match`, `if`, `run async`, `catch`, and `recover`. +#### Inline shell lines (workflows only) + +Any workflow body line that does **not** parse as a managed Jaiph step is treated as **inline shell**: the text is Jaiph-interpolated, then executed with `sh -c` in the workspace (same working-directory rules as `run` on scripts — see [Script isolation](#script-isolation)). Prefer a top-level `script` and `run name()` for non-trivial shell. + +The compiler still inspects shell lines (for example a first word that names a local script or workflow must be written as a managed `run`/`ensure` step, not as bare shell). **`wait`** is not a step — using it is a parse error (`"wait" has been removed from the language`). + +**Rules cannot** contain inline shell; unstructured shell there fails validation (`inline shell steps are forbidden in rules; use explicit script blocks`). + ### Rules Named blocks of structured validation steps. Rules are called with `ensure` and are meant for checks and gates. @@ -214,7 +224,9 @@ rule gate(path) { } ``` -Rules are more restricted than workflows: the compiler rejects `prompt`, `send`, and `run async` in rule bodies, and `run` may only target **scripts** (never workflows or other rules via `run` — use `ensure` for rules). Those restrictions are **static** (see `validateReferences` in `src/transpile/validate.ts`). At runtime, `run` inside a rule still launches a normal managed script subprocess with the same **environment model** as workflow scripts (see [Script isolation](#script-isolation)); scripts can perform side effects — the language simply keeps orchestration-heavy steps out of rules. +Rules are more restricted than workflows: the compiler rejects `prompt`, `send`, and `run async` in rule bodies, and `run` may only target **scripts** (never workflows or other rules via `run` — use `ensure` for rules). Rule bodies also reject `const … = prompt`. Those restrictions are **static** (see `validateReferences` in `src/transpile/validate.ts`). At runtime, `run` inside a rule still launches a normal managed script subprocess with the same **environment model** as workflow scripts (see [Script isolation](#script-isolation)); scripts can perform side effects — the language simply keeps orchestration-heavy steps out of rules. + +`catch` and **`recover`** on **`run`** are allowed in rules the same as in workflows. **`recover` never attaches to `ensure`** — only `run` steps support `recover`. ### Scripts @@ -423,7 +435,7 @@ workflow default() { **Constraints:** - `recover` requires exactly one binding: `recover(name)`. Bare `recover` without bindings is a parse error. - All call arguments must appear inside parentheses **before** `recover`. -- `recover` is available on `run` steps in workflows only (not `ensure`). `recover` also works with `run async` — see [`run async`](#run-async--concurrent-execution-with-handles). +- `recover` is only valid on **`run`** steps (`ensure` supports `catch`, not `recover`). It is allowed in both workflow and rule bodies. `recover` also works with `run async` — see [`run async`](#run-async--concurrent-execution-with-handles). - `recover` and `catch` are mutually exclusive on the same step — use one or the other. ### `prompt` — Agent Interaction @@ -666,6 +678,20 @@ workflow default(env) { ``` +### `for` — Iterate lines of a string + +```jaiph +for line in paths_blob { + if line != "" { + run process_one(line) + } +} +``` + +`for in { … }` splits the **string value** of the right-hand variable on newlines (`\r\n` is normalized to `\n`). If the string ends with a final newline, the trailing empty segment is **not** iterated (so `"a\nb\n"` yields two lines, not three). **Interior** empty lines are still yielded as empty strings. There is **no** automatic trimming of whitespace; use an `if` guard, `match`, or a script when you need to skip blanks or strip indentation. + +The iterator name must not conflict with an existing parameter, `const`, or capture in the same scope. After the loop completes, the iterator variable remains set to the last line visited (same shared scope as other workflow bindings). + ## Inline Scripts Embed a shell command directly in a step without a named `script` definition. Single backticks for one-liners, triple backticks for multiline. @@ -719,9 +745,7 @@ If the inline capture fails, the enclosing step fails. Nested inline captures ar **Emitted script files** do not embed module `const` values or other Jaiph “shims” — the transpiler writes the authored body plus a shebang (see `emitScriptsForModule` / `emit-script.ts`). Anything a script needs from the module must be passed as **positional arguments** (`$1`, `$2`, …), read from paths under `JAIPH_WORKSPACE`, or live in shared script sources (`import script`). -**Subprocess environment (`NodeWorkflowRuntime`):** When the AST interpreter runs `run` / inline scripts, it spawns the emitted executable with the **current workflow scope environment** — a copy of the runner’s `process.env` merged with Jaiph-populated keys (`JAIPH_SCRIPTS`, `JAIPH_WORKSPACE`, `JAIPH_RUN_DIR`, `JAIPH_ARTIFACTS_DIR`, prompt-related `JAIPH_AGENT_*` variables when set, and values derived from `config { … }` via metadata). It is **not** reset to a tiny fixed allowlist; anything visible to the workflow runner is visible to child scripts unless your deployment strips the parent environment. - -The kernel helper `run-step-exec.ts` still uses a **minimal** env (`PATH`, `HOME`, `TERM`, `USER`, `JAIPH_SCRIPTS`, `JAIPH_WORKSPACE`) for its own **internal** `spawnSync` script-capture paths — that is not the same code path as ordinary `NodeWorkflowRuntime` `spawn()` for user `script` steps. +**Subprocess environment (`NodeWorkflowRuntime`):** Managed **script** steps (`run` on a named script, script import, or inline `` `…` `` / fenced body), and **workflow inline shell** lines, all use the same **`scope.env`**: the runner’s `process.env` as adjusted by Jaiph (for example `JAIPH_SCRIPTS`, `JAIPH_WORKSPACE`, `JAIPH_RUN_DIR`, `JAIPH_ARTIFACTS_DIR`, prompt-related `JAIPH_AGENT_*` when set, and keys derived from `config { … }`). It is **not** reset to a small fixed allowlist; anything visible to the workflow runner is visible to child processes unless your deployment strips the parent environment. **Interpolation rules by body form:** From d9922da8d760da84f9ee342833ecb52c4ee50b7e Mon Sep 17 00:00:00 2001 From: Jakub Dzikowski Date: Wed, 13 May 2026 14:56:57 +0200 Subject: [PATCH 4/5] docs: refresh site pages, README, and jaiph compile help Update documentation across docs/* (architecture, CLI, grammar, inbox, libraries, sandboxing, hooks, testing, and related pages). Align README and static site snippets where needed. Clarify `jaiph compile` in CLI usage and compile command help: validateReferences / import closure only, without script emission or the workflow runner. Co-authored-by: Cursor --- README.md | 9 +++ docs/_layouts/docs.html | 1 + docs/architecture.md | 36 +++++----- docs/artifacts.md | 23 +++--- docs/cli.md | 42 ++++++----- docs/configuration.md | 11 +-- docs/contributing.md | 19 +++-- docs/getting-started.md | 10 +-- docs/grammar.md | 85 +++++++++++----------- docs/hooks.md | 102 ++++++++++++++------------- docs/inbox.md | 137 +++++++++++++++++------------------- docs/index.html | 9 ++- docs/jaiph-skill.md | 20 +++--- docs/libraries.md | 71 ++++++++++++++----- docs/sandboxing.md | 36 +++++----- docs/setup.md | 71 +++++++++++++------ docs/spec-async-handles.md | 65 ++++++++++------- docs/testing.md | 19 +++-- src/cli/commands/compile.ts | 6 +- src/cli/shared/usage.ts | 3 +- 20 files changed, 449 insertions(+), 326 deletions(-) diff --git a/README.md b/README.md index c113e01e..d4aceaa2 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,15 @@ > [!WARNING] > Jaiph is still in an early stage. Expect breaking changes. +## Features + +- **Workflows** — Compose `prompt`, `run`, `ensure`, channel sends, conditionals, `run async` with implicit join, `catch`, and repair-and-retry `recover`. +- **Rules and scripts** — Rules stay structured (no raw shell lines); **`script`** steps run bash or polyglot code as subprocesses. +- **Agents** — Backends include Cursor, Claude, Codex (HTTP), or a custom `agent.command`. +- **Testing** — `*.test.jh` files run in-process (`jaiph test`) with mocks and `expect_*` assertions ([Testing](docs/testing.md)). +- **Safety and inspectability** — Docker-backed sandbox for **`jaiph run`** (env-controlled; see [Sandboxing](docs/sandboxing.md)); live **`__JAIPH_EVENT__`** on stderr and durable **`.jaiph/runs/`** artifacts ([Architecture](docs/architecture.md)). +- **Tooling** — `jaiph compile`, `jaiph format`, `jaiph install` / `.jaiph/libs/`, and optional `hooks.json` ([CLI](docs/cli.md), [Hooks](docs/hooks.md)). + ## Core components - **CLI** (`src/cli`) — `jaiph run` / `test` / `compile` / `format` / `init` / `install` / `use`; prepares scripts, spawns the workflow runner (or in-process test runner), parses `__JAIPH_EVENT__` on stderr, runs hooks on `jaiph run` only. diff --git a/docs/_layouts/docs.html b/docs/_layouts/docs.html index e0d98280..bb4f5fd2 100644 --- a/docs/_layouts/docs.html +++ b/docs/_layouts/docs.html @@ -52,6 +52,7 @@
  • CLI
  • Configuration
  • Testing
  • +
  • Async handles
  • Inbox
  • Hooks
  • Sandboxing
  • diff --git a/docs/architecture.md b/docs/architecture.md index 8b8a9e2d..fa2f7b73 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -9,7 +9,7 @@ redirect_from: Jaiph is a workflow system with a **TypeScript CLI** and a **JavaScript kernel** (`src/runtime/kernel/`) that interprets the workflow AST in process — there is no separate “workflow shell” emitted for execution. -This page describes **how Jaiph is built**: repository layout of major subsystems, **core components**, compile and run pipelines, and **runtime contracts** (events, artifacts on disk, distribution). It is the map of the implementation. +This page describes **how Jaiph is built**: repository layout of major subsystems, **core components**, compile and run pipelines, and **runtime contracts** (events, artifacts on disk, distribution). It is the map of the implementation. For workflow syntax and semantics, see the [Language](language.md) guide; this document stays on implementation boundaries. For **how to contribute** — branches, test layers, E2E assertion policy, and bash harness details — see [Contributing](contributing.md). For the `*.test.jh` **language** and test blocks, see [Testing](testing.md). @@ -18,7 +18,7 @@ For **how to contribute** — branches, test layers, E2E assertion policy, and b Workflow authors write `.jh` / `.test.jh` modules. The toolchain turns those files into **validated** modules plus **extracted script files**, then the **same AST interpreter** runs workflows whether you use local `jaiph run`, Docker, or `jaiph test`. 1. Parse source into AST (the CLI parses once up front for `jaiph run` metadata such as `runtime` config; `buildRuntimeGraph` and transpilation use the same parser on disk contents). -2. **Compile-time** validation (`validateReferences`, invoked from **`emitScriptsForModule`** / **`buildScripts()`**) runs before script extraction, not inside `buildRuntimeGraph()` (the graph loader only parses modules and follows imports). The **`jaiph compile`** command runs the same validation over files or directories without executing workflows (see `src/cli/commands/compile.ts`). +2. **Compile-time** validation (`validateReferences`, invoked from **`emitScriptsForModule`** / **`buildScripts()`**) runs before script extraction, not inside `buildRuntimeGraph()` (the graph loader only parses modules and follows imports). The **`jaiph compile`** command runs **`validateReferences` only**: it parses each module in the computed import closure and **does not** run **`buildScriptFiles`** or **`buildScripts`**, and never spawns the runner (`src/cli/commands/compile.ts`). For a **directory** argument it discovers `*.jh` via `walkjhFiles`, which **skips** `*.test.jh`; to validate a test module, pass that file explicitly. Imported modules in the closure are still validated recursively either way. 3. **CLI** (`dist/src/cli.js` via npm, or a **Bun-compiled** `dist/jaiph` binary) prepares script executables (scripts-only), then spawns a **detached child** that loads **`node-workflow-runner.js`**. That child calls `buildRuntimeGraph()` and runs **`NodeWorkflowRuntime`**. The child’s interpreter is **`process.execPath`** of the CLI process (Node when you run `node dist/src/cli.js`, the standalone Bun binary when you run `dist/jaiph`). Script steps execute as managed subprocesses; prompt, inbox I/O, and event/summary emission are handled by the kernel under `src/runtime/kernel/`. 4. Stream live events to the CLI and persist durable run artifacts. @@ -27,7 +27,7 @@ All orchestration — local `jaiph run`, `jaiph test`, and **Docker `jaiph run`* ## Core components - **CLI (`src/cli`)** - - Entry point (`run`, `test`, `compile`, `init`, `install`, `use`, `format`). + - Entry point (`run`, `test`, `compile`, `init`, `install`, `use`, `format`). Paths ending in `.jh` / `.test.jh` are also accepted as implicit commands (see `src/cli/index.ts`). - **Workflow launch** is owned in TypeScript (`src/runtime/kernel/workflow-launch.ts` + `src/cli/run/lifecycle.ts`): spawns **`node-workflow-runner.js`** with `process.execPath`, which calls `buildRuntimeGraph()` then `NodeWorkflowRuntime`. `setupRunSignalHandlers` accepts an optional `onSignalCleanup` callback for Docker sandbox teardown on SIGINT/SIGTERM. - Parses runtime events and renders progress; dispatches hooks. @@ -39,10 +39,10 @@ All orchestration — local `jaiph run`, `jaiph test`, and **Docker `jaiph run`* - Shared compile-time schema (`jaiphModule`, step defs, test defs, hook payload types). - **Validator (`src/transpile/validate.ts`)** - - Resolves imports and symbol references; emits deterministic compile-time errors. Import resolution (`resolveImportPath` in `resolve.ts`) checks relative paths first, then falls back to project-scoped libraries under `/.jaiph/libs/` — the workspace root is threaded through all compilation call sites. Export visibility is enforced by `validateRef` in `validate-ref-resolution.ts`: if an imported module declares any `export`, only exported names are reachable through the import alias. + - Resolves imports and symbol references; emits deterministic compile-time errors. Import resolution (`resolveImportPath` in `transpile/resolve.ts`) checks relative paths first, then falls back to project-scoped libraries under `/.jaiph/libs/` — the workspace root is threaded through all compilation call sites. Export visibility is enforced by `validateRef` in `validate-ref-resolution.ts`: if an imported module declares any `export`, only exported names are reachable through the import alias. - **Transpiler (`src/transpiler.ts`, `src/transpile/*`)** - - **`emitScriptsForModule`** parses, runs **`validateReferences`**, and **`buildScriptFiles`** — the only compile path for `jaiph run` / `jaiph test` — **persists only atomic `script` files** under `scripts/`. Inline scripts (`` run `body`(args) ``) are also emitted as `scripts/__inline_` with deterministic hash-based names. There is no workflow-level bash emission. + - **`emitScriptsForModule`** parses, runs **`validateReferences`**, and **`buildScriptFiles`** — the only compile path for `jaiph run` / `jaiph test` — **persists only atomic `script` files** under `scripts/`. **`buildScripts()`** can also take a **directory** of non-test `*.jh` modules (`transpile/build.ts` uses `walkjhFiles`); the **`jaiph run`** and **`jaiph test`** commands always pass a **single entry file** (`.jh` or `*.test.jh`). Inline scripts (`` run `body`(args) ``) are also emitted as `scripts/__inline_` with deterministic hash-based names. There is no workflow-level bash emission. - **Node Workflow Runtime (`src/runtime/kernel/node-workflow-runtime.ts`)** - `NodeWorkflowRuntime` interprets the AST directly: walks workflow steps, manages scope/variables, delegates prompt and script execution to kernel helpers, handles channels/inbox/dispatch, owns the frame stack and heartbeat, and writes run artifacts. @@ -50,13 +50,13 @@ All orchestration — local `jaiph run`, `jaiph test`, and **Docker `jaiph run`* - **`runtime-arg-parser.ts`** — stateless interpolation and call-argument parsing (`interpolate`, `parseInlineCaptureCall`, `commaArgsToInterpolated`, `parseArgsRaw`, `parseInlineScriptAt`, `parseManagedArgAt`, `parseArgTokens`, `stripOuterQuotes`, `parsePromptSchema`, `sanitizeName`, `nowIso`) plus shared constants and the `ParsedArgToken` / `PromptSchemaField` types. Direct unit tests live in `runtime-arg-parser.test.ts`. - **`runtime-event-emitter.ts`** — `RuntimeEventEmitter` owns the `__JAIPH_EVENT__` stderr stream and `run_summary.jsonl` writes for workflow/step/prompt/log events, plus the monotonic step and prompt sequence counters. Constructed with `{ runId, runDir, env, getFrameStack, getAsyncIndices, suppressLiveEvents? }`; the runtime delegates all event emission to it. The optional `suppressLiveEvents` flag (forwarded from `NodeWorkflowRuntime`'s `suppressLiveEvents` option) skips the live stderr write while leaving the durable `run_summary.jsonl` append intact — used by in-process callers like the test runner that share stderr with `node --test` reporter output. The CLI's spawned `node-workflow-runner` child does not set it, so production runs stream events to stderr as before. - **`runtime-mock.ts`** — `executeMockBodyDef` and `executeMockShellBody` for `*.test.jh` workflow/rule/script mocks. Shell-kind mocks run `bash -c`; steps-kind mocks dispatch back into the runtime via an `executeStepsBack` callback so the body runs against the full step interpreter. - - `buildRuntimeGraph()` (`graph.ts`) loads reachable modules with **`parsejaiph` only** (import closure); it does **not** run `validateReferences`. Cross-module refs are resolved from that graph at runtime. + - `buildRuntimeGraph()` (`graph.ts`) loads reachable modules with **`parsejaiph` only** (import closure); it does **not** run `validateReferences`. Cross-module refs are resolved from that graph at runtime. For **`script import`** declarations, `buildRuntimeGraph()` injects synthetic `ScriptDef` stubs (`graph.ts`) so reference resolution matches the validated compile path without re-reading external script bodies at graph-build time. - **Node Test Runner (`src/runtime/kernel/node-test-runner.ts`)** - Executes `*.test.jh` test blocks using `NodeWorkflowRuntime` with mock support (mock prompts, mock workflow/rule/script bodies). Pure Node harness — no Bash test transpilation. - **JS kernel (`src/runtime/kernel/`)** - - Prompt execution (`prompt.ts`), streaming parse (`stream-parser.ts`), schema (`schema.ts`), mocks (`mock.ts`), **`emit.ts`** (live `__JAIPH_EVENT__` + `run_summary.jsonl`), **`workflow-launch.ts`** (spawn contract). Script subprocesses are launched directly from `NodeWorkflowRuntime`. + - Prompt execution (`prompt.ts`), streaming parse (`stream-parser.ts`), schema (`schema.ts`), mocks (`mock.ts`), **`emit.ts`** (durable **`run_summary.jsonl`** helpers — `appendRunSummaryLine`, `formatUtcTimestamp` — consumed by `RuntimeEventEmitter`), **`workflow-launch.ts`** (spawn contract). **`RuntimeEventEmitter`** (`runtime-event-emitter.ts`) owns live **`__JAIPH_EVENT__`** lines on stderr and coordinates summary writes plus step/prompt sequence counters. Script subprocesses are launched directly from `NodeWorkflowRuntime`. - **Formatter (`src/format/emit.ts`)** - `jaiph format` rewrites `.jh` / `.test.jh` files into canonical style. Pure AST→text emitter; no side-effects beyond file writes. @@ -73,7 +73,7 @@ All orchestration — local `jaiph run`, `jaiph test`, and **Docker `jaiph run`* - Manage channels (`send`, routes, queue drain) through kernel logic. - Emit step/log events; persist run logs and summary timeline. - Prompt steps and managed script subprocesses: Node kernel owns execution, events, and control flow. -- Execute test blocks with mock support (`NodeTestRunner`). +- Execute test blocks with mock support (`runTestFile()` in `node-test-runner.ts`). ### CLI responsibilities @@ -99,19 +99,19 @@ The runtime persists step captures and the event timeline under a UTC-dated hier / # UTC date (see NodeWorkflowRuntime) -/ # UTC time + JAIPH_SOURCE_FILE or entry basename 000001-module__step.out # stdout capture per step (6-digit seq prefix) - 000001-module__step.err # stderr capture (when non-empty) + 000001-module__step.err # stderr capture (may be empty) artifacts/ # user-published files (JAIPH_ARTIFACTS_DIR); created at run start - inbox/ # inbox message files (when channels are used) + inbox/ # audit copies of routed channel payloads (optional) heartbeat # liveness: epoch ms, refreshed about every 10s return_value.txt # when `jaiph run` default workflow returns a value (success only) run_summary.jsonl # durable event timeline ``` -Step sequence numbers are monotonic and unique per run: `NodeWorkflowRuntime` allocates them in memory when opening each step’s capture files (`%06d-.out|.err`). There is no `.seq` file in the run directory. +Step sequence numbers are monotonic and unique per run: `RuntimeEventEmitter` allocates them in memory (`allocStepSeq`) when opening each step’s capture files (`%06d-.out|.err`). There is no `.seq` file in the run directory. ## Channels and hooks in context -Channels are validated at compile time (`validateReferences` / send RHS rules) and executed via in-memory queue and dispatch in the Node runtime; durable inbox files under the run directory are for audit and reporting. See [Inbox & Dispatch](inbox.md). Hooks are CLI-only: they load from `hooks.json` and run as shell commands with JSON on stdin, driven by the same `__JAIPH_EVENT__` stream as the progress UI — see [Hooks](hooks.md). +Channels are validated at compile time (`validateReferences` / send RHS rules) and executed via in-memory queue and dispatch in the Node runtime; durable **`inbox/`** files under the run directory appear only for **routed** sends (audit — see [Inbox & Dispatch](inbox.md)). Hooks are CLI-only: they load from `hooks.json` and run as shell commands with JSON on stdin, driven by the same `__JAIPH_EVENT__` stream as the progress UI — see [Hooks](hooks.md). ## Test runner integration (`*.test.jh` in the kernel) @@ -122,7 +122,7 @@ Authoring rules, fixtures, and mock syntax for `*.test.jh` are documented in [Te ## CLI progress reporting pipeline -Static tree from AST (`progress.ts`); runtime events (`events.ts`, `stderr-handler.ts`); emitter (`emitter.ts`); display (`display.ts`, `progress.ts`). Async branch numbering (subscript ₁₂₃… prefixes) is driven by `async_indices` on step and log events — the runtime propagates a chain of 1-based branch indices through `AsyncLocalStorage`, and the stderr handler renders them at the appropriate indent level. `const` steps whose value is a `match_expr` are walked for nested `run`/`ensure` arms; matched targets appear as child items in the step tree (e.g. `▸ script safe_name` under the `const` row). +The progress UI combines a **static** step tree derived from the workflow AST (`cli/run/progress.ts`) with **live** updates from the runtime event stream. Event wiring: `events.ts` and `stderr-handler.ts` parse `__JAIPH_EVENT__` lines; `emitter.ts` bridges into the renderer. Line-oriented formatting (`formatStartLine`, `formatHeartbeatLine`, `formatCompletedLine`) lives primarily in `display.ts`, which shares some display helpers with `progress.ts`. Async branch numbering (subscript ₁₂₃… prefixes) is driven by `async_indices` on step and log events — the runtime propagates a chain of 1-based branch indices through `AsyncLocalStorage`, and the stderr handler renders them at the appropriate indent level. `const` steps whose value is a `match_expr` are walked for nested `run`/`ensure` arms; matched targets appear as child items in the step tree (e.g. `▸ script safe_name` under the `const` row). ## Distribution: Node vs Bun standalone @@ -147,7 +147,7 @@ flowchart TD CLI -->|jaiph run| BS1[buildScripts] BS1 --> Transpile - CLI -->|jaiph test| BS2[buildScripts workspace] + CLI -->|jaiph test| BS2[buildScripts(entry .test.jh)] BS2 --> Transpile BS2 --> TR[Node Test Runner in-process] @@ -166,7 +166,7 @@ flowchart TD TR --> RT RT -->|script steps| SCRIPT[Managed script subprocesses] - RT -->|prompt steps| KERNEL[JS kernel: prompt / emit / inbox / stream / schema / mock] + RT -->|prompt steps| KERNEL[Kernel libs: prompt, events, inbox, stream, schema, mock] RT -->|live events| EV["__JAIPH_EVENT__ stderr only"] EV --> CLI @@ -226,7 +226,7 @@ sequenceDiagram participant User participant CLI as CLI jaiph test participant Parser as parsejaiph - participant Prep as buildScripts workspace + participant Prep as buildScripts(test file) participant TestRunner as runTestFile / runTestBlock participant Graph as buildRuntimeGraph participant Runtime as NodeWorkflowRuntime @@ -235,7 +235,7 @@ sequenceDiagram User->>CLI: jaiph test flow.test.jh CLI->>Parser: parse test file Parser-->>CLI: jaiphModule + tests[] blocks - CLI->>Prep: buildScripts(workspace) workspace .jh only + CLI->>Prep: buildScripts(test path, tmp) import closure Prep-->>CLI: scriptsDir CLI->>TestRunner: runTestFile(test path workspace scriptsDir blocks) TestRunner->>Graph: buildRuntimeGraph(test file) once per file @@ -256,7 +256,7 @@ sequenceDiagram ## Summary - `.jh` / `*.test.jh` share parser/AST; **compile-time** validation runs in **`emitScriptsForModule`** during **`buildScripts`**. **`buildRuntimeGraph`** loads modules with **parse-only** imports. -- **`jaiph compile`** walks the same import closures as a normal compile check, runs **`validateReferences`** on each module, and exits — no **`buildScriptFiles`** emission, no **`buildScripts`**, no runner spawn. +- **`jaiph compile`** walks import closures **`validateReferences` only**, and exits — no **`buildScriptFiles`** emission, no **`buildScripts`**, no runner spawn. Directory discovery omits **`*.test.jh`** unless you pass a test file explicitly. - **Node-only runtime:** all execution — local `jaiph run`, Docker `jaiph run`, and `jaiph test` — goes through `NodeWorkflowRuntime`. Docker containers run `node-workflow-runner` with the compiled JS tree and scripts mounted, using the same semantics as local execution. - **CLI** owns launch, observation, hooks, and runtime preparation (`buildScripts`). Workflow execution runs in **`NodeWorkflowRuntime`**, with **script steps** as managed subprocesses. - No workflow-level `.sh` files or `jaiph_stdlib.sh` are produced or required. diff --git a/docs/artifacts.md b/docs/artifacts.md index 23ecab86..af9d6e50 100644 --- a/docs/artifacts.md +++ b/docs/artifacts.md @@ -9,7 +9,7 @@ redirect_from: Workflow and test runners need two kinds of output: **what humans see right now** (progress, status) and **what is left behind** after the process exits (replay, diffs, CI reports). Jaiph keeps those separate: the **live** channel is `__JAIPH_EVENT__` JSON lines on the child process’s **stderr**; the **durable** side is a tree of files under the project workspace so you can inspect, diff, and archive a run after it finishes. -When you run a workflow, or `jaiph test` executes workflows inside test blocks, the **Node** workflow runtime materializes that durable tree. By default it lives at `/.jaiph/runs/`; you can point it elsewhere with `run.logs_dir` / `JAIPH_RUNS_DIR` (see [Configuration — Run keys](configuration.md#run-keys)). The layout below is what `NodeWorkflowRuntime` writes. +When you run a workflow, or `jaiph test` executes workflows inside test blocks, **`NodeWorkflowRuntime`** materializes that durable tree. By default it lives at `/.jaiph/runs/`; you can point it elsewhere with `run.logs_dir` / `JAIPH_RUNS_DIR` (see [Configuration — Run keys](configuration.md#run-keys)). The layout below matches what `NodeWorkflowRuntime` creates at startup (see [Architecture — Durable artifact layout](architecture.md#durable-artifact-layout) for where this fits in the overall pipeline). In Docker mode, paths inside recorded events may use container prefixes (`/jaiph/run/…`); the CLI maps them to host paths when reporting failures — see [Sandboxing — Path remapping](sandboxing.md#path-remapping). ## Run directory layout @@ -20,24 +20,29 @@ The runtime uses a UTC-dated hierarchy. Each run gets its own folder: UTC date, / # UTC date (see NodeWorkflowRuntime) -/ # UTC time + basename (see above) 000001-module__step.out # stdout capture per step (6-digit seq prefix) - 000001-module__step.err # stderr capture (when non-empty) + 000001-module__step.err # stderr capture (may be empty) artifacts/ # user-published files (`jaiphlang/artifacts`); `JAIPH_ARTIFACTS_DIR` - inbox/ # inbox message files (when channels are used) + inbox/ # audit copies of routed channel payloads (optional) heartbeat # liveness: epoch ms, refreshed about every 10s return_value.txt # present if `default` workflow exited 0 and returned a value run_summary.jsonl # durable event timeline (JSON Lines) ``` -Sequence numbers in those filenames are **monotonic and unique** per run: a single in-memory counter in `NodeWorkflowRuntime` increments for each step capture. The separate `seq-alloc` helper is a **file-backed** allocator for tooling; ordinary runs do not use a `.seq` file in the run directory. For the full system picture, see [Architecture — Durable artifact layout](architecture.md#durable-artifact-layout) and [Architecture — Contracts](architecture.md#contracts) (`__JAIPH_EVENT__` on stderr is the live path). +Sequence numbers in those filenames are **monotonic and unique** per run. `RuntimeEventEmitter` owns a single in-memory counter (`allocStepSeq`) that advances whenever a step needs paired capture files — managed steps (`script`, nested `workflow` / `rule`, single-line `shell`, and similar) and `prompt` steps that write transcripts all draw from the same sequence. There is **no** `.seq` file in the run directory. For the live vs durable split, see [Architecture — Contracts](architecture.md#contracts): `__JAIPH_EVENT__` on stderr is the streaming path; `run_summary.jsonl` is the durable timeline. ## What each artifact is for -- **`*.out` / `*.err`** — Per-step capture files for managed work (script subprocesses, nested workflows, rules, and prompt steps). **Stdout** is written to a `.out` file as the step runs; a **`.err` file appears when stderr is non-empty** (see [Architecture — Durable artifact layout](architecture.md#durable-artifact-layout)). The live CLI stream is still separate: see [Architecture — Contracts](architecture.md#contracts). -- **`run_summary.jsonl`** — JSON Lines timeline mirroring what also goes to `__JAIPH_EVENT__` (where enabled): workflow boundaries, step start/end, log lines, inbox-related events. The file is created at runtime startup and lines are appended as the run progresses. -- **`inbox/`** — When you use channels, copies of message payloads can appear here for inspection (see [Inbox & Dispatch](inbox.md)). +- **`*.out` / `*.err`** — Paired capture files for steps that record subprocess or prompt I/O. The runtime creates both paths at **`STEP_START`**. For **managed** steps (extracted scripts, nested workflows/rules, single-line `shell`, and similar), stdout/stderr are **streamed** into the files during execution, then **rewritten** with the final aggregated strings at step end — so a long-running step’s `.out` can be tailed while it runs (see [CLI — Run artifacts and live output](cli.md#run-artifacts-and-live-output)). **Prompt** steps stream the model transcript into `.out`; `.err` is only overwritten when stderr from the backend is non-empty (otherwise the placeholder file stays zero-length). **Errors and CLI progress** still use the live `__JAIPH_EVENT__` stream on stderr; these files are the on-disk record. + +- **`run_summary.jsonl`** — Append-only JSON Lines timeline: workflow boundaries, step start/end, `LOG` / `LOGERR`, prompt lifecycle, inbox events, and the same step payload fields as the live stream. It is **truncated to empty at runtime startup**, then each event appends a line via `appendRunSummaryLine` as execution proceeds. **Note:** the in-process test runner can set `suppressLiveEvents`, which **stops** `__JAIPH_EVENT__` lines from going to stderr while **`run_summary.jsonl` keeps updating** (see [Architecture — Core components](architecture.md#core-components), `RuntimeEventEmitter`). + +- **`inbox/`** — When channels are used, **routed** `send` steps may persist a copy of the payload here (`NNN-.txt`) for audit. Files are created **only if** the send resolves to a context that has dispatch routes for that channel (no file for unrouted sends — see `NodeWorkflowRuntime` send handling and [Inbox & Dispatch](inbox.md)). Delivery stays in-memory; this directory is not a mailbox API. + - **`heartbeat`** — Best-effort file containing a wall-clock millisecond timestamp, rewritten on a timer (~10s). Liveness for external watchdogs; not required for normal CLI use. -- **`return_value.txt`** — Written after a successful `default` workflow when the workflow returns a value (including empty string, which yields a zero-length file so it is distinct from “no return”). Other entry paths (e.g. `test_run_workflow`) are not required to create this file. -- **`artifacts/`** — The runtime creates this directory in the run folder before execution and sets `JAIPH_ARTIFACTS_DIR` to it (along with `JAIPH_RUN_DIR`, `JAIPH_RUN_ID`, and `JAIPH_RUN_SUMMARY_FILE`). User code typically writes here via the `jaiphlang/artifacts` library (`artifacts.save`). In Docker mode this directory is under the **host-writable** run mount (`/jaiph/run/...` in the container), not the read-only workspace overlay. See [Libraries — `jaiphlang/artifacts`](libraries.md#jaiphlangartifacts--publishing-files-out-of-the-sandbox) and [Sandboxing](sandboxing.md). + +- **`return_value.txt`** — Written after a successful **`default`** workflow when `executeWorkflow` reports a defined `returnValue` (including `""`, which produces a zero-length file — distinct from “no file / no return”). **`runNamedWorkflow`** (used by `test_run_workflow` and similar) does not write this file by that path. + +- **`artifacts/`** — Created before steps run. The runtime sets **`JAIPH_ARTIFACTS_DIR`**, **`JAIPH_RUN_DIR`**, **`JAIPH_RUN_SUMMARY_FILE`**, and **`JAIPH_RUN_ID`** (a new UUID when `JAIPH_RUN_ID` was not already set in the environment). User workflows usually publish into this directory through **`jaiphlang/artifacts`** (`artifacts.save`). In Docker mode it sits under the **host-writable** run mount (`/jaiph/run/...` inside the container), not the read-only workspace overlay. See [Libraries — `jaiphlang/artifacts`](libraries.md#jaiphlangartifacts--publishing-files-out-of-the-sandbox) and [Sandboxing](sandboxing.md). ## Keeping runs out of git diff --git a/docs/cli.md b/docs/cli.md index a5258692..7b633af9 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -9,15 +9,17 @@ redirect_from: Jaiph is a workflow system: authors write `.jh` modules, and a **TypeScript CLI** prepares scripts, launches a **Node workflow runtime**, and surfaces progress while the **JavaScript kernel** executes the AST in process (no separate workflow shell). The CLI is what you install as the `jaiph` binary — it is the boundary between your terminal or CI and the interpreter. -This page lists **commands**, important **flags**, and **environment variables**. It focuses on how the tool behaves, not on the language itself. For syntax and step semantics, see [Grammar](grammar.md). For repository layout, pipelines, and contracts (`__JAIPH_EVENT__`, artifacts, Docker vs local), see [Architecture](architecture.md). +This page lists **commands**, important **flags**, and **environment variables**. It focuses on how the tool behaves, not on the language itself. For semantics and the overall language model, see [Language](language.md). For concrete syntax rules (imports, orchestration strings, managed calls, …), see [Grammar](grammar.md). For repository layout, pipelines, and contracts (`__JAIPH_EVENT__`, artifacts, Docker vs local), see [Architecture](architecture.md). **Commands:** `run`, `test`, `compile`, `format`, `init`, `install`, `use`. -**Global options:** `-h` / `--help` and `-v` / `--version` are recognized only as the **first token after `jaiph`** (e.g. `jaiph --help`). They are not treated as global flags after a subcommand or a file path (`jaiph run --help` is **not** usage — use `jaiph --help`, or `jaiph compile -h` for compile-specific usage). +**Global options:** `-h` / `--help` and `-v` / `--version` are recognized only as the **first token after `jaiph`** (e.g. `jaiph --help`). They are not treated as global flags after a subcommand or a file path (`jaiph run --help` is **not** usage — use `jaiph --help`, or **`jaiph compile -h`** / **`jaiph compile --help`** for compile-specific usage — the `compile` command parses `-h` / `--help` after the subcommand). Running **`jaiph`** with no arguments prints the same overview and exits **0**. ## File shorthand -If the first argument is an existing file, Jaiph routes it automatically based on the extension. Files ending in **`*.test.jh`** are run as tests (same as `jaiph test `). Other files ending in **`*.jh`** are run as workflows (same as `jaiph run `). The `*.test.jh` check happens first, so test files are never mistaken for workflows. +If the **first argument after `jaiph`** is an **existing path** (resolved relative to the current working directory), Jaiph routes it automatically based on the extension. Files ending in **`*.test.jh`** are run as tests (same as `jaiph test `). Other paths ending in **`.jh`** are run as workflows (same as `jaiph run `). The `*.test.jh` check happens first, so test modules are never mistaken for workflows. + +Additional positional tokens after a **workflow** shorthand are forwarded to **`workflow default`**, matching `jaiph run`. Tokens after a **test** shorthand are accepted but **ignored** (same as `jaiph test ` with extra arguments). ```bash # Workflow shorthand @@ -45,7 +47,7 @@ Any path ending in `.jh` is accepted (including `*.test.jh`, since the extension **Flags:** - **`--target `** — keep emitted script files and run metadata under `` instead of a temp directory (useful for debugging). -- **`--raw`** — skip the banner, live progress tree, hooks, and CLI failure footer. The workflow runner child uses **inherited stdio** so `__JAIPH_EVENT__` JSON lines go to **stderr** unchanged. The **host** CLI relies on this for Docker-backed runs (the container invokes `jaiph run --raw` so the host parses events from Docker’s stderr); you can also use it when embedding Jaiph in another tool. See [Sandboxing — Runtime behavior](sandboxing.md#runtime-behavior). +- **`--raw`** — skip the banner, live progress tree, hooks, and CLI failure footer. The workflow runner child uses **inherited stdio** so `__JAIPH_EVENT__` JSON lines go to **stderr** unchanged. The **host** CLI relies on this for Docker-backed runs (the container invokes `jaiph run --raw` so the host parses events from Docker’s stderr); you can also use it when embedding Jaiph in another tool. There is no PASS/FAIL line, **`return_value.txt` is not printed to stdout**, and the process exit code alone reflects success or failure. See [Sandboxing — Runtime behavior](sandboxing.md#runtime-behavior). - **`--`** — end of Jaiph flags; remaining args are passed to `workflow default` (e.g. `jaiph run file.jh -- --verbose`). **Examples:** @@ -73,7 +75,7 @@ workflow default() { } ``` -Workflow and rule bodies contain structured Jaiph steps only — use `run` to call a `script` for shell execution. In bash-bearing contexts (mainly `script` bodies, and restricted `const` / send RHS forms), `$(...)` and the first command word are validated: they must not invoke Jaiph rules, workflows, or scripts, contain inbox send (`<-`), or use `run` / `ensure` as shell commands (`E_VALIDATE`). See [Grammar — Managed calls vs command substitution](grammar.md#managed-calls-vs-command-substitution). +**Rule** bodies are **managed steps only** — no raw shell lines; use `run` to a `script` for shell execution. **Workflow** bodies may include **inline shell** lines that do not parse as a Jaiph step (the compiler still validates them); for anything non-trivial, prefer a top-level `script` and `run`. In bash-bearing contexts (mainly `script` bodies, and restricted `const` / send RHS forms), `$(...)` and the first command word are validated: they must not invoke Jaiph rules, workflows, or scripts, contain inbox send (`<-`), or use `run` / `ensure` as shell commands (`E_VALIDATE`). See [Grammar — Language concepts](grammar.md#language-concepts) and [Grammar — Managed calls vs command substitution](grammar.md#managed-calls-vs-command-substitution). For `const` in those bodies, a reference plus arguments on the RHS must be written as `const name = run ref([args...])` (or `ensure` for rule capture), not as `const name = ref([args...])` — the latter is `E_PARSE` with text that explains the fix. @@ -107,6 +109,8 @@ The root PASS/FAIL summary uses the format `✓ PASS workflow default (0.2s)`. C **TTY mode:** one extra line at the bottom shows the running workflow and elapsed time: `▸ RUNNING workflow (X.Xs)` — updated in place every second. When the run completes, it is replaced by the final PASS/FAIL line. +**Successful exit:** when the default workflow exits **0**, the CLI prints `✓ PASS workflow default (...)` plus elapsed time (see above). If the workflow **returns** a value, the runtime writes `return_value.txt` under the run directory; the CLI prints that value on stdout **after** the PASS line, separated by a blank line (host paths are unchanged; Docker runs remap container paths when reading the file). See [Architecture — Durable artifact layout](architecture.md#durable-artifact-layout). + **Non-TTY mode** (CI, pipes, log capture): no RUNNING line and no in-place updates. Step start (▸) and completion (✓/✗) lines still print as they occur. Long-running steps additionally print **heartbeat** lines to avoid looking like a hang: - Format: `· (running s)` — entire line dim/gray (plain text with `NO_COLOR`). @@ -272,16 +276,16 @@ Parse modules and run **`validateReferences`** (the same compile-time checks as jaiph compile [--json] [--workspace ] ... ``` -At least one path is required. +At least one path is required. **`jaiph compile -h`** or **`jaiph compile --help`** prints command-specific usage and exits **0**. **File arguments** — Each `*.jh` file is expanded to its **transitive import closure**; every module in the union is parsed and validated once. -**Directory arguments** — The tree is scanned for `*.jh` files whose basename is **not** `*.test.jh`; each such file is treated as an entrypoint and its closure merged into the same validation set. To validate a test module’s graph explicitly, pass that **`*.test.jh` file** as a path (directories never pick up `*.test.jh` as roots). +**Directory arguments** — The tree is scanned for `*.jh` files whose basename is **not** `*.test.jh` (same rule as `walkjhFiles` in the transpiler: files like `foo.test.jh` are skipped). Each non-test `*.jh` under the tree is treated as an entrypoint and its closure merged into the same validation set. To validate a test module’s graph explicitly, pass that **`*.test.jh` file** as a path (directories never pick up `*.test.jh` as roots). **Flags:** - **`--json`** — On success, print `[]` to stdout. On failure, print one JSON **array** of objects `{ "file", "line", "col", "code", "message" }` to stdout and exit **1** (non-JSON errors use a synthetic `E_COMPILE` object when the message is not in `file:line:col CODE …` form). -- **`--workspace `** — Override the workspace root used for **library import resolution** (`/.jaiph/libs/`, etc.) for all derived paths. When omitted, the workspace is auto-detected per file the same way as `jaiph run`. +- **`--workspace `** — Override the workspace root used for **library import resolution** (`/.jaiph/libs/`, etc.) for **all** modules reached from the given paths. When omitted, the workspace is **auto-detected** from each path’s location (`detectWorkspaceRoot` — same algorithm as `jaiph run`, starting from the file’s directory or from a directory argument). ## `jaiph format` @@ -327,22 +331,22 @@ Creates: - `.jaiph/.gitignore` — lists `runs` and `tmp`. If the file already exists and does not match this exact list, `jaiph init` exits with a non-zero status. - `.jaiph/bootstrap.jh` — canonical bootstrap workflow; made executable. The template uses a triple-quoted multiline prompt body (`prompt """ ... """`) so the generated file parses and compiles as valid Jaiph. It asks the agent to scaffold workflows under `.jaiph/` and ends by logging a summary (`WHAT CHANGED` + `WHY`). Docker sandboxing uses the default `ghcr.io/jaiphlang/jaiph-runtime` image unless you set `runtime.docker_image` or `JAIPH_DOCKER_IMAGE`. -- `.jaiph/SKILL.md` — copied from the skill file bundled with your Jaiph installation (or from `JAIPH_SKILL_PATH` when set). If no skill file is found, this file is not written and a note is printed. +- `.jaiph/SKILL.md` — copied from the skill file bundled with your Jaiph installation. If **`JAIPH_SKILL_PATH`** is set, that path is used **only when it exists on disk**; otherwise the CLI searches known install-relative locations and `docs/jaiph-skill.md` from the current directory. If no file is found, `SKILL.md` is not written and a note is printed. ## `jaiph install` -Install project-scoped libraries. Libraries are git repos cloned into `.jaiph/libs//` under the workspace root. A lockfile (`.jaiph/libs.lock`) tracks installed libraries for reproducible setups. +Install project-scoped libraries. Libraries are git repos cloned into `.jaiph/libs//` under the **workspace root**. The workspace is determined from the **current working directory** (`detectWorkspaceRoot(process.cwd())` — walk upward until `.jaiph` or `.git`, with the same temp-directory guards as `jaiph run`). A lockfile (`.jaiph/libs.lock`) under that root tracks installed libraries for reproducible setups. ```bash jaiph install [--force] ... jaiph install [--force] ``` -**With arguments** — clone each repo into `.jaiph/libs//` (shallow: `--depth 1`) and upsert the entry in `.jaiph/libs.lock`. The library name is derived from the URL: last path segment, stripped of `.git` suffix (e.g. `github.com/you/queue-lib.git` → `queue-lib`). Version pinning uses `@` after the URL. +**With arguments** — clone each repo into `.jaiph/libs//` (shallow: `--depth 1`) and upsert the entry in `.jaiph/libs.lock`. The library name is derived from the URL: last path segment, stripped of `.git` suffix (e.g. `github.com/you/queue-lib.git` → `queue-lib`). Version pinning is usually written as **`https://…/name.git@`**; other URL shapes with a trailing **`@ref`** are also accepted when the parser can split URL and version unambiguously. -**Without arguments** — restore all libraries from `.jaiph/libs.lock`. Useful after cloning a project or in CI. +**Without arguments** — restore all libraries from `.jaiph/libs.lock`. Useful after cloning a project or in CI. If the lockfile exists but lists **no** libraries, the command prints `No libs in lockfile.` and exits **0**. -If `.jaiph/libs//` already exists, the library is skipped. Use `--force` to delete and re-clone. +If `.jaiph/libs//` already exists, the library is skipped. Use **`--force`** (anywhere in the argument list) to delete and re-clone. **Lockfile format** (`.jaiph/libs.lock`): @@ -408,14 +412,16 @@ These variables apply to `jaiph run` and workflow execution. Variables marked ** **Internal variables:** -- `JAIPH_META_FILE` — path to the metadata file the CLI writes under the build output directory; the workflow runner reads it after exit. Set by the launcher on the child process; `resolveRuntimeEnv` removes any inherited value from the parent. -- `JAIPH_RUN_DIR`, `JAIPH_RUN_ID`, `JAIPH_RUN_SUMMARY_FILE` — `JAIPH_RUN_ID` is generated by the host CLI as a UUID per `jaiph run` invocation and forwarded to the runtime (and into the Docker container when sandboxed). The runtime uses this value as the workflow run identifier; if unset, the runtime generates its own UUID. `JAIPH_RUN_DIR` and `JAIPH_RUN_SUMMARY_FILE` are set by `NodeWorkflowRuntime` to the run directory and `run_summary.jsonl` path. -- `JAIPH_SOURCE_FILE` — set automatically by the CLI to the entry file basename. Used to name run directories. +- `JAIPH_META_FILE` — path to the run metadata file (under the CLI’s build output directory for that invocation). Set on the **detached workflow child** only; the parent strips any inherited value so leftover exports do not collide. The runner writes `run_dir=` / `summary_file=` lines for the host to read after exit. +- `JAIPH_SOURCE_ABS` — absolute path to the entry `.jh` file; set by the CLI for **`jaiph run`** before spawn. Required by the runner (local and Docker). +- `JAIPH_SCRIPTS` — directory containing emitted **`script`** files for this run; set after **`buildScripts()`**. Any **`JAIPH_SCRIPTS`** exported in the parent shell is cleared before launch so nested toolchains do not point at the wrong tree. +- `JAIPH_RUN_DIR`, `JAIPH_RUN_ID`, `JAIPH_RUN_SUMMARY_FILE` — for a normal (**non-raw**) **`jaiph run`**, the host generates **`JAIPH_RUN_ID`** once per invocation (UUID), passes it through to the detached child (and into Docker when sandboxed), and Docker failure-path discovery can match summaries by this id. The runtime uses **`JAIPH_RUN_ID`** as the stable run identifier; if it is absent, the runtime may assign its own UUID. **`JAIPH_RUN_DIR`** and **`JAIPH_RUN_SUMMARY_FILE`** are set inside the runner once the UTC run directory exists. +- `JAIPH_SOURCE_FILE` — set automatically by the CLI to the entry file **basename**. Used to name run directories (see [Architecture — Durable artifact layout](architecture.md#durable-artifact-layout)). **Workspace and run paths:** - `JAIPH_WORKSPACE` — workspace root, set by the CLI. Detected by walking up from the entry `.jh` file's directory until `.jaiph` or `.git` is found. Guards in `detectWorkspaceRoot` skip misleading markers under shared system temp directories (`/tmp`, `/var/tmp`, macOS `/var/folders/.../T/...`) and nested `.jaiph/tmp` trees. In Docker sandbox mode the runtime remaps it inside the container (see [Sandboxing](sandboxing.md)). -- `JAIPH_RUNS_DIR` — root directory for run logs (default: `.jaiph/runs` under workspace). +- `JAIPH_RUNS_DIR` — root directory for run logs. If unset in the environment, the CLI merges the entry module **`config`** field **`run.logs_dir`** (when present) into the spawned process environment; otherwise the default layout is `.jaiph/runs` under the workspace. Exporting **`JAIPH_RUNS_DIR` yourself locks that choice: in-file **`run.logs_dir`** cannot override an environment-provided value. **Agent and prompt configuration:** @@ -461,4 +467,4 @@ For overlay vs copy workspace mode, mounts, and stderr wiring, see [Sandboxing]( ### `jaiph init` -- `JAIPH_SKILL_PATH` — path to the skill markdown copied to `.jaiph/SKILL.md` when running `jaiph init`. +- `JAIPH_SKILL_PATH` — absolute or relative path to the skill markdown copied to `.jaiph/SKILL.md` when running `jaiph init`. The file **must exist**; otherwise this variable is ignored and the default search runs. diff --git a/docs/configuration.md b/docs/configuration.md index 85dbf640..88d04b51 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -11,7 +11,7 @@ When you need the same workflow sources to behave differently on different machi All execution is interpreted by the Node workflow runtime (`NodeWorkflowRuntime`): the AST, managed scripts, prompts, channels, inbox, and `.jaiph/runs` artifacts (see [Architecture](architecture.md)). Configuration only adjusts that stack; it does not change the workflow language or the compile graph. -`jaiph compile` and `buildScripts()` use the same parser, so **unknown `config` keys and wrong value types** fail with deterministic parse errors. Runtime graph loading is parse-only; **compile-time** validation of references runs in the transpile path, not in `buildRuntimeGraph()` (see [Architecture — Summary](architecture.md#summary)). +`jaiph compile` parses each module in the import closure (same grammar as `buildScripts()`), so **unknown `config` keys and wrong value types** produce the same parse errors as a normal build. `jaiph compile` then runs **`validateReferences` only** — it does not emit scripts or spawn the runner (see [Architecture](architecture.md#summary)). Runtime graph loading is parse-only; **compile-time** reference validation runs in the transpile path, not in `buildRuntimeGraph()`. **Source of truth:** When this document and the implementation disagree, treat the source code as authoritative. @@ -27,6 +27,8 @@ For **agent and run keys**, the full precedence chain is: > **environment > workflow-level config > module-level config > defaults** +`run.recover_limit` is an exception: only **module-level** values affect `run … recover` (see [Run keys](#run-keys)). + For **`runtime.*` (image, network, timeout)**, the CLI merges at **`jaiph run` launch** — not inside `NodeWorkflowRuntime` — in the order **`JAIPH_DOCKER_*` environment > in-file `runtime.*` > defaults** (and separately: Docker on/off is env-only, see above and [Precedence in detail](#precedence-in-detail)). `runtime.*` cannot appear in workflow-level `config` blocks. ## In-file config blocks @@ -98,7 +100,8 @@ workflow default() { - At most one per workflow; it must be the first non-comment construct in the body. A duplicate is `E_PARSE`: `duplicate config block inside workflow (only one allowed per workflow)`. - Only **`agent.*` and `run.*` keys** are allowed. Any `runtime.*` or `module.*` key is `E_PARSE`. -- Workflow-level values apply to all steps in that workflow, including `ensure`d rules and scripts called from it. When the workflow finishes, the previous environment is restored. +- Workflow-level values apply to all steps in that workflow, including `ensure`d rules and scripts called from it, for **`agent.*`** and **`run.logs_dir`** / **`run.debug`** (merged when the workflow or cross-module `ensure` runs). **`run.recover_limit` is different:** the retry limit for `run … recover` comes only from the **module-level** `config` of the **`.jh` file that owns the current scope** when the step runs; a workflow-level `run.recover_limit` assignment is valid syntax but does **not** change recover behavior today. +- When the workflow finishes, the previous environment is restored. **Sibling isolation:** Each workflow gets its own clone of the parent environment. Sibling workflows never see each other's config — even when they execute sequentially. If workflow `alpha` sets `agent.backend = "claude"` and workflow `beta` only sets `agent.default_model = "beta-model"`, `beta` still sees the module-level backend (e.g. `"cursor"`), not `alpha`'s. @@ -134,7 +137,7 @@ These control runtime behavior unrelated to the agent. |-----|------|---------|--------------|-------------| | `run.logs_dir` | string | `.jaiph/runs` | `JAIPH_RUNS_DIR` | Step log directory. Relative paths are joined with the workspace root; absolute paths are used as-is. | | `run.debug` | boolean | `false` | `JAIPH_DEBUG` | Enables debug tracing for the run. | -| `run.recover_limit` | integer | `10` | _(no env override)_ | Maximum number of retry attempts for `run … recover` loops before the step fails. See [Language — `recover`](language.md#recover--repair-and-retry-loop). | +| `run.recover_limit` | integer | `10` | _(no env override)_ | Maximum attempts for `run … recover` loops before the step fails (see [Language — `recover`](language.md#recover--repair-and-retry-loop)). Effective value comes **only** from the **module-level** `config` block of the **`.jh` file that owns the current scope** (the file containing the workflow or rule that executes the step). Workflow-level `run.recover_limit` does not apply. | ### Module keys @@ -343,4 +346,4 @@ The runtime also sets `JAIPH_ARTIFACTS_DIR` — the absolute path to the writabl ## Created by `jaiph init` -`jaiph init` creates `.jaiph/bootstrap.jh` and writes `.jaiph/SKILL.md` from the skill file bundled with your installation (see `JAIPH_SKILL_PATH` in the CLI reference). It does not add a separate config file — use `config { ... }` in your workflow sources. +`jaiph init` creates `.jaiph/bootstrap.jh`, writes `.jaiph/SKILL.md` from the skill file bundled with your installation (see `JAIPH_SKILL_PATH` in the [CLI](cli.md) reference), and ensures `.jaiph/.gitignore` matches the canonical template (lists `runs` and `tmp` under `.jaiph/`). It does not add a separate config file — use `config { ... }` in your workflow sources. diff --git a/docs/contributing.md b/docs/contributing.md index a53f3fac..355b511a 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -7,7 +7,9 @@ redirect_from: # Contributing to Jaiph -A shared workflow needs shared expectations: which branch to target, how to build from a clone, and what evidence a change should carry. **This page is that contract for Jaiph** — branching, local install, code and testing philosophy, the layered test stack (TypeScript, txtar, goldens, bash E2E), and what CI enforces. It does **not** teach the language; for that, use [Getting Started](getting-started.md) (documentation map), [Setup](setup.md) (install and workspace), and [Grammar](grammar.md). For **how the implementation is structured** (components, compile and run pipelines, `buildRuntimeGraph` vs validation, runtime contracts, artifact paths), use [Architecture](architecture.md) as the source of truth. +Open source only works when expectations match reality: where to land changes, how to reproduce CI locally, and how to prove behavior with the right test layer. **This page is that contract** — branching, install-from-clone, engineering conventions, the test stack (TypeScript, txtar, golden AST, bash E2E), and what GitHub Actions run on each push. + +**Related documentation:** [Getting Started](getting-started.md) (map), [Language](language.md) (patterns), [Setup](setup.md), and [Grammar](grammar.md). **Implementation boundaries** (pipelines, `buildRuntimeGraph` vs `validateReferences`, runtime contracts, artifact layout) are spelled out in [Architecture](architecture.md); if this page disagrees with that file, **Architecture wins**. ## Branching and pull requests @@ -105,7 +107,7 @@ Jaiph uses several test layers. Each layer catches a different class of bug. Use ### Key principles -1. **Compile-time validation vs graph loading.** `buildScripts` / `emitScriptsForModule` run **`validateReferences`** before any script files are written. **`buildRuntimeGraph()`** only parses modules and follows imports — it does **not** re-run that validation. Lock compile errors in the compiler/validator tests; the runtime graph is the wrong layer for that (see [Architecture — Transpiler / Node workflow runtime](architecture.md#core-components)). +1. **Compile-time validation vs graph loading.** `buildScripts` / `emitScriptsForModule` run **`validateReferences`** before any script files are written. **`buildRuntimeGraph()`** only parses modules and follows imports — it does **not** re-run that validation. Lock compile errors in the compiler/validator tests; the runtime graph is the wrong layer for that (see [Architecture — Core components](architecture.md#core-components)). **`jaiph compile`** runs **`validateReferences` only** (no **`buildScripts`**, no runner); cover it with txtar/acceptance/E2E such as `e2e/tests/109_compile_command.sh`, not by expecting the full transpile path — see [Architecture — System overview](architecture.md#system-overview). 2. **Tests are behavior contracts.** E2E tests and acceptance tests define what the product does. Default approach: change production code to satisfy tests, not the other way around. 3. **Modify existing tests only with a strong reason:** intentional product behavior change, incorrect test expectation, or removal of an obsolete feature. Any such change should be minimal and paired with a clear rationale. 4. **Golden tests are the compiler's safety net.** After transpiler changes, run `npm test`. Failures in `src/transpile/compiler-golden.test.ts` usually mean updating an explicit expected string or fixture in that file — there is no separate dump script; align expectations with intentional emitter changes and re-run `npm test`. **Golden AST tests** (`test-fixtures/golden-ast/`) complement this by locking in the parse tree shape — if those fail, regenerate with `UPDATE_GOLDEN=1 npm run test:golden-ast` and review the diff. @@ -167,14 +169,16 @@ The project uses GitHub Actions (`.github/workflows/ci.yml`). The workflow defin |-----|--------|---------| | **ShellCheck** | `ubuntu-latest` | Runs `shellcheck` on `runtime/overlay-run.sh` to lint the standalone shell script shipped in the npm package. | | **Compiler and unit tests** | `ubuntu-latest` | `npm test` (TypeScript unit + acceptance + golden tests), plus a `curl` check that the public install URL responds and a git-tag verification on `main`. | -| **E2E install and CLI workflow** | Matrix: **`ubuntu-latest` twice** + **`macos-latest`** | `npm run test:e2e` — full build-and-run E2E suite. In **CI**, the **docker** matrix leg builds `jaiph-ci-runtime:local` from `runtime/Dockerfile` and sets **`JAIPH_DOCKER_IMAGE`** so the job does not pull the public GHCR image during the run. **Ubuntu — docker:** `JAIPH_UNSAFE` unset (container sandbox). **Ubuntu / macOS — host:** `JAIPH_UNSAFE=true` (no Docker; macOS does not run the docker leg). On a **developer machine**, with `JAIPH_UNSAFE` unset, the CLI still resolves the default image (typically `ghcr.io/jaiphlang/jaiph-runtime`) for Docker-backed runs — see `src/runtime/docker.ts` and [Architecture](architecture.md). | +| **E2E** | Matrix: **`ubuntu-latest` twice** + **`macos-latest`** | Job id `e2e`; in the Actions UI each leg appears as **`E2E (,