Conversation
* chore(deps): standardize on zod v4 across the codebase Two zod versions coexisted in the tree (v3 from the project + @hono/zod-openapi@0.19, v4 from @earendil-works/pi-coding-agent), so a ZodError thrown by one copy was not instanceof the ZodError imported from the other — breaking cross-version instanceof checks (sessions.test.ts) and risking subtle validation bugs. Standardize the project's own packages on zod v4: - Bump zod ^3 -> ^4.4.3 and @hono/zod-openapi ^0.19.6 -> ^1.4.0 (the zod-v4 line) in @archon/core, @archon/workflows, @archon/server. All @archon/* packages now resolve a single zod@4.4.3 (remaining v3/older-v4 copies are nested in unrelated third-party deps and don't touch our code's instanceof). v4 breaking-change fixes: - z.record(value) -> z.record(z.string(), value): v4 requires an explicit key type (dag-node, hooks, workflow-run, core schemas, server routes). - parseOptionalField: v4 dropped the 3-arg z.ZodType<O, Def, I>; infer from the schema via <S extends z.ZodType> returning z.output<S>. - betasSchema .nonempty() no longer infers a [string, ...string[]] tuple in v4; loader's local type is now string[]. - agents record: v4 collapses a failing key-schema into a generic "Invalid key in record" and drops the custom message, so the kebab-case check moved into a superRefine that preserves the guidance (with the offending key in the path). Tests: - Update two loader assertions for v4 error wording (v3 "Required" -> v4 "Invalid input: expected number, received undefined"); behavior unchanged. - Add a GET /api/openapi.json regression test confirming the spec generates across all routes under @hono/zod-openapi v1 (the major-bump risk). bun run validate is green: type-check (10 packages), lint, format, tests; the previously failing sessions ZodError test now passes. * chore(review): address zod v4 PR review findings - loader.ts: fix stale betas comment that still claimed the [string, ...string[]] tuple type after the v4 change to string[]; move the parseOptionalField generic rationale into the JSDoc and make the ZodTypeDef note precise - dag-node.ts: reword betasSchema comment to a version-agnostic description - api.health.test.ts: harden the openapi.json test to assert components.schemas is populated (Conversation/Codebase/WorkflowEvent) and that the datetime-heavy conversations/codebases routes serialize — the actual zod-to-openapi v7->v8 risk - CLAUDE.md: document the z.record(z.string(), value) v4 requirement
* fix(workflows): fail dag node on idle-timeout with zero output (#1807) The `!nodeIdleTimedOut` exemption in the empty-output guard caused `review-classify` (and any node with `output_format` + idle_timeout) to report `completed` with empty output when the provider never emitted any tokens before the watchdog fired. Downstream aspects then evaluated `$review-classify.output.run_X` against an empty string, every `when:` gate failed, all aspects were skipped, `synthesize-review` skipped on `one_success`, and the workflow reported success having reviewed nothing. Changes: - dag-executor: gate the "completed via idle timeout" warning behind `hasAnyOutput` so it only surfaces when the AI actually produced content (the "subprocess hung after finishing" case). - dag-executor: remove the `!nodeIdleTimedOut` exemption from the empty-output failure guard; emit a distinct, actionable error message for the idle-timeout-with-zero-output case. - maintainer-review-pr.yaml: drop the 60s `idle_timeout` on `review-classify` (copy-paste artefact, tightest timeout on the largest prompt in the workflow); inherits the 30-minute default. - dag-executor.test.ts: add cases documenting that a failed `review-classify` correctly cascades to skipped aspects and a skipped synthesize step. Fixes #1807 * fix: address review findings for PR #1812 - Condense multi-line comment blocks to single lines (CLAUDE.md compliance) - Add invariant comment for structuredOutput check - Fix time unit inconsistency: idle-timeout failure message now uses minutes to match the existing completion warning (both paths now use / 60000) - Add integration test: idle-timeout with zero output → node_failed, not node_completed - Add integration test: idle-timeout WITH output → node_completed + warning sent - Update stale troubleshooting row in quick-reference.md to reflect the new explicit failure behavior instead of the old "workflow hangs" description - Add CHANGELOG [Unreleased] entry for the zero-output idle-timeout fix (#1807) * simplify: reduce nesting in idle-timeout guard and inline one-shot callback
…events, machine context, custom-vs-default) (#1815) * feat(telemetry): expand anonymous PostHog capture (started/completed events, machine context, custom-vs-default) Expands the post-#1780 telemetry from a single intent-only `workflow_invoked` event into a small, decision-useful event set — all categorical/anonymous and inside the existing IP-free promise. Geo is deliberately excluded. Events: - archon_started (once per CLI invocation / server boot) — the basis for honest active-install + DAU/WAU/MAU counting (users who only run doctor/serve/chat were previously invisible). - workflow_invoked (extended) — workflow_source/is_builtin, provider, model, node_count, uses_loop/approval/script/bash, interactive, used_isolation, is_resume, schema_version. - workflow_completed / workflow_failed (new) — outcome, duration_ms, node counts, categorical exit_reason. Fired at the existing emitter sites so de-dup is inherited (no double-count on the unhandled-throw path). Privacy: - Machine context (os/arch/version/is_binary/runtime/is_ci/is_tty) attached to every event via PostHog register() super-properties. - The privacy invariants ($process_person_profile:false, $ip:'') are kept per-event via a shared PRIVACY_INVARIANTS const (not super-properties), so a super-property regression can never silently leak IP or create a profile. - Bundled workflows report their real name; custom/global report "custom" so private names never leave the machine. - README, .env.example, and docs updated; first-run notice copy refreshed and the notice stamp bumped to -v2 so existing installs re-consent once. Plumbing: WorkflowSource threaded from discovery through ExecuteWorkflowOptions to the executor/dag-executor (CLI, background dispatch, approval-resume, and the single-codebase resolve path are exact). Two orchestrator routes (handleWorkflowInvocationResult, handleWorkflowRunCommand) keep the privacy-conservative default (source undefined -> "custom"); threading them fully is a fast-follow. install_method (brew/curl/npm/docker) deferred to a follow-up; is_binary ships now. * refactor(telemetry): address PR review findings Follow-up fixes from the multi-agent review of the telemetry expansion: - Remove dead 'cancelled' outcome from WorkflowCompletedProperties (no emitter; cancellation is intentionally untracked) and document why. - Narrow exitReason from string to a WorkflowExitReason union so the "categorical, never error text" contract is compile-time enforced. - Add sanitizeModelForTelemetry guard: model ids are user-supplied, so only forward values matching a real model-ref shape; drop free-text. - Narrow classifyWorkflowForTelemetry return to WorkflowTelemetrySource. - Extract a single fireAndForget helper for the three capture functions so the swallow-at-debug error policy lives in one place. - Coerce is_tty so it is present-and-false on non-TTY (server) processes instead of silently omitted (bun-types mis-types isTTY as always-boolean). - Reword captureArchonStarted docstring (call-site once-per contract, no in-function dedup) and note captureWorkflowCompleted skips the notice. Tests: - executor: assert workflow_failed/unhandled_error fires on the throw path, is not fired on success, and that source threads to executeDagWorkflow. - dag-executor: assert completion telemetry at all three terminal sites (completed / no_nodes_completed / node_error) with the threaded source. - telemetry: cover sanitizeModelForTelemetry; drop a stale archonVersion prop. Docs: - security.md: add an Anonymous Telemetry section cross-linking the config reference (the privacy page previously covered only local logging). - README + configuration.md: clarify model id ships only on workflow_invoked. * fix(telemetry): address CodeRabbit review on the expansion - cli: emit archon_started before the no-args / --version / --help / parse- failure early returns so every CLI invocation is counted (was placed after them, undercounting the metric). Each of those paths now flushes telemetry before exit; the version path already did. - tests (dag-executor): assert completion telemetry fires exactly once at each terminal site and pin workflowSource on the failure paths, guarding against the double-count risk and failed-path source loss. - tests (executor): assert the unhandled-throw completion fires exactly once. - tests (telemetry): stub fetch and flush deterministically in the enabled-path tests so they never touch the network or leave a live flush timer. Skipped (with reason): - "align @archon/paths mock.module shape across workflow tests": not applicable here — the workflows test script runs each file in its own `bun test` process (see package.json), so there is no cross-file mock.module pollution. - "add an unhandled_error test in dag-executor.test.ts": that exit reason is emitted by executor.ts's catch, not the DAG executor; it is covered in executor.test.ts.
…-C) (#1823) * feat(core): per-user GitHub token store + device-flow engine (PR-C foundation) Foundation layer for per-user GitHub attribution (PRD Phase 3). Pure additive infrastructure — nothing wires into the adapters/workflows/web yet, so there is zero behavior change. Subsequent increments add the invocation gate, token policy, git-config plumbing, connect surfaces, and interim web auth. - token-crypto: AES-256-GCM encrypt/decrypt + TOKEN_ENCRYPTION_KEY validation (cherry-picked from the #1774 donor branch; decrypt rewritten for strict TS) - github-auth/config: isPerUserGitHubEnabled (App + key), device-flow client-id loader, assertEncryptionKeyAtBoot fail-fast - remote_agent_user_github_tokens table (PG migration + bundled-schema regen + SQLite createSchema); one row per user, encrypted access/refresh tokens - db/user-github-token-store: save (UPSERT), get, decrypt-with-refresh-on-read (5-min buffer, per-user mutex against double-refresh), delete, no-reply email - github-auth/device-flow: hand-rolled start/poll/refresh/fetchUser over fetch (no client_secret, slow_down + authorization_pending handling) - github-auth/connect-service: device flow -> profile -> conflict-guarded identity bind -> encrypted token persist -> cached display_name/email - users: updateUserGithubProfile, linkGithubIdentity, GithubIdentityConflictError Tests: 30 new (crypto/device-flow/token-store, 10 each). bun run validate green. TS filenames carry -row/-store suffixes to satisfy the local secret-guard hook; the DB table is remote_agent_user_github_tokens. * fix(core): address multi-agent review findings on per-user github tokens Hardening + test coverage for the dormant per-user GitHub token store and device-flow engine. No behavior change for existing installs. Correctness: - token store: separate the refresh call from the DB save so a persist failure after a successful refresh is no longer mislabeled `refresh_failed` and the freshly-issued (now-valid) token is returned instead of discarded; the failure logs `persist_failed` (error) (I1) - linkGithubIdentity: add UNIQUE-violation race recovery (re-SELECT, conflict if a different user won, no-op if same user) so a concurrent double-connect no longer surfaces a raw 23505 (I2) - getUserGithubTokenRecord: type the raw read as number|string and coerce github_user_id, honest against node-postgres returning BIGINT as a string (I5) Types/logging/docs: - row schema: widen token-expiry timestamps to date|string (PG Date vs SQLite ISO text) (I6) - connectGithubForUser: emit github_connect.started/.failed per logging convention (I7) - token store: debug log on the not-connected path; relinked + race-recovered audit logs on linkGithubIdentity (S3/S5) - device-flow: floor-guard the slow_down backoff; surface GitHub's error_description from postForm (S1/S2) - getEncryptionKey: accept an env param so assertEncryptionKeyAtBoot is consistent and injectable - fix stale updateUserGithubProfile comment (I8); CLAUDE.md 11->12 tables and migration header table list, regenerated bundled schema (D1/D2) Tests (+20): linkGithubIdentity (conflict/same-user/new/race) and updateUserGithubProfile; token-store mutex + re-read fallback + persist-failure; new config.test.ts and connect-service.test.ts (ordering: no token saved when the identity bind throws). Deferred: discriminated-union return for getDecryptedAccessToken (no callers yet - premature); literal-union DeviceFlowError.code (clean form needs a string & {} eslint-disable for marginal benefit). Rejected the review's suggestions to key identity on numeric id (breaks adapter login matching), to drop a dead data.interval operand (it is a live field GitHub returns), and to remove a fresh && guard (TS cannot narrow it). * feat(workflows,isolation): requires:[github] gate, per-user token policy, worktree git identity (PR-C G4–G6) Attribution core for PR-C. All three paths are no-ops unless per-user GitHub is enabled (GITHUB_APP_ID + TOKEN_ENCRYPTION_KEY), so solo PAT installs are unchanged. G4 — requires gate: - workflowRequirementSchema + `requires: [github]` on workflowBaseSchema - workflow-requirements.ts: assertWorkflowRequirementsMet + WorkflowRequirementError - orchestrator hard-fails a requiring workflow at invocation (before isolation/ worktree/AI cost) when the originating user hasn't connected G5 — per-user token in workflow subprocess env: - github-token-policy.ts cherry-picked from #1774; KEYCLOAK_URL mode-detector replaced by an injected isPerUserGitHubEnabled flag (kept pure) - WorkflowDeps gains getUserGithubToken + isPerUserGitHubEnabled - executor merges per-user overrides last into config.envVars (user token wins, or '' scrubs the org/bot token when unconnected + fallback off) - store-adapter wires getUserGithubToken → getDecryptedAccessToken G6 — worktree git identity: - IsolationRequest/ResolveRequest gain gitIdentity; resolver forwards it - WorktreeProvider stamps `git config user.email/name` on new worktrees - orchestrator resolves the no-reply email (<id>+<login>@users.noreply...) from the originating userId and passes it on the resolve request Tests: workflow-requirements (3), github-token-policy (8). Type-check + isolation suite green. * feat(adapters,server): route PR/issue comments through the originating user's token (PR-C G7) When per-user GitHub is enabled (App mode + TOKEN_ENCRYPTION_KEY), the GitHub adapter authors outbound comments under the human who triggered the thread, instead of archon[bot]. - handleWebhook caches conversationId → originating archon userId (actorByConversation; App mode only, lost on restart → graceful bot fallback) - postComment prefers a per-user Octokit (getUserOctokit, 5-min cached); on 401 (revoked/expired) it evicts and falls back to the installation token rather than failing the reply - adapter constructor gains an optional getUserToken resolver; server injects it (→ getDecryptedAccessToken) only when isPerUserGitHubEnabled() - no MessageMetadata / core streaming changes — the actor is resolved entirely inside the adapter from the webhook it already processes Existing 64 adapter tests pass (PAT path: actor undefined → unchanged). A dedicated App-mode user-token test is deferred (no mock-provider harness yet); the path is type-safe with a conservative bot fallback. * feat(cli,adapters): connect-github surfaces for CLI and Slack (PR-C G8) - CLI: `archon auth github` — device flow for the CLI user. Identity from ARCHON_USER_ID else $USER/$USERNAME, resolved to a stable 'cli' Archon user. Guarded on isPerUserGitHubEnabled; clear errors for conflict / device-flow / not-enabled. Wired into the cli.ts switch + noGitCommands + usage. - Slack: `/archon connect github` — resolves the invoking user, drives the device flow detached, and delivers the code + result as ephemeral follow-ups via response_url (within its 30-min/5-use window). No orchestrator dispatch. Both reuse the shared connectGithubForUser engine. CLI identity uses ARCHON_USER_ID rather than a config.yaml field (smaller surface than expanding the config schema this PR). Type-check + slack adapter suite (38) green. * feat(server,web,docs): web device-flow endpoints, X-Archon-User auth, Connect UI, docs (PR-C G9–G10) Web surface + interim web auth + docs for per-user GitHub. Device flow (browser-driven, non-blocking): - decomposed device-flow: pollDeviceFlowOnce + persistGithubConnection (connect-service tail), shared by the blocking CLI/Slack path - POST /api/auth/github/device/start | /poll, GET/DELETE /api/auth/github - web api client fns + a Settings "GitHub Identity" card (connect → show code → poll → connected; disconnect) Interim web auth (X-Archon-User): - resolveWebUserId(c) reads ARCHON_WEB_AUTH_HEADER (default X-Archon-User) and resolves a stable 'web' user; absent → NULL (never elevated) - the three web userId TODO sites (create conversation, send message, run workflow) now attribute conversation/message rows and the workflow run Boot: assertEncryptionKeyAtBoot fails fast on a malformed key; App-without-key WARNs + disables per-user (App-for-bot-only stays valid). Docs: .env.example (GITHUB_APP_CLIENT_ID, TOKEN_ENCRYPTION_KEY, ARCHON_ALLOW_ORG_GITHUB_TOKEN_FALLBACK, ARCHON_WEB_AUTH_HEADER); CLAUDE.md (requires:[github], auth endpoints). Test fixes: orchestrator-isolation mocks the new db dep; api.messages / api.workflow-runs updated for the addMessage userId arg. bun run validate green. * fix(pr-c): address multi-agent review findings on per-user github tokens Critical: - C1 (core): wrap all three decryptToken calls in resolveAccessToken in try/catch -> log user_github_token.decrypt_failed and return null, honoring the documented "null on irrecoverable failure" contract. A TOKEN_ENCRYPTION_KEY rotation or tampered ciphertext no longer throws an opaque crypto error and crash workflow dispatch (the requires:[github] gate treats null as unconnected). - C2 (workflows): until_bash loop subprocess now spreads ...(config.envVars ?? {}) last, matching executeBashNode/executeScriptNode. The per-user token scrub and managed per-project env vars reach until_bash; it no longer inherits the server's ambient GitHub token and bypasses the scrub. - C3 (docs): .env.example and the config.ts module doc now name all three vars correctly — GITHUB_APP_ID + TOKEN_ENCRYPTION_KEY arm the gate; GITHUB_APP_CLIENT_ID is additionally required for the connect flow. Previous text named CLIENT_ID as the gate trigger, which could cause a silent lockout. Important: - I1 (server): boot guard warns web_auth.header_trust_on_public_bind when the X-Archon-User trust is live (header set, or per-user mode since the default header is trusted) on a non-loopback bind, mirroring the /internal/* guard. - I2 (server): new strict requireWebUser helper for the 4 auth-required endpoints distinguishes missing header (401) from identity-resolution backend failure (503, added to apiError's union). Best-effort resolveWebUserId kept for attribution paths but now logs headerPresent. - I3 (server): GET/DELETE /api/auth/github wrapped in try/catch with userId context instead of an opaque 500 from the global handler. - I4 (server): corrected the /internal/* inline comment — the guard refuses to start (fatal), it does not merely WARN. Cheap correctness wins: - S1 (core): assertKeyLength 32-byte guard in encryptToken/decryptToken surfaces an actionable error instead of Node's opaque "Invalid key length". - S3 (server): replaced the nested ternary in the poll-status mapping with a testable mapDeviceFlowErrorToPollStatus helper. - S5 (cli): corrected the stale TODO — archon auth github has landed; remaining work is threading resolveCliUserId() (ARCHON_USER_ID/$USER), not config.yaml. Docs: - README table count 7 -> 12; CLAUDE.md @archon/core batch count 7 -> 20 and a new `auth github` CLI entry. - docs-web: `auth github` CLI reference, per-user GitHub env var table in configuration.md, `requires: [github]` in authoring-workflows.md, and a "Step 8: Enable per-user GitHub identity" section in github-app-setup.md. Tests: - T1 (core): direct pollDeviceFlowOnce coverage (pending / slow_down / default-interval / authorized / error). - T2 (server): auth-poll-status.test.ts covers the expired/denied/error mapping; registered in server package.json. Deferred: - T3: locking the isPerUserGitHubEnabled()===false no-op for requires:[github] at its only call site (dispatchOrchestratorWorkflow) needs ~100 lines of new mock scaffolding in orchestrator-agent.test.ts (which today only tests command parsing and mocks neither the config gate nor the token store) to verify a one-line && short-circuit whose two halves are already unit-tested (config.test.ts + workflow-requirements.test.ts). Not worth the scaffolding. bun run validate passes (all 6 checks).
…n resume works (#1840) * fix(providers/codex): capture thread id from thread.started so resume works CodexProvider snapshotted `thread.id` before consuming the event stream and emitted that as the result chunk's sessionId. For a NEW thread the SDK assigns the id during the run — via the `thread.started` event, whose own type comment says it "can be used to resume the thread later" — so the snapshot was null and Codex returned no sessionId at all. That silently broke session resume on Codex: persist_session nodes never received an id to persist, and any suspend/resume could not re-thread the session. Capture `thread_id` from the `thread.started` event inside streamCodexEvents and use it for every result emission (turn.completed, turn.failed, stream_incomplete), falling back to the snapshot for the resume case where the id is already known. Verified live (gpt-5.5, ChatGPT-account auth): a two-turn same-cwd round-trip now returns a thread id and the second turn recalls a token planted in the first — context survives. With Pi and Claude already warm-resuming, all three in-process providers now resume, not just Claude. Test: a thread whose .id is null still surfaces the thread.started thread_id as the resumable sessionId. * fix(providers/codex): address PR #1840 review — SDK type, no silent degrade, tests, docs - I1: cast thread.started to the SDK's exported ThreadStartedEvent instead of a re-declared inline shape; the guard collapses to a truthiness check (CLAUDE.md SDK Type Patterns). - I2/S1: on a (contract-impossible) empty/missing thread_id, warn instead of silently emitting sessionId: undefined — which the dag-executor reads as session-less and uses to drop persist_session continuity. Promote the success log to info: the one place a new thread's resumable id is established. - I3/S3/S4: tests — captured id flows through turn.failed and stream_incomplete; resume keeps the snapshot id when thread.started doesn't re-fire; an empty thread_id keeps the snapshot. - I4: update the stale Codex snippet in architecture.md (it taught the pre-fix sessionId: thread.id pattern this PR fixes). - I5/S2/S6: quote the SDK doc comment in full, correct "may or may not re-fire" to "only for new threads", document the continue. Deferred: S5 (narrow resolvedThreadId to string|undefined) — the code-simplifier judged it churn that only mirrors the param type; kept as-is.
* fix(core): guard workflow resume/cancel against concurrent double-claim
resumeWorkflowRun had no status guard — two callers racing the same run (the web
Resume button + a chat re-dispatch, or the lock-less CLI path; the conversation
lock is keyed per-conversation, not per-run) could both flip it to 'running' and
double-claim the worktree. Make it a compare-and-swap: the UPDATE only matches a
row still in a resumable state (the exact findResumableRun predicate —
failed/paused, or a stale 'running' orphan), and because it refreshes
last_activity_at a second concurrent resumer is excluded in every case. On a CAS
miss, probe the current status and throw an actionable "not resumable
(status: X)" instead of silently proceeding.
cancelWorkflowRun was unguarded (WHERE id=$1) and could re-stamp completed_at or
resurrect a terminal run. Guard it with status NOT IN ('completed','cancelled')
(keeping 'failed' and 'running' cancellable by design) and treat a no-match as an
idempotent no-op.
This is the foundational concurrency-safety primitive the pending-state /
suspend-resume refactor builds on — usable by the console UI and every other
surface that resumes or cancels a run.
Tests: CAS predicate + not-resumable/not-found/probe-error paths for resume;
terminal guard + idempotent no-op + db-error paths for cancel.
* fix(core): address PR #1830 review — fix CAS param-bind bug (C1) + hardening
C1 (merge-blocking): the resume CAS predicate interpolated nowMinusDays(3) → $3
but bound only [id], leaving the day placeholder unbound — resume threw on
Postgres (bind error: 1 param supplied, 3 required) and orphan recovery silently
died on SQLite ($3 → NULL → `last_activity_at < NULL` is false). nowMinusDays
takes a PARAMETER INDEX, not a day count. Fixed by binding the day value and, per
S1, extracting the shared resumableStatusClause(dialect, idx) helper used by BOTH
findResumableRun and resumeWorkflowRun so the predicates can't drift again (the
hand-duplication is what caused C1). The day value is the named
ORPHAN_RESUME_STALE_DAYS constant, bound identically at both call sites.
- I1: cancelWorkflowRun returns { cancelled: boolean }; the cancel route reports
"nothing to cancel" instead of a false "Cancelled" when the run already finished
(TOCTOU no-op). IWorkflowStore updated to match.
- I2: resumeWorkflowRun throws a typed WorkflowNotResumableError; the orchestrator
resume path catches it and surfaces "already being resumed" instead of leaking
the raw DB-id string to the generic failure catch.
- I3: real-SQLite integration test (workflows.resume-cas.integration.test.ts)
exercises orphan recovery + the dialect date SQL end-to-end — it fails under C1.
Added params-array assertions to the mock tests (would have caught C1 directly).
- S2: cancel no-op and resume CAS-miss both log at info (consistent).
- S3: corrected the "mirrors exactly" comment (now a shared helper), stated the
atomic-UPDATE exclusion mechanism, relabeled the probe "informational only".
- S4: capture probeRows across the try/catch; thread { cause } into the rethrow.
) * feat(auth): opt-in web login via Better Auth + user-identity seam Add an opt-in email/password web login (Better Auth, invite-allowlist gated) and the {userId, role} identity seam so resources can be user-scoped later without a rewrite. - A Better Auth session maps to the canonical remote_agent_users row via user_identities('web', <betterAuthUserId>); resolveAuthContext resolves session-first then the X-Archon-User header at one chokepoint, returning { userId, role }. resolveWebUserId delegates to it; requireWebUser is now session-aware. - Better Auth mounts at /api/auth/* and owns four remote_agent_auth_* tables (Postgres only). The mount falls through for Archon-owned /api/auth/status and /api/auth/github* paths so they are not shadowed. - Add a role column to remote_agent_users (default 'admin') and a non-enforcing ?mine filter on the runs + conversations lists. - Web UI: login/signup page + session gate (shown only when enabled via GET /api/auth/status) + "signed in as" / sign out. Fully opt-in and Postgres-only (DATABASE_URL + BETTER_AUTH_SECRET). SQLite and solo installs are completely unchanged. Visibility stays open; role and ?mine are the scoping seam, not a boundary. * fix(auth): address multi-agent review findings on web auth PR Review fixes for the opt-in Better Auth web login: - LoginPage: declarative <Navigate> instead of navigate() in the render body (fixes the form-flash + Strict-Mode double-fire); consistent staleTime on the auth-status query. - getAuth(): guard buildAuth() construction (log + cache null) so a bad DATABASE_URL surfaces as a clear log line instead of a swallowed "session resolve failed"; add closeAuth() to release the dedicated pool on shutdown; parse the email allowlist once at construction. - resolveAuthContext/requireWebUser: return role: UserRole (not string); export UserRole through @archon/core; add request path to the soft-path catch logs and document the warn-vs-error posture. - mine query param: z.enum(['true','false']) to make the boolean contract explicit. - Tests: ?mine=true with no identity returns all (runs + conversations); session-throws falls through to the header; member role round-trip. - Docs/comments: correct "created only when web auth is enabled" (the auth tables are always created on Postgres) in the migration + CLAUDE.md; fix the /api/auth/status fallthrough comment; database.md (16 tables, role column, migrations 021/022); security.md, docker.md, configuration.md web-auth framing; auth-client/SessionGate comments. Deferred (out of scope / over-engineering): regenerating api.generated.d.ts (would pull unrelated drift; hand-written web types are correct), extracting a shared resolver from requireWebUser (intentional soft-vs-strict split, already documented), and a startServer integration test for the route-shadowing guard. * fix(auth): gate /api/* server-side + safe-by-default signup when web auth enabled Two security closures so enabling web auth actually protects the app instead of just adding a login screen. Both gated on isWebAuthEnabled() → zero change for solo/local (SQLite) installs. Finding 1 — server-side API gate (was: login UI only, API open to curl): - new isApiGateEnabled() (on by default when web auth enabled; ARCHON_WEB_AUTH_ REQUIRED=false escape hatch). - api.ts: an app.use('/api/*') gate (after CORS, reuses resolveAuthContext) → 401 without a session/identity. Public allowlist: /api/auth/* (login surface) and /api/health* (Docker healthcheck must stay reachable). /webhooks/* and /internal/* are outside /api/* and untouched. This makes Better Auth the real access boundary so the Caddy forward_auth sidecar can retire. Finding 2 — signup safe-by-default (was: empty allowlist = silent open signup): - getSignupMode() is now 3-state: allowlist | open | disabled. Empty allowlist + no ARCHON_AUTH_OPEN_SIGNUP=true → 'disabled' (login only) + a boot WARN, never silently open on a reachable URL. - instance.ts: emailAndPassword.disableSignUp when posture is 'disabled' (the allowlist hook stays as belt-and-suspenders for 'allowlist' mode). - /api/auth/status.signup gains 'disabled'. Tests: config (3-state signup, isApiGateEnabled) and api.auth (6 gate cases: off→reachable, on→401, /api/auth + /api/health public, session/header pass). Docs: .env.example, CLAUDE.md, security.md (the flip-the-sidecar runbook), configuration.md. bun run validate green. Out of scope (unchanged): resource visibility/ACL scoping, role enforcement — ?mine stays non-enforcing; this is access-gating only. * fix(auth): address re-review of /api/* gate (web signup UX, doc/comment accuracy, hardening) Addresses the multi-agent re-review of commit 2b5c9b6. I1 (web UX bug): web AuthStatus.signup was missing 'disabled' — the server now defaults to disabled, so LoginPage showed the signup affordance for a registration the server would only 403. Add 'disabled' to the type and hide the signup toggle when signup is off. I2/I3: stale comments — 'empty allowlist means open signup' (now disabled by default) and 'never an access gate' (resolveAuthContext now backs the /api/* gate). I4: the gate trusts X-Archon-User; the sidecar-retirement runbook now warns to keep stripping that header at the proxy (or bind 127.0.0.1), plus a SECURITY note at the gate. I5: /api/stream/* (SSE) is under /api/* and is now gated — runbook corrected. I6: docker.md no longer claims 'allowlist-gated accounts' (default is disabled); notes the gate can retire the forward_auth sidecar. S1: defense-in-depth — re-check signupDisabled inside the create.before hook. S2: tests — assert 401 on gate-on + session-throws + no-header (pins fail-closed); assert signup: 'disabled' on /api/auth/status; fix the status-test default to the real 'disabled' posture. S3: structured boot warn key web_auth.signup_disabled_no_allowlist. S7: document allowlist-beats-ARCHON_AUTH_OPEN_SIGNUP precedence in getSignupMode. Deliberately skipped: S4 (exempt-prefix asymmetry — harmless, no bare /api/auth endpoint), S5 (web AuthStatus dedup — tied to deferred types-regen), S6 (hard-fail boot — trades fail-closed-but-up for fully-down on a transient DB hiccup). * fix(web): disable declaration emit for the web SPA (fixes docker-build TS2742) The Docker web-build stage (tsc --noEmit, root tsconfig has declaration: true) ran the declaration-portability check and failed with TS2742 on Better Auth's hash-named internal dist chunks under a hoisted node_modules — only reproducible in the flat Docker install layout, not the local isolated one. The web package is a leaf SPA bundled by Vite and is never emitted or consumed as .d.ts, so override the inherited declaration emit off (declarationMap follows — it can't be set alone). Real type errors are still caught by tsc --noEmit; only the declaration check that produced TS2742 is skipped.
…ed agent chat, manage_run native tool (Claude + Pi) (#1819) Promotes the console experiment surfaces and ships the cross-provider-foundation manage_run native tool (Claude + Pi), project-scoped agent chat, and inline approvals in the workflow dock. Includes multi-agent review fixes (short-id prefix resolution, confirm gates, honest resume/cancel wording, error surfacing).
Normalize Codex output_format JSON Schemas for OpenAI Structured Outputs strict-mode: inject additionalProperties:false on every object node, warn when an open-record subschema is closed, and narrow the normalizer's types. Claude and other providers untouched. Closes #1843
Resolve markdown command files reached through symlinks during command discovery. Closes #1501
Detect Claude session-limit notices surfaced in AI node output so they are handled explicitly rather than treated as normal completion. Closes #1844
* docs: sync workflow catalogs and add missing workflows - Add 9 missing workflows to essential-workflows.md - Sync workflow tables across overview.md, guides/index.md, README.md - Fix spelling: revertable → revertible in archon-architect - Update README workflow count from 17 to 19 - Remove archon-test-loop-dag from user-facing catalogs (test fixture) Closes #1835 * feat(docs-web): add llms.txt plugin for AI-tool ingestion Add starlight-llms-txt plugin to generate LLM-friendly documentation endpoints at /llms.txt, /llms-full.txt, and /llms-small.txt. - Configure plugin with exclude, customSets, promote/demote, minify - Create topic-based subsets: quick-start.txt, adapters.txt, reference.txt - Add post-build script for Unicode normalization (browser compatibility) - Reduce llms-small.txt from 8,370+ to 5,474 lines (-35%) - Add "For AI Tools" section in reference docs - Add llms.txt mention to README Closes #1379 * fix(docs-web): address PR review feedback - I1: Fix bare catch {} that swallowed all errors, not just ENOENT - Move for loop outside try block so I/O failures are fatal - Only catch ENOENT (expected when no customSets configured) - S1: Use bun run instead of node for consistency with repo conventions - S2: Move starlight-llms-txt from devDependencies to dependencies - S3: Remove dead 'reference/changelog' exclude entry (no such file) - S4: Re-order Quick Reference table to match canonical README order - S5: Simplify normalizeFile() - compare strings directly, drop test() flag - S6: Add warning when no llms*.txt files found (plugin may be disabled) Addresses review by @Wirasm * fix(docs-web): regenerate bun.lock to match package.json starlight-llms-txt was recorded under devDependencies in bun.lock but is declared as a regular dependency in package.json. That mismatch made `bun install --frozen-lockfile` fail on every CI job (test ubuntu/windows and docker-build). Regenerated the lockfile so manifest and lock agree. --------- Co-authored-by: Rasmus Widing <rasmus.widing@gmail.com>
* feat(docs): add marketing landing page above docs site - Add self-contained landing at / built on the Archon brand system (Geist + Geist Mono, duotone magenta->violet->teal gradient, cool charcoal surfaces); hero, feature cards, how-it-works, CTA, footer - Relocate docs home from / to /docs/ (rename index.mdx -> docs.mdx) so the landing can own the root - Repoint 'Docs' nav links in roadmap and workflows pages to /docs/ * docs: address landing-page review feedback - README: point the Docs badge and 'Full documentation' link at /docs/ (the root now serves the marketing landing, not the docs home), and drop a pre-existing stray /docs/ from the AI Assistants binary-path link - landing: add og:image for link previews; extract a --font-mono token so the Geist Mono stack is declared once instead of five times; correct the brand-token comment (a subset of app.css, not a full mirror)
…manage-run skill, orchestrator delivery (#1853) Adds cross-provider workflow run management via the CLI: new `workflow get`/`workflow runs`, `--json` on approve/reject/abandon/resume, `--detach`, a bundled manage-run skill, and a run-management section in the orchestrator system prompt for non-native-tool providers. Includes multi-agent review fixes: --json stdout safety (logger silent), `workflow get` exit codes, no-silent-failure on verbose events, fd-leak guard, scopeFallback flag, doc accuracy, and added tests.
…runs (DB-tail poller + Postgres LISTEN/NOTIFY) (#1861) * feat(server): live console updates for out-of-process (CLI) workflow runs The __dashboard__ SSE stream that drives the console's Workflow dock was fed only by the server's in-process WorkflowEventBridge. Runs started in a separate process — the archon CLI, especially `workflow run --detach` — write events to the workflow_events table but never reach the server's emitter, so the dock reflected them only on refetch, never live. Add a server-side event source that tails the events table and replays new rows to __dashboard__, covering any process that writes events to the DB: - DashboardEventPoller (SQLite + Postgres baseline): tails workflow_events with a created_at cursor (id is a UUID, no monotonic int). Uses `created_at >= cursor` + a boundary seen-id set so SQLite's 1-second CURRENT_TIMESTAMP ties aren't skipped; duplicate emits are harmless (the dock reacts by refetching, idempotent). Gated on hasActiveStream('__dashboard__') and keeps the cursor fresh when idle so a late-connecting client doesn't replay history. - Postgres LISTEN/NOTIFY real-time push: an AFTER INSERT trigger pg_notify()s, and a PgNotifyListener wakes the poller to drain immediately (notification carries no payload — cursor/mapping/dedup stay in one place, so a dropped/coalesced notify can't desync). Trigger is Postgres-only (applied in PostgresAdapter.initSchema, never the shared schema — SQLite can't run it). Reconnects with backoff; the poller's backstop interval reconciles anything missed. - Dialect-selected wiring: SQLite polls @1.5s; Postgres polls @10s backstop + NOTIFY. - mapWorkflowEventRow: persisted event_type → the dashboard's workflow_status/dag_node shapes (incl. step_*); skips high-frequency tool_* and internal markers. - idx_workflow_events_created_at added to both schema paths (migrations + sqlite.ts). - DbNotificationListener: narrow capability interface (ISP) implemented by Postgres only. No frontend changes — the dock already reacts to __dashboard__ events. * fix: address PR #1861 review (C1 SQLite dead-path + hardening + tests) Critical: - C1: the poller was silently inoperative on SQLite (the default DB). SQLite stores created_at as datetime('now') ("YYYY-MM-DD HH:MM:SS") and compares TEXT lexicographically, but the cursor bound .toISOString() ("…T…Z") which sorts below it (space < 'T'), so created_at >= cursor matched nothing. listWorkflowEventsSince now formats the cursor to the stored shape per dialect (toDbDateParam). Added a real-:memory:-SQLite regression test (the mocked unit tests masked this). Hardening: - I1: listen() releases the held client if LISTEN setup throws (no pool-slot leak). - I2: per-row JSON.parse — a malformed data column degrades to {} instead of throwing the whole batch and freezing the poller cursor. - I3: poller escalates warn → error after N consecutive failed drains. - I4: the listener's unsubscribe catches log at debug instead of swallowing silently. - I5: dropped the dead `destroy === false ? true : destroy` ternary. - Race: guard the stop()/connect() race so a listen() resolving after shutdown doesn't leave a live subscription. - S5: the pg_notify trigger installs in its own best-effort step — a role without CREATE TRIGGER degrades to poll-only instead of failing boot. Overflow: filter listWorkflowEventsSince to the dashboard-relevant event types (DASHBOARD_SOURCE_EVENT_TYPES), keeping high-frequency tool_* out so a 1-second bucket can't realistically exceed the drain limit (avoids the >LIMIT-per-second stall without the cross-dialect precision pitfalls of a (created_at,id) tuple cursor). Polish: S2 named type guard for the notification capability; S3 typed SSE payload shapes; S4 narrow DashboardTransport interface (removes test casts); S6 docstring. Tests: real-row SQLite regression (C1) + event-type filter + comparison direction + malformed-data (I2); multi-second cursor advancement + boundary dedup (T1); drain coalescing (T3); mapper branches incl. approval_received / error fields / node_skipped_prior_success / loop (T4); listener reconnect-with-backoff (T2). Docs: __dashboard__ now covers CLI/--detach runs (web.md); the created_at index + Postgres trigger (database.md); table count 10 → 16 (architecture.md).
…uns (#1865) The console Workflow dock now updates live for detached/CLI runs — a server-side poller tails the workflow-event table and replays new rows to the console feed (PostgreSQL NOTIFY trigger pushes within the second; SQLite picks them up on its short interval), shipped in #1861. The skill's Console UI note still told users detached runs don't stream live and to refresh manually; corrected to describe the live behavior.
Pure resolver for the model-alias tier system: buildAiProfile + resolveModelSpec in @archon/workflows, small/medium/large ladder with universal fallback orders, @Custom aliases, seeded tier-defaults.json, and the aliases: config key. Resolver only — execution wiring + cross-provider tiers tracked in #1872.
… codex skills capability (#1868) * feat(cli): install bundled skills into Codex .agents/skills/ + enable codex skills capability (#1803) archon skill install previously only wrote to .claude/skills/. Codex auto-discovers skills from .agents/skills/ — its canonical project-level skill path — so the bundled archon + manage-run skills were invisible to Codex. CODEX_CAPABILITIES.skills was also false, triggering a spurious dag-executor warning whenever a Codex node declared skills. Changes: - skill.ts: copyArchonSkill now writes the same bundled archon + manage-run files to .agents/skills/ alongside .claude/skills/; install output mentions both Claude Code and Codex - providers/codex/capabilities.ts: skills: false → true (Codex supports skills via filesystem auto-discovery) - skill.test.ts: assert .agents/skills/archon + manage-run get every bundled file; assert success message mentions Codex - setup.test.ts: each copyArchonSkill test now also verifies the Codex path is written and overwritten - providers/codex/provider.test.ts + dag-executor.test.ts: update capability matrix and invert the now-stale "Codex warns on skills" test to assert no warning is emitted - docs (reference/cli.md, guides/skills.md): document the dual install path, explain Codex's filesystem auto-discovery semantics vs Claude's per-node AgentDefinition injection, update troubleshooting Fixes #1803 * fix: address review findings from PR #1868 - Update copyArchonSkill JSDoc to mention both .claude/skills/ and .agents/skills/ destinations - Clarify fileCount log as "per destination" to avoid confusion about total files written - Add inline comment to CODEX_CAPABILITIES.skills explaining filesystem autodiscovery vs per-node injection - Update CLAUDE.md DAG-node skills description to reflect Codex auto-discovery support - Add .agents assertion to error-path test for symmetry - Update setup.ts completion summary to show both Claude and Codex skill paths - Add Skills subsection to Codex section in ai-assistants.md * simplify: reduce complexity in changed files
* Fix: unreadable active sidebar link on mobile docs (#1776) Active page link in the mobile Starlight sidebar rendered as white text on a bright light-blue background (contrast ~1.78:1). Root cause: our --sl-color-accent-high: #93c5fd override in :root propagates to Starlight's --sl-color-text-accent (which it uses as the active item background-color), and dark mode never re-overrode it. A prior partial fix set the text color to white, which made the contrast worse rather than better. Changes: - Override --sl-color-text-accent (semi-transparent purple) and --sl-color-text-invert (white) inside [data-theme='dark'] so the active link renders dark purple with white text (~8.5:1 contrast). Fixes #1776 * docs: add upgrade-warning comment to --sl-color-text-accent override --sl-color-text-accent is a global Starlight token; note the mobile sidebar active-link intent so the override is re-tested after upgrades, consistent with the existing warning comment at the selector below. * simplify: use 'white' instead of verbose hsl(0, 0%, 100%)
…ar metadata (#1851) * feat(workflows): typed artifacts — output_type + engine-written sidecar metadata Foundation slice of the context-passing epic (#1847). Lets a node declare a semantic `output_type`; when set, the executor persists the node's output as a typed sidecar artifact so other nodes and later runs can locate it by type instead of guessing filenames. Foundation for cold-resume pointer recovery (#1846) and selective/by-reference context passing (#1848). - schema: optional `output_type` on dagNodeBaseSchema (general field — valid on every node type incl. bash/script; NOT added to *_AI_FIELDS), threaded through the transform's `base` group so it reaches all variants. - artifacts-index: new helper writes `nodes/<id>.md` + per-node `<id>.meta.json` (no shared index file → no parallel-layer write contention; index derived on read by globbing). `readNodeArtifacts`/`latestNodeArtifactOfType` for lookup. Corrupt/missing entries are non-fatal (skip + warn). Node id sanitized to a safe path segment. - executor: best-effort sidecar write at the single layer-result funnel (covers all node types); a metadata write never fails an otherwise-successful node. - nodeArtifactSchema in schemas/ (named to avoid clashing with the unrelated workflow-event artifactTypeSchema). Opt-in and additive: default behavior is unchanged when no node sets output_type. Tests: artifacts-index unit suite (write/read/latest/corrupt-skip/path-escape); dag-executor integration (typed node writes sidecar; untyped node writes none). Validation: bundled gates, all-package type-check, lint --max-warnings 0, format:check, full @archon/workflows test suite — all pass. * fix(workflows): harden typed-artifacts per PR review Address review findings on the typed-artifacts feature. All changes are additive/local; behavior is unchanged for nodes that don't set output_type. Correctness / invariants - node-artifact schema: producedAt -> z.string().datetime() so the lexicographic "latest" sort in latestNodeArtifactOfType can't silently return the wrong artifact for a corrupt/non-ISO value (rejected on read); size -> .int().nonnegative(); outputType -> .min(1) (matches output_type on the node config and the sibling artifactFileSchema). - writeNodeArtifact: collision guard. Two distinct node ids that sanitize to the same filename segment (e.g. `a.b` / `a_b`) now throw instead of silently overwriting — first writer wins, the loser is logged via the best-effort caller. Re-writing the same id (resume) still overwrites without error. Error handling (fail-fast) - readNodeArtifacts: only swallow readdir ENOENT ("no artifacts yet"); a real EACCES/ENOTDIR/EIO now warns + rethrows instead of masquerading as empty, matching script-discovery / workflow-discovery precedent. - index_entry_invalid warn now includes the Zod issues so schema drift is debuggable. Tests - best-effort write failure is non-fatal (node still completes when the sidecar write throws). - bash node with output_type writes a sidecar with no sessionId. - assert sessionId propagation in the AI-node integration test. - artifacts-index: collision throws, same-id overwrite, schema-invalid skip. Simplification - writeNodeArtifact params -> Omit<NodeArtifact, 'path' | 'size'> (DRY). - drop the redundant call-site conditional-spread for sessionId (the guard lives inside writeNodeArtifact). - latestNodeArtifactOfType: reduce -> explicit loop. - correct the misleading "concurrent contention" rationale in the JSDoc (writes are sequential post-allSettled; per-node files isolate nodes/runs). Docs - CLAUDE.md: output_type in the node-field list, nodes/ in the artifacts layout, node-artifact in the engine-schema list. - authoring-workflows guide: output_type row in Common fields, a Typed Artifacts subsection, and a quick-reference entry. Deferred (with rationale) - No user-facing chat warning on best-effort write failure: there are no consumers of these artifacts yet (cold-resume/context-passing land in #1846/#1848); a chat message per miss would be noise. Revisit with the first latestNodeArtifactOfType caller. - Did NOT constrain the global node `id` charset to fix the collision: that touches every workflow and could reject existing definitions — out of scope for this PR. The local write-time guard covers it without a breaking change. - Kept the lazy logger (review suggested removing it): dag-executor.test.ts mocks @archon/paths createLogger and imports this module transitively, so the deferral is load-bearing for test isolation. - AGENTS.md mirror skipped: it doesn't exist on this branch (added to dev after the branch forked); the 1:1 copy syncs when the branch catches up.
…lizer (#1878) * fix(web/console): gate /console behind auth + correct run-event normalizer Two correctness/security fixes that block the console from replacing the old chat + dashboard UI. Security: /console/* mounted OUTSIDE SessionGate, so with web auth enabled the console was reachable without login while every other route is gated. Wrap it in SessionGate — still outside Layout so it keeps omitting TopNav. SessionGate is a passthrough when web auth is disabled (the solo default), so this is a no-op there. Correctness: the run-event normalizer read keys the server never writes — - node duration read `duration`, but the row carries `duration_ms`, so every rendered node duration was null; - approvals checked `approval_pending`/`approval_resolved` with a `resolution` key, but the server writes `approval_requested`/`approval_received` with `decision` + `comment`/`reason`, so approvals fell through to the raw-JSON text fallback; - `node_skipped_prior_success` (emitted on resume for already-done nodes) was dropped entirely. Also carry the node_completed enrichment already in the payload (output preview, cost, stop reason, turns) for the upcoming per-node detail view. Adds the first test under experiments/console and wires src/experiments/console/ into the web test script so CI starts covering the console as it is promoted. * fix(web/console): address PR #1878 review (I1/I2 + S1/S2/S4/S5/S6 + docs) I1 approval_received now matches `decision` explicitly — an unknown/missing decision stays unresolved (null) instead of silently rendering as approved (the exact silent-mismatch class this normalizer set out to kill). I2 reworded the approval comment to future tense and noted that nothing renders approval events in the run stream today (paused gates use run.approval metadata). S1 transition mapping → NODE_TRANSITION_BY_EVENT record lookup (flatter, shows the whole map; node_failed kept explicit so its test guards the default). S2 enrichment JSDoc: dropped the "per-node accordion (follow-up)" forward reference; stated the completed-only null contract instead. S6 App.tsx comment: SessionGate is a passthrough after a brief (cached) auth-status check, not a literal no-op. S4 added a node_failed test (guards the map vs the `?? 'skipped'` default). S5 added error (error>message priority), workflow_*→system, and nodeName fallback tests; plus an I1 guard test (unknown decision → unresolved). docs narrowed the stale "CI does not guarantee these routes work" line in experiments/README.md now that console primitives run in CI. Skipped with rationale: S3 discriminated-union-on-transition refactor (defer to the PR that renders the fields); argsSummary dead-read cleanup (pre-existing, no consumer yet); a SessionGate automated test (needs jsdom/testing-library infra not present in @archon/web — a future SessionGate unit test is the right target).
…ng (#1877) (#1879) * Fix: reuse Pi extension loader per process so 2nd+ session doesn't hang (#1877) With Pi extensions enabled (the default), the 2nd and every subsequent sendQuery() per process hung in session setup and idle-timed-out after 30 min, breaking every Pi workflow with 2+ AI nodes. Pi's reload() re-invokes every installed extension factory from scratch; that process-scoped state is never torn down between calls (dispose() never emits session_shutdown), so the 2nd reload() deadlocks on the first call's still-live state. Changes: - Add getOrCreateReloadedExtensionLoader(): a process-level cache that builds and reload()s the extension-bearing DefaultResourceLoader once per (cwd, systemPrompt, skillPaths) and reuses it across sendQuery calls. createAgentSession with a supplied loader skips its own reload(), so reuse is safe; each call still gets its own session + bindExtensions(session_start). - provider.ts: use the cache on the extensions-on path; keep a fresh per-call loader on the extensions-off path (no reload(), no re-entrancy hazard). - Add regression tests: reload/construct run once across identical sequential calls; distinct cwd or systemPrompt each reload once; extensions-off never caches and never reloads. Fixes #1877 * docs(pi): document loader-cache growth bound and why eviction is omitted (#1877) Address self-review feedback on the unbounded-growth concern: clarify the cache is bounded by distinct (cwd, systemPrompt, skillPaths) tuples and explain that naive size-cap/LRU eviction is unsafe — dropping an in-use loader would make a running workflow's next node reload a second copy of the same extensions while the first is live, re-introducing the deadlock the cache prevents. * Address ultrareview findings on the Pi loader-cache fix (#1877) - I1: test the primary documented invariant — concurrent same-layer callers share a single in-flight reload() (gated mock + Promise.all; asserts one construct/one reload/same loader). Caching the resolved value instead of the Promise would fail this. - I2/S1: trim the docblock's unverifiable agent-rooms specifics (keep the general "factories build process-scoped singletons" mechanism) and drop the Pi SDK `sdk.js` build-artifact filename that will rot on restructure. - I3: correct the inline comment — setFlagValue itself unconditionally sets; the no-op comes from the provider's `if (runner)` guard, skipped when the runner is undefined. - S2: test failure-eviction — a rejected reload evicts its cache entry so the next call retries cleanly instead of returning a poisoned promise. - S3: rethrow reload() failures with an actionable pointer (extensions dir / enableExtensions: false), preserving the original error as `cause`. - S4: build the shared loader options once, then dispatch — removes the duplicated spread across the if/else branches. - S6: test additionalSkillPaths keying (distinct skill sets → distinct loaders). Deferred S5 (positional key fn → Pick<>): the public helper already uses Pick<> to document the keyed fields; the 3-line internal key fn stays explicit.
…ns (#1881) * feat(web/console): show run provenance — input message + platform icons Console run cards/rows/detail showed status, workflow, cost, and elapsed but never what input started a run — the prompt was parsed into the Run primitive (toRun) yet rendered nowhere, and the origin badge was text-only. This surfaces it: - OriginBadge: a Lucide icon per platform (web/cli/slack/telegram/discord/github; none for unknown), mirroring the old dashboard's PLATFORM_ICONS. - ActiveRunCard: an `input` row folded into the activity grid (truncated, full text on hover) for running + paused cards. - RecentRunRow: the message truncated inline beside the workflow name; the row stays h-9 (title tooltip for the full text). - RunStartedLine: a blockquote input line under "Workflow X started". - RunDetailHeader: a full-width `input` sub-row. - run.test.ts: first test for the primitive — normalizeOrigin (all platforms + case-insensitive + unknown fallback), userMessage default, pending→running, conversationPlatformId passthrough. (normalizeOrigin is now exported.) Deviation from plan (documented): the "from chat →" link is DEFERRED, not built. The console has no conversation-deep-link route — ChatPage is mounted at p/:projectId/chat and manages its active conversation internally, so a chat link has no valid destination. Per the plan's "do not invent a route" constraint, the link AND the workerPlatformId/parentPlatformId fields that only existed to feed it were dropped (they'd be unread data). This ships the unambiguous wins that fully address the stated need ("what did I enter… cli/chat"); the link moves to a follow-up once console chat supports conversation deep-linking. * fix(web/console): address PR #1881 review (I1-I3 + S1-S5) I1 RecentRunRow layout regression: when userMessage is empty the workflow name now fills the full width (flex-1) again instead of being capped at 55% with blank space + needless truncation; capped at 55% only when a message shares the line. I2 ActiveRunCard comment: "always" → "when present" (the input row is gated on a non-empty message). I3 OriginBadge comment: "mirrors" → "extends" — the icon map is a 7-origin superset of the old 5-entry map, adds discord, and renders no icon for unknown (vs the old Globe fallback). S1 Extracted a local `hasValue` type-guard to replace the repeated null/undefined/'' chains on currentNode/lastTool (also narrows to string). S2 Named the grid guard `showDetailGrid` (matches the file's elapsed/canOpen idiom). S3 RecentRunRow: rerun-param guard uses `!== ''` to match the render guard. S4 Backfilled run.ts coverage: unknown-status→running, readCost (positive, the $0.00 >0-guard, and non-numeric→null), and approval-metadata parsing (well-formed, message default, malformed→null). 29 → 36 console tests. S5 RunDetailHeader: the deferred "from chat" link now has a tracked anchor — TODO(#1882) (filed the follow-up issue).
…vider tiers (#1873) Wires the model-alias resolver (PR #1867) into execution: cross-provider `tiers:` config key, aiProfile threaded through executeWorkflow → executeDagWorkflow → resolveNodeProviderAndModel (loop nodes included), warn-on-conflict, per-provider effort routing, internal call-sites (chat=large, title=small), and bundled defaults retrofitted to tiers-only with a validate gate. Closes #1872.
… + repair + fail-fast (PR 1/2) (#1883) * feat(workflows): reliable cross-provider structured output — validate + repair + fail-fast (PR 1/2) Keystone of the cross-provider structured-output plan (#1849). Makes output_format handoffs trustworthy across every provider and removes two silent failures. - Schema validation for ALL providers: parsed structured output is validated against the node's declared JSON Schema (ajv) — even SDK-enforced providers (Claude/Codex/OpenCode) need this net for refusal / max_tokens-truncation edges. - JSON repair tier: jsonrepair recovers trailing commas, single quotes, unquoted keys, and truncated tails (gated to object-shaped input + object-only results so it can't coerce prose into bogus arrays). - Honest capability tier: ProviderCapabilities.structuredOutput is now 'enforced' | 'best-effort' | false (was a meaningless boolean true for all five). Claude/Codex/OpenCode = enforced; Pi/Copilot = best-effort. Threaded through the OpenAPI schema and regenerated web type. - Fail-fast (no silent degrade): a node that declares output_format but returns no schema-valid structured output now FAILS instead of completing with poisoned prose. Routed through the existing per-node catch. - No-silent-drop field access: $node.output.field resolution is strict — a field not in the producer's declared schema, or a schemaless node whose output isn't JSON / lacks the key, throws (fails the consuming node). The only value that resolves to '' is an author-declared-optional field. Captured via NodeOutput.declaredFields; shared by prompt/script substitution and when:. Behavior changes (intended, fail-fast): schemaless bash/script nodes missing a referenced key now throw; when: field-ref errors throw (reverses #1673 fail-closed for field refs); explicit null on a declared field resolves to '' (was "null"). Bundled-default workflows audited safe (all field refs target AI nodes whose schemas declare the fields as required). Reask loop (Tasks 10-13) is PR 2. * test(workflows): e2e smokes for cross-provider structured output (PR 1) Two CLI-runnable smokes exercising the new behavior against Claude (enforced): - e2e-structured-output (positive): output_format validation (Task 7), declared field access, declared-OPTIONAL absent → '' (no throw), schemaless bash JSON field access. Completes successfully. - e2e-structured-output-failfast (negative): a $node.output ref to a field NOT in the producer's schema fails the node with an OutputRefError (Task 9 no-silent-drop) instead of silently resolving to ''. Both verified end-to-end via `bun run cli workflow run ... --no-worktree`. * Address ultrareview findings on structured-output PR 1 (#1883) Comment/doc accuracy (the contract-inverting set — highest value): - I1: structured-output.ts module + tryParseStructuredOutput docstrings no longer say "degrades via warning" (now fail-fast); "three failure modes" → four (jsonrepair). - I2: types.ts + pi/copilot capability comments stop describing the reask loop as present (it's PR 2) — now "validate; reask = PR 2". - I3: condition-evaluator module docstring distinguishes fail-closed-skip (bad syntax) from throw-and-fail (.field ref that can't resolve). - I4: docs updated where now-wrong — dag-workflows.md ("fails open" → the two real modes), ai-assistants.md (Pi/Copilot "degrades to warning" → validates + fails), authoring-workflows.md (output_format validation + strict field access), CLAUDE.md/AGENTS.md (output_format parenthetical + structuredOutput tiered union). - S7: condition-evaluator return-'' comment corrected (lenient null → "null"). Code: - I5: tryJsonParseObject is object-only across all tiers (tier-1 no longer returns bare arrays); removes the tier-1-accepts / tier-3-rejects inconsistency. Tests aligned. - I7: uncompilable output_format schema now surfaces a user-facing warning (not just a log) — the silent validation-bypass is no longer silent. - S2: dropped "// Task 7/8 —" planning labels (kept the prose). - S3: StructuredValidationResult is a discriminated union ({valid:true} | {valid:false; errors}). - S4: $node.output.field on a skipped/pending producer throws a clear 'producer-not-run' OutputRefError instead of a misleading "not a JSON object". Tests: - I6: executor integration test — a when: ref to a field not in the producer's schema FAILS the node (regression guard against a silent skip). - S8: declaredFieldsFromSchema(properties:null) + producer-not-run cases. Deferred (with rationale): S1 (keep `false` tier — plan-specced, honest "none" state), S6 (unknown-node throw — direction/scope expansion beyond field resolution), S8 best-effort integration mirror (PR1 fail-fast is tier-agnostic; meaningful only once PR2 adds reask), S5 (tier-3 swallow already observable at executor level; would add noise + a logger dep to a pure module), S9 (no real CLAUDE.md violation).
…-bottom (#1885) * feat(web/console): chat workflow-completion card + SSE-lock + jump-to-bottom Three changes that make the console's project-scoped chat a credible /chat replacement — all web-side; the data already flowed and was being dropped. Completion card: a chat-launched workflow's `workflow_result` message (summary + {workflowName, runId}) was swallowed because ChatStream filters the whole `workflow_` category prefix and toMessage never parsed `workflowResult`. Now message.ts parses `workflowResult` (mirroring the `dispatch` parse, both fields guarded), and ChatStream lets `workflow_result` through the filter — without un-suppressing the other workflow_* narration — and renders a new ConsoleWorkflowResultCard: status icon + node counts + duration + cost + "Open run →" + the summary body. The card fetches authoritative state via skill.getRun (same call RunDetailPage uses) and degrades to the summary alone if the run can't be loaded. SSE-lock: the composer stayed disabled ~6s (SETTLE_MS) after each reply because the SSE lock event wasn't wired. ChatPage now passes a useCallback-stable onLockChange to useConversationSSE that clears the settle timer + setBusy(false) on conversation_lock:false. The settle timer remains the correctness fallback when SSE is absent. (Stable callback is required — the hook's effect depends on it, so an inline lambda would reconnect the EventSource every render.) Jump-to-bottom: an `atBottom` state (from an onScroll handler) shows a floating button when scrolled up; kept in sync with the existing auto-scroll lastBottomRef. Tests: message.test.ts covers the workflowResult parse (well-formed, malformed→null, dispatch regression, isSystemCategory). 36 → 42 console tests. * fix(web/console): address PR #1885 review — render the card (C1) + hardening C1 (critical): the completion card never rendered its rich content. useEntity returns `error: Error | undefined` (cache.ts), so `error !== null` was always true and `run` was always null → the summary-only fallback every time. Compare against `undefined`. Verified end-to-end against a real completed run: GET /api/workflows/runs/:id returns {run, events}, so the rich path now executes (16/22 nodes computed on that run). I1: guard meta.workflowResult with `!= null` (not `!== undefined`) so an explicit JSON null can't reach `typeof wr.workflowName` and throw. +regression test. I2: countTerminalNodes deduped by nodeId — a resumed run reuses one run id and emits both the original `completed` and a later node_skipped_prior_success, which double-counted nodes. Relocated to primitives/event.ts (pure RunEvent reducer, its natural home) and unit-tested (6 cases incl. the dedup guard). I3: console.warn on the two silent swallows (getRun error path, parseMetadata catch) — the PR's whole point is to stop silently dropping completions. S1: dropped the dead `| null` from the useEntity generic (getRun never resolves to null; undefined is the cache-miss signal). S3: extracted NEAR_BOTTOM_PX for the duplicated 120px scroll threshold. S5/S6: corrected the card JSDoc (loading also degrades to summary) and documented why ParsedMetadata.workflowResult stays inline/untrusted. Deferred — S2 (overeng): kept RESULT_GLYPH/RESULT_LABEL as Partial<Record> plus a clarifying comment rather than padding them to a total record with running/paused entries a completion card never displays. Validation: web type-check / lint / format:check clean; 51 console tests pass.
…1890) * feat(web/console): settings core — assistant config + system panel Adds the console's first settings surface (/console/settings, global) — the parity floor before cutover. PR #4 of the console sequence (after #1878/#1881/#1885). - skills/settings.ts: getConfig/updateAssistantConfig/getHealth/getUpdateCheck plus the pure buildAssistantUpdate(form) transform (8 unit tests). skills/providers.ts: listProviders. Types from @/lib/api.generated (console isolation boundary). - store/keys.ts: config/health/providers/updateCheck keys (health reuses the literal 'health' so it shares lib/health's cache entry). - lib/health.ts: full HealthResponse + useHealth(); useIsDocker derives from it. - AssistantConfigPanel: default-assistant picker (registered providers) + free-text model per provider + codex reasoning/web-search; dirty-gated Save → PATCH /api/config/assistants → ~/.archon/config.yaml → invalidate(K.config) re-seeds. Model is free-text for every provider (Archon does not validate model strings). - SystemPanel: status/adapter/db/version, concurrency (active/maxConcurrent, coerced defensively — concurrency is an open record), running workflows, platform badges, update-check. - ConsoleApp: /console/settings route, gear header link, ',' global keybinding; shortcuts.ts catalogue entry. Honors the error-is-undefined cache contract throughout (the #1885 gotcha). Excludes the GitHub device-flow panel (PR #5) and env-var editing (project-scoped). Validation: web type-check / lint / format:check clean; 59 console tests pass (8 new). Verified end-to-end on an isolated server: read APIs return the expected shapes, save round-trips to config.yaml (binary paths preserved via the server deep-merge), and a browser smoke of /console/settings renders both panels (5 providers, codex effort/web-search, system grid, Save dirty-gated). * fix(web/console): address PR #1890 review — comments + update-check silent failure I1: correct the buildAssistantUpdate JSDoc. Verified the PATCH route does NOT safe-filter per field on the write path — it validates only provider ids and merges the body into config.yaml unfiltered (safe-filtering is read-path only). The real invariant is that this function only ever attaches codex-only fields to the codex entry; the comment now says that instead of the false "server safe-filters" claim. I2: rewrite the K.health note. This PR routes lib/health through K.health, so the old "lib/health already caches under this literal" premise is stale; the invariant is that both consumers read via useHealth() to share one cache entry. I3 (silent failure): SystemPanel showed "checking…" forever on a failed update-check (the error was destructured away). Surface updateError via an UpdateStatus helper → "update check unavailable". S1: SettingsSection children typed ReactNode (ReactElement|ReactElement[] fought the `cond && <el/>` pattern). S5: replaced the nested update-status ternary with the UpdateStatus helper + early returns for the health loading/error states. S6: extracted the shared SettingsSection card shell (PR #5 is the 3rd consumer), a SELECT_CLASS const for the two codex selects, and bound activePlatforms once. Docs: added /console/settings to the console README routes. Deferred (with rationale): - S2 (re-seed on providers identity): latent only — nothing invalidates K.providers, and a ref-snapshot "fix" introduces a config-vs-providers load-order race. Keep the simple [config, providers] effect. - S3 (literal-union effort/webSearch types): kept bare string so seedForm tolerates an out-of-enum value in config.yaml; the <select> is the write-side enforcement. - S4 (show both load errors): an error panel showing the first error is acceptable. Validation: web type-check / lint / format:check clean; 59 console tests pass.
…utput (PR 2/2) (#1889) * feat(workflows): validate-and-reask loop for best-effort structured output (PR 2/2) Completes the cross-provider structured-output plan (#1849). Best-effort providers (Pi/Copilot) now get a bounded reask loop instead of failing on the first miss. - Task 10 — reask loop. When a node declares output_format AND the provider is 'best-effort' AND the parsed output is missing/schema-invalid, the executor re-runs sendQuery with the schema errors appended, up to STRUCTURED_OUTPUT_MAX_REASKS (3). Enforced providers and non-output_format nodes get 0 reasks (unchanged fail-fast). Fresh session per attempt; cost accumulates across attempts; no reask on idle-timeout/abort/SDK-error. Exhaustion → the existing fail-fast throw. Implemented by extracting the stream into a runStreamPass() closure (no body re-indent) wrapped in the reask while-loop. - Task 11 — observability. Logs dag.structured_output_reask per attempt and sends one chat notice on the first reask so auto-correction isn't invisible. - Task 13 — docs. "Structured output guarantees" matrix in ai-assistants.md. - Tests: best-effort malformed→fixed recovers within reasks (2 sendQuery calls → completed); exhaustion fails loudly (1 + 3 calls → node_failed). Registered Pi in the dag-executor test registry so capabilities resolve to 'best-effort'. Task 12 (capability tests) landed in PR 1. Depends on #1879 (Pi extension-loader reuse) — already merged — since reask fires multiple Pi sendQuery calls per process. * Address ultrareview findings on the reask loop (#1889) Functional bugs (the 3 real-but-narrow ones): - I1: reset batchMessages per attempt in runStreamPass — else a failed reask attempt's prose could flush to the user via an intermediate `msg.flush` chunk. - I2: an idle-timeout on a best-effort output_format node now throws a timeout-specific error instead of the misleading "the model replied with prose" message; corrected the inaccurate "post-loop guards handle them" comment. - I3: carry the accumulated reask cost onto nodeCostUsd every pass so the exhaustion throw path reports cost across ALL attempts, not just the last. Docs (self-contradiction + stale "PR 2" references now that reask ships): - I4: ai-assistants.md Pi/Copilot rows said reask was "planned" — now "re-asks 3×". - S4: dropped "reask = PR 2"/"lands in PR 2" from pi/copilot capabilities, the shared structured-output docstring, and the types.ts capability doc. - S5: added a reask mention to CLAUDE.md/AGENTS.md output_format + authoring docs. - S6: comment fixes (runStreamPass now states what it resets; buildReaskPrompt no longer overclaims clean prompt/schema separation). Tests: - I5: enforced provider (claude) makes exactly ONE sendQuery on a validation miss (regression guard against the best-effort gate silently allowing 4× calls). - I6: best-effort MISSING structured output (not just invalid) triggers reask and recovers. - S1: recover test asserts accumulated cost (0.01 + 0.02 = 0.03); idle-timeout on an output_format node does NOT reask (1 call, "timed out" error). - S2: extracted scheduleReask() so the two reask blocks can't drift. - S3: dropped the (Task 10) labels from the reask test names. Deferred S7 (for-loop vs while(true)): while(true) with inline throws is clearer for the two distinct exhaustion messages (invalid vs missing); a for-loop would push the throws after the loop and need failure-reason tracking.
…s authoritative (#2404)
…ck .archon fixes (#2417)
* Fix: snapshot resolved workflow run configuration (#2397) Persisted workflow_started events only recorded the workflow name, forcing observability consumers to reconstruct effective run settings from other records. Changes: - Snapshot resolved assistant, provider, model, isolation, base, user, input, and origin fields - Preserve workflowName and asynchronous persistence behavior - Add focused coverage for precedence, modes, nulls, platform origin, and child runs Fixes #2397 * simplify: reduce complexity in changed files
* Fix: retry Codex availability failures (#2386, #2425) Codex 503 circuit-breaker responses and model-capacity errors were classified as fatal or unknown, bypassing the default transient retry loop. Changes: - Treat generic auth error text as fatal only after concrete transient signals - Classify model-capacity errors as transient - Add regression coverage and update retry precedence documentation Fixes #2386 Fixes #2425 * simplify: reduce complexity in changed files
Contributor
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Wirasm
added a commit
that referenced
this pull request
Aug 4, 2026
Applied 7 of 8 findings. Testing the fixes surfaced two further bugs in the first pass that the review had not flagged. Found while testing this commit (not in the review): - The boundary grep `^Release [0-9]` matched main's SQUASH commit (`Release 0.7.1 (#2435)`) in preference to dev's own version-bump commit (`Release 0.7.1`). The squash commit's ancestry excludes dev's individual history, so the range expanded to 383 commits — the same class of failure this whole change exists to prevent. Anchored with `$` so only the bare subject matches. - `^chore(homebrew):` under --extended-regexp parses the parens as a group and matches the literal `chorehomebrew:`, silently filtering nothing. Escaped. From the review: - Implement the first-release fallback that the prose already promised. Verified that an empty LAST_RELEASE yields `..origin/dev`, which resolves against HEAD and returns zero commits — a false "nothing to release". - Move the release/homebrew plumbing exclusions into the git log invocation instead of leaving them as prose the reader must apply by hand. Anchored the Release pattern to a full x.y.z so a feature commit starting with that word is not swallowed. - Derive the previous changelog heading from CURRENT_VERSION rather than hard-coding 0.7.0/0.6, which would have rotted at 0.7.2. - Make the Step 9 and Step 10 convergence checks exit non-zero. Previously they printed divergence and continued into formula and tap publication. - Replace the formula SHA check. `grep -q "$digest" formula` only asked whether a value appeared anywhere, so swapped platform digests both passed, and an empty extraction degenerated to `grep -q ""` which matches any file. Now parses the sha256 following each platform's own URL, requires a 64-char hex digest, and compares exact values. - Assert the formula version equals the release instead of printing it. - Pin the env-leak assertion to the test repo's resolved path and an exact key count of 1. The prior regex accepted any positive count from any path. Declined: - MD028 blank-lines-in-blockquote. Nothing lints markdown in CI or package.json, and the blank lines separate distinct warnings that read worse merged. Verification: - Corrected Step 4 reproduces the real 0.7.1 range: boundary resolves to 6c6945c, and 0.7.0..0.7.1 yields 26 commits against the 25 hand-derived for the shipped changelog. The extra is ae704a7, which genuinely entered dev via the mid-release resync and is then excluded editorially as internal docs. - New formula check passes on the real v0.7.1 formula and, in a negative test with two digests swapped, fails on both — the case the old check missed. - Exact-match env-leak assertion verified against a live binary run; the `pwd -P` resolution is required because the binary logs /private/tmp where the test path is /tmp.
Wirasm
added a commit
that referenced
this pull request
Aug 4, 2026
… form, and installer env-var docs (#2437) * fix(release,install): correct the commit boundary, git pull form, and installer env-var docs Five defects found by running /release and /test-release end to end for v0.7.1. release skill: - Step 4 derived the changelog from `git log main..dev`. main squash-merges, so that range never loses previously-released commits and grows every release: it returned 61 commits for 0.7.1 when 25 were new, and the other 36 were already written into the 0.7.0 changelog. Use dev's last `Release x.y.z` commit as the boundary, exclude the two release-plumbing commit kinds, and cross-check the result against the previous version's recorded PR numbers. - `git pull origin main` aborts with "Need to specify how to reconcile divergent branches" wherever pull.rebase is unset, and would rewrite dev's history where it is set to true. Pass --no-rebase explicitly at all four call sites. - Step 1 had no guard for commits stranded on main. ae704a7 had sat there since 0.7.0; the content was hand-forward-ported via #2403 but the commit never was, so main was not an ancestor of dev. Add an ancestry check that fails closed. - Document that CI's update-homebrew job races the Step 9/10 pushes, and that a clean homebrew/archon.rb merge is not evidence the SHAs are right — both branches edit the same four lines. Verify against the published checksums. - Require flagging a bump that contradicts the range (feat: commits under patch). test-release skill: - The curl-mac snippet set INSTALL_DIR on curl rather than bash, so the installer never saw it, targeted /usr/local/bin, and failed on sudo after the checksum had already verified. - Test 4 asserted a refusal from the env-leak gate. The shipped guard strips the keys and proceeds, so the old criteria reported FAIL against a binary that was working correctly. Assert on the strip line and its key count instead. installer: - scripts/install.sh documented both VERSION= and INSTALL_DIR= on the curl side, so users copying either example silently get the defaults. Mirror in packages/docs-web/public/install updated in lockstep (test-install.sh enforces byte-equality). install.ps1 is unaffected. Refs #2436. * fix(release,test-release): address CodeRabbit review on #2437 Applied 7 of 8 findings. Testing the fixes surfaced two further bugs in the first pass that the review had not flagged. Found while testing this commit (not in the review): - The boundary grep `^Release [0-9]` matched main's SQUASH commit (`Release 0.7.1 (#2435)`) in preference to dev's own version-bump commit (`Release 0.7.1`). The squash commit's ancestry excludes dev's individual history, so the range expanded to 383 commits — the same class of failure this whole change exists to prevent. Anchored with `$` so only the bare subject matches. - `^chore(homebrew):` under --extended-regexp parses the parens as a group and matches the literal `chorehomebrew:`, silently filtering nothing. Escaped. From the review: - Implement the first-release fallback that the prose already promised. Verified that an empty LAST_RELEASE yields `..origin/dev`, which resolves against HEAD and returns zero commits — a false "nothing to release". - Move the release/homebrew plumbing exclusions into the git log invocation instead of leaving them as prose the reader must apply by hand. Anchored the Release pattern to a full x.y.z so a feature commit starting with that word is not swallowed. - Derive the previous changelog heading from CURRENT_VERSION rather than hard-coding 0.7.0/0.6, which would have rotted at 0.7.2. - Make the Step 9 and Step 10 convergence checks exit non-zero. Previously they printed divergence and continued into formula and tap publication. - Replace the formula SHA check. `grep -q "$digest" formula` only asked whether a value appeared anywhere, so swapped platform digests both passed, and an empty extraction degenerated to `grep -q ""` which matches any file. Now parses the sha256 following each platform's own URL, requires a 64-char hex digest, and compares exact values. - Assert the formula version equals the release instead of printing it. - Pin the env-leak assertion to the test repo's resolved path and an exact key count of 1. The prior regex accepted any positive count from any path. Declined: - MD028 blank-lines-in-blockquote. Nothing lints markdown in CI or package.json, and the blank lines separate distinct warnings that read worse merged. Verification: - Corrected Step 4 reproduces the real 0.7.1 range: boundary resolves to 6c6945c, and 0.7.0..0.7.1 yields 26 commits against the 25 hand-derived for the shipped changelog. The extra is ae704a7, which genuinely entered dev via the mid-release resync and is then excluded editorially as internal docs. - New formula check passes on the real v0.7.1 formula and, in a negative test with two digests swapped, fails on both — the case the old check missed. - Exact-match env-leak assertion verified against a live binary run; the `pwd -P` resolution is required because the binary logs /private/tmp where the test path is /tmp.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Release 0.7.1
Workflow runs now record what they actually resolved to — assistant, model, effort, isolation, base branch — so two runs can be told apart after the fact. Plus retry classification for transient Codex failures, and a batch of installer and CLI repairs.
Added
--baseoverride —archon workflow run --base <branch>sets the branch the worktree is cut from and the PR targets, for that run only. Base resolution was static per codebase (worktree.baseBranch→default_branch→ git auto-detect), all of which describe the repo rather than the run, so fanning out parallel dispatches against one repo with different bases had no encoding short of editing repo config. (feat(cli): per-dispatch --base override for worktree cut-from + PR target #2203)archon-parse-user-requestreplacesextract-issue-number, parsing the operator's message into structured fields — the verbatim request, issue number, repo shorthand, and repo URL — instead of a number alone. (feat(commands,workflows): archon-parse-user-request replaces extract-issue-number #2420)--verbose --jsonreturns ordered node summaries includingstartedAtby default, with stable ordering for tied timestamps, so machine consumers no longer have to recreate the CLI's node-state fold or filter tool-event noise. (feat: summarize verbose workflow JSON nodes #2414)Changed
workflow_startedpreviously carried only the workflow name, forcing consumers to reconstruct run context from other records. It now records the resolved assistant/provider/model, isolation mode, base branch, and persisted user/input context, so a run is classifiable from a single durable event. (Snapshot resolved workflow run configuration #2428)tool_calledandtool_completednow carry the resolvedtool_call_id, a structuredtool_outcome, and an optionalexit_codethrough persistence, SSE, and CLI — so repeated or interleaved tool calls can be paired, and per-invocation latency and failures are traceable. (Correlate workflow tool lifecycle events #2421)node_startedwas persisted, leaving two runs indistinguishable when effort was their only material difference. (Record resolved workflow effort in node events #2415)archon completecounts only commits reachable solely from the refs being deleted. Dev-based worktree branches holding no unique work are no longer blocked behind--force, which would also have bypassed unrelated safety checks. (Protect unique commits during worktree completion #2416)Fixed
classifyErrortestedFATAL_PATTERNSfirst, and that list held the bare substringauth error— so Codex's circuit-breaker textauth error: 503classified FATAL and never reached the transient check two lines below. Because FATAL is an absolute veto,on_error: alldid not rescue it either; there was no author-side escape hatch. Classification is now three-tier: decisive fatal evidence (explicit credentials, authorization, quota and limit windows) first, transient second, and the ambiguousauth errorwrapper last. Separately,Selected model is at capacitymatched neither list and fell through toUNKNOWN; it is now transient. (fix: retry transient Codex availability failures #2434, closes classifyError fatal-pattern precedence misclassifies transient codex 5xx as FATAL, bypassing DAG retry #2386 and fix(workflows): "Selected model is at capacity" classifies as UNKNOWN and never retries #2425)event_order— the index and trigger ran before theALTERthat adds the column. (fix(db): SQLite upgrade broke on event_order — index/trigger ran before the ALTER #2418).archon/itself. (fix(workflows,commands): preserve repo identity for issue URLs; unblock .archon fixes #2417)--jsonoutput no longer truncates mid-stream. (Fix piped workflow JSON output truncation #2389)workflow resumeguidance in the CLI is correct. (Fix CLI workflow resume guidance #2422)archon doctorhonors theclaudeBinaryPathconfig fallback. (fix(cli): doctor honors claudeBinaryPath config fallback (#2263) #2275, doctor: Claude binary check ignores the assistants.claude.claudeBinaryPath config fallback (false FAIL) #2263)Merging this PR releases 0.7.1 to main.