Repository navigation
feat(journey): run sets of journeys from ax journey - #95
assafelovic wants to merge 23 commits into
Conversation
Co-authored-by: Cursor <cursoragent@cursor.com>
When ora or the platform answers an error with a JSON body, --json now prints that body unchanged on stdout and sends the message and the Next line to stderr. The { error: { code, message, next } } envelope is kept for failures with no server body: local usage errors, network failures, timeouts and responses that are not JSON. Exit codes and MCP_AUTH_REQUIRED are unchanged.
Co-authored-by: Cursor <cursoragent@cursor.com>
After a registry failure or an unknown skill name, the Next call ax skill suggests now carries --json when the command was run with it, like every other command's Next call. Co-authored-by: Cursor <cursoragent@cursor.com>
Regenerate the contract types against ora 1.42.0. ax deep-journey now prints the blocked and answered-from-memory flags under the verdict (on stderr with --json, which stays raw passthrough), and every Next line renders the next_action ora served when a payload carries one: a run left unfinished, --no-stream included, names its stream; a refusal names the request ora asks for. Fixtures are real local 1.42.0 bodies. Co-authored-by: Cursor <cursoragent@cursor.com>
A served next action that points at a docs page (the partner key page on a 401) was rendered as curl of an HTML page. The Next line is now the keyed ax retry, with ora's reason and the page link as the why. Co-authored-by: Cursor <cursoragent@cursor.com>
…nd the Studio client The pure core of `ax trial`, with no command wired yet: - levels and the trial spec: caps, the site rule, prompt templates, skip notes - outcome rules: final statuses and the per-run bucket from Studio's verdict - flags: ora's blocked rule vendored verbatim with its spec and fixtures, plus never visited - counting: cells, rates from 5 gradable runs, gap lines per task - the local record under ~/.ax/trials/, written atomically - a Studio client over the existing key exchange: token refresh, scopes, the create body contract, error mapping, a gateway 502 told apart from Studio's own Co-authored-by: Cursor <cursoragent@cursor.com>
After a clean deep-journey run, the Next line offers `ax trial run <site> --task <goal>` when a Front key is set, and a one-line hint about trials when it is not. A goal that already names the site says the need level will be skipped. Never shown after a failed run or when ora served its own next action. Co-authored-by: Cursor <cursoragent@cursor.com>
…quest Every Studio request now carries a 60 second timeout. A request that gets no answer, or a key exchange that cannot connect, raises StudioConnectionError; an answer without its spec shape raises StudioShapeError. isStudioFailure is the one rule for which errors are the platform's, so callers retry or report those and let a bug in ax surface. The error types move to studio-errors.ts and studio.ts re-exports them. Co-authored-by: Cursor <cursoragent@cursor.com>
- preview: resolve agents against the catalog, check scopes, the 200 run cap and the month's allowance, and decide go or refuse before anything launches - launch: one journey per task, level and repeat, repeat first, each recorded as it returns; stops on quota, a platform failure, an API error or Ctrl-C - follow: poll until every run is final and its grade has settled, within the wait limit - collect: one counted run per launched run, with flags from each graded run's trajectory Co-authored-by: Cursor <cursoragent@cursor.com>
…able runs A quota stop or excluded runs leave adjacent levels with different gradable runs, and comparing raw satisfied counts then printed a gap between two levels at the same rate (9 of 9 against 3 of 3 read as worse with help). The rule now scales the rate gap to the smaller level and compares it in whole numbers; with equal gradable runs it is the same rule as before. Co-authored-by: Cursor <cursoragent@cursor.com>
The preview, refusals, confirm prompt and key copy a trial prints before it launches; the lines a launch stops with; the report matrix with gap lines, skip notes and the incomplete line; and the JSON document. Every string the server supplied passes through plain() first. Co-authored-by: Cursor <cursoragent@cursor.com>
`list` prints the trials recorded on this machine without asking the server. `cancel` sends every run id a record holds in one call and prints how many were canceled, already finished or skipped; under --json it prints the platform's answer as served. A damaged record or an unknown id exits 2. Co-authored-by: Cursor <cursoragent@cursor.com>
`run` previews a trial, asks before launching (or takes --yes), writes the record, launches, follows the journeys and prints the report; `view` picks a recorded trial up and prints the same report. Ctrl-C stops a phase and prints the commands to pick the trial up. The record lookup and the --json error codes are shared with `list` and `cancel`, and runs whose steps could not be read are named on stderr, since the report cannot flag them. Co-authored-by: Cursor <cursoragent@cursor.com>
citty keeps only the last value of a repeated flag, so every --task is read from the raw arguments. The help screens say which system each command calls: journey and trial call your Front account at app.ora.ai, and the key they need is named a Front API key on every line of the root help. Co-authored-by: Cursor <cursoragent@cursor.com>
A trial section covers the levels, the preview and its refusals, the report and its flags, the JSON document, the local record and the exit codes. The key that journey, trial and audit --tunnel ora share is now named a Front API key made in your Front account at app.ora.ai, and is no longer described as partner-only; partner and scan keys are still issued by ora. Co-authored-by: Cursor <cursoragent@cursor.com>
ax journey now covers what the unreleased ax trial did. One goal and one agent, with no --repeat and no levels, still runs one live journey with the same flags and exit codes (0, 1, 2). Several goals or agents, --repeat, or levels (--levels, --docs, --exact, --brand) run a set of journeys: a preview, a confirm prompt or --yes, a local record in ~/.ax/journeys/j-<id>.json, and a counted report (exit 0, 2, 3, 130). - ax journey view|list|cancel replace ax trial view|list|cancel, dispatched on the first goal; a goal cannot be just one of those words - goals are positionals (1 to 5), --task is gone, and a set requires --domain - --agents names 1 to 4 agents and cannot be combined with --harness or --model - without levels a set runs one As written level (JSON key written) - a set defaults to the plan's Claude Code agent, else the first agent offered - JSON trialId becomes setId; codes INVALID_SET_ID, UNKNOWN_SET, CORRUPT_SET_RECORD - src/trial moves to src/journey-set; vendored code is unchanged - deep-journey's Next line and keyless hint point at ax journey --levels - README: one ## journey section replaces ## trial and the partner journey subsection Co-authored-by: Cursor <cursoragent@cursor.com>
One journey without a key told you to set a "platform key" while a set of journeys, the README and the help text all call the same key a Front API key. The Next line now names the Front API key and points at Get a key. Co-authored-by: Cursor <cursoragent@cursor.com>
…rney A set that was a set only because of --repeat, with one run left on the plan, suggested --repeat 1. That command runs one live journey instead of a set, without saying so. The refusal now carries oneJourney, the hint says "1 run fits" and that it runs as one journey, and the Next line is the one journey call with no --repeat and no set-only flags. Co-authored-by: Cursor <cursoragent@cursor.com>
…xt in errors A .env in the directory ax runs from could name ORA_PLATFORM_URL or ORA_API_URL, and ax then sent the Front key, the partner key or the scan key to that host over any scheme. Every keyed request now checks its destination first: the default host passes, any other host needs https (plain http only to this machine), and a host that came only from ./.env is refused with a message that says to export the variable in the shell. The set commands check before they create a client and exit 2 with KEY_DESTINATION_REFUSED. reportError now drops terminal control bytes from the message, keeping line breaks, and display takes that cleaned message instead of a prebuilt string, so no caller can print a server's escape sequences. The key exchange is bounded at 30 seconds. Co-authored-by: Cursor <cursoragent@cursor.com>
…used Co-authored-by: Cursor <cursoragent@cursor.com>
…out of .env Windows reads variables without regard to case, so a lowercase ora_platform_url in a cwd .env set ORA_PLATFORM_URL while the file check missed it. Names are now compared in upper case on win32. A .env may now name only localhost, 127.0.0.1 or [::1]. A *.localhost name still works when exported, but some Linux resolvers ask the network for it. Co-authored-by: Cursor <cursoragent@cursor.com>
The set example now runs claude-code and codex together (18 runs) and shows a shortened report with agents across and levels down, as ora.ai's docs do. Co-authored-by: Cursor <cursoragent@cursor.com>
liady
left a comment
There was a problem hiding this comment.
Thanks for this. The set logic is careful: pure functions, atomic record writes (temp file and rename, 0600), and server text cleaned everywhere it's printed. I ran the changed suites at cea36b5 (36 files, 796 tests pass, tsc --noEmit clean). I also probed the key destination check with localhost.evil.com, localhost., 0.0.0.0, [::ffff:127.0.0.1] and userinfo URLs; all are handled.
Requesting changes for three reasons.
1. It breaks easily against a real platform. The follow loop fails on a single bad read per round, and reads in flight ignore --wait and Ctrl-C (inline). Since no set has run against the real Front platform and the fixtures are hand-built, these are the paths most likely to be hit first. Please run one real set (even 2 goals x 2 agents) before release, and record its responses as fixtures in place of the hand-built ones.
2. Counting rules need real data. Excluding timeout may raise rates exactly on bad sites (inline on outcome.ts). This decides what the matrix tells a customer, so it needs evidence from Studio's source or a real run, not a guess.
3. Size and invariants.
- 16k lines in one PR mix three things that deserve separate reviews. I'd split into:
- (a) the key destination check and terminal sanitizer: small and security sensitive, so it can merge first;
- (b)
Next:lines, the--jsonerror envelope andax learn; - (c) sets.
- The PR knowingly departs from AGENTS.md invariants 1, 4, 5, 7 and 9 but leaves AGENTS.md unchanged. The next agent working here will read the invariants and "fix" the code back. Please land the invariant 16 amendment in this PR, or drop the departures.
- Smaller: the vendored run-flags copy needs a check that catches drift (inline). There are three em dashes in error strings the PR touches (
src/api/deep-journey.ts, the--taskpartner key message, the mutually exclusive message); please fix them while you're in these files.
A smaller v1 would also cut most of the fixture risk: goals x agents x repeat, without levels and gap lines, run against the real platform. Then add levels once there's data showing the gap lines say something true.
Generated by Claude Code
| input.onProgress?.(progressOf(state)); | ||
| const now = input.now(); | ||
| allFinalAt = everyRunFinal(input.journeyIds, state) ? (allFinalAt ?? now) : undefined; | ||
| failedRounds = round.failed ? failedRounds + 1 : 0; |
There was a problem hiding this comment.
Blocking: one bad read in each of three rounds ends the follow for the whole set.
readRound marks the round failed if any single journey() or insightLanded() read fails, and three failed rounds in a row (READ_FAILURE_LIMIT) return read_failed. There is no backoff and no special case for 429 or 404.
Two ways this fires on a healthy set:
- With up to 200 journeys read one after another every 10s, one rate limited or flaky GET per round is enough. After about 30s the set reports
read_failed, and every journey that hasn't settled shows as in flight. - One journey that always 404s does the same, every time. A likely way to get one: a journey recorded from Studio's no-agent 502 (
launch.ts:139) whose row the server never kept.
Suggested shape:
- Count consecutive failures per journey (
Map<string, number>). End withread_failedonly when every journey that hasn't settled has hit the limit. - On a 404 (
StudioApiErrorwithstatus === 404), stop reading that journey and report it as gone, rather than failing the round. - On 429 or 5xx, wait before the next round (use
Retry-Afterwhen present, else double the interval up to a cap).
Please add a test where 1 of 50 journeys 404s forever and the other 49 settle. Today that ends read_failed.
Generated by Claude Code
| method, | ||
| headers, | ||
| body: body === undefined ? undefined : JSON.stringify(body), | ||
| signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), |
There was a problem hiding this comment.
--wait and Ctrl-C don't stop a read already in flight. The follow signal never reaches fetch; each request is bounded only by AbortSignal.timeout(REQUEST_TIMEOUT_MS). The wait deadline is checked between rounds, so against a hanging platform one round can run up to 200 x 60s, far past --wait. Ctrl-C can also sit for up to 60s before it detaches.
Engines is >=22.4.0, so AbortSignal.any is available. Let callers pass a signal and combine the two:
signal: options.signal
? AbortSignal.any([AbortSignal.timeout(REQUEST_TIMEOUT_MS), options.signal])
: AbortSignal.timeout(REQUEST_TIMEOUT_MS),Then thread the follow signal through journey() and insightLanded(), and bound each round by the remaining --wait time, not only the gaps between rounds.
Generated by Claude Code
|
|
||
| const agents = decided.agents.map(({ harness, model }) => ({ harness, model })); | ||
| const record = createRecord({ spec, agents, cells }, input.setsDir); | ||
| console.error(closeTerminalLine(record.id)); |
There was a problem hiding this comment.
"You can close the terminal" prints before any journey is created. Launching up to 200 creates one after another takes a while. A user who takes the line at its word and closes the terminal sends SIGHUP to the process:
- The set is cut short partway through launching.
markStoppednever runs, so the record doesn't say it stopped.ax journey cancelfrom another terminal cancels only what's recorded so far, while the first process may still be creating journeys.
Print this line once launching has finished, from launchAndReport once launch returns. Until then, print something like "Launching N runs; keep this terminal open until they have all started." It would also be worth handling SIGHUP the same way as SIGINT during launch, so the record is marked stopped either way.
Generated by Claude Code
| export const EXCLUDED_STATUSES: readonly string[] = [ | ||
| "failed", | ||
| "timeout", | ||
| "timed_out", | ||
| "error", | ||
| "canceled", | ||
| "cancelled", | ||
| "lost", | ||
| ]; |
There was a problem hiding this comment.
Product question before this ships: should timeout really be excluded? On a site that is hostile to agents, the most likely way a run ends is that the agent wanders until its time runs out. If Studio stores that as timeout, the set drops exactly the runs that show the site's problem. The matrix then reports a higher rate than reality, and a set can look healthy because the site is bad.
The comment says a harness crash is stored as failed and the reaper writes lost. Those are clearly infrastructure. timeout is ambiguous. Can we confirm from Studio's source which code paths write timeout and timed_out? If any of them is "the agent ran out of time on the site", that path should count as unsatisfied (or at least show as its own column), not be excluded.
Related: any final status not in either list counts as in flight forever, so an unknown value like expired or aborted makes the set wait the full --wait. Treat unknown statuses as excluded with their raw value shown, so a new status from the platform is visible rather than silent.
Generated by Claude Code
| @@ -0,0 +1,131 @@ | |||
| // Copied from ora's journey code. DO NOT EDIT BY HAND. | |||
There was a problem hiding this comment.
This copy has nothing that catches drift. The header says to re-copy whenever ora's file changes, but nothing enforces it: the tunnel vendor is tested against its upstream, and this one isn't.
It also matters right away: I'm asking for a change to the blocked first-request rule on eralabs-ai/ora#1699 (a single 401 on the first request can raise the flag). If that rule changes there, this copy is already stale on the day it lands.
Options, cheapest first:
- Record the source file's hash in this header, and add a CI step that fetches
src/lib/journey/run-flags.tsfrom oramainand fails when the hash differs. - Publish the rule in a small shared package and depend on it.
Either is fine for this PR; (1) is about 20 lines.
Generated by Claude Code
|
CI is red on 1. Fix: strip the codes in the test helper (verified: 8/8 pass with and without import { join } from "node:path";
+import { stripVTControlCharacters } from "node:util";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
@@
-const logged = () => vi.mocked(console.log).mock.calls.map((c) => String(c[0]));
+const logged = () =>
+ vi.mocked(console.log).mock.calls.map((c) => stripVTControlCharacters(String(c[0])));Worth checking whether other suites that assert on rendered lines rely on colour being off too. Setting 2. Generated by Claude Code |
…precated alias ax journey with no goal of its own now runs one of ora's curated tasks on ora.ai's public journeys, free and with no key (--intent, --agent, --api-key, --no-stream). A goal still runs on the Front account. Mixing the two kinds of flag is a usage error naming both. ax deep-journey keeps its flags and exit codes, prints the ax journey call it stands for on stderr, and is hidden from help. Next lines from either name now print the ax journey form, and audit's Next line points at ax journey --domain. Also reads --no-stream the way citty parses it (stream: false); before this the flag never reached deep-journey. Co-authored-by: Cursor <cursoragent@cursor.com>
liady
left a comment
There was a problem hiding this comment.
Re-checked at 0205173. None of the earlier review points are addressed yet, and this commit adds scope rather than shrinking it: deep-journey becomes a hidden, deprecated alias, and ax journey now covers two platforms.
State of this head
- Merge conflict with
maininsrc/contract/types.gen.tsandsrc/contract/version.ts: #94 regenerated the types for 1.41.0. Because of it, Test, Lint, Build and the contract drift job didn't run on this commit; only CodeQL did. - Same two CI-only failures: I ran the full suite with
CI=trueon0205173. 926 pass, and the same 2 tests inreport-flow.test.tsfail (the colour codes issue from my earlier comment).tsc --noEmitis clean.
Problems this commit adds
- Platform routing: which platform and key a run uses now depends on which environment variables are set and on a one-letter flag difference (inline).
- Cross-PR drift: eralabs-ai/ora#1699 still teaches
ax deep-journeyin/docs/cliand/docs/journeys, and its/clipage follows the old split. With this commit those docs teach a command that ax hides from--helpand calls deprecated. The two PRs need to describe the same CLI. - Em dashes: the
via === "deep-journey"branches keep the em dash messages flagged earlier, so they will now live on as the deprecated path's text.
I'd hold further feature commits until the split and the blocking items from the first review land. Each new commit makes the split harder.
Generated by Claude Code
| if (publicFlags.length > 0) return { route: "public" }; | ||
| if (frontFlags.length > 0) return { route: "front" }; | ||
| if (goals.length === 0) return { route: "public" }; | ||
| if (goals.length === 1 && !input.frontKey && input.partnerKey) return { route: "public" }; |
There was a problem hiding this comment.
The same command now runs on a different platform depending on which environment variables happen to be set. With no routing flags, ax journey "Find the API docs" --domain x.com:
- runs on the Front account (
ORA_API_KEY, Front billing, Front's agents) whenORA_API_KEYis set; - runs on ora.ai's public partner tier (a different key, a different agent roster, a single public run) when only
ORA_PARTNER_API_KEYorORA_SCAN_API_KEYis set.
The output doesn't say which one ran. A developer whose laptop has one key and whose CI has both gets two different products from the same line, billed to different accounts, and their results won't compare.
On top of that, routing also flips on a single flag, and two of those flags differ by one letter:
--agent claudesends the run to ora.ai's public tier and needs the partner key;--agents claude-codesends it to Front and needsORA_API_KEY.
A typo there doesn't fail; it quietly switches platform and key.
Suggestion: make the platform explicit rather than inferred, e.g. --on front|ora with no environment fallback, and print the platform on the first line of every run ("Running on your Front account" / "Running on ora.ai public journeys"). Then a missing key becomes a clear usage error rather than a silent reroute.
Generated by Claude Code
| function present(rawArgs: readonly string[], names: readonly string[]): string[] { | ||
| const typed = new Set<string>(); | ||
| for (const arg of rawArgs) { | ||
| if (arg === "--") break; | ||
| if (arg.startsWith("--")) | ||
| typed.add( | ||
| arg | ||
| .slice(2) | ||
| .split("=")[0] | ||
| .replace(/^no-(?!stream)/, ""), |
There was a problem hiding this comment.
present() re-parses rawArgs by hand next to citty's own parsing. Routing (and so which key gets sent where) now depends on two parsers agreeing about flag names, --no-* negation and -- handling. If citty already gives you the parsed args, deciding from which keys are defined there keeps one parser, one truth. If there's a reason it can't (citty filling in defaults), please add a test per flag that the raw parser and citty agree.
Generated by Claude Code
What this PR does
Journeys go from one sample to a test you can run deeply on your own site.
ax journeycan now run a set: the same goals at up to four levels of help, across up to four agents, repeated, on your Front account. It previews the run count and what is left of your plan, asks before launching, follows the runs until the platform grades them, and prints a matrix with gap lines that name the layer where agents lose your site;ax journey view|list|cancelpick a set up again, list sets and stop one. One goal with one agent still runs one journey and draws it live.ax journeyis also now the one journey command: with no goal of its own it runs one of ora's curated tasks free, whichax deep-journeydid, andax deep-journeykeeps working as a deprecated alias. Every command that sends a key now checks where it goes first.Why
One journey says an agent failed on your site once, but not how often, with which agents, or whether a little help would have got it through. Experiment platforms for developer tools answer those questions for CLIs and SDKs; this brings the same depth to websites, adapted to ora's score layers:
blockedandnever visited, counted beside the verdictsNext:lines, one--jsonerror envelope, andax learnax journey view,listandcancel; Ctrl-C detachesNot in this release: checks beside the judge, sets in CI with a PR comment, a query over runs, and an MCP tool for sets.
How
ax journeyworks nowflowchart TD Z["ax journey ..."] --> G{"A goal of your own?"} G -- "no, or --intent / --agent / --no-stream" --> FR["ora.ai's public journeys: a curated task,<br/>free, no key (was ax deep-journey)"] G -- yes --> A["ax journey 'goal' --domain site ..."] A --> M{"One goal, one agent,<br/>no --repeat, no levels?"} M -- yes --> O["One journey, drawn live as a graph<br/>(unchanged; exit 0, 1 or 2)"] M -- no --> V["Check the set: one site, 1 to 5 goals,<br/>up to 4 levels, 1 to 4 agents,<br/>1 to 10 repeats, at most 200 runs"] V --> P{"Preview: 'This set uses 30 runs ...'<br/>'Launch 30 runs? [y/N]'"} P -- "no, or a refusal" --> X["Nothing launched"] P -- "yes, or --yes" --> R["Write the record first:<br/>~/.ax/journeys/j-id.json"] R --> L["Create one journey per goal, level and repeat;<br/>each runs every agent"] L --> F["Follow until every run is graded,<br/>or --wait runs out"] F --> C["Count per goal, level and agent<br/>(the platform grades, ax only counts)"] C --> T["Report: the matrix plus gap lines,<br/>e.g. 'loses agents at the Discovery layer'"] T --> N["Next: ax journey view id while runs are going,<br/>otherwise ax audit site"] S[("Your Front account at app.ora.ai")] O -.-> S L -.-> S F -.-> Sax journey --domain your-site.com [--intent pricing]ax journey "Find the API docs" --domain your-site.com--levels,--docs <url>,--exact <url>or--brand <name>, and/or--agents a,band--repeat N(plus--yes,--wait <min>,--json)ax journey view <id>ax journey listax journey cancel <id>--docs) and Exact target (with--exact). Every level is a fixed prompt template; no model writes a prompt. Without levels, a set runs the goal as written.60% (3 of 5)from 5 gradable runs and3 of 4below that. Excluded runs (never started, failed, timed out, canceled) never count against the site. Two flags are counted beside the verdicts and never change one:blocked(the rule ora.ai uses for its own runs, vendored undersrc/journey-set/vendor/) andnever visited.0when a report printed (complete or not),2usage or a refusal before launch,3the platform failed with no report,130Ctrl-C after printing theviewline.ORA_API_KEYwithjourneys:write,journeys:read,runs:read,runs:write,intents:read. The record never holds the key or a token.The README's
## journeysection is the full reference.One journey command
There were two:
ax deep-journeyfor ora's free public journeys andax journeyfor your Front account. Which system runs a journey is now read from what you type:ax journey --domain stripe.com [--intent pricing] [--agent id] [--no-stream] [--json]runs a curated task on ora.ai's public journeys, free, exit codes0any verdict,1run failed,2usage,3API error, as before.ORA_API_KEY, unchanged. Without the key, the error now also names the free form.--agent,--intentor--no-stream, or a lone goal when onlyORA_PARTNER_API_KEYis set, takes the public keyed tier, whichax deep-journey --taskdid.--intentwith--levels, say) is a usage error that names both and gives the call for your Front account.ax deep-journey <url>still runs, with the same flags (--taskincluded) and exit codes. It prints one line on stderr naming theax journeycall it now stands for, keeps its own error prefix, and is hidden from the help screen; it should be removed in a future major release. Next lines from either name print theax journeyform, andax auditnow ends onNext: ax journey --domain <site>. The README's#deep-journeyanchor still lands on the free section.Also fixed:
--no-streamnever reachedax deep-journey, because citty parses--no-streamasstream: false; both names now read it that way.Keys go only where you chose
A set sends your Front API key on every call, so this branch also checks where every key goes, for every command that sends one (
ax journeyon both paths,ax auditand--tunnel ora,ax webmcp-audit):ORA_API_URLorORA_PLATFORM_URLgets a key only over https, or over plain http to this machine..envin the current directory names is refused before anything is sent, exceptlocalhost,127.0.0.1and[::1]. That file belongs to whatever repository you run in; to use another host, export the variable in your shell.ax journeyexits 2 withKEY_DESTINATION_REFUSED.ora_platform_urlin a.envis caught too.reportError'sdisplayis now a function that receives the cleaned message.The README says this under the env table (9b2e830, 932f671, 7278a72).
Other changes on this branch
--jsonerrors share one envelope that passes the server body through raw;ax learnprints the whole workflow (c6fcb63, c7e68e0, 6aa88a0).Testing
pnpm lint,pnpm typecheck,pnpm build,pnpm smokeandpnpm verify:packpass. The full suite passes: 928 tests, 2 skipped (both skips predate this branch). Lint's two warnings and one info are on lines that predate it.ax deep-journeystill runs, prints its new name and keeps its prefix and exit code;ax journeywith no goal takes the free path and never asks for a Front key.ax journey --domainandax deep-journeyon an unknown intent (both list the intents, exit 2), a goal with no Front key (names the free form),ax audit,ax skill,ax generate-files,ax journey list, and one journey and a set without a key (both exit 2 with the key steps)..envnaming a remote host, a*.localhostname or plain http is refused with nothing sent; an exported host and the default host get through; a server answering with escape codes leaves no raw escape bytes on stderr. Not run on Windows; the case rule is covered by tests that stub the platform.NEED_LEVEL_NOTE) is worded before any real need-level run. The first real set should be run before release.Reviews
Reviewed for engineering (approved), protocol (approved) and security (approved on the third pass). The first two security passes blocked on where keys go, on server text reaching the terminal, and on a lowercase variable name getting past the check on Windows; all are fixed above.
Open for the reviewer
AGENTS.md: a set departs from invariants 1, 4, 5, 7 and 9: ax counts verdicts and writes gap lines, prints its own--jsondocument because no single platform payload exists, adds exit code130, keeps a run ledger (not config) under~/.ax/journeys/, and addsview|list|cancelunderjourney, which invariant 9 reserves. Invariant 9 also says new journey commands land asax deep-journey; this PR goes the other way and foldsdeep-journeyintojourney, keeping the old name as an alias. A proposed invariant 16 amending them is ready;AGENTS.mdis not edited in this PR.ax audit --tunnel oraand its errors still say "platform key" forORA_API_KEY; the rest now says Front API key.src/api/deep-journey.ts, and the--taskpartner key and mutually exclusive messages, whichax deep-journeystill prints).ax deep-journey: at the next major release. ora.ai never sends it in a response, so only old scripts call it.Release
Merging lets release-please cut the next minor (
0.11.0). ora.ai's docs, MCP page and journey launchpad teachax journeysets and wait for that npm release before they merge.