Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 18 additions & 14 deletions .jaiph/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
FROM ubuntu:latest

# Standard utilities
# Standard utilities + fuse-overlayfs for CoW sandbox
RUN apt-get update && \
apt-get install -y --no-install-recommends \
bash \
curl \
git \
ca-certificates \
gnupg && \
gnupg \
fuse-overlayfs \
fuse3 \
rsync && \
rm -rf /var/lib/apt/lists/*

# Node.js latest LTS (required by jaiph::stream_json_to_text in prompt.sh)
Expand All @@ -19,32 +22,33 @@ RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - && \
# when running as root. Jaiph only passes --user on Linux hosts; on macOS the container
# defaults to root unless the image sets USER.
RUN useradd --create-home --uid 10001 --shell /bin/bash jaiph && \
mkdir -p /jaiph/workspace && \
mkdir -p /jaiph/workspace /jaiph/workspace-ro /jaiph/run && \
chown -R jaiph:jaiph /jaiph

# Claude Code CLI (Anthropic) — global install for all users
RUN npm install -g @anthropic-ai/claude-code

# cursor-agent (Cursor) — installed via Cursor's official distribution.
# The installer currently places the CLI in ~/.local/bin and names it "agent".
# Normalize to /usr/local/bin/cursor-agent so jaiph runtime can invoke it.
RUN curl -fsSL https://cursor.com/install -o /tmp/install-cursor-agent.sh && \
USER jaiph
ENV HOME=/home/jaiph
ENV PATH="/home/jaiph/.local/bin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"

# cursor-agent (Cursor) — install as the runtime user so the binary is
# reachable after switching away from root. The installer currently places
# the CLI in ~/.local/bin and may name it "agent" or "cursor".
RUN mkdir -p "$HOME/.local/bin" && \
curl -fsSL https://cursor.com/install -o /tmp/install-cursor-agent.sh && \
bash /tmp/install-cursor-agent.sh && \
export PATH="$HOME/.local/bin:$PATH" && \
if command -v cursor-agent >/dev/null 2>&1; then \
ln -sf "$(command -v cursor-agent)" /usr/local/bin/cursor-agent; \
true; \
elif command -v agent >/dev/null 2>&1; then \
ln -sf "$(command -v agent)" /usr/local/bin/cursor-agent; \
ln -sf "$(command -v agent)" "$HOME/.local/bin/cursor-agent"; \
elif command -v cursor >/dev/null 2>&1; then \
ln -sf "$(command -v cursor)" /usr/local/bin/cursor-agent; \
ln -sf "$(command -v cursor)" "$HOME/.local/bin/cursor-agent"; \
fi && \
command -v cursor-agent >/dev/null 2>&1 && \
rm -f /tmp/install-cursor-agent.sh

USER jaiph
ENV HOME=/home/jaiph
ENV PATH="/home/jaiph/.local/bin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"

# jaiph (official installer: https://jaiph.org/install)
RUN curl -fsSL https://jaiph.org/install | bash
RUN jaiph use nightly
Expand Down
17 changes: 7 additions & 10 deletions .jaiph/architect_review.jh
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,16 @@
import "jaiphlang/queue" as queue

config {
agent.backend = "claude"
agent.claude_flags = "--permission-mode bypassPermissions"
agent.backend = "cursor"
agent.default_model = "composer-2"
agent.cursor_flags = "--force"
# agent.backend = "claude"
# agent.claude_flags = "--permission-mode bypassPermissions"
}

script first_line_str = ```
source "$JAIPH_LIB/strings.sh"
first_line "$1"
```
script first_line_str = `printf '%s\n' "$1" | head -n 1`

script rest_lines_str = ```
source "$JAIPH_LIB/strings.sh"
rest_lines "$1"
```
script rest_lines_str = `printf '%s\n' "$1" | tail -n +2`

script arg_nonempty = `[ -n "$1" ]`

Expand Down
28 changes: 16 additions & 12 deletions .jaiph/docs_parity.jh
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,14 @@ rule docs_files_present(list) {
}

script assert_worktree_clean_for_docs = ```
source "$JAIPH_LIB/git_paths.sh"
local current_changed_files
current_changed_files="$(git_changed_paths_sorted)"
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
Expand All @@ -48,9 +53,14 @@ rule worktree_is_clean() {

script assert_only_allowed_changed = ```
local allowed="$1"
source "$JAIPH_LIB/git_paths.sh"
local after_changed_files
after_changed_files="$(git_changed_paths_sorted)"
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
Expand All @@ -67,15 +77,9 @@ rule only_expected_docs_changed_after_prompt(allowed) {

script arg_nonempty = `[ -n "${1:-}" ]`

script first_line_str = ```
source "$JAIPH_LIB/strings.sh"
first_line "$1"
```
script first_line_str = `printf '%s\n' "$1" | head -n 1`

script rest_lines_str = ```
source "$JAIPH_LIB/strings.sh"
rest_lines "$1"
```
script rest_lines_str = `printf '%s\n' "$1" | tail -n +2`

script list_docs_md_paths = ```
local out=""
Expand Down
35 changes: 11 additions & 24 deletions .jaiph/language_redesign_spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ These are not options. Implementation starts from this table.
| Custom shebang | Yes | `#!/usr/bin/env node` (first line of body; omit for default `#!/usr/bin/env bash`) |
| All body content | Yes | Full language content matching the shebang (bash by default) |
| Nested bash functions | Yes (bash) | `helper() { ... }` (internal to the script body) |
| `source` shared lib | Yes (bash) | `source "$JAIPH_LIB/utils.sh"` |
| Shared bash via workspace lib dir | **No** | Use `import script`, a sibling module, or inline bash in a `script` block — `JAIPH_LIB` is not provided |
| `return N` / `exit N` | Yes (bash) | Exit code (integer only) |
| stdout (`echo`, `printf`) | Yes | Value output mechanism |
| `local` | Yes (bash) | Bash variable declarations |
Expand Down Expand Up @@ -159,16 +159,7 @@ script check_is_number() {

### Shared utility code (bash scripts only)

Scripts that need common logic `source` a shared bash library rather than calling other Jaiph scripts. Libraries live in a conventional location (e.g. `.jaiph/lib/`) and are plain bash files.

```
script check_is_number() {
source "$JAIPH_LIB/validators.sh"
is_integer "$1"
}
```

The runtime sets `$JAIPH_LIB` to the project's shared library path. Libraries are not Jaiph constructs — they are plain bash, managed outside the Jaiph compiler.
Scripts cannot call other Jaiph scripts. Factor repeated bash into **`import script "./helper.sh" as helper`** (path relative to the `.jh` file), another `.jh` module, or a small extra `script` in the same module. Do not use a workspace-wide bash drop directory outside the compiler model.

Non-bash scripts use their language's own module system for shared code.

Expand Down Expand Up @@ -432,7 +423,7 @@ Every `.jh` file was scanned. Below are all patterns found that require migratio

**Problem**: the loop body contains orchestration keywords (`run`, `ensure`, `prompt`, `log`). Cannot be pushed to a script.

**Resolution**: use **workflow recursion**. Extract per-item logic into a workflow, then recurse over the list. Shared utility scripts `first_line` and `rest_lines` (in `$JAIPH_LIB`) split newline-delimited lists.
**Resolution**: use **workflow recursion**. Extract per-item logic into a workflow, then recurse over the list. Split newline-delimited lists with tiny `script` steps (e.g. `printf '%s\n' "$1" | head -n 1` / `tail -n +2`) or `import script`.

```
script list_docs_files() {
Expand Down Expand Up @@ -531,7 +522,7 @@ if run matches "$verdict" "dev-ready" {
}
```

These are small, reusable utility scripts. Candidates for a shared library (`$JAIPH_LIB/checks.sh`).
These are small, reusable utility scripts in the same module (or behind `import script`).

### P7: `return "$(command)"` in scripts (Jaiph value return)

Expand Down Expand Up @@ -606,15 +597,13 @@ workflow ensure_ci_passes() {

**File**: docs_parity.jh — rule `only_expected_docs_changed_after_prompt` calls script `is_allowed_file` directly.

**Migration**: under full isolation + no script-to-script calls, inline the logic or use a shared lib:
**Migration**: under full isolation + no script-to-script calls, inline the logic or add a dedicated `import script` helper:

```
script check_only_expected_changed(allowed, changed) {
source "$JAIPH_LIB/file_checks.sh"

while IFS= read -r f; do
[ -z "$f" ] && continue
if ! is_in_list "$allowed" "$f"; then
if [[ $'\n'"$allowed"$'\n' != *$'\n'"$f"$'\n'* ]]; then
echo "Unexpected file changed: $f" >&2
return 1
fi
Expand Down Expand Up @@ -709,12 +698,11 @@ script check_only_expected_changed(allowed, changed) {
**4a. Implement full isolation for script execution**
- Scripts run as separate processes (inherent from separate files + exec)
- Only positional args available (inherent from separate executable)
- Set `$JAIPH_LIB` env var for shared library access
- Set `$JAIPH_SCRIPTS` env var for build output scripts path
- Set `$JAIPH_SCRIPTS` and `$JAIPH_WORKSPACE` for script steps (no workspace bash lib dir)

**4b. Reject script-to-script calls**
- Parser/validator: detect when a script body references another Jaiph script name
- Error: `"scripts cannot call other Jaiph scripts; use a shared library or compose in a workflow"`
- Error: `"scripts cannot call other Jaiph scripts; use import script, inline bash, or compose in a workflow"`

### Phase 5: Remove shell (breaking changes)

Expand Down Expand Up @@ -742,7 +730,7 @@ script check_only_expected_changed(allowed, changed) {

- Rewrite all `e2e/*.jh` fixtures
- Rewrite all `.jaiph/*.jh` workflows
- Create shared libraries in `.jaiph/lib/` for common patterns (P6, P11)
- Factor repeated bash into `import script` or extra `script` blocks in the same module (P6, P11)
- Update test fixtures and golden transpilation outputs
- Update docs and README examples

Expand Down Expand Up @@ -770,7 +758,7 @@ script check_only_expected_changed(allowed, changed) {
| `src/transpile/validate.ts` | Collapse duplicate ref resolution. Rename `function` → `script` in errors/lookups. Allow `run` in rules (scripts only). Remove shell-condition validation. Add script isolation validation. |
| `src/transpile/shell-jaiph-guard.ts` | Scope down — only applies to bash scripts now. |
| `e2e/*.jh` | Rewrite all fixtures to new syntax. |
| `.jaiph/*.jh` | Rewrite all workflows to new syntax. Create `.jaiph/lib/` shared libraries. |
| `.jaiph/*.jh` | Rewrite all workflows to new syntax. |
| `test/fixtures/**` | Update golden transpilation outputs. |
| `docs/*` | Update grammar, getting-started, CLI docs for `script` keyword and shebang. |

Expand All @@ -785,7 +773,7 @@ script check_only_expected_changed(allowed, changed) {
| `const` scoping conflicts with bash `local` | Low | `const` is parser-level immutability; transpiles to `local` |
| Return semantics confusion during migration | Medium | Parser errors guide users: `"return 'value' not allowed in script; use echo"` |
| Script isolation perf overhead (fork+exec per call) | Medium | Measure fork cost; scripts are already logically isolated. Optimize hot paths if needed |
| Shared lib mechanism needs runtime support (`$JAIPH_LIB`) | Low | Simple: runtime sets one env var before script execution |
| Users want a global bash grab-bag | Medium | `import script` + small modules; no `JAIPH_LIB` |
| `.jaiph/` workflow migration is large (9 files) | High | Migrate in parallel with parser changes; each file is independently testable |
| Separate file management complexity | Medium | Deterministic naming (`scripts/<name>`), cleanup on rebuild |
| Custom shebang scripts may have missing dependencies | Low | Not Jaiph's problem — user owns their runtime. Document clearly |
Expand All @@ -803,7 +791,6 @@ script check_only_expected_changed(allowed, changed) {
- Scripts execute in full isolation (no inherited variables)
- `const` declarations work in workflows and rules with all RHS forms
- `if` brace syntax works with `not` and `else if`
- Shared libraries loadable from bash scripts via `$JAIPH_LIB`
- Parser errors for raw shell include actionable rewrite examples
- `jaiph::set_return_value` removed from script transpilation paths
- `validate.ts` under 500 lines after dedup
Expand Down
14 changes: 0 additions & 14 deletions .jaiph/lib/checks.sh

This file was deleted.

10 changes: 0 additions & 10 deletions .jaiph/lib/fs.sh

This file was deleted.

10 changes: 0 additions & 10 deletions .jaiph/lib/git_paths.sh

This file was deleted.

10 changes: 0 additions & 10 deletions .jaiph/lib/strings.sh

This file was deleted.

9 changes: 7 additions & 2 deletions .jaiph/simplifier.jh
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,20 @@ const simplifier_principles = """
"""

script assert_no_test_or_e2e_in_changed = ```
source "$JAIPH_LIB/git_paths.sh"
local bad=0
while IFS= read -r f; do
[ -z "$f" ] && continue
if [[ "$f" == test/* ]] || [[ "$f" == e2e/* ]]; then
echo "Simplifier workflow must not modify tests or e2e: $f" >&2
bad=1
fi
done < <(git_changed_paths_sorted)
done < <(
{
git diff --name-only --cached
git diff --name-only
git ls-files --others --exclude-standard
} | sort -u
)
return $bad
```

Expand Down
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
# Unreleased

# 0.9.2

## Summary

- **Runtime:** Remove `JAIPH_LIB` in favor of workspace-relative paths and `import script`; strip inherited `JAIPH_LIB` so parent shells cannot inject stale library paths.
- **Runtime:** Write a `heartbeat` file in the run directory (refreshed every 10s) so external tooling can tell whether a run is still alive.
- **Docker:** Base images without `jaiph` now work generically: Jaiph builds a thin derived image, installs the current package into it, and writes artifacts directly to the configured host runs root.
- **Docs / E2E:** Align documentation and tests with the `JAIPH_LIB` removal.

## All changes

- **Breaking — Runtime:** Remove `JAIPH_LIB` — The Node runtime no longer sets `JAIPH_LIB`, and isolated script subprocesses no longer receive it (`run-step-exec.ts`). `resolveRuntimeEnv` still deletes inherited `JAIPH_LIB` so a parent shell cannot inject a stale path. Workflows that used `source "$JAIPH_LIB/…"` must use `JAIPH_WORKSPACE`-relative paths, `import script`, or inline bash. Project-scoped **`.jaiph/libs/`** (`jaiph install`) is unchanged.
- **Docs / E2E:** Documentation and tests no longer describe or assert `JAIPH_LIB` / `.jaiph/lib` (singular).
- **Feature — Runtime:** Heartbeat file in run directory — The runtime now writes a `heartbeat` file (containing epoch-ms timestamp) to the run directory (`.jaiph/runs/<date>/<time>-<source>/heartbeat`) immediately on construction and refreshes it every 10 seconds. External tooling can `stat()` or read this file to detect whether a Jaiph process is still alive; a stale heartbeat (>~20s) means the process is dead. The timer is `.unref()`ed so it never keeps the Node process alive past its natural exit. Implementation: `startHeartbeat()` / `stopHeartbeat()` in `NodeWorkflowRuntime`. Unit test added.
- **Fix — Docker:** Generic runtime image bootstrap and host run-dir mapping — Docker no longer assumes the selected image already contains `jaiph`, but it also no longer relies on a host-mounted `dist/` tree. When the selected base image lacks `jaiph`, Jaiph now builds a thin derived image from that base and installs the current local package with `npm install -g`, then runs `jaiph run --raw` there. Docker-backed runs now mount the resolved host runs root directly at `/jaiph/run`, so the default `.jaiph/runs`, relative `JAIPH_RUNS_DIR`, and absolute in-workspace `JAIPH_RUNS_DIR` all persist artifacts in the expected host location; absolute paths outside the workspace fail with `E_DOCKER_RUNS_DIR`. Implementation: `resolveImage()`, `resolveDockerHostRunsRoot()`, and `findRunArtifacts()` in `src/runtime/docker.ts`; `spawnExec()` in `src/cli/commands/run.ts`. Unit and E2E coverage updated.

# 0.9.1

## Summary
Expand Down
Loading
Loading