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
11 changes: 11 additions & 0 deletions .claude/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "web",
"runtimeExecutable": "npm",
"runtimeArgs": ["--prefix", "web", "run", "dev"],
"port": 5173
}
]
}
36 changes: 36 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: tests

# Runs the unit suite (`npm test`) on every pull request. Install mirrors the
# Dockerfile: yarn.lock is the lockfile, and --ignore-scripts skips the root
# `prepare` (tsc + web install + web build) — tsx runs the .ts tests directly,
# and the two web tests only import root sources. The graph and STT suites need
# Neo4j / a model download and stay opt-in (see AGENTS.md).

on:
pull_request:
workflow_dispatch:

permissions:
contents: read

concurrency:
group: tests-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: "22"
cache: yarn

- name: Install
run: yarn install --frozen-lockfile --ignore-scripts

- name: Test
run: npm test
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,6 @@ package-lock.json
.idea
.cursorrules
.cursorignore

# Claude Code worktrees (EnterWorktree sessions) live here — never commit them
.claude/worktrees/
55 changes: 49 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,14 @@ strut/
│ ├── runner.ts # execution engine: DAG (topological), retry, onError, control flow, journal replay
│ ├── run-control.ts # RunController: cooperative cancel/pause/resume for run TREES (RUN_CONTROL_SPEC.md)
│ ├── journal.ts # resume journal: step.end outputs → {path→output}; `from` invalidation
│ ├── store.ts # RunStore interface (writes + reads + tail) + FileRunStore + MemoryRunStore + tailJsonl / tailFromPolling
│ ├── store.ts # RunStore interface (writes + reads + tail) + FileRunStore + MemoryRunStore + tailJsonl / tailFromPolling. Keys are workflow names, plus two non-workflow buckets no workflow listing can see: `step:<type>` → steps/<type>/runs/ (kept run_step runs), `check:<id>` → checks/<id>/runs/ (paid check runs)
│ ├── claims-authoring.ts # the policy layer behind BOTH claim doors (chat tools + meta/* twins): check-spec validation + write-time defaults (presumed-paid → on_change), the additive `claims` publish arg, publisher scoping (fixed point 1), the grader deny-list over the check closure (fixed point 2; STRUT_VERIFY_DENY)
│ ├── claims-schemas.ts # zod shapes + model-facing docs for subjects / check specs / the `claims` arg, shared by ai/tools.ts and the meta/* claim steps
│ ├── verify.ts # the verify pass (plans/claims.md §4): subjectsOfRun (a run's event log → observed subjects + the version each executed), mapCheckResult (the check contract; a check that cannot run writes NOTHING), policyFires (always / on_change / sample / manual), budget (presumed-paid skipped at a cap; reported cost persisted under `check:<id>` and counted), planned slots for external checks, addEvidence, verifyPublish. Triggered from `services.onRunEnd` for every top-level run and after a kept run_step; check runs (`origin: "verify"`) are never verified
│ ├── ledger.ts # the ledger (plans/claims.md §5): buildLedger (claims per subject with computed status + each check's lastVerify: pending | ran | skipped | planned), subjectsOfFlow (what a launch can execute), the [verify-notification] text. The forcing function — the model reads its contract in a tool RESULT, not an instruction
│ ├── claims-routes.ts # the Claims panel's HTTP door: GET /claims?kind=&name= (contract + computed status + latest evidence + open slots; `{ enabled: false }` on a filesystem workspace), POST/PATCH/DELETE /claims[/:id], /claims/:id/{attach,detach,checks,evidence}, PATCH/DELETE /checks/:id. Mutations behind requireApiKey; the actor is a PERSON (unscoped, stamped `person`; evidence `asserted`, `by: person`)
│ ├── closure.ts # what a flow can EXECUTE: walkSteps (loop/foreach bodies, onError), flowClosure (nested subflows via the workspace, agentTools grants; templated/missing child → unresolvable), stepHashesFor → run.start.stepHashes
│ ├── run-step.ts # runSingleStep (one step, in memory, optional cassette) + runStep — the run_step surfaces: records stepHashes, then persists the run under `step:<type>` only when the step has claims or `keep: true` (plans/claims.md §3)
│ ├── chat-store.ts # ChatStore interface + FileChatStore + MemoryChatStore (chats/<id>/: meta.json + messages.jsonl + events.jsonl) + truncateToolMessages
│ ├── workspace.ts # WorkspaceStore interface + FileWorkspaceStore (alias WorkspaceManager): versioning, _metadata.json, YAML loading
│ ├── storage-conformance.test.ts # the storage boundary's spec: one suite per layer, run over every impl
Expand Down Expand Up @@ -78,11 +85,14 @@ strut/
│ │ └── routes.ts # /audio/models (+ SSE download), /audio/transcribe (WAV body), /audio/hotwords/:name, /audio/sessions/:id (+ corrections)
│ ├── graph/ # jarvis-compatible Neo4j graph backend over bolt, no jarvis in the loop (plans/jarvis-graph-compat.md). Opt-in via openGraphBackend
│ │ ├── bolt.ts # neo4j-driver wrapper; int() for Integer writes (plain JS numbers write as FLOAT)
│ │ ├── strut-schemas.ts# the 9 Strut node types + 14-row edge registry (label registry in plans/generic-storage.md); author-time checks
│ │ ├── strut-schemas.ts# the 9 Strut node types + 15-row edge registry (label registry in plans/generic-storage.md); author-time checks
│ │ ├── schema-seed.ts # idempotent domain registration: Thing root, Schema nodes, CHILD_OF, constraints, vector/fulltext indexes, migration stamp
│ │ ├── node-writer.ts # §6 validation gate + node_key composition + Data_Bank + MERGE (create/upsert/restore/update), UNWIND batches
│ │ ├── edge-writer.ts # edge MERGE by ref_id with IS_ALIAS rewrite (ON CREATE only); closed (source, edge, target) registry; update() = jarvis PATCH /v2/edges/:ref_id (stamps protected)
│ │ ├── schema-crud.ts # createNodeSchema(): register a non-Strut node type like jarvis POST /v2/schema (parent, attribute grammar, node_key, CHILD_OF, constraint) or add-only extend an existing one
│ │ ├── claims.ts # the truth layer (plans/claims.md): Claim/Check/Evidence contract (ids, check subject + result shapes), claimStatus() — status computed on read per (claim, subject) — and ClaimsReader (claimsFor/checksFor/evidenceFor/statusFor; muted edges invisible). `strut.claims` is null unless the workspace is graph-backed
│ │ ├── claims-writer.ts # ClaimsWriter: the claim graph's invariants — ≥1 check per claim, edits SUPERSEDE (successor claim carries ABOUT + checks; successor check takes over TESTS), retire = timestamp, detach = muted edge, last check / last subject refused
│ │ ├── claim-schema-upgrade.ts # one-shot standalone mirror of jarvis migration 124 (Claim re-keyed on id, Epistemic/Thing); runs before the ontology seed, only with STRUT_GRAPH_SEED_ONTOLOGY
│ │ ├── embeddings.ts # local all-MiniLM-L6-v2 via transformers.js, tokenized like sentence-transformers (256 incl. specials); NULL-scan backfill
│ │ ├── search.ts # the read surface: hybrid search (RRF + title boost + usage tiebreak), get/neighbors/counts, ontology, namespaces
│ │ ├── backend.ts # openGraphBackend(): cached per config; runs seed + backfill on first open
Expand Down Expand Up @@ -183,7 +193,10 @@ docker compose run --rm --no-deps --service-ports -e STRUT_WORKSPACE_BACKEND=fs
| `NEO4J_URI` / `NEO4J_HOST` | (unset) / `localhost:7687` | Graph backend connection — same names and defaults as mcp's own Neo4j client: `NEO4J_URI` wins, else `bolt://<NEO4J_HOST>`; `NEO4J_USER`/`NEO4J_PASSWORD` default `neo4j`/`testtest`; optional `NEO4J_DATABASE`. The `graph/*` lib steps read these via the secrets capability (secret store → env) and need nothing configured for a local Neo4j; `openGraphBackendFromEnv` stays opt-in (null when neither is set). |
| `STRUT_GRAPH_NAMESPACE` | `default` | jarvis namespace every Strut node is written into |
| `STRUT_GRAPH_EMBEDDINGS` | (on) | `off` disables the local MiniLM embedder (vectors stay NULL; search is fulltext-only) |
| `STRUT_GRAPH_SEED_ONTOLOGY` | (off) | `1` seeds the bundled jarvis ontology (151 schemas + edge schemas + indexes, add-only) on first open, so a standalone Neo4j can host jarvis-typed data (Document, EvalSet, Concept, …) with no jarvis process. No-op on a jarvis-seeded DB. |
| `STRUT_GRAPH_SEED_ONTOLOGY` | (off) | `1` seeds the bundled jarvis ontology (153 schemas + edge schemas + indexes, add-only) on first open, so a standalone Neo4j can host jarvis-typed data (Document, EvalSet, Concept, …) with no jarvis process. No-op on a jarvis-seeded DB. Also turns on the one-shot `Claim` schema upgrade (the standalone mirror of jarvis migration 124) — never run against a jarvis-hosted graph. |
| `STRUT_VERIFY_BUDGET_USD` | `1` | Verify-pass spend cap PER VERIFIED RUN: once the pass's checks have reported this much, remaining checks presumed paid (an `agent`/`llm` step anywhere in the check closure, or an unresolvable one) are skipped (`lastVerify: { skipped: "budget" }`) and the claim stays `unknown` — never `supported`. Checks that report no cost never count. |
| `STRUT_VERIFY_BUDGET_USD_PER_DAY` | `5` | The same cap PER SUBJECT PER (UTC) DAY, computed from the run store alone: the cost of today's runs under `check:<id>` tagged with that subject. A harness that verifies many candidates raises it, or sets its paid checks to `manual`. |
| `STRUT_VERIFY_DENY` | (none) | Comma-separated step-type globs added to the grader deny-list (`gaia/*`, `harvey/*`, `eval/*`, `meta/*`): an `ai`-stamped check may not reach any of them — by name, through a subflow, or via an `agentTools` grant (plans/claims.md §4.1, fixed point 2). |
| `STRUT_MODEL_DIR` | `~/.cache/strut-models` | Local model files: MiniLM's ONNX cache and STT models under `stt/<id>/`. `STRUT_MODEL_CACHE` is the older alias. |
| `STRUT_STT_MODEL` | `zipformer-en-kroko` | Finals recognizer for `/audio/stream` + `/audio/transcribe` (hotword-capable) |
| `STRUT_STT_PARTIAL_MODEL` | `nemo-fast-conformer-en-80ms` | Fast greedy recognizer whose output is shown as live partials; `off` for single-recognizer streams |
Expand Down Expand Up @@ -330,7 +343,7 @@ and the child env is scrubbed by construction).
SIGTERMs the process group (SIGKILL 2s later) when the run starts
cancelling, then `checkpoint()` raises the canonical `CancelledError`.
Timeout (default 10 min) is SIGKILL.
- **Output:** each stream is capped (default 200k chars) keeping head + tail,
- **Output:** each stream is capped (default 500k chars) keeping head + tail,
so a JSON result and the error that ended a build both survive; the child
is NOT killed for being chatty. Big results belong in artifact files.
- **Environment ≠ step:** what's on PATH (python, ffmpeg, yt-dlp) is the
Expand Down Expand Up @@ -712,6 +725,31 @@ and the child env is scrubbed by construction).
`streamChat` + `getChat`) persists the active `chatId` in
localStorage and reattaches to a still-live turn on reopen.

- **Claims, checks, evidence — the truth layer** (`plans/claims.md`; graph
workspaces only, on by default there — on `STRUT_WORKSPACE_BACKEND=fs`,
or with `STRUT_CLAIMS=0` / `createStrut({ claims: false })`, no claim
tool is offered, `strut.claims` / `strut.verifier` are null, and nothing
below runs. `createStrut` decides this once and threads the
`ClaimsAuthoring | null` to every consumer; nothing else reads
`workspace.graph`). A `Claim` states how a step or workflow should BEHAVE, a `Check`
is an instrument that tests it (a registry step run over the subject, or
an external check answered through a planned slot), `Evidence` is what
one check observed on one run — all three are jarvis types, written
through the ordinary node/edge writers. Status (`supported | refuted |
stale | unknown`) is COMPUTED ON READ per (claim, subject) by
`claimStatus()` and never stored. Authoring: the `claims` arg on the
publish tools + `add_claim` / `edit_claim` / … and their `meta/*` twins
(`src/claims-authoring.ts`). Evidence: every top-level run is verified,
detached, by `src/verify.ts`, hooked where `services.onRunEnd` fires;
check runs carry `origin: "verify"` and are never verified. The builder
reads its contract in tool RESULTS (`src/ledger.ts`): run results list
the claims `pending`, and a `[verify-notification]` (or the run's
`[run-notification]`, when the pass settles within 5 s —
`src/ai/verify-waker.ts`) starts the next turn with each claim's status.
Versions are recorded, never inferred: `run.start.stepHashes` /
`workflowHash`, and a subflow step's `step.start.subflow` — no record, no
evidence.

- **Dispatch-mode `run_workflow` + run notifications**
(`src/ai/notifier.ts`, `plans/dispatch-run-notifications.md`). The chat
agent's `run_workflow` tool races the run against a wait window
Expand All @@ -735,8 +773,13 @@ and the child env is scrubbed by construction).
launches until a human replies. The seam is `AiDeps.detach` (absent →
the tool awaits to completion, unchanged for tests/embedders). The
flyout polls `GET /chat/:id` (~4s, idle+open only) to notice
server-initiated turns and renders `[run-notification]` messages as a
dashed notice, not a user bubble.
server-initiated turns and renders `[run-notification]` /
`[verify-notification]` messages as a collapsed notice card, not a user
bubble (`web/src/notice.ts` parses the model-facing text —
`notice.test.ts` runs it against the server's own formatters, so a
format change there fails a test; `NoticeView` opens it level by level:
card → claim → evidence + checks → raw message. Unparseable → the old
dashed text notice).

- **`agent` core step** (`src/steps/core/agent.ts`). A general
tool-using agent loop (AI SDK `ToolLoopAgent`) — distinct from the
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ Create a `.env` in the repo root:

```ini
STRUT_WORKSPACE_BACKEND=fs # keep workflows on disk, no Neo4j needed
# STRUT_CLAIMS=0 # graph workspaces: turn the claims/checks/evidence layer off
ANTHROPIC_API_KEY=sk-ant-... # for llm/agent steps and the AI builder — or OPENAI_API_KEY,
# GOOGLE_API_KEY, OPENROUTER_API_KEY, XAI_API_KEY; pick the model in
# the AI chat. Keys can also be pasted under Secrets in the UI.
Expand Down
4 changes: 4 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ services:
- "${STRUT_NEO4J_BOLT_PORT:-7689}:7687"
environment:
NEO4J_AUTH: neo4j/testtest # strut's own default credentials (bolt.ts)
# Neo4j Browser prefills its connect form from the advertised address,
# which defaults to the container port (7687) — on the host that's some
# other Neo4j, or nothing. Advertise the mapped host port instead.
NEO4J_server_bolt_advertised__address: "localhost:${STRUT_NEO4J_BOLT_PORT:-7689}"
volumes:
- strut-neo4j:/data
healthcheck:
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"package:desktop": "node scripts/package-desktop.mjs",
"dev": "npm run build:web && tsx --env-file=.env src/server.ts",
"start": "node build/server.js",
"test": "tsx --test src/expr.test.ts src/core.test.ts src/runner.test.ts src/run-control.test.ts src/control-flow.test.ts src/store.test.ts src/workspace.test.ts src/storage-conformance.test.ts src/integration.test.ts src/services.test.ts src/cassette.test.ts src/run-step.test.ts src/createStrut.test.ts src/authoring.test.ts src/ai-integration.test.ts src/chat-store.test.ts src/chat-endpoints.test.ts src/llm.test.ts src/pricing.test.ts src/steps/registry.test.ts src/steps/core/agent.test.ts src/steps/core/pack.test.ts src/steps/core/exec.test.ts src/steps/core/llm.test.ts src/auth.test.ts src/secret-store.test.ts src/artifacts.test.ts src/slack.test.ts src/gdrive.test.ts src/html-extract.test.ts src/shell.test.ts src/validate.test.ts src/model-dir.test.ts src/audio/hotwords.test.ts src/audio/stt.test.ts src/audio/ws.test.ts web/src/run-inputs.test.ts",
"test": "tsx --test src/expr.test.ts src/core.test.ts src/runner.test.ts src/run-control.test.ts src/control-flow.test.ts src/store.test.ts src/workspace.test.ts src/storage-conformance.test.ts src/integration.test.ts src/services.test.ts src/cassette.test.ts src/run-step.test.ts src/closure.test.ts src/verify.test.ts src/ledger.test.ts src/ai/verify-waker.test.ts src/createStrut.test.ts src/authoring.test.ts src/ai-integration.test.ts src/chat-store.test.ts src/chat-endpoints.test.ts src/llm.test.ts src/pricing.test.ts src/steps/registry.test.ts src/steps/core/agent.test.ts src/steps/core/pack.test.ts src/steps/core/exec.test.ts src/steps/core/llm.test.ts src/auth.test.ts src/secret-store.test.ts src/artifacts.test.ts src/slack.test.ts src/gdrive.test.ts src/html-extract.test.ts src/shell.test.ts src/validate.test.ts src/model-dir.test.ts src/audio/hotwords.test.ts src/audio/stt.test.ts src/audio/ws.test.ts web/src/run-inputs.test.ts web/src/notice.test.ts",
"test:stt": "STRUT_TEST_STT=1 tsx --test src/audio/stt.live.test.ts",
"test:graph": "tsx --test --test-concurrency=1 \"src/graph/*.test.ts\" \"src/steps/lib/graph/*.test.ts\""
},
Expand Down
Loading
Loading