You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This commit was created on GitHub.com and signed with GitHub’s verified signature.
[0.1.7] - 2026-05-12
Added
Pre-return answer lift for delegate sub-agents.createDelegateTool's execute in packages/core/src/delegate/index.ts now routes every successful answer (whether emitted as plain text or via legacy finish({ok:true, answer})) through the new shared liftAnswerForModel walker in packages/core/src/artifacts/scrubber.ts before returning. Answers under the existing 8 KiB TEXT_THRESHOLD_CHARS pass through verbatim; over-threshold answers are uploaded as a text/csv / text/markdown / etc. artifact (MIME sniffed via sniffTextMime) and the returned answer becomes the [attachment lifted to artifact …]REF_MARKER_PREFIX marker text. Persistence's existing REF_MARKER_PREFIX short-circuit then skips re-lifting at the persist boundary, so a single artifact represents the answer end-to-end. The supervisor's next step references the artifact via display_artifact instead of re-emitting the prose it already saw stream, cutting the double-decode cost on document-shaped sub-agent answers (long emails, multi-row tables, document summaries, batched fetch results). (#271)
liftAnswer flag on DelegateDeps. New liftAnswer?: boolean field on the DelegateDeps interface (default true) toggles the pre-return answer lift. Direct-execute callers that parse the answer text themselves opt out with liftAnswer: false — needed because the marker string is opaque to a downstream verdict parser. packages/system/agents/judge-agent/judge-agent.ts sets it on the judge's delegate deps so parseVerdict keeps seeing the raw verdict string. JSDoc on the field warns that any code path feeding the answer into another LLM should leave the default on. (#271)
liftAnswerForModel export from @atlas/core/artifacts. New public function in packages/core/src/artifacts/scrubber.ts (re-exported from packages/core/src/artifacts/mod.ts) wraps the upload-and-marker projection as (answer, { workspaceId, chatId, logger, serverId, toolName }) => Promise<{ value, artifactId? }>. Short-circuits on the REF_MARKER_PREFIX (no double-lift on a re-emit) and on under-threshold inputs, never throws — upload failures log a warn and return { value: answer } so a degraded artifact backend falls back to the original prose rather than dropping the delegate result. (#271)
@friday/using-elicitations system skill teaching FSM actions and Python type: user agents how to use the platform HITL surface. New packages/system/skills/using-elicitations/SKILL.md (201 lines) documents the two MCP-registered elicitation tools — request_tool_access(toolName, reason) for allowlist approvals (fixed Allow once / Allow always / Deny options) and request_human_input(question, options?) for HITL decisions, approvals, confirmations, and disambiguation — both of which block the running tool call until terminal and return the answer envelope inline. Job authors already had these tools wired through MCP; they were undocumented anywhere centrally and the contract was discoverable only by reading tools/agent-playground/src/lib/human-input/nested-choice.test.ts and the MCP factory source. (#275)
Skill covers the nested per-item choice grouping shape the playground UI parses. Documents the exact [N] action — title label / N:action value pattern that triggers the per-item choice-set renderer, the four detail-line keys the parser lifts into per-item headers (Subject: / From: / Date: / Preview:), the grouping constraints (at least two items, at least two actions per item, every option's value of the form N:action where N matches an item index in the question and every label starts with [N]), and the fall-back-to-flat-radio behavior when any constraint fails. Answer arrives in answer.value as a JSON-array string (e.g. "[\"1:archive\",\"2:keep\"]") that the action must JSON.parse before dispatching; optional per-item free-text comments arrive in answer.note as [N] free text and are explicitly flagged as advisory rather than a parallel command stream. (#275)
Skill documents the blocking return envelope and status branches for request_human_input. Return shape pinned as { ok: boolean, status: "answered" | "declined" | "expired", elicitationId: string, answer?: string, note?: string } with explicit handling guidance — branch on answer for "answered" (parse first for grouped prompts), halt the side effect and summarize what was skipped on "declined" / "expired" without faking a value. Python type: user agents call the same primitive through ctx.tools.call("request_human_input", {...}) with the identical return shape. (#275)
Skill documents the request_tool_access outcome matrix and authoring rules. Seven-row reason → granted table covering bypass (workspace/daemon dangerouslySkipAllowlist proceed-silently), persistent_allow (prior Allow-always), answered (user picked one of the three buttons), pending_user_approval (created but unanswered — rare), declined, expired, and unknown_tool (not in the runtime catalog — stop guessing, do not retry). Spells out that the reason field is shown to the user verbatim so it must be moment-specific ("Need fs_write_file to save the patch we just drafted"), that list_capabilities / list_mcp_tools is the right pre-flight when the tool name is uncertain, and that request_tool_access should only be declared on actions that can produce a useful denial outcome rather than as a generic escape hatch. (#275)
Skill documents elicitation lifecycle, TTL inheritance, and retry idempotency. TTL inherits from the parent job's config.timeout (default 30 minutes; per-job override via jobs.<name>.elicitations.timeout); the platform reuses any still-pending elicitation matching the same (question, options, sessionId, actionId) tuple rather than creating a duplicate so accidental LLM retries are idempotent; expired prompts move to a read-only Activity log entry that cannot be answered retroactively, and acting on a timed-out prompt does not reify the underlying job. (#275)
Skill enumerates the high-traffic authoring gotchas. Plain-text-only question rendering (markdown tokens become literal ** / # / - / fenced-code characters in the UI), no schema support for multi_select / default / required / nested option objects (item grouping must be encoded into label/value strings), grouping fallback when fewer than two items or fewer than two actions per item, the request_tool_access vs request_human_input mix-up where "which provider" is a request_human_input question with [Gmail, Outlook] options not a tool-access request, the dead-end where an outputTo LLM action prints a prose menu and tries to wait for a future user message (the run ends without the answer because the action only resumes through the elicitation envelope), and the advisory-only nature of answer.note content. (#275)
apps/atlasd/src/migrations/index.test.ts manifest-membership guard so a forgotten registration trips CI. Adds three sibling assertions that read index.ts as text (importing it would pull @db/sqlite transitively, which the existing tests in the file already avoid): every m_*.ts on disk has a from "./<file>" import line AND a \bm_<id>\b token inside the sliced MIGRATIONS array body, and the array's non-comment entry count matches listMigrationFiles().length exactly so deleting a file but forgetting to remove its array entry (or vice versa) fails the size assertion. The slice is computed by a factored-out extractMigrationsArrayBody(indexSource) helper — regex /const MIGRATIONS:\s*readonly Migration\[\]\s*=\s*\[([\s\S]*?)\];/ followed by .replace(/\/\/.*$/gm, "") — so a bare m_<id> reference in a doc comment elsewhere in the file, or a temporarily //-commented array entry, cannot satisfy a whole-file scan and silently let a missing registration through. The size check filters lines via /^m_\d{8}_\d{6}_[a-z][a-z0-9_]*$/ after trimming trailing commas. A dedicated describe("extractMigrationsArrayBody") block locks in the predicate with three cases (slice between brackets, comment-only entry rejected, null when the array literal is absent) so a future refactor of the assertion shape can't quietly weaken the guard. (#277)
Changed
Delegate sub-agent system prompt: text-on-success contract. The child-prompt template at createDelegateTool (packages/core/src/delegate/index.ts) now directs the sub-agent to emit final answers as plain text content — "your text reply IS the answer" — and to call the finish tool ONLY on failure with { ok: false, reason }. Removes the wasted double-decode where the model wrote the same long answer once as text and once as a finish argument. The instruction against "markdown tables, section headers, or other human-facing formatting" was dropped from the terseness directive so the child can still produce structured document-shaped output (the whole point of the lift path). The finishTool description in packages/core/src/delegate/finish-tool.ts was rewritten to match: "ONLY when the task is impossible to complete"; the {ok: true, answer} variant remains for back-compat but is no longer the recommended success path. (#271)
Empty delegate output now surfaces as a structured failure. Both the finish({ok:true, answer}) branch and the text-on-success branch in createDelegateTool.execute now .trim()-check the candidate answer and, when it's empty/whitespace-only, return { ok: false, reason: "delegate produced no answer", …resultBase } instead of the previous silent { ok: true, answer: "" }. Under the new text-on-success contract a malformed step that completes cleanly with no output is otherwise indistinguishable from real success; the supervisor now sees the structured failure and can act on it. (#271)
tool-suite-management promptfoo suite surfaces the real workspace-chat prompt.tools/qa/live-daemon/promptfoo/tool-suite-management-config.yaml now loads packages/system/agents/workspace-chat/prompt.txt as vars.system_prompt and prompts: points at a new shared tools/qa/live-daemon/promptfoo/prompt-template.txt that renders the system prompt + per-test user_message side-by-side — mirroring the tool-choice suite. The six scenarios in tool-suite-management-tests.yaml now declare each user_message literally (the actual text postChatMessage forwards to the chat endpoint). The promptfoo view prompt panel now shows what the model saw rather than just the scenarioId placeholder; other daemon-level suites (retrieval-gated-config.yaml, table-outcomes-config.yaml, delegate-skills-config.yaml, load-skill-injection-config.yaml) keep the {{scenarioId}} placeholder because they exercise daemon plumbing rather than a single LLM prompt. All 62 promptfoo cases still pass. (#271)
Fixed
Migration manifest replaced with static imports so deno compile daemons actually run migrations.apps/atlasd/src/migrations/index.ts's getAllMigrations() previously walked the directory with readdir(fileURLToPath(new URL(".", import.meta.url)), { withFileTypes: true }) and pulled each m_*.ts file in via await import(new URL(file, dir).href). Inside a deno compile binary import.meta.url resolves into Deno's compile-internal virtual FS that node:fs/promises#readdir returns nothing for, and dynamic-import-by-URL isn't wired into the bundled module graph — so shipped daemons silently enumerated zero migrations, friday migrate reported ran 0 migration(s); skipped 0 already applied, and downstream bootstrap (notably m_20260511_110800_provision_workspace_members stamping owner rows into WORKSPACE_MEMBERS) never executed, leaving every workspace-scoped HTTP route to 403 because the membership row was missing. The new module hard-codes 22 named imports (one import { migration as m_<id> } from "./m_<id>.ts" per file from m_20260501_120000_chat_to_jetstream through m_20260511_120000_cleanup_userid_as_name) into a const MIGRATIONS: readonly Migration[] literal; getAllMigrations() is now a synchronous spread + [...MIGRATIONS].sort((a, b) => a.id.localeCompare(b.id)) returned as Promise.resolve(...) for back-compat with the daemon's and CLI's existing await callers. Verified by deno task atlas migrate --dry-run --json against the compiled binary enumerating all 22 migrations in skipped (vs. 0 on main). (#277)
Block-comment attribution corrected: defensive sort lives in getAllMigrations, not runMigrations. The new manifest's leading doc comment originally claimed that runMigrations re-sorts the array defensively, but packages/jetstream/src/migrations.ts iterates the input array in input order — the only sort is the localeCompare inside getAllMigrations(). The comment now correctly attributes the defensive sort so future maintainers don't assume out-of-order entries in MIGRATIONS are harmless at the consumer end. (#277)
<details> cards in tool-burst.svelte and delegate-tool-card.svelte preserve the user's manual open/closed state across re-renders. Both components used <details open={initialOpen}> with initialOpen snapshotted once via untrack, but Svelte 5 re-applies the bound open attribute on every parent re-render of the surrounding reactive scope — so any downstream re-render (post-elicitation cache update, SSE push, virtualizer measurement) wrote open={false} back over a user-driven open=true from a summary click. Symptom: an already-answered elicitation card could not stay expanded — clicking the burst summary opened it momentarily, then the next re-render snapped it shut. Switched both components to bind:open={isOpen} with isOpen declared as $state(untrack(() => ...)) initialized from the same snapshot, so the user's click and the live-only force-open $effect (which now writes to the state, not directly to detailsEl.open) are the two sources of truth; the bind:this={detailsEl} handle is removed entirely. Plain open={state} one-way binding has the same re-apply problem — bind:open is the only stable shape. (#275)
Configuration
docker/run-platform.sh swaps FRIDAY_LOCAL_ONLY for FRIDAY_ENV=dev.FRIDAY_LOCAL_ONLY was renamed to FRIDAY_ENV in #265, but docker/run-platform.sh was missed in that sweep and still exported the dead var, which left the containerized daemon in the new fail-closed posture — every /api/* request hit the apps/atlasd/src/session-middleware.ts 401 path because no session-minting gate was tripped. The launcher now exports FRIDAY_ENV="${FRIDAY_ENV:-dev}" so the daemon auto-mints a local session and skips remote credential fetching, mirroring what the Studio installer (apps/studio-installer/src-tauri/src/commands/env_file.rs) and atlas daemon start already do. (#274)
.env.example documents FRIDAY_ENV with the new fail-closed semantics. The commented-out FRIDAY_LOCAL_ONLY=0 example (whose "disable outbound calls" copy no longer matched the var's behavior even before the rename) is replaced with # FRIDAY_ENV=dev plus a description block spelling out that dev auto-mints a local /api/* session and skips remote credential fetching, while any other value — including unset — requires a valid friday_session cookie or Bearer token and returns 401. Notes that Friday Studio and atlas daemon start set this automatically, so the example matters only for operators with hand-rolled .env files. (#274)
Removed
Placeholder-JWT FRIDAY_KEY auto-generation deleted from docker/run-platform.sh. The 13-line node -e block that minted an unsigned HS256 JWT with iss: 'friday-platform', email: 'platform-local@hellofriday.ai', sub: 'local-user', and a .local signature suffix is gone, along with the [platform] Auto-generated FRIDAY_KEY for local user identity log line. The daemon stopped decoding FRIDAY_KEY for user identity in #265 (that JWT-decode path and the literal "local-user" sentinel were removed in favor of UserStorage.resolveLocalUserId()), and an unset FRIDAY_KEY already disables the upstream gateway path in the CLI, so the fake JWT served no purpose except masking that the var was unconfigured. Net change: docker/run-platform.sh drops from 30 lines of identity bootstrap to 4 lines of dev-mode setup. (#274)