Why this page exists. Agentic work needs the same things human teams need: a clear sequence of steps, explicit checks, and a record of what ran. Jaiph is a small workflow language for that: workflows sequence orchestration, rules express checks, script holds real shell, and the runtime logs steps and writes run artifacts. The payoff is behavior that is easier to repeat, verify, and debug than ad-hoc shell snippets alone.
This page is an agent skill: it tells an AI assistant how to author Jaiph workflows (.jh files) and what a sensible .jaiph/ layout looks like. It is not a full language specification — use Getting started as the documentation map, Grammar for syntax and validation details, Configuration for config keys, Inbox & Dispatch for channels, and Sandboxing for rule design vs optional Docker isolation.
Jaiph is a small language for agentic workflows: orchestration (rules, prompts, managed calls) and shell in script definitions. The Node workflow runtime (NodeWorkflowRuntime) interprets the parsed AST in process — there is no separate transpiled workflow shell on the execution path (Architecture). Before jaiph run or jaiph test, buildScripts() takes a single entry .jh path (the workflow file, or the *.test.jh file for tests), runs compile-time validation (validateReferences inside emitScriptsForModule), and writes extracted script files under scripts/ for that module and every file reachable from it via transitive import — not the whole workspace unless those files are imported. jaiph compile runs the same validateReferences checks by parsing each module in the computed closure without buildScripts, script emission, or the runner (Architecture). The runner’s buildRuntimeGraph() then loads the graph with parse-only imports (it does not re-run validateReferences).
Contracts (CLI vs runtime): Live: __JAIPH_EVENT__ JSON lines on stderr only (CLI progress and hooks — hooks are CLI-only, driven by that stream). Durable: .jaiph/runs/... and run_summary.jsonl. Channels are enforced at compile time and executed in the runtime (in-memory queue + inbox files under the run dir); they are not hooks.
The JS kernel (src/runtime/kernel/) handles prompt execution, managed script subprocesses, inbox queues and dispatch, and event/summary emission. Rule bodies run in-process; user script bodies run as separate OS processes (bash by default, polyglot via fence lang tags like node , python3 or a leading #! shebang in the body).
Test lane: jaiph test runs *.test.jh in-process (node-test-runner.ts): for each file it calls buildScripts(testFile, …) (same helper as jaiph run, with the test file as the entry so its import closure is validated and scripts are emitted), then buildRuntimeGraph(testFile) once per file, mocks, and assertions — same NodeWorkflowRuntime as jaiph run. The runtime enables suppressLiveEvents for those workflow runs so __JAIPH_EVENT__ lines are not written to stderr (keeping node --test output readable); run_summary.jsonl under the run directory is still updated where the emitter records workflow traffic (Architecture).
After jaiph init, a repository gets .jaiph/bootstrap.jh (a triple-quoted prompt that tells the agent to read .jaiph/SKILL.md) and a copy of this file. The bootstrap prompt asks the agent to scaffold workflows under .jaiph/ and to end with a clear WHAT CHANGED + WHY summary. The expected outcome is a minimal workflow set for safe feature work: preflight checks, an implementation workflow, verification, and a workflow default entrypoint that wires them together (with an optional human-or-agent “review” step when you use a task queue). Docker-backed runs use the official ghcr.io/jaiphlang/jaiph-runtime image by default; see Sandboxing to override with runtime.docker_image or JAIPH_DOCKER_IMAGE.
Concepts:
ensure (other rules only), run (scripts only — not workflows), const, match, if, for … in … (line iteration over a string binding), fail, log/logerr, return "…" / return run script() / return ensure rule(), ensure … catch, run … catch, run … recover. No raw shell lines, prompt, inbox send/route, or run async. Under jaiph run, rule bodies are executed in-process by the Node runtime; when a rule runs a script, that script is a normal managed subprocess (same as scripts from workflows) — see Sandboxing.ensure, run, prompt, const, fail, return, log/logerr, inbox send, match, if, for … in …, run async, ensure/run with catch or recover, …) plus optional inline shell lines: a line that does not parse as a managed step is treated as bash stored in a shell AST node (validated like other shell text). Prefer top-level script definitions and run for multi-line or reusable shell. Route declarations (->) belong on top-level channel lines, never inside a workflow body (a -> in a body is E_PARSE).script definitions are bash (or shebang interpreter) source, not Jaiph orchestration. Defined with script name = `body` (single-line backtick) or script name =[lang] ... (fenced block). Double-quoted string bodies (script name = "body") and bare identifier bodies (script name = varName) are removed — both produce parse errors with guidance to use backtick delimiters. The compiler treats all script bodies as opaque text: it does not parse lines as Jaiph steps, reject keywords, strip quotes, or validate cross-script calls. This means embedded node -e heredocs, inline Python, const assignments in JS, and any other valid shell construct compile without interference. Jaiph interpolation (${...}) is forbidden in single-line backtick script bodies — use $1, $2 positional arguments to pass data from orchestration to scripts. In fenced (triple-backtick) blocks, ${...} is passed through to the shell as standard parameter expansion (${VAR}, ${VAR:-default}, etc.). A single-backtick body containing a newline is a hard parse error — use a fenced block for multi-line scripts. Use return N / return $? for exit status and stdout (echo / printf) for string data to callers. From a workflow or rule, call with run fn(). Can be exported (export script name = ...) for use by importing modules. Cannot be used with ensure, are not valid inbox route targets, and must not be invoked through $(...) or as a bare shell step. Polyglot scripts: use a fence lang tag (<tag> ) to select an interpreter — the tag maps directly to #!/usr/bin/env channel <name> [-> workflow, ...] declarations with optional inline routing; send uses channel_ref <- …. Routes are declared on the channel declaration, not inside workflow bodies (see Inbox & Dispatch). Channel names share the per-module namespace with rules, workflows, scripts, and module-scoped local / const variables.Step semantics (ensure, run, prompt, catch, recover, match, if, for, log, fail, return, send, run async) are detailed in the Steps section below.
Audience: Agents that produce or edit .jh files.
Use this loop whenever you add or change Jaiph workflows so failures surface before work is handed back. When the repo defines a workflow default entrypoint (often .jaiph/main.jh) that wires preflight → implementation → verification, use jaiph run on that file for end-to-end delivery after the narrower checks below pass.
jaiph run .jaiph/readiness.jh or a named preflight workflow). When the repo ships native tests (*.test.jh), run jaiph test before large edits when practical..jh modules using only constructs described in Grammar; keep managed-call rules (ensure for rules, run for workflows and scripts); put multi-line or reusable bash in script definitions (rules never allow raw shell lines — use run to a script; workflows may use optional inline shell where the grammar allows, but prefer script + run for anything non-trivial — see Grammar — Language concepts).jaiph format <file.jh ...> on all authored or modified .jh files before committing. This normalizes whitespace, indentation, and top-level ordering (imports, config, and channels hoisted to the top; everything else kept in source order). Use jaiph format --check <file.jh ...> to verify formatting without writing (non-zero exit on drift — useful in CI).jaiph compile <file-or-dir> on the paths you touched (or jaiph compile --json … in automation). Same validateReferences checks as before a run, without executing workflows or writing scripts/ (Architecture). With a directory argument, only non-test *.jh files are used as entrypoints (*.test.jh is skipped); pass a test file path explicitly to validate it.jaiph test (whole workspace or a focused path) and any verification workflow the repo defines (commonly jaiph run .jaiph/verification.jh). Fix failures you introduce..jaiph/runs directly when you need raw step logs or run_summary.jsonl instead of only the terminal tree.CLI commands:
| Command | Purpose |
|---|---|
jaiph run [--target <dir>] [--raw] <file.jh> [--] [args...] |
Execute workflow default in the given file (--raw: no banner/tree/hooks; used for embedding and Docker inner runs) |
jaiph test [path] |
Run *.test.jh test files (workspace, directory, or single file) |
jaiph format [--check] [--indent <n>] <file.jh ...> |
Reformat .jh files (or verify formatting without writing) |
jaiph compile [--json] [--workspace <dir>] <.jh files or dirs…> |
Parse and validateReferences only (no script emission, no run) |
jaiph init [workspace] |
Scaffold .jaiph/ with bootstrap workflow and skill file |
jaiph install [--force] [<url[@version]> …] |
Clone libraries into .jaiph/libs/ or restore from .jaiph/libs.lock |
jaiph use <version\|nightly> |
Reinstall Jaiph at a specific version or nightly |
File shorthand: jaiph ./file.jh auto-routes — *.test.jh files run as tests, other *.jh files run as workflows.
Full flags and environment variables: CLI Reference.
Use this guide when generating or updating .jaiph/*.jh workflows for a repository after jaiph init.
When this skill conflicts with the compiler or runtime, follow the implementation. For language rules and validation codes, Grammar is the detailed reference. Published docs: jaiph.org.
jaiph init writes this skill to .jaiph/SKILL.md when the installer resolves a skill file: if JAIPH_SKILL_PATH is set, it is used only when that path exists on disk; otherwise the CLI searches install-relative locations and docs/jaiph-skill.md from the current working directory (CLI Reference). If no file is found, init skips SKILL.md — set JAIPH_SKILL_PATH to an existing markdown file (for example docs/jaiph-skill.md in a checkout) and run jaiph init again.
Ignore any outdated Markdown that contradicts the above.
A minimal workflow set under .jaiph/ that matches the delivery loop above:
runtime.docker_image / JAIPH_DOCKER_IMAGE match the tooling the team needs; the default is ghcr.io/jaiphlang/jaiph-runtime (see Sandboxing).ensure for repo state and required tools (e.g. clean git, required binaries). Expose a small workflow (e.g. workflow default in readiness.jh) that runs these checks.ba_review.jh). An agent prompt evaluates the next task for clarity, consistency, conflicts, and feasibility, then either marks it as ready or exits with questions. The implementation workflow gates on this marker so unreviewed tasks cannot proceed. This repository’s .jaiph/architect_review.jh is one concrete example; it uses QUEUE.md as the task queue.prompt), e.g. workflow implement in main.jh. When using a task queue, the implementation workflow should check that the first task is marked as ready (e.g. via a <!-- dev-ready --> marker) before proceeding.workflow default for lint/test/build (e.g. verification.jh). Complement this with repo-native *.test.jh suites run by jaiph test where appropriate.workflow default (e.g. in .jaiph/main.jh) that runs: preflight → (optional) review → implementation → verification. This is what jaiph run .jaiph/main.jh "..." executes.Prefer composable modules over one large file.
import "path.jh" as alias. Path must be double-quoted. Path is relative to the importing file first; if no file is found and the path contains /, the resolver falls back to project-scoped libraries under <workspace>/.jaiph/libs/ (e.g. import "queue-lib/queue" as queue resolves to .jaiph/libs/queue-lib/queue.jh). If the path has no extension, the compiler appends .jh. Install libraries with jaiph install <url[@version]>. Script imports: import script "./helper.py" as helper imports an external script file and binds it as a local script symbol — callable with run helper(args) exactly like an inline script definition. The path resolves relative to the importing file. Shebangs in the imported file are preserved. Missing targets fail with E_IMPORT_NOT_FOUND.channel name (inbox endpoint); rule name() { ... } or rule name(params) { ... }, workflow name() { ... } or workflow name(params) { ... }, script name = `body` or script name =[lang] ... . Parentheses are required on all rule and workflow definitions — even when parameterless (e.g. workflow default() { ... }, rule check() { ... }). Omitting () before { is a parse error with a fix hint. Named parameters go inside the parentheses — e.g. workflow implement(task, role) { ... }, rule gate(path) { ... }. At runtime, named params are the only way to access arguments. The compiler validates call-site arity when the callee declares params. Named scripts require a name at the definition site; for anonymous one-off commands use inline scripts: run `echo ok`() or run...(args). Optional export before rule, workflow, or script marks it as public (see Grammar). Optional config { ... } at the top of a file sets agent, run, and runtime options. An optional config { ... } block can also appear inside a workflow { ... } body (before any steps) to override module-level settings for that workflow only — only agent.* and run.* keys are allowed; runtime.* and module.* yield E_PARSE (see Configuration). Config values can be quoted strings, booleans (true/false), bare integers, or bracket-delimited arrays of strings (see Grammar and Configuration).local name = value or const name = value (same value forms). Prefer const for new files. Values can be single-line "..." strings, triple-quoted """...""" multiline strings, or bare tokens. A double-quoted string that spans multiple lines is rejected — use """...""" instead. Accessible as ${name} inside orchestration strings in the same module. Names share the unified namespace with channels, rules, workflows, and scripts — duplicates are E_PARSE. Not exportable; module-scoped only.ensure ref() or ensure ref(args…) runs a rule (local or alias.rule_name). Parentheses are required on every call site, including zero-argument calls (ensure check(), not bare ensure check). Arguments are comma-separated inside (). Bare identifier arguments are supported and preferred (when valid): ensure check(status) is equivalent to ensure check("${status}") — the identifier must reference a known variable (const, capture, or named parameter); unknown names fail with E_VALIDATE. Standalone "${identifier}" in call arguments is rejected — use the bare form instead. Quoted strings with extra text (e.g. "prefix_${name}") stay valid. Jaiph keywords cannot be used as bare identifiers. Optionally ensure ref(…) catch (<name>) <body>: the recovery body runs once on failure (no built-in retry on ensure — use run … recover for loops). The binding receives merged stdout+stderr from the failed rule. Full output also lives in .out / .err artifacts. Works in workflows and rules.run ref() or run ref(args…) runs a workflow or script (local or alias.name). Same required () on every call site as ensure, including zero args (run setup()). In a workflow, the target may be another workflow or a script; in a rule, the target must be a script only (E_VALIDATE if you name a workflow). run does not forward CLI positional args implicitly — the entry workflow binds them into named params and must pass values explicitly into callees. Bare identifier arguments follow the same rules as ensure when applicable. Nested managed calls inside argument lists must use keywords: run foo(run bar()), run foo(ensure check()); bare run foo(bar())/run foo(\…`()) forms are rejected. Optionally catch (log "message" writes the expanded message to stdout and emits a LOG event; the CLI shows it in the progress tree at the current depth. Double-quoted string; ${identifier} interpolation works at runtime. For multiline messages, use triple quotes: log """...""". Bare identifier form: log foo (no quotes) expands to log "${foo}" — the variable’s value is logged. Works with const, capture, and named parameters. Inline capture interpolation is also supported: ${run ref([args])} and ${ensure ref([args])} execute a managed call and inline the result (e.g. log "Got: ${run greet()}"). Nested inline captures are rejected. LOG events and run_summary.jsonl store the same message string (JSON-escaped for the payload). No spinner, no timing — a static annotation. See CLI Reference for tree formatting. Useful for marking workflow phases (e.g. log "Starting analysis phase").logerr "message" is identical to log except the message goes to stderr and the event type is LOGERR. In the progress tree, logerr lines use a red ! instead of the dim ℹ used by log. Same quoting, interpolation, bare identifier, and triple-quote rules as log (e.g. logerr err_msg, logerr """...""").<-, use a double-quoted literal, triple-quoted block (channel <- """..."""), ${var}, or run ref([args]). An explicit RHS is always required — bare channel <- (without a value) is invalid. Raw shell on the RHS is rejected — use const x = run helper() then channel <- "${x}", or channel <- run fmt_fn(). Combining capture and send (name = channel <- …) is E_PARSE. See Inbox & Dispatch.channel name -> workflow_ref or channel name -> wf1, wf2. A -> inside a workflow body is a parse error with guidance to move it to the channel declaration. When a message arrives on the channel, the runtime calls each listed workflow (local or alias.workflow), binding the dispatch values (message, channel, sender) to the target’s 3 declared parameters. Route targets must declare exactly 3 parameters. Scripts and rules are not valid route targets. The dispatch queue drains after the orchestrator completes. NodeWorkflowRuntime does not cap dispatch iterations — avoid circular sends that grow the queue without bound. See Inbox & Dispatch.const name = … (the const keyword is required for all captures). All bindings are immutable: a name bound by a parameter, const, capture, or script cannot be rebound in the same scope — the compiler rejects it with E_VALIDATE: cannot rebind immutable name "…". For ensure / run to a workflow or rule, capture is the callee’s explicit return "…". For run to a script, capture follows stdout from the script body. prompt capture is the agent answer. const RHS cannot use $(...) or disallowed ${...} forms — use a script and const x = run helper(…). const must not use a bare ref(args…) call shape: use const x = run ref(args…) (or ensure for rules), not const x = ref(args…) — the compiler fails with E_PARSE and suggests the run form. Do not put Jaiph symbols inside $(...) — use ensure / run. See Grammar and Grammar.return "value" / return "${var}" / return """...""" sets the managed return value. Also supports direct managed calls: return run ref() or return run ref(args) and return ensure ref() or return ensure ref(args) — these execute the target and use its result as the return value, equivalent to const x = run ref(args) then return "${x}". Parentheses are required on all call sites.fail "reason" or fail """...""" aborts with stderr message and non-zero exit (workflows; fails the rule when used inside a rule).run async ref([args...]) starts a workflow or script concurrently and returns a Handle<T>. Capture is supported: const h = run async ref(). The handle resolves on first non-passthrough read (string interpolation, passing as arg to run, comparison, conditional, match subject). Passthrough (initial capture, re-assignment) does not force resolution. Unresolved handles are implicitly joined at workflow exit. recover (retry loop) and catch (single-shot) composition work with run async: run async foo() recover(err) { … }. Workflows only — rejected in rules.match var { "literal" => …, /regex/ => …, _ => … } pattern-matches on a string value. The subject is always a bare identifier (no $ or ${}). Arms are tested top-to-bottom; the first match wins. Patterns: double-quoted string literal (exact match), /regex/ (regex match), or _ (wildcard — exactly one required). Usable as a statement, as an expression (const x = match var { … }), or with return (return match var { … }). Using $var or ${var} as the match subject is a parse error. Allowed in both workflows and rules. See Grammar.if var == "value" { … } or if var =~ /pattern/ { … }. Subject is a bare identifier. Operators: == (exact string equality), != (inequality), =~ (regex match), !~ (regex non-match). Operand is a "string" for ==/!= or /regex/ for =~/!~. Body is a brace block of valid workflow/rule steps. No else branch — use match for exhaustive value branching. if is a statement (no value production; cannot use with const or return). Allowed in both workflows and rules.for iterVar in sourceVar { … } runs the body once per line of the string bound to sourceVar (newline-separated text, e.g. from const/prompt/run capture). Each iteration binds iterVar to one line (trimming rules match the runtime’s line split — a trailing empty line after a final newline is not an extra iteration). Allowed in workflows and rules. See Grammar for the formal production.prompt "..." — double-quoted, single line only; (2) identifier prompt myVar — uses the value of an existing binding; (3) triple-quoted block prompt """ ... """ — for multiline text, opening """ on the same line as prompt. Triple backticks ( ) in prompt context are rejected with guidance — they are reserved for scripts. Multiline double-quoted strings are rejected — use a triple-quoted block instead. All forms support ${identifier} interpolation (${varName}, ${paramName}). **Inline capture interpolation** is also supported: ${run ref([args])} and ${ensure ref([args])} inside the prompt string or triple-quoted body (e.g. prompt “Fix: ${ensure get_diagnostics()}”). Nested inline captures are rejected. Bare $varName is not valid in orchestration strings. $(…) and ${var:-fallback} are rejected. Capture: const name = prompt “…”, const x = prompt myVar, const y = prompt “”” … “””. Optional **typed prompt:** const name = prompt “…” returns “{ field: type, … }” or const name = prompt myVar returns “…” (flat schema; types string, number, boolean) validates the agent's JSON and sets ${name} plus per-field variables accessible via **dot notation** — ${name.field}. Dot notation is validated at compile time: the variable must be a typed prompt capture and the field must exist in the schema. **Orchestration bindings are strings:** typed fields are coerced with String() after JSON validation, so e.g. a numeric field is still the text “42”` in scope. See Grammar.Quick reference examples:
# catch — one-shot failure handling
ensure ci_passes() catch (failure) {
prompt "CI failed — fix the code."
run deploy(env)
}
# recover — repair-and-retry loop (retries until success or limit)
run deploy(env) recover(err) {
log "Deploy failed: ${err}"
run auto_repair(env)
}
# match — value branching (statement and expression forms)
const label = match status {
"ok" => "success"
/err/ => "something went wrong"
_ => "unknown"
}
# if — conditional guard (no else; use match for exhaustive branching)
if env == "" {
fail "env was not provided"
}
if mode =~ /^debug/ {
log "Debug mode enabled"
}
# for — iterate over lines of a string variable
const paths = """
docs/a.md
docs/b.md
"""
for path in paths {
log "${path}"
}
# typed prompt — structured JSON with dot-notation field access
const result = prompt "Analyze this code" returns "{ type: string, risk: string }"
log "Type: ${result.type}, Risk: ${result.risk}"
# const capture — from run, ensure, prompt
const tag = run get_version()
const ok = ensure validate(tag)
const answer = prompt "Summarize the changes"
# inline scripts — one-off commands without a named script definition
run `echo $1`("hello")
const ts = run `date +%s`()
Conventions:
jaiph run <file.jh> executes workflow default in that file. The file must define a workflow default (the runtime checks for it and exits with an error if missing).run ref(). Free-form bash can appear as inline shell lines when the grammar allows; prefer script + run for anything non-trivial. Never use fn args or $(fn …) as a substitute for run.ensure for rules and run for scripts only — not prompt, send, or run async.run async ref([args...]) for managed async with implicit join. For concurrent bash, use & and the shell builtin wait inside a script and call it with run. Do not call Jaiph internals from background subprocesses unless you understand how isolation and logging interact with the runtime.run to a script and handle failure with catch, or use if / match for value branching. Short-circuit brace groups remain valid inside script bodies: cmd || { ... }.run foo() > file, run foo() | cmd, run foo() & are all E_PARSE errors — shell operators (>, >>, |, &) are not supported adjacent to run or ensure steps. Move shell pipelines and redirections into a script block and call it with run.import script "./tool.py" as tool (or a sibling .jh module) instead of maintaining ad-hoc bash outside the compiler. Avoid informal workspace-level shared-bash directories that bypass the module graph.local/const share a single namespace per module (E_PARSE on collision).ensure must target a rule — using it on a workflow or script is E_VALIDATE. run in a workflow must target a workflow or script; run in a rule must target a script only. Type crossing: string and script are distinct primitive types — prompt rejects script names, run rejects string consts, assigning a script to a const or interpolating ${scriptName} are all E_VALIDATE. See Grammar — Types. Jaiph symbols must not appear inside $(...) in bash contexts the compiler still scans (principally script bodies). Script bodies cannot contain run, ensure, config, nested definitions, routes, or Jaiph fail / const / log / logerr / return "…".ensure; reuse workflows and scripts via run.jaiph format on .jh files you create or modify before committing. This ensures canonical whitespace, indentation, and top-level ordering. In CI, use jaiph format --check to gate on formatting.config block, export, prompt capture), see the grammar. For testing workflows, see Testing (expect_contain, expect_not_contain, expect_equal, mocks).Test files use the *.test.jh suffix and contain test "name" { ... } blocks. They import the workflows under test and use mocks to replace live agent/script behavior. The test runner uses the same NodeWorkflowRuntime as jaiph run. See Testing for the full reference.
Running: jaiph test (all *.test.jh in workspace), jaiph test <dir> (recursive), or jaiph test <file.test.jh> (single file).
Available mocks:
mock prompt "fixed response" — queues a fixed response for the next prompt call (multiple queue in order).mock prompt responseVar — uses the string already bound as responseVar (e.g. a const earlier in the block) as the next response.mock prompt { /pattern/ => "response", _ => "default" } — content-based dispatch.mock workflow alias.name() { return "stubbed" } — replaces a workflow body.mock rule alias.name() { return "ok" } — replaces a rule body.mock script alias.name() { … } — replaces a script body with shell lines between the braces (same line as { is not enough; put the shell on the following lines, then } on its own line).Assertions:
expect_contain var "expected substring"expect_not_contain var "unwanted text"expect_equal var "exact expected value"Minimal example:
import "main.jh" as app
test "happy path produces greeting" {
mock prompt "hello from mock"
const out = run app.default("task")
expect_contain out "hello from mock"
}
test "handles failure gracefully" {
mock prompt "error"
const out = run app.default("bad input") allow_failure
expect_contain out "error"
}
allow_failure on a run step (with or without const … =) prevents a non-zero workflow exit from failing the test — useful for testing error paths. For mock script, put shell lines on lines after the opening {, then close with } on its own line (see Testing).
.jaiph/bootstrap.jh — Created by jaiph init; contains a single triple-quoted prompt (prompt """ ... """) that points the agent at .jaiph/SKILL.md (a copy of this guide)..jaiph/readiness.jh — Preflight: rules and workflow default that runs readiness checks..jaiph/ba_review.jh (or any name you choose) — (Optional) Pre-implementation review: reads tasks from a queue file, sends one to an agent for review, and marks it dev-ready or exits with questions. This repository uses .jaiph/architect_review.jh with QUEUE.md..jaiph/verification.jh — Verification: rules and workflow default for lint/test/build..jaiph/main.jh — Imports readiness, optional review, and verification; defines implementation workflow and workflow default that orchestrates preflight → (optional) review → implementation → verification.Optional: .jaiph/implementation.jh if you prefer the implementation workflow in a separate module; otherwise keep it in main.jh.
After scaffolding workflows, print the exact commands the developer should run. The primary command runs the default entrypoint (typically preflight, then implementation, then verification — plus any optional review step you added). Point users to the canonical skill URL for agents: https://raw.githubusercontent.com/jaiphlang/jaiph/refs/heads/main/docs/jaiph-skill.md.
Include a compile check and, when the repository has native tests (*.test.jh), jaiph test (see Testing); skip jaiph test if there are no test files, since discovery mode exits with an error when nothing matches.
jaiph format .jaiph/*.jh
jaiph compile .jaiph
# Omit the next line when the repo has no *.test.jh files (workspace discovery exits 1 with "no *.test.jh files found").
jaiph test
jaiph run .jaiph/main.jh "implement feature X"
# Or run verification only:
jaiph run .jaiph/verification.jh
Arguments after the file path are passed to workflow default as named parameters (when declared) and as $1, $2 in script bodies.
Use this as a shape to adapt. Paths and prompts should match the target repository. All three files live under .jaiph/. Imports in main.jh are relative to that file (e.g. "readiness.jh" resolves to .jaiph/readiness.jh). When you run jaiph run .jaiph/main.jh "implement feature X", the default workflow receives "implement feature X" as ${task} (named parameter). Note that run does not forward args implicitly, so the default workflow passes task as a bare identifier to run implement(task) so the implement workflow’s prompt can use ${task}.
File: .jaiph/readiness.jh
script git_is_clean = `test -z "$(git status --porcelain)"`
rule git_clean() {
run git_is_clean() catch (err) {
fail "git working tree is not clean"
}
}
script require_git_node_npm = ```
command -v git
command -v node
command -v npm
rule required_tools() { run require_git_node_npm() }
workflow default() { ensure required_tools() ensure git_clean() }
**File: .jaiph/verification.jh**
```jaiph
script npm_test_ci = `npm test`
rule unit_tests_pass() {
run npm_test_ci()
}
script run_build = `npm run build`
rule build_passes() {
run run_build()
}
workflow default() {
ensure unit_tests_pass()
ensure build_passes()
}
File: .jaiph/main.jh
import "readiness.jh" as readiness
import "verification.jh" as verification
workflow implement(task) {
prompt """
Implement the requested feature or fix with minimal, reviewable changes.
Keep edits consistent with existing architecture and style.
Add or update tests for behavior changes.
User asks for: ${task}
"""
}
workflow default(task) {
run readiness.default()
run implement(task)
run verification.default()
}