Changelog
Every release's notable changes, generated from junco's canonical
CHANGELOG.md and refreshed by node scripts/build-docs.mjs on every
build — this page cannot drift from the source of truth.
0.14.0 — 2026-09-06
Added
- The dashboard chat streams the model's reasoning as a live thinking block (spec
docs/superpowers/specs/2026-09-06-chat-streaming-design.md): a· thinking · 3sheader ticking while it streams, the reasoning dim beneath, folding to a single▸ thinking · 3srow when the answer starts.tpins thinking open — the live block and every finished turn's▸ thinkingheader — and folds it back; the header is the same string live and finished, so the row never changes shape at turn end. - Live tool cards in the chat:
▸ bash npm test ⠋with a spinner and the last 6 lines of streamed output while a call runs,▸ bash npm test ✓over a→ N linessummary once it finishes,✗with the body open by default on an error.x(also⏎/space) toggles the card under the cursor, andtabwalks running cards too — a card keeps one cursor index across the turn end. The samerenderToolCarddraws the ticket transcript andjunco transcript, so the three surfaces cannot drift. - Chat answers are typeset as markdown — headings, lists, quotes, rules, tables, and code fences highlighted by the agent's own highlighter (raw fences when it cannot be loaded) — while they stream: a junco-owned block parser with a prefix-stable open tail, so a frame re-renders only the block still growing and the finished turns above never re-run.
chat.thinkTags(auto|on|off, defaultauto, live): literal<think>…</think>spans an OpenAI-compatible endpoint streams inside the answer are split into the thinking block on the fly by a total, never-dropping splitter;autosteps aside for a turn that already streams native reasoning.chat.maxFps(10–120, default 60): the dashboard's paint-rate ceiling while a chat turn streams — lower it on a slow terminal, a small board, or over SSH.junco doctorgains an advisoryℹverdict (exit code unaffected) and its first use,chat thinking: when the chat's model resolves inline to an OpenAI-compatible endpoint, the hint names the server-side flag that moves reasoning out of the text — the server's own reasoning-format or separate-reasoning-field option — recognized from the endpoint's port, with no extra network probe.chat.theme(auto|dark|light, defaultauto): the palette for the chat's highlighted code fences — pinlighton a light terminal that does not exportCOLORFGBG, where the highlighter's guess lands on the dark palette (#512).- A dashboard that opens or reconnects mid-turn gets the turn so far:
GET /chat/eventssends onejunco_chat_partialsnapshot (thinking, tool cards, text) after the transcript replay and before any live frame, so a reconnect never shows a half-turn missing its beginning.
Changed
- Chat wire format: the
/chat/eventsstream no longer carries the SDK's rawmessage_updateevents. Each turn is accumulated in the daemon and put on the bus as three slim, bus-only records —junco_chat_delta(one text/thinking chunk, tagged with the content index and a per-turnseq),junco_chat_tool(start/output/end, output delta-only, the final result capped at 8 KiB), andjunco_chat_partial— none of which is written to the transcript or carries an SSEid, soLast-Event-IDalways names a persisted line. The record bus stops re-serializing the whole assistant message per chunk. A pre-upgrade dashboard against an upgraded daemon degrades to the answer appearing at turn end; upgrade both together for streaming.junco_chat_turn_startgains aturnid (additive) the live records reference. - The
/chat/eventsresponse socket issetNoDelay, so a delta reaches the dashboard the moment it is written instead of waiting on the previous frame's ACK. - The dashboard renders at up to 60 fps (Ink
maxFps, was Ink's default 30): the chat's per-frame flush leans on Ink's own throttle instead of a trailing 50 ms timer, so a delta is on screen within one frame;chat.maxFpsdrops it back where needed. - The dashboard chat view keeps the finished turns and the streaming turn in two memoized halves: a streaming flush costs O(the live turn) and never re-renders the history, however long the conversation (
tests/tuiChatPerf.test.tsxpins it — 200 turns, 300 deltas/s). junco transcriptand the dashboard's ticket transcript print each tool call as a header row (▸ read game.js ✓,✗on an error,…while it has no result) over a→ 214 linessummary row instead of one combined line; an errored call's body is open by default.
Fixed
- The dashboard's repo-detail overlay no longer answers a raw
owith the browser: the derived keymap'sbalready ran ahead of it, soowas an undocumented alias that contradicted the key table;docs/dashboard.md's three remainingo-opens-the-browser paragraphs now sayb(#492, #501). - The newline a model writes right after
</think>no longer reaches the wire: the think-tag splitter swallows exactly one\n(or\r\n) following a close tag, held across chunk boundaries like a tag prefix, so ajunco_chat_deltaof just"\n"— invisible in the dashboard, visible to every other/chat/eventsconsumer — is gone andsplitThinkingTextagrees (#509). - A tool output snapshot that does not extend the previous one (the tool's own truncation kicked in) is now put on the wire with
replace: true, and the dashboard's reducer overwrites the card's output instead of appending it, so the card no longer shows the prefix twice; a thinking block carriesdoneAtfrom the daemon, so the folded header's duration is the daemon's, not the client's guess (#507). - Chat markdown follow-ups (#512): a code fence still streaming is highlighted line by line through a per-line cache, so a frame re-highlights only the line being typed instead of the whole block (the closing fence re-highlights it whole, once); a markdown link renders as
text (url)with the text an OSC 8 hyperlink (cmd/ctrl+click in terminals that support it) and the(url)dimmed; the finished turns' markdown caches are dropped when the dashboard switches chats instead of outliving them. chat.thinkTagsis classifiedrestart, notlive: a chat session snapshots its config when it is built, so a hot-reloaded value only ever reached sessions opened afterwards — the lever,junco configanddocs/configuration.mdnow say so (#515).- Documentation accuracy sweep against 0.13.0's code: README's dashboard key legend and CLI table (eight subcommands were missing), the sandbox's
autobackend degrading rather than failing closed, the per-repo worktree path, the/healthbody, the/chat/*Host check, the audit fingerprint's 20-line bucket, dashboard keys in the audit/investigate/github-mode/bot-account guides (aadd repo,Uunwatch,Ddiscard), the apply-ticketpatch_no_amendrule, and the ARCHITECTURE prose that still described the module shapes the shared-module consolidation (#500) replaced.
Internal
- Behaviour-preserving consolidation: dead exports and stale re-exports removed (#497); the fenced-block parsers share one
fences.ts(#498); the assess/analyze repo-target preamble lives inrepoTarget.tsand the read-only tool set inreadOnlyTools.ts, which also removes therunOnce↔ flow import cycle (#499); the GitHub issue-comment and transcript-target helpers sharegithubComment.ts/transcriptTarget.ts, with six smaller two-copy helpers folded likewise (#500). A project-level harness setting now disables agent commit and PR attribution. - The
node_modulessymlink the dead-export sweep (#497) committed by accident is untracked again and.gitignoreignores a symlink at that path too (#502).
0.13.0 — 2026-09-04
Added
- On the dashboard,
ton a GitHub issue opens the transcript of the ticket the bridge built for it — live-following while the run is in flight — so a labeled issue can be watched without hunting for its row in the queue section (#330). Works from the issue list and from the open issue detail view; a toast explains when nothing is in flight for that issue (enterstill opens the issue body). - Apply tickets: a ticket body carrying a
junco-patchfence (agit format-patchmbox series) executes with zero model turns —git am --3wayapplies AND commits the series directly in the worktree, so the series' own commit messages become the PR's commits, in order. Verification (## Verification) still runs and still gates the PR; the critic pass is skipped, since the diff already is the spec. A conflict fails the ticket outright rather than requeuing it (a conflict is deterministic, not transient), with thegit amoutput folded into the failure note and the worktree preserved for inspection. Newjunco lint/submit --dry-runrules:patch_parsesandpatch_paths_sane(errors — a malformed series, or one touching traversal/absolute paths or carrying a binary hunk) andpatch_has_verification(warning); the prose rules are skipped for these tickets. See Apply tickets. - Apply tickets gain a one-shot escalation ladder, gated by
worker.applyFallbackToAgent(default true): a failedgit am --3way, or a clean apply whose## Verificationblock then fails, dispatches exactly one agent session instead of failing the ticket — the patch series handed to the agent as a specification to implement against the current tree, plus the failure detail, never as bytes to replay. Because a fallback breaks the GitHub route's byte-identical approval guarantee (a human's trigger label approved a specific diff, not whatever the agent produces instead), the PR body carries an explicit disclosure banner whenever it fires, and the critic pass — skipped on a clean apply — runs again once an agent has touched the ticket. Set the lever tofalsefor the old hard-failure behavior. See Tickets § Escalation ladder and Configuration § Apply-ticket fallback. junco submit --patch <file> --repo <path> [--title T] [--why W] [--verify CMD]composes and submits an apply ticket directly from an existinggit format-patchfile — no hand-authoredjunco-patchfence required — and composes into the same--as-issue/--dry-runrouting as a file-sourced ticket.- The
junco-dispatchskill now teaches apply-mode authoring: a drafting-time mode question (does the agent already know the exact bytes to land?), a dedicated "Apply mode (patch tickets)" section covering how to produce a series, the fence-length rule, when not to use it, and what the fallback means for the byte-identical guarantee, plus a minimal apply-ticket shape inTEMPLATE.md. - Apply-ticket execution now shows up in observability: each pull-request-kind task-history record (
<dataDir>/data/history/tasks-YYYY-MM.jsonl) carries amodefield —"apply"(clean apply, no fallback),"apply_fallback"(either escalation rung fired), or"agent"(an ordinary agent-driven ticket) — so the wall-clock/token difference between modes is queryable instead of inferred from logs. Apply runs also now write a transcript:junco_meta+ ajunco_run_start(flow: "apply") + ajunco_run_endframe bracket thegit amattempt on both the success and the failure path, and a failed-then-escalated ticket's fallback session appends to the same file — one chronological record, not two. - Apply tickets can now be claimed while the inference endpoint is unreachable or the provider gate is latched —
claimNextTasknarrows an otherwise-blocked claim pass to just the eligible apply tickets instead of leaving the whole inbox untouched, sincegit amneeds neither the endpoint nor a provider quota. The daemon's startup endpoint wait is unaffected (this narrowing is per-claim only), and a claimed apply ticket whose patch then fails to apply can still fail on the fallback's own agent session being unable to reach the same downed endpoint — cleanly, bounded by the existing transient-retry cap. See Operations § Inference endpoint unreachable at boot. - End-to-end test layer (
npm run test:e2e, run in the quality gate): the built CLI runs as a child process in a sandboxed HOME with the real agent SDK driven against a scripted OpenAI-compatible model stub — PR happy path, read-only Q&A, transient-failure requeue, and daemon lifecycle (start,/health, poll-driven drain, SIGTERM) — andscripts/package-smoke.shnow runs the PR scenario through the tarball-installed binary.npm run test:e2e:liveruns the same harness against a real inference endpoint (JUNCO_E2E_MODEL_ID/JUNCO_E2E_BASE_URL/JUNCO_E2E_API_KEY). SECURITY.mdgives junco a disclosure channel (#383): the supported version window, reporting through this repository's private vulnerability reporting with a 72-hour acknowledgement, and the four in-scope boundaries — the execution sandbox, the GitHub bridge's approval chain, bot-account auth, and the health endpoint — alongside an explicit out-of-scope list, so a reporter knows up front what will and will not be treated as a vulnerability. A pull-request template and an.editorconfigland beside it..nvmrcpins the exactenginesfloor (22.19.0), sonvm useand anything else that reads it lands on the version CI treats as the floor, and the quality gate gained a non-blocking node 26 canary leg (#382). Node 26 becomes LTS in October 2026 while the gate otherwise exercises only 22.19 and 24; the canary annotates a run but is deliberately kept out of the aggregatequality-gatejob, so it can never block a merge.- Plan sets are documented, and two new ticket templates ship in the package. Plan sets landed in 0.12.0 behind
[planSets], but a reader ofdocs/could not find the section, its three levers, or thejunco-planfence they actually write (#375). Configuration § Plan sets now covers the levers and the fact that the Layer-1depends_onmachinery stays on with the lever off, and Tickets § Plan sets covers the local and bridge doors, a minimal fence, the field table, and what the compiler builds and refuses.examples/apply-ticket.mdandexamples/plan-set.mdjoin the shipped examples, checked against the real parsers by the suite so a compiler or lint change cannot break them silently. - Every version heading in this file resolves to its tag diff (#374). The file declared Keep a Changelog conformance, but the
[x.y.z]:reference block that resolves the headings never existed — so on npm and GitHub all sixteen rendered as literal bracket text. A release that adds a heading without its link now fails the gate. - Per-repo chat with the coding agent inside the dashboard (
ton a repo row): an ongoing, read-only conversation, file-backed and daemon-owned so it survives a dashboard quit or a daemon restart. Every dispatch branch is reachable from it — a plain ticket, a ticket set, a plan set, an amend on an open PR, an apply-ticket patch, or a request toaudit/investigate— each parked for the same human confirm audit/investigate findings already use (ssubmit,eedit,rcycle route,Ddiscard). New config blockchat.*(chat.enabled,chat.modelId,chat.thinkingLevel,chat.turnTimeoutMinutes); new data-tree dirsdata/chats,data/chat-drafts; new loopback-only/chat/*routes on the health server (GET /chat/events,POST /chat/prompt,POST /chat/abort,POST /chat/new,POST /chat/note,GET /chat/status);junco transcript --chat <owner/repo>prints a session's transcript outside the TUI. - The dashboard chat can now submit the work it drafts. Asking the agent to submit/queue a parked draft makes it call its one action tool,
junco_submit, which proposes the submission as a card in the chat —yruns the samejunco submitthe card'ssdoes (inbox, or--as-issue),nkeeps the draft parked — and the agent's turn waits for the answer (its own timeout paused) so it reports the real outcome in the same reply./submit [id]in the composer submits a parked draft directly. New leverschat.submitTool(default on) andchat.confirmTimeoutMinutes(default 10); new transcript recordjunco_chat_command; new loopback routePOST /chat/decide. The agent's file tools stay read-only and it still cannot run a shell; nothing else on the dashboard is reachable from the chat. - Dashboard chat: the header's cost figure is now today's chat spend rather than the daemon's lifetime total —
/health.chatsgainschatTodayUsd, a chat-only bucket the manager rolls over on the spend ledger's own day boundary (the ledger stays single-writer; the amount is deliberately chat-only, since the ledger's day bucket counts ticket runs too), and the label readschat $X todayagain (#456). The in-pane hint row under the transcript is gone — the two-row shortcut bar already lists those keys — and its one unique piece of information, the window indicator, moves to the header's right edge beside the follow state (following/paused), giving the reclaimed row to the transcript (#471). - The
junco_submitcard no longer claims to be awaiting you while the CLI is still running. The momentyreleases the tool intojunco submit, the daemon records an additiverunningstatus onjunco_chat_command; the card reads▸ submitting …,y/nare disarmed, and the header says▸ submitting…(#478). The record is the honest one: it survives a reconnect, and a daemon crash mid-submit resolves it asexpired (daemon restarted)exactly like a proposal. The handshake is also now pinned inside a real turn — a fake session that actually invokes the tool proves theabortedrecord lands beforejunco_chat_turn_aborted, that the turn deadline pauses while the tool blocks, and thatabort()fires the SDK's own per-call signal (#477). - A chat turn that was soft-aborted —
/abort, the per-turn timeout, a daemon stop — no longer drops a draft it had already finished emitting: a complete, closed fence is parked with a warning row (emitted before the turn was aborted) sosstill works; an unclosed fence is never parked, and an aborted turn never starts the auto-lint follow-up (#454). Parity with the PR flow, where a guard kill still salvages the commits into a PR. - The dashboard has a process-level safety net: an unhandled promise rejection is written to the worker log and surfaced as a toast instead of the hard quit with no message that Node 22 would otherwise perform (#455).
Changed
- The dashboard chat view scrolls like a chat:
↑/↓(andj/k,[/]) walk the transcript a row at a time,PgUp/PgDnmove a page — still working while the composer is focused, so you can read back without blurring — andtab/shift+tabwalk the draft cards↑/↓used to. The pane doors are gone with them (h/l,←/→and the rail branch): the view is full-screen with no rail painted, so moving the rail selection no longer switches an open chat to another repo. Both transcript surfaces (chat and ticket transcript) also gained a live scrollbar — press a row of the track to jump there, hold and drag to keep scrolling — and the chat footer now lists what actually works: real⇞ ⇟keycaps in place of the old dim "esc, then …" reminder, and the target label is the bare repo key (the header crumb already says chat). - Dashboard shortcut bar redesigned as two rows (spec
docs/superpowers/specs/2026-09-02-footer-redesign-design.md): the actions row names the thing under the cursor and lists its verbs — the chat chip first, as a filled pill — and a stable navigate row keeps the structural keys in the same place in every view; the toast now paints over the actions row instead of occupying its own line.?opens help from every overlay and returns to it on any key. Key changes:cis chat on every screen with a repo in context — including the repo-detail overlay and the transcript and review views, which also carry the chat chip — and from an issue or PR it prefills/issue N//pr N;tis the ticket transcript only (it no longer opens chat from the rail or the PR pane); thec commandschip is retired — the palette is:. - Dashboard idle cost: a poll that delivers unchanged data no longer commits an Ink frame — every poll sink keeps its previous state on structural equality, and the age clock ticks on its own 5 s interval instead of every queue poll. Sub-minute "Ns ago" strings now step in 5 s increments. Spinners share Ink's animation timer, and the dashboard renders incrementally so an animation frame rewrites only changed lines. A frame-level perf test (
tests/framePerf.test.tsx) pins zero frames per constant-data poll tick. - Corrects four Stage-1 apply-tickets claims that the escalation ladder above supersedes, all previously stated as unconditional facts about every apply ticket and now true only for a clean apply with the fallback lever left untriggered: (1) apply runs are not transcript-less —
docs/tickets.md/docs/github-mode.mdsaid an apply ticket runs no agent session sojunco transcript/the dashboard's transcript view have nothing to show for it, but apply runs now write their own transcript frames (see the Added entry above); (2) a conflict does not always fail the ticket outright — with the fallback lever on (the default) it escalates to an agent instead; (3) the critic pass is not always skipped — it runs again once either escalation rung fires; (4) "no agent session runs" and "leftovers are never swept" hold only until a fallback fires, after which the ticket is an ordinary agent ticket for the rest of the pipeline.docs/github-mode.md's claim that labeling a parked apply-mode issue approves "the exact diff that will land" is corrected the same way: labeling approves the diff shown in the issue, which is what lands only when it applies cleanly. - A parked
--as-issuebody now carries the plan once — rendered markdown wrapped in invisiblejunco:ticketdelimiters — instead of a rendered copy plus a collapsed machine fence (#329). Half the issue size, no desync surface between the two copies, and editing the parked plan in GitHub's UI before applying the label now edits exactly what runs. The bridge reads markers first and still falls back to ajunco-ticketfence, so issues parked by older versions launch unchanged;--as-issue --plankeeps its fence (YAML payload). Mixed-fleet note: an older bridge sweeping an issue parked by a newer CLI finds no fence (markers are invisible to it) and routes the issue to the planner instead of queueing the parked plan verbatim — upgrade the daemon before parking with a newer CLI. - The
junco-dispatchskill drops authored boilerplate the CLI and worker now own: the per-ticketcreated:timestamp, runningjunco lintagain after a cleandry-run(whose output already carries the full lint verdict), the separate monitor-the-ticket and repo-target-confirmationAskUserQuestionasks (monitoring is now folded into the same call as the dispatch preview; the repo target is confirmed by the preview'srepo:line instead of a standalone question), the unconditionalEXAMPLE.mdread before drafting (now conditional on an unfamiliar plan shape or a prior structural lint failure), and authoring-timegh label listpre-checks (labels are validated by dry-run/lint's ownlabels_existcheck instead). The preview itself is now codified as a curated essence — key design decisions, a Files/Steps summary, and the dry-run's destination/reason/carried/discard/timeout lines — plus the temp-file path for full reading, never the raw ticket body. - The worker's prompt preamble now carries the full strict working discipline (trust-the-ticket, no-scope-expansion, graceful-stop-on-mismatch, final-summary) for every run, so
TEMPLATE.mdandEXAMPLE.mdno longer ship an authored "Notes for the agent" block and plan-lint'snotes_block_presentrule is retired. An existing ticket that still carries a Notes block stays valid — it's only wasted tokens now, not a required section. - Breaking:
junco dispatchis renamedjunco import,junco assessis renamedjunco audit, andjunco analyzeis renamedjunco investigate. The old verb names are gone, not aliased — running any of them now prints a usage error and exits2; scripts, muscle memory, and thejunco-dispatchskill all move to the new names. Every CLI usage line,doctor/statuslabel, dashboard mnemonic (uaudit,ninvestigate,mimport), and doc reference has moved with them; the guide pages move too (docs/assess.md→ docs/audit.md,docs/analyze.md→ docs/investigate.md). - Ticket frontmatter gains
audit:/investigate:as the canonical mapping names for the renamed flavors (authored byjunco audit/junco investigaterespectively). The legacyassess:/analyze:keys are permanently accepted as aliases — never removed — so tickets and tooling authored against the old names keep working; when a ticket carries both the old and new key, the new one wins. See Tickets § Key frontmatter fields. - The
junco-dispatchskill decomposes a plan into a ticket set on four seams in the work — independent reviewability, an ordering dependency, separate verification, or mixed certainty — instead of the old ">180 min → decompose" clock rule;timeout_minutesis now sizing for an already-scoped ticket, not the decomposition trigger. The skill also states the audit-vs-investigate distinction plainly (audit sweeps a repo into findings that become issues; investigate deep-reads one issue into a comment) and distinguishes its own "dispatch to junco" act from the CLI's separatejunco importverb. - The documentation caught up with the surface junco actually ships, and each correction is now pinned by a test so it cannot drift again.
docs/operations.md's self-described "full CLI reference" had stopped at the 0.7-era commands — about twenty subcommands and their flags were missing, and barejuncowas described as an alias forstartwhen it ensures the supervised daemon and then opens the dashboard — and is now regenerated fromjunco --help, which the section names as the authority (#371). The README documents the threejunco submitroutes beyond a plain file:--patch(apply tickets),--plan(plan sets) and--as-issue(the parked, bot-authored issue door) (#372).CONTRIBUTING.mdquotes the real five-step gate CI enforces instead ofnpm testalone, the real suite size, thefeat/<topic>branch convention, and the exact-pin dependency policy (#373).ARCHITECTURE.md's module map gained rows for 23src/modules it never listed — the whole setup wizard, the lever registry and hot-reload watcher, the provider gate, the spend ledger and the small shared primitives — and itscli.tsrow now points at theUSAGEstring rather than repeating a verb list that had already drifted (#376). Two statements in it were plainly wrong and are corrected: the execution sandbox is on by default, not opt-in (#370), andjunco audit's filing dedup is marker-based across the repository's most recent 500 issues — any author, closed included — not author-scoped (#378). - Dependency major: zod 3.25.76 → 4.5.4 (#381). Junco imports zod in two source files, and three of v4's breaking changes land on the config schema. The one that mattered:
.default()no longer parses what it returns, so everyz.object({…}).default({})section would have handed back a literal{}and left the roughly 97 leaf defaults below itundefined— a defaultconfig.jsonwould not have loaded at all. Sections now use v4's.prefault({}), which is the v3 behaviour exactly;z.record()calls name their key schema, and one.refine()moved from{ message }to{ error }. No validation was loosened and no schema semantics were rewritten. Lockfiles will see the major; the agent SDK's own transitive zod dependencies dedupe onto 4.5.4 with no nested v3 copy. - Patch/minor dependency sweep, all exact-pinned:
@earendil-works/pi-coding-agent0.84.2 → 0.84.4,ink7.1.1,react19.2.8,vitestand@vitest/coverage-v84.1.11,eslint10.9.1,typescript-eslint8.69.0 (#380). The deliberate security-only Dependabot policy now has a cadence to go with it: a monthly workflow runsnpm outdatedand files one issue with the table, leaving the actual bump to go through the full gate by hand.prettierdeliberately stays at 3.8.4 — 3.9.6 reformats sixteen files and would failformat:check. - The published tarball is a third smaller:
dist/**/*.js.mapis excluded from thefilesallowlist (#379). Every map pointed itssourcesat../src/, which the package does not ship, so an installed copy carried 223 maps that resolved to nothing — 685 files → 462, 1.08 MB → 849 KB packed. The package also declares"exports": {}, making "bin-only CLI, no import surface" a stated contract: the installed binary is unaffected (npm linksbinwithout consultingexports) and dispatchers keep consumingjunco schemaJSON, while bare and deep imports ofdist/now fail withERR_PACKAGE_PATH_NOT_EXPORTED. junco restartpreflights a checkout-backed install (#384). When the runningdist/resolves inside a git checkout, it now refuses — exit 1, before any unit scan or restart kick — if the working tree is dirty orHEADis notorigin/main, printing the checkout path, both commits, and up to ten dirty paths;junco restart --forcerestarts anyway with a warning. An ordinary npm install (no.gitbesidedist/), or a host where git cannot answer, is untouched. On the release side, the npm publish job now deploys through a reviewer-protected GitHub environment, so a release run parks for an explicit approval instead of publishing on a tag alone. Both are documented under Operations § Releasing.junco data migrate, and the pre-0.10 flat-layout /vaultRootmigration path it exists for, will be removed in 1.0 (#360). Nothing changes before then and nothing is ever deleted by the migration — but an operator on an older install should migrate on any 0.x release, because from 1.0 on nothing in junco relocates an unmigrated root. See Configuration §junco data migrate.- Internal structure: the 2026-09-01 repository sweep (#387) reorganized the runtime without changing what it does —
submitandstartmoved out ofcli.tsinto their own modules behind a dispatch table, the PR flow's Phase 9 intopostSessionReview.ts,runDoctor's 25 checks into a check table, the dashboard's 458-line action memo into three hooks,config.tsinto paths/schema/assembly, and one writer, oneResulttype, one set of shared run/error helpers and one set ofghtimeout budgets replaced the hand-copied versions scattered across modules. Comment cross-references now cite symbol names rather than line numbers that had drifted, git invocations terminate their options with--before positional refs and paths,no-explicit-anyand a 400-line function ceiling are enforced by the gate instead of by review, the coverage floor was ratcheted and given per-tree floors for the sandbox and session modules, and the test suite's residual flake surface and duplicated harness helpers are gone. Three user-visible consequences ride along:junco skill install --harness <dir>now refuses to write over aconfig.jsonthat is already schema-invalid elsewhere (exit 1, file untouched) instead of writing it and then dying at the next load; a save failure in the dashboard's config editor surfaces as an error toast rather than an escaping exception; andauth login,data migrateandskill installpick up the0600config mode they were missing. t's dashboard meaning is now cursor-aware: on an issue row in the issue list it still opens that issue's ticket transcript, unchanged (#330); everywhere else on a repo — the rail, the repo detail panel, and the PR pane — it now opens the new per-repo chat instead, one derived key with two honest labels decided byviewActions.ts'sbodyVerbs.ReviewView's chat-draft list gainss/e/r(submit / edit / cycle route), derived and appended after the existingall/none/file/discardso those four keys stay pinned.- Dashboard shortcut-bar follow-ups from the two-row redesign, all about the bar telling the truth. The keycaps get an explicit foreground (
theme.keycapFg) so they read on a light terminal theme instead of assuming a dark ground (#465); the pinned? help quitrun sits flush with the right edge rather than three columns in (#460) and survives down to 60 columns — the fit order now drops the row-label slot and/ filterafter the four keys before anything pinned would clip (#464); and the target label truncates the owner half of a longowner/repo(alxedelwe…/arkanoid_oq4e), keeping the half that discriminates (#472). The verbs are honest too: an empty issue list dropsimport · approve · investigate · transcript(#473), a local-only checkout on the rail no longer offersUnwatch(#459),tis bound on the issue list only — not the rail or the PR pane (#466) — a system rail row's navigate row says→ open · ⏎ open(⏎ logon logs) instead of→ issues · ⏎ detail(#470), the logs row no longer opens with a dangling│(#458), and over a pending submit card the composer'sescchip readsesc blur, which is all it does there (#479).docs/dashboard.mdand the footer spec caught up with the redesign (#468). - One
labelDeltatable now drives both the dashboard's optimistic label update and the realghlabel op, so a transition can no longer differ between what the screen shows and what GitHub receives (#443). The deliberateplanning/queuedprediction overlay is named and tested as the only permitted divergence. One consequence: the optimistic set for an ask now carries the configured ask label, which the old switch silently dropped.
Fixed
- Dashboard chat: sending a message re-follows the tail, so the message you just wrote (and the answer streaming under it) is on screen however far back you had scrolled to read; the agent's answers are labelled
junco:opposite youryou:— while streaming too, so the label does not appear only when the turn ends; and the agent now knows how a parked draft is submitted (the draft card'ss, or the review view), so asking it to "submit" points you at the card instead of at copying the fence into a file and runningjunco submitby hand. - Dashboard scrolling, three ways. (1) The chat and ticket-transcript windows re-nudged themselves onto the cursor's card or tool call on every render — right while
↑/↓moved the cursor, wrong once they scrolled rows — so any scroll that took the anchor off screen snapped straight back to it: with a parked card near the top,PgUpfrom the tail landed on the card,PgDndid nothing, one↑jumped to the top and↓was inert. A cursor move now brings its anchor into view exactly once (tab, or a click on a card), and the scroll keys are free to leave it behind. (2) A held navigation key auto-repeats into one stdin burst under load ("kkk"), which the input layer had been treating as a single unmatched keypress — every press in the run vanished, so holdingj/kon the rail or in a transcript felt like dropped keys; the burst is now replayed press by press, and every list cursor steps from its pending position so the presses compose. (3) Closing a chat left you on the issue list whichever panecwas pressed on: the chat view no longer touches the pane, soesc/qreturn to where you came from. - The phantom
todo_writecall: both the fresh and amend worker preambles instructed the agent to call atodo_writetool that does not exist in the agent layer, wasting a failed tool call whenever the agent obeyed. The instructions and the deadtodo_writeloop-guard threshold that referenced it are removed. - The outbox no longer writes a corrupted PR URL into a finished ticket (#348). When an offline-queued PR op replays and
gh pr createreports that the PR already exists, the outbox recovers the URL withgh pr view … --jq .url— and took that stdout on faith. Ifghexited 0 with empty output or a barenull, the op checkpointed an empty PR URL, wrote it into the done ticket's## Resultblock (where the dependency sweep reads it to unblock dependents, rewriting it on every subsequent flush), and posted an "Opened " comment with nothing after it on the linked issue. An empty ornullrecovery now fails the op retryably with the URL still unset: nothing is written back, no comment or label flip is attempted, and the next flush retries the create from scratch instead of resuming from a bogus checkpoint. - The shipped config templates no longer earn a deprecation warning on day one (#377).
examples/config.jsonandexamples/config.hosted.json— what a fresh install copies from, and part of the npm package — still opened with the deprecatedvaultRoot/juncoSubdirqueue-root override and set the equally deprecatedgit.worktreeRoot, so a copy produced a startup deprecation and ajunco doctorfinding immediately; the canonicaldataDirkey never appeared, and neither did thegithub,botAccount,planSets,skills,updateCheckorworker.applyFallbackToAgentsections. Both templates now lead with"dataDir": "~/.junco", drop the three legacy path keys, and carry every missing section at its schema default, with a test that fails if they drift from the schema again. - Dashboard chat drafts: a redrafted fence keeps its first slot with its last content, so a ticket set's order — and a
depends_onsibling's place after its dependency — survives a redraft (#448); a parked plan set'splan.mdis written behind a fence one backtick longer than any run inside it (minimum four), so a plan body containing a column-0 ``no longer truncates on relint orjunco submit --plan(#451); archiving a draft moves its files directory with its JSON (submitted/<id>/,discarded/<id>/) instead of leavingdata/chat-drafts/<id>/behind forever (#450); the automatic lint retry spends its one slot whenever the retry comes back, not only when it parks a replacement, so a later retry cannot remove the wrong draft (#449); and a draft whose JSON no longer parses is reported as unreadable rather than "no longer parked", with the draft-resolution rule (a ref matches an id or a file name; no ref means the only parked draft) now existing once, shared byfindChatDraft, the composer's/submitandjunco_submit` (#480). - Dashboard chat runtime: a prompt admitted while the daemon is draining answers
503 {error:"draining"}before any session is resolved, so it cannot rebuild the SDK sessiondrain()just disposed, anddrain()itself is bounded by a 10 s grace instead of the fullchat.turnTimeoutMinutes(#446);junco unwatchno longer races a live chat — the manager evicts sessions whose repo left the watchlist on the next poll, ending subscribers cleanly (#452); a session's checkout path is re-resolved when it is reopened after/newor after its watchlist entry moves, recordingjunco_chat_session_reset{reason:"cwd_changed"}so the transcript explains the change (#453); and the/chat/notevalidation sets derive from the transcript schema's own draft vocabularies, so an added draft kind is a compile error rather than a silent 400 (#447). - Dashboard: the navigate row's
: paletteand→ issueskeycaps — and the wide layout's combined←→ panescap — answer a click (#461, #488);cfrom a live ticket transcript closes the transcript first, so its poll is released instead of leaking under the chat (#462); the command palette is clipped to the body at 30 rows instead of bleeding its bottom border onto the footer (#463); and ajunco_submitthat queued but failed to archive renders its caveat on theranrow instead of only in the model's reply (#479).
Security
- A ticket's
## Verificationbash blocks now run under the same sandbox backend and policy as the agent's ownbash(#335). They previously spawned as a bare/bin/bash -cwith only the environment scrub applied — no OS confinement, unrestricted network, the whole filesystem — while executing whatever the agent had just written into the worktree: apackage.jsonscript, a Makefile target, aconftest.py. Tokens were scrubbed from the environment, but the bot'sghcredential,~/.ssh, and every other checkout on the host live on disk and stayed reachable. Blocks now get network persandbox.networkand the per-ticketnetwork:opt-in, writes limited to the worktree, its scratch dir, the linked worktree's git metadata andsandbox.extraAllowWrite, andTMPDIRredirected into scratch. The sandbox is resolved lazily (a block-less ticket never pays for the probe) and once per ticket, and a resolver failure fails every block closed rather than running it unconfined. The new live-reload leververify.sandboxed(defaulttrue) opts out, and is inert whensandbox.enabledisfalse. See Configuration § Verification sandbox. - The sandbox's builtin read deny-list now covers the standard credential files (#336). The read side is "the whole host minus a short list", and that list stopped at
~/.ssh,~/.aws,~/.config/gh,~/.gnupgand~/.pi— so on every ticket the agent could read~/.npmrc,~/.git-credentials,~/.netrc,~/.docker/config.json,~/.kube,~/.config/gcloud,~/.cargo/credentials.toml,~/.pypirc,~/.gem/credentialsand the coding-agent harness's own home directory. Where the credential is a single file inside a directory the agent legitimately needs — cargo'sbin/andregistry/, docker'scli-plugins/— only the file is denied, not the directory, and the deny is rendered as a file mask or a subtree deny according to what the path actually is, so it means the same thing on all three backends. The list is not operator-removable, as before. - junco now scans the diff it is about to push and refuses the push on a hit (#337).
sandbox.network: denygoverns the agent's tool calls; it is never consulted when junco itself runsgit add -Aover the worktree and pushes under the bot identity — so the push was an egress channel no sandbox rule saw, and content copied into a source file left the host through junco's own privileged push. The added lines of<base>..HEADare now matched against a deliberately narrow set of high-confidence shapes: PEM private-key headers, GitHub token prefixes,AKIA…access-key IDs,sk-ant-, Stripe live keys, Slackxox…-tokens, npm tokens, and credentials embedded in a URL — the shape a copied.netrc,.npmrcor git remote takes. On a hit the push is refused, the worktree is preserved, and the ticket fails with a note namingpath:lineand the rule that matched — never the matched content, which is not logged, stored, or written to the ticket record.pr.secretScan(defaulttrue) opts out a repository that legitimately vendors credential-shaped fixtures. See Configuration § Pre-push secret scan. junco ask,junco auditandjunco investigatecan no longer write to the checkout they read (#346). All three run the agent in the operator's real checkout, and the sandbox policy made that cwd a writable root unconditionally — the read-only promise rested entirely on the tool allowlist. A locally dispatched ticket'stools:is used verbatim, so one namingbashorwritesilently turned a read-only run into full write access to the live checkout,.gitincluded: two layers that should be independent were one. The three flows now build their policy with scratch as the only writable root, so the in-process path jail and both OS backends refuse writes into the checkout whatever the allowlist says. Reads of the checkout are unaffected, and the PR flow and the critic keep their existing policy.- An apply ticket's patch series can no longer reach
git amwith a path junco never inspected (#338, #339). The containment check — traversal paths, absolute paths, binary hunks — was enforced only by plan-lint'spatch_paths_sanerule, which runs only whileplanLint.enabledandplanLint.blockOnErrorare both on, both live-editable levers an operator may plausibly turn off for the style rules. With lint off, a series naming../escaped.txtreachedgit am, and git's own rejection rolled into the escalation ladder, which by default handed the series to an agent as its "reviewed" specification.applyPatchSeriesnow runs the same checks itself, unconditionally, before any side effect — no temp file, no transcript frames, no git call — and the ticket fails withapply failed: refused before git am: …regardless of the fallback toggle. Separately, the series parser recognised only the plaindiff --git a/… b/…header, while git C-quotes a path whenever it contains a quote, backslash, tab or non-ASCII byte under the defaultcore.quotePath: such a hunk's path never entered the file list the containment check reads, so one ordinary hunk was enough to keep the series "well-formed" while a quoted-path hunk rode along unexamined. Anydiff --gitline that is not the plain form now rejects the whole series, andrename from/to/copy from/topaths are extracted explicitly, since git applies those names over the header line's. - Both OS sandbox backends tightened what a sandboxed agent can reach beyond the filesystem. On macOS the Seatbelt profile opened with
(deny default)and then granted unrestricted Mach name lookup, so the agent's bash could reach every service on the host — securityd (the login keychain thatgit credential-osxkeychainreads), fseventsd, the TCC-mediated daemons;security list-keychainsexited 0 from inside the sandbox. That blanket grant is replaced by an enumerated allowlist, established empirically by running node, git, npm, python3, go, ruby, perl, bun, swiftc, cc and make under the profile and adding names until they passed: exactly one turns out to be needed, the directory-services lookup without whichgit commitfails andwhoamiprints a raw id (#340). On Linux the agent's bash now gets its own IPC and UTS namespaces and a fresh session —--new-session, which closes the TIOCSTI terminal-injection escape outright — on top of the pid and network isolation it already had (#345). sandbox.requireBackend(boolean, defaultfalse, live-reload) makes the defaultsandbox.backend: "auto"fail closed on a failed probe (#344). Until now a host with nobwrap, or a kernel that refuses unprivileged user namespaces, silently degraded the session to thenonebackend: the environment scrub and the in-process path jail survived, but agentbashran with no OS confinement whilejunco statuslooked healthy and only a log warning and ajunco doctorwarn: said otherwise. With the lever on, an unavailable auto-selected backend refuses the session outright andjunco doctorreports✗ sandboxand exits 1; both refusal messages name the lever, since it is the opt-out rather thansandbox.enabled. It is inert for an explicitly named backend and forbackend: "none", which already failed closed. See Configuration § Sandbox backend.- The GitHub bridge now vouches a sub-issue's parent body against the trigger label, not just the child's (#342). When a labeled issue is a sub-issue, the bridge injects the parent's title and body into the planner prompt as background context — but only the child was checked by the edited-after-label guard, and the parent is a different issue with a different author that could be edited at any time after the label vouched the child. The same
lastEditedAtcheck now runs at the parent's own repository and number (sub-issues may live across repos); a parent that postdates the label, cannot be verified, or is missing its coordinates has its context dropped with awarnlog line. A failed parent check never blocks the child, which was vouched on its own. - The final issue comment junco posts at finalize — the Q&A answer, or the PR-link excerpt — is sanitized before it is published (#341). It carried the model's text verbatim, and junco recovers plans by looking for a
<!-- junco:plan -->marker in its own bot-authored comments, so a prompt-injected agent could plant a forged marker on the very issue the plan flow reads back. The text now goes through the same sanitizer the parked audit and investigate drafts already use — HTML comments and control characters stripped, capped at the comment limit — and an answer that sanitizes to nothing falls back to the existing_(no answer text)_placeholder. - The data tree is created private:
0700on the root and every subdirectory,0600onconfig.jsonand on transcript.jsonlfiles (#343). On a shared or multi-account host, every local user could previously read the config — which may hold a literalmodel.apiKey— and every transcript under the data root, which is the full agent event stream including verbatim contents of private-repo files; all of it was created under the default umask. The mode rides the temp file through the atomic rename, so a rewrite tightens a loose file too. Becausemkdirnever re-modes a tree that already exists,junco doctorgains adata tree modescheck over the config file, the data root and the transcripts directory that warns — never fails — with the exactchmodto run; it is skipped on Windows, where the bits are meaningless.
0.12.0 — 2026-08-30
Added
junco lint <file>validates a ticket — plan-lint plus repo/branch preflight (repo path exists and is a git repo, origin remote exists and is GitHub, the derived branch is not already on origin, frontmatter labels exist on the repo) — without submitting it.junco submit --dry-run <file>reports the routing verdict (destination: issue|inboxplus areason:per cause), on the issue route thewatched:repo, thecarried:/would discard:frontmatter and the effectivetimeout:, and the same lint results asjunco lint— all without writing anything.- Bounded
timeout_minutescarry throughsubmit --as-issue: a valid value is now clamped to 1–480 minutes and stamped as a marker in the vouched issue body by the bridge's fence door, instead of always falling back to the daemon's default. sandbox.bashTimeoutSeconds(default 600, 0 = none): a wall-clock ceiling on one sandboxedbashcall when the agent passes notimeout. The agent's explicittimeoutwins when positive and finite; a zero, negative or malformed value falls back to the ceiling. Live-reload lever (junco config set sandbox.bashTimeoutSeconds 900).- Transcript viewer.
junco transcript <ticket-id|path.jsonl> [--thinking] [--tools] [--width N] [--json]prints a ticket's recorded event transcript — one header per run (flow, model, outcome, duration, tokens; the error line for a failed attempt), each turn's text, and every tool call with a one-line result summary (--toolsprints the bodies,--thinkingthe model's reasoning). The dashboard opens the same view withenteron any running or recent queue row:j/kmove over tool calls,enterexpands a result inline (capped at 400 lines),ttoggles thinking, and a running ticket's transcript follows live (f/Gfollow, scrolling pauses) until the run ends. RUNNING queue rows are now selectable for this; retry/delete stay inert on them. - Plan-driven ticket sets: tickets gained
depends_on:frontmatter (with worker-manageddeps_satisfiedstamping), and the worker now claims a ticket only after every dependency has finished AND its PR — when one was opened — has merged, checked by a throttled dependency sweep in both the serial loop and the scheduler. A failed dependency cascades its dependents tofailed/with adependency_failedmarker naming the edge, andjunco retry <parent>resurrects the cascaded dependents transitively.junco list,statusandsubmitsurface dependency state, andsubmitwarns on adepends_onthat names nothing queued or finished — sets may arrive out of order, so it never refuses. - Plan-set compiler, behind the new
[planSets]config section (enabled— default false,mergePollSeconds,maxTasks): one fencedjunco-plandocument compiles into a dependency-ordered ticket set — each task its own ticket and pull request, executed via thedepends_onmachinery above. Two doors:junco submit --plan <file> --repo <path>locally, and — with the GitHub bridge — a labeled issue whose vouched body carries ajunco-planfence or whose planning session emits one. An edited, re-approved plan supersedes the previous revision's unclaimed tickets; the sweep maintains a plan-set dashboard comment, lifecycle labels, and a degraded-mode comment when compilation fails. junco submit --as-issue <file>(--as-issue --plan <file> --repo <path>for a plan-set fence) files a locally-authored ticket as a parked, unlabeled GitHub issue instead of dropping it in the inbox — bot-authored (requiresbotAccount.enabled), refused unless the target repo is owned, bridge-watched, and GitHub integration is enabled. No labels are applied; a human's own trigger label is what launches it later. The GitHub bridge's labeled-issue sweep gained a matching precedence door: a vouched issue body carrying ajunco-ticketfence (or, withplanSets.enabled, ajunco-planfence, checked first) now queues verbatim — skipping the planning session entirely — while ask still wins over any fence and a fenceless body still falls through to the planner unchanged. Zero new config keys: the planner remains the fence producer for issues that arrive without one, not a route. The ticket'srepo:may be the watched clone path or the operator's own checkout of a watched repo (matched by itsorigin). For a plain fresh single ticket, thejunco-dispatchskill takes this route by default whenever GitHub integration and the bot account are on and the target repo is bridge-watched (amend tickets and hand-authoreddepends_onsets stay on the inbox;junco-local:forces the inbox; "park it on github" / "junco as issue: …" / "dispatch as issue" force the issue).JUNCO_CONFIGnames the config file explicitly, for scripted, CI and sandbox contexts. It overrides the canonical~/.junco/config.json(a leading~expands; the value must be absolute — a relative value is rejected with an error; an empty value is ignored), and the daemon'sworker.lockfollows it — so two configs each take their own lock. They are independent instances only if they also set differentdataDirs: the lock sits next toconfig.json, not under the data root, so two configs over onedataDirtake two independentworker.locks that never see each other — it's the shared-root claims below (#310) that stop the second one from actually starting. Seedocs/configuration.md. The overridden config is denied to the agent sandbox alongside the canonical and legacy ones.- The daemon now writes
metrics.jsoninto the data tree (<dataDir>/data/metrics.json;<dataDir>/metrics.jsonon a pre-0.10flatroot) — the countersjunco statusand the dashboard read over/health, persisted for out-of-process readers. Written atomically, debounced, and stamped at startup and shutdown; a write failure is logged and never interrupts the worker. - Split-queue detection (#274):
junco startwarns at startup — ahead of both destructive recovery steps (recoverOrphans,pruneStaleWorktrees) and ahead of the endpoint wait, so the operator sees the mismatch before the daemon acts on the wrong root — andjunco doctorreports a matching warn when the resolved queue'sinbox/is empty while another queue root this installation could own still holds pending tickets. The roots enumerated are the canonical~/.junco/queue, the legacy~/.local/state/junco/queuedata root, and whateverqueueRootresolves to on this run (labeledconfiguredwhen it is neither of those — a vault or an explicitdataDir/stateDiroverride lands here), deduped by resolved path. A root that is no longer the resolved one is not checked and cannot be: a vault or explicit override is folded intoqueueRootby config assembly and not retained separately, so an abandoned one leaves no path behind to look at. Onlyinbox/is counted: a completedjunco data migrateleaves the legacydone/populated forever, and a warning that fires on every start is one operators learn to ignore. An all-empty (fresh) install stays silent, and the check never moves an exit code — a split queue is an operator decision, not a broken install. junco startnow claims two additional pidfiles in the shared state a daemon actually contends for, without movingworker.lock(#310):<dataDir>/daemon-tree.lockand<queueRoot>/daemon-queue.lock.worker.lockalone only catches a second daemon resolving the same config file — two configs sharing adataDir(or, via a legacyvaultRoot, sharing a queue with two different data roots) previously took two independentworker.locks that never saw each other, and both ran, polling one queue. Now the secondjunco startrefuses outright, printing the shared root, the claim file, and the first daemon's pid; there is no--forceescape hatch forstart. The refusal exits 0, like the existingworker.lockrefusal and for the same reason — the rendered service units restart on failure, so a non-zero exit would respawn-loop a misconfigured second unit every 30 seconds forever; the message carries the loudness and says the daemon did not start. All three pidfile paths are resolved through symlinks, so two configs that reach one data root or queue by different names collide on one claim rather than taking two and both starting.junco doctorsurfaces the conflict as adaemon claimwarning naming the root, the holding pid and the claim file — the refusal itself is written before the log sink opens and never reachesworker.log— andjunco restart/ barejuncopoint atdoctorwhen the daemon never comes up.junco data migrate's daemon-up guard gained the same two claims as a third signal alongside its existing/healthandworker.lockchecks, closing the matching blind spot where aJUNCO_CONFIG-overridden peer's pidfile sat somewhere migrate never looked; a new phase also clears (and never touches a live one) a claim left behind by a crashed daemon, before it can be mistaken for real queue or data content by a later conflict check. Two incidental fixes rode along:junco doctor's daemon check now normalizes the config path like every other lock-path reader, instead of reporting a live daemon as "not running" for a relative config path; and a pre-existing leak where a throw between acquiringworker.lockand entering the main loop left it behind for the next start to steal is closed by releasing every pidfile claim from one outerfinally— which now also covers the two claim acquisitions themselves, and, injunco data migrate, the stale-claim sweep's ownmigrate.locks.
Changed
- The
junco-dispatchskill now delegates destination and validation tosubmit --dry-run/lintinstead of probing config and re-deriving the verdict itself, previews quote the dry-run's carried/discarded frontmatter and effective timeout before approval, both destinations now offer to monitor the ticket through to completion, and every dispatch ends with a machine-checkableDISPATCHED <id> -> <destination>line. - The
junco-dispatchskill's preview gate is now destination-templated: theYes, dispatchoption's description must be copied from a per-destination template (Park as a GitHub issue on <owner>/<repo> — nothing runs until a human applies the trigger label.vsSubmit to the local junco inbox — the worker claims and runs it within ~15s; no further gate.), so the approval prompt itself names where the ticket goes. Previously the destination lived only in a free-text line before the preview plus the question sentence, and a dispatching model could present an inbox-worded gate for an issue-routed dispatch. junco data migratereports what it did more honestly (#281). Two user-visible changes: on a machine where the canonical and legacy roots both hold a subtree pending the same destination, the first run now exits 1 with askipped-conflict (contended source)naming both sources, where it previously exited 0 and silently left the legacy straggler unplanned for a later run to fail on — the old success was the bug. Andjunco data --jsongains an always-present top-levelpendingConfigRelocation: {from,to} | null; no existing key changed, and it is deliberately kept out ofpendingMigrations, which also carries doctor's unmigrated data dirs. Alongside those: an interrupted cross-device copy now records itself, so a later run names the obstruction instead of reporting a generic "destination already exists"; a failure right after the config relocation no longer printsconfig: nothing to relocateabout a file that just moved; gh-creds conflicts print under their own heading instead of the data-root one; the--dry-runpreview no longer promises a legacy-root removal the acting run would skip; andjunco doctorandjunco datanow surface a pending config relocation the way they already surfaced pending data pairs — except underJUNCO_CONFIG, where an explicitly-named config is deliberately never relocated and a warning could never be cleared.- The setup walkthrough now refuses to run on the fresh path when this machine already has a junco (#273), instead of scaffolding a competing config: bare
junco(orjunco dashboard) with no config at the resolved path exits 1 when a daemon is live — theworker.lockholder beside the config, or a/healthanswer on the defaults — or when the resolved data root is already populated. Each refusal names what it found, the config path it expected, andjunco doctor— the one diagnostic that still works with no config, sincejunco statusand friends load it unconditionally and would die with a fatal ENOENT in exactly the state the refusal prints in. The populated-tree refusal additionally offersjunco config initto scaffold at that path deliberately; the live-daemon refusal does not, and should not be "reconciled" to match —config initis ungated and would write the very competing config this guard exists to prevent when HOME is what moved. The re-run path is deliberately never gated: it rewrites the same file it read, so it cannot split a queue, and with nojunco setupsubcommand it is the only door for repairing a broken config. - Upgraded
@earendil-works/pi-coding-agentto 0.84.2. Session creation migrated from the removedauthStorage/modelRegistryoptions to the SDK's asyncModelRuntime, with the operator's API key supplied through a junco-owned in-memory credential store — it still never reaches disk or the agent's environment. Operatormodels.jsonfiles usingcompat.sendSessionIdHeadermust switch tosessionAffinityFormat(removed upstream in 0.80.7).
Fixed
- Thinking level now reaches chat-template models: the default thinking format emitted only enable/preserve-thinking flags and never forwarded
reasoning_effort, so on templates that steer thinking depth through that kwarg every configuredmodel.thinkingLevelsilently ran at the template's own default. The default is now the generic chat-template format with declared kwargs, and the newmodel.thinkingLevelMapleaf (+ lever) collapses junco's six levels onto the template's vocabulary;examples/config.jsondrops its explicitthinkingFormatso copies inherit the fixed default. - Sandbox: the agent could not
git commit. junco runs the agent in a linked worktree, whose index/HEAD live in<repo>/.git/worktrees/<name>and whose commits write<repo>/.git/objectsandrefs— none of it under the cwd, which was the only writable root besides scratch. The first commit died withfatal: Unable to create '<repo>/.git/worktrees/<name>/index.lock': Operation not permitted, and an agent that went looking for a writable path could burn the whole ticket timeout on it (#320).resolveSandboxnow asksgit rev-parse --git-dir --git-common-dirin the cwd at session start and adds exactly what a commit needs to the writable roots — the worktree's gitdir plus the common dir'sobjects/,refs/andlogs/, creating a fresh clone's missinglogs/first because bubblewrap aborts on a missing bind source; a standalone clone adds nothing. The common dir itself —hooks/,config,info/,packed-refs— stays unwritable, so an agent cannot plant a hook or acore.hooksPaththat junco's own unsandboxed git calls would execute at daemon privilege; an in-sessiongit config,gc,pack-refsor ref deletion fails loud instead (none is needed). docs/operations.md states the posture. The automatic grant needs git ≥ 2.31 (--path-format=absolute); on older git the resolver detects the echoed flag and keeps the cwd-only policy. The integration suite now commits for real inside the sandboxed worktree on both backends. - Sandbox: a killed bash command looked like a success, and the agent's
timeoutwas off by 1000×. Pi hands a customBashOperations.execthe model'stimeoutin seconds; junco fed it tosetTimeoutas milliseconds, sotimeout: 60killed after 60 ms. And after any kill (timeout or abort) junco resolved{ exitCode: null }, which Pi's tool treats as success — the agent saw a truncated result with no status.execnow converts seconds, applies the new default ceiling, and rejects with the sametimeout:<secs>/abortederrors Pi's own backend throws, so the tool reports "Command timed out after N seconds" / "Command aborted". Found while diagnosing the sandbox commit denial (#320), where an unboundedgrep -rover a source tree pinned the worker for the whole 60-minute ticket timeout. Limits above 2 147 483 s (Node's setTimeout ceiling) are clamped rather than firing after 1 ms, and a reaped command whose escaped descendant keeps the stdio pipes open now settles on exit plus a short grace instead of hanging until the ticket timeout. - Sandbox (macOS): the agent's
gitcould not run at all. Since the data root started being denied wholesale,git rev-parse,git statusandgit difffailed inside the agent's own worktree withfatal: Invalid path '<dataDir>': Operation not permitted— every PR ticket, on the default configuration (sandbox.enableddefaults to true,backendtoauto). junco gives the agent a linked worktree, so its.gitis a file pointing at a gitdir under the denied data root, and git resolves that path bylstating every component; the Seatbelt profile deniedfile-read*on the root, which denieslstattoo. Reading files kept working, which is why nothing caught it. The generated profile now grantsfile-read-metadataon exactly the denied directories that are path components of an allowed path (two of them under the v2 layout, one under flat) —stat()on those directory nodes and nothing else: their listings, their contents, and every receipt,config.json, transcript, queue entry, mirror and github-cache under them stay denied. Linux/bubblewrap and the in-process path jail were never affected — bubblewrap masks a denied directory with a tmpfs, which leaves the node stattable.junco doctorand the real-backend integration suite now rungitunder the generated profile in both data-tree layouts. junco doctornow preflights the sandbox policy, not just backend availability. A configuration thatbuildPolicyrefuses (an allow-back or writable root sitting above a by-name denied file — agit.worktreeRoot,github.externalReposRoot,sandbox.extra_allow_writeorJUNCO_CONFIGpointed into the wrong tier) used to report healthy and then fail 100% of tickets at sandbox setup. It is now a✗ sandbox policyfailure carrying the same actionable message, and that message now namesJUNCO_CONFIG— a verified trigger it used to omit — and says plainly that the refusal is sandbox setup rather than something the ticket did.- Skill links:
junco skill installnow decides its exit code and its per-line output from structured report entries instead of prefix- and suffix-matching rendered warning strings, so rewording a message can no longer silently change behaviour; the wizard and the CLI compare harness directories by one normalized rule, so an already-consented harness no longer renders unchecked on a rerun (or gets written twice in two spellings); and the setup walkthrough now surfaces skill-link failures instead of discarding them. One observable side effect of the structural comparison:junco skill install --harness <x>no longer exits non-zero for a failure on an unrelated harness whose path merely shares a string prefix with a requested one (e.g. a siblingskills-extradirectory) — the old rule matched raw string prefixes against the rendered warning text, the new one matches harness directories exactly. - Sandbox: plan-set records (
plans/,data/plans) andmigrate.lockare now denied to the agent sandbox. The plan-set records directory joined the data tree with the plan-sets work but was never added to the sandbox deny list, leaving control-plane state (repo paths, issue numbers, task ids) agent-readable. A new classification test fails if a future data-tree entry is added without being denied or explicitly exempted. - Plan sets: a dependent ticket no longer claims while its dependency's PR is still queued for offline delivery — the offline endgame now records a machine-readable
pr_queuedmarker, the dependency sweep waits on it, and the outbox writes the realpr_urlback onto the finalized ticket when the PR opens. - Plan sets: the maintenance sweep stops probing the plan comment of sets closed more than 30 days ago, so its per-sweep GitHub cost no longer grows with every set ever created.
- Plan sets: a disposed child now renders as
supersededon the dashboard instead of counting as a failure; a deferred supersede no longer skips the record's whole maintenance sweep; a child stranded by a fan-out failure is retried instead of waiting for another plan edit;junco submit --plansupersedes the previous revision's unclaimed tickets on a re-run and prints real destination paths; and the plan compiler now refuses smuggled frontmatter delimiters in every free-text field plus code fences inverification. junco data migratenow rewrites the absolute paths stored inside data files — the watchlist, queue tickets (repo:/workdir:), pending assess batches, pending comment drafts, outbox push/PR ops, and plan-set records — so a migrated install keeps working instead of pointing at the removed root. It also moves the plan-set records tree, which had no migration pair, and unlinks theskillssymlink mount that made the legacy-root removal fail on every machine whose daemon had run.- Sandbox: an unavailable backend now says why it is unavailable. The availability probe discarded the child's stderr and reported a bare exit code, so
junco doctor, the setup walkthrough and the daemon's auto-degrade warning could only say "bwrap unavailable" and advise "install bubblewrap" — actively wrong advice when bubblewrap is installed and something else refused (a kernel policy such as ubuntu-24.04'skernel.apparmor_restrict_unprivileged_userns=1, which blocks an unprivileged user namespace). The probe's own words are now quoted in all three places, ahead of the install hint, collapsed to one line and truncated at 400 characters. The exit code remains the sole decision input — availability, degrade and fail-closed behaviour are unchanged. junco doctorwarns when both the canonical and legacy data roots hold a tree. That signal is ambiguous — it can mean a pre-0.10 binary was run after a completed migrate and recreated the legacy root, or an interruptedjunco data migrateleft stragglers behind — so the warning names both possibilities and recommends re-runningjunco data migrate, which is safe under either (it resumes safely and never overwrites).
Security
- Sandbox reads are now deny-by-default across the junco data tree. The three sandbox backends gained allow-overrides-deny precedence (longest-prefix-wins, shared by the Seatbelt profile, the bwrap mount list, and the in-process path jail), and the data root (
~/.junco) is denied wholesale instead of by a hand-maintained enumeration of its sensitive subtrees. Only the agent's execution roots are allowed back —cache/clonesandcache/worktreesunder the v2 layout,clones/andworktrees/under the flat one — withcache/mirror,cache/github-cacheand the root receipt files denied at their own depth. Everything else the daemon owns (tickets, review queues, outbox ops, transcripts, plan-set records, task history, logs,config.json) is now unreadable to the agent by containment rather than by remembering to list it, closing the class of gap that leftplans/agent-readable until 2026-08-21. Every daemon-owned subtree is also denied at its own depth, so the deprecatedgit.worktreeRoot/github.externalReposRootoverrides cannot re-expose transcripts, plan-set records, the outbox or task history by being pointed at a whole tier — containment covers what nobody listed, the explicit denies survive an allow-back that moves.<dataDir>/skills— a symlink mount for external harnesses that the sandboxed agent never reads — is denied along with the rest of the root. - Sandbox (Linux/bubblewrap): the TUI's GitHub issue/PR cache directory is now created at daemon startup instead of on its first write. bubblewrap skips a deny mount whose target does not exist, so on a tree where the TUI had never cached anything that deny was dropped while the surrounding
cache/tier stayed bound read-only — opening the TUI during a long agent run put its token-fetched GitHub data inside the agent's readable view. macOS/Seatbelt and the in-process path jail deny it by name unconditionally and were never affected. - Sandbox: an allow-back can no longer sit above a denied file.
buildPolicyrefuses any policy in which an allow — a read allow-back or a writable root — is a strict ancestor of a by-name denied file, and the v2 allow-back narrowed from the wholecache/tier tocache/clones. bubblewrap must skip a deny mount whose target does not exist (it cannot create a mountpoint under a read-only bind), so the lazily-written receipts —spend.json,metrics.json, and v2'scache/update-check.json— had no surviving deny inside an allow-back on Linux: each became readable through that bind the moment the daemon wrote it. Reaching the first two took a mis-setgit.worktreeRoot/github.externalReposRootpointed at a whole tier;cache/update-check.jsonwas inside the default v2 allow-back, which is why that allow-back moved down. macOS/Seatbelt and the in-process path jail deny by name regardless of existence and were never affected. The refusal is fail-closed and names the allow, the file, and the settings to check — the alternatives were worse: silently dropping the allow costs the agent a tier it may legitimately need, and for a writable root it walls the agent out of its own worktree. Deny directories nested inside an allow-back are unaffected and still supported: every one of them is materialized at daemon startup, so bubblewrap never skips one, and a deny at its own depth already out-specifies a shallower allow on all three backends. - Sandbox:
sandbox.extra_allow_write: ["/"]no longer escapes the allow-above-a-denied-file refusal. The boundary test appended a path separator to the allow, which produces"//"at the filesystem root and therefore matched nothing — so the guard could not fire for an allow at/, and on bubblewrap that allow was emitted as--bind / /after every deny, re-exposing~/.sshand the whole data tree read-write. A pathological configuration, but a real escape; it is now refused like every other allow above a denied file. - Sandbox:
sandbox.extra_deny_readentries are now classified by what they actually are, rather than all being forced through the subtree deny kind. An entry observed to name a regular file becomes a by-name file deny (Seatbeltliteral, bubblewrap/dev/nullmask, path-jail exact match) — bubblewrap renders a subtree deny as--tmpfs, which cannot be mounted on a file at all, so denying an existing file used to abort the whole sandbox spawn on Linux. Denying a.envinside the agent's own worktree keeps working exactly as documented, and now means the same thing on all three backends. A directory — and a path that does not exist yet, which has nothing to observe — stays a subtree deny: that rule is the stronger of the two wherever the name is what's enforced, so it is right whichever the path turns out to be, and on bubblewrap a mount at a missing target is skipped either way. - Cleared 9 Dependabot alerts (4 high, 5 moderate). They were unfixable downstream: the SDK's own
npm-shrinkwrap.jsonpinned vulnerableundici,protobufjs, andbrace-expansion, which root-leveloverridescannot reach. 0.84.2's shrinkwrap ships patched pins;npm auditnow reports 0 vulnerabilities.
0.11.0 — 2026-08-19
Added
junco replay <ticket-id|path.jsonl> [--budget-per-kind N] [--escalation-window N] [--output-budget-per-turn N] [--output-budget-post-commit N] [--json]— re-runs a recorded per-ticket event transcript through a fresh guard manager under a chosen (or default) policy and reports what the guards would decide today: a per-run recorded-vs-replayed decision comparison, a verdict, and caveats (--jsonprints the raw report instead). Each of the four supervisor knobs resolves independently by precedence — an explicit flag, then the file's firstjunco_run_start.guard, then the loaded config, then GuardManager's own built-in defaults.- Per-ticket event transcripts (
<dataDir>/data/transcripts/<id>.jsonl) now carry v2 frame records:junco_run_start/junco_run_endbracket every agent run (flow, body, cwd, model id, tools, timeout, and the guard policy in effect) andjunco_guard_decisionrecords each nudge/kill — enough to reconstruct a run's identity and guard history from the transcript alone, and whatjunco replayreads. junco unwatch <owner/repo> [--plan]— stop watching a repo and delete its junco-owned operational state: inbox tickets, the worktree namespace, and outbox/review/history/mirror/cache traces.--planprints the itemized deletion as JSON and deletes nothing; a ticket for the repo inprocessing/blocks the run; re-running sweeps residue idempotently (including orphaned worktree namespaces). The dashboard's unwatch (u) plans first, shows the itemized confirm, and executes through the same CLI core.- Skill-link distribution:
<dataDir>/skillsis a junco-managed symlink mount to the packagedskills/directory, and the newskills.harnessDirsconfig key lists the harness skills directories (standing consent — junco never writes into a directory not listed) that receive ajunco-dispatchlink through the mount. Links are created and self-healed at daemon startup and afterjunco update;junco skill install [--harness <name|path>]...(a known harness name, or any skills-directory path) adds consent and links on demand; the setup walkthrough gains a Skills chapter that detects installed harnesses and offers them as a multi-select;junco doctorreports link health, including the blocked-by-a-real-file state that self-heal deliberately refuses to fix. A valid symlink is never repointed, so a hand-managed mount survives. - The dashboard's add-repo flow now runs a bot-grant preflight and asks for explicit confirmation before inviting the bot account into a private personal repository.
Changed
- Every agent flow (Q&A, assess, analyze, PR main, PR corrective) now runs through one wrapper,
runEnveloped(src/agent/runEnvelope.ts), which builds the guard manager, opens and frames the per-ticket transcript, calls the agent, and records spend — replacing five hand-copied call sites whose parity previously rested on comments.
Internal
- Lockfile security bumps: brace-expansion 5.0.9 and nanoid 3.3.18 (transitive). Runtime dependencies unchanged.
0.10.0 — 2026-08-16
Changed
- Breaking: junco now lives under a single root:
~/.junco. The config's canonical location is~/.junco/config.json, and the default data root is~/.juncowith a reorganized v2 layout — durable state underdata/(outbox, history, assess-history, transcripts, spend.json, metrics.json), regenerable state undercache/(mirror, clones, worktrees, github-cache, update-check.json —rm -rf ~/.junco/cacheis always safe), logs underlogs/, andqueue/,review/,watchlist.jsonat the root. Existing installs keep working untouched: while~/.juncoholds no data tree and the legacy~/.local/state/juncoroot exists, junco keeps using the legacy root in its flat layout and surfaces a deprecation pointing atjunco data migrate. An explicitdataDiris honored verbatim and never triggers the fallback. - Breaking: config resolution is environment-only. The config is found at
~/.junco/config.json(the legacy~/.config/junco/config.jsonis honored until migrated);./config.jsonin the current working directory is no longer consulted, so the directory a command runs from can never select a different config — the root cause of a split-queue incident where the dashboard and the worker disagreed about the inbox. The--configflag is deprecated and inert: still parsed so installed pre-0.10 service units don't crash, ignored for resolution, with a one-line stderr notice. - Service units are rendered flagless — launchd/systemd invocations end at
startwith no--config;junco restartstill discovers pre-0.10 flagged units.worker.locknow sits next to the canonical config at~/.junco/worker.lock. - The bot account's gh config home default moves from
~/.config/junco/ghto~/.junco/gh. An existing legacy login keeps working untiljunco data migratemoves it; an explicitly configuredbotAccount.configDiris honored as-is. - The setup wizard follows the single root: the workspace default and placeholder are
~/.junco, and when existing legacy data is detected the Workspace chapter says so ("found existing data at … — junco will keep using it") instead of silently defaulting elsewhere. Saving with defaults still writes no explicitdataDirkey. - The agent sandbox now denies the config file itself (it can hold
model.apiKey) — at the canonical location, and on a not-yet-migrated machine at the legacy location too, since that is the active config there.
Added
junco data migratenow unifies everything under~/.junco: it moves the data tree (including the flat→v2 restructure), the bot gh credentials, and the config file itself into the single root, journaling every step tomigrated.json. It refuses to run while the daemon is up (--forceoverrides), takes amigrate.lockat every root it may touch, resumes cleanly after an interruption, and never overwrites: conflicts are reported, exit non-zero, and nothing is rolled back.--dry-runprints the full plan without touching anything. The emptied legacy root is removed afterwards.- Pending single-root migration is reported everywhere it matters:
junco datamarks a legacy root withlegacy — run 'junco data migrate'(and gainslayout+legacyfields in--json),junco doctorfolds the pending moves into itsunmigrated data dirswarning, and daemon startup prints the config deprecations.
Fixed
junco data migratecould never actually remove the emptied legacy root: the root's own scaffolded.gitignoremade the removal fail every time. It is now removed when its content is exactly the scaffolded*line; a customized one is left in place and reported.- On a not-yet-migrated machine the active (legacy-path) config file was readable from inside the agent sandbox; both config locations are now on the deny list.
- A live config edit while a daemon restart was pending could pair the frozen
dataDirwith a livedataLayoutand split the data tree;dataLayoutis now pinned alongsidedataDiracross restarts.
Internal
- All data-tree path construction routes through one
dataTreePaths(cfg)table (~15 modules previously joined paths ad hoc); the packaged smoke test now asserts containment — a freshconfig initwrites nothing outside$HOME/.junco. - Dependency bumps via dependabot: eslint 9 → 10 (dev), plus brace-expansion and postcss security patches (transitive). Runtime dependencies unchanged.
- Docs:
configuration.mdandoperations.mdrewritten for the single root (v2 tree diagram, migrate walkthrough, flagless service snippets); new watercolor junco mascot artwork.
0.9.1 — 2026-07-26
Changed
- README now leads with the junco mascot on a transparent background, rendered for both GitHub and npm; the package description reads "coding-agent worker" rather than "task-queue worker". No runtime changes.
0.9.0 — 2026-07-22
Changed
- Breaking: dashboard shortcuts are now derived mnemonics. Every named option's hotkey is the highlighted letter inside its own footer chip (first letter when free, cascading otherwise; uppercase = shift-guarded destructive verbs, which keep their confirm modals;
qquit and?help reserved everywhere). Notable changes: browsero→b, approvea→o, analyzec→n, add-repow→a, unwatchx→u, queue-jumpt→e, requeueR→t(retry), deletex→D, prunex→P, restartX→R, review discardx→D, assess auto-planS→A. One derived table per view drives the chips, the help modal, and key dispatch, so they can never disagree. - Breaking: the dashboard's GITHUB/LOCAL two-mode split is gone — one unified view. The rail lists every repo junco knows about (watched GitHub repos and local checkouts discovered on disk) with a pinned
systemgroup (queue, outbox, worktrees, daemon, logs) below; the body follows the cursor. Them/Shift+Tab mode toggle and the header tab pair are removed;tnow jumps to the queue system row instead of opening a separate queue view;enteron a rail repo row opens a new repo detail panel (path, origin, branch@sha, dirty flag, worktrees, recent queue activity — also the body for local-only checkouts, and for every repo whengithub.enabled = false). Queue snapshot rows now carry the ticket'srepo:path so the panel can scope queue activity per repo. junco assessfiling (CLIassess file, TUIf) stamps per-finding filed accounting (created/queued/deduped — shown as "dup" in the TUI — + timestamp + URL) and keeps the batch in the review list; the TUI review view shows batch age,filed n/mchips, and per-finding ✓ accounting, andxdiscards an open batch.- Dashboard polish: digit-free panes (1/2/3 keys removed), grouped live-metric top bar (24h record, running ticket, ETA, gate/restart warnings; breadcrumb trail), columnar issue/PR tables with header strips and state pills, always-reserved third column (activity card), daemon panel stat grid with refresh stamp and spend gauge, scrollbars, clickable confirm buttons, bot-authored rows highlighted.
- The dashboard re-renders far less: its large components — the issue and PR lists, the unified rail, and the repo/daemon detail panels — now re-paint only when their own data changes, not on every background health/queue poll. The view is memoized and its callback props are stabilized.
Added
junco assess discard <id>— explicitly archive a pending review batch; filing no longer auto-archives.- Durable per-task history: every finalized ticket (PR flow, ask, assess, analyze, or a crash-containment path) appends a record — kind, terminal status, duration, tokens, cost, and (for GitHub-bridged tickets)
nwo/issue/PR url — to<dataDir>/history/(UTC-month JSONL shards). Requeues never append. - Queue monitoring in the dashboard's queue view (
t, and the LOCAL queue section): a▸ paused — <reason>banner when the daemon's provider gate is backed off or blocked (rate limit, outage, budget, or an auth/config problem), a↻ poll Ns agoheartbeat on the RUNNING header,warn: no activity Nmstall warnings on a wedged running task, deferred/oldest-wait context on the WAITING header plus aqueued Nmage per row, real result status + duration on RECENT rows, and an always-on STATS section (24h ok/failed counts and success rate, avg duration, ETA, a 7-day sparkline, spend and token totals, guard/requeue counts, outbox depth, and a pending-restart notice) built from the new history ledger with a done/failed-dir-mtime fallback when the ledger is still empty. - The rail's queue card and
junco statuspick up the same signals: a paused line andoldest Nmon the waiting line in the rail, and astats:line (24h ok/failed · avg duration · oldest wait) plus agate:line (when not healthy) injunco status. - Live daemon-log view in the TUI: a compact
logssection in the LOCAL rail tails the latest lines, and Enter/click expands it into a full-screen overlay with level-threshold, ticket, and text filters, a follow toggle, and scrollback.junco logsand the TUI now share one tail implementation.
Internal
- Behaviour-preserving engineering work under the hood: the test suite's duplicated scaffolding was consolidated into one shared
Config/harness fixture layer behind a@vitest/coverage-v8floor; the ~3,000-line dashboardAppcomponent was decomposed into per-domain hooks; andeslint-plugin-react-hooks(rules-of-hooks + exhaustive-deps) is now enforced across the dashboard.
0.8.0 — 2026-07-17
Added
- Full-TUI mouse control with hover feedback: config editor, command palette, queue, review, help, add-repo, LOCAL dashboard, plus clickable footer hint chips in every view.
- First-run setup lives in the dashboard:
junco dashboard(or barejunco) with no config opens the guided walkthrough, then lands in the dashboard. Re-run it anytime from the command palette ("setup"). junco config init— headless default-config scaffold (the oldjunco init --yes).- Mouse support in the setup walkthrough: clickable choices, back/quit chips, click-to-finish.
- Dedicated bot-account identity for daemon GitHub traffic (
junco auth login, wizard Account chapter,botAccountconfig block). - Permission-aware repo access:
junco auth grant <owner/repo>(invite as you, accept as the bot), dashboard auto-grant after adding a watched repo, and doctor/wizard grant hints. Dispatch to an unwatched repo the bot can push to now goes direct instead of forking — and auto-onboards that repo permanently into the watchlist (bridge-swept from then on, no confirmation step); this includesjunco assessscoped to a single issue on such a repo. Fork-PR mode is unchanged for public repos the bot can't push to. junco assessnow records a per-repo history (last successful audit, its finding counts, and a marker when the most recent attempt failed), surfaced as a column in the dashboard rail and injunco status/junco doctor. Issue-scoped runs (junco assess owner/repo#N) deliberately do not refresh a repo's freshness — they audit only the code the issue implicates. The history lives at<dataDir>/assess-history/(one file per repo).- Unified data root: a new top-level
dataDirconfig key (default~/.local/state/junco) that every on-disk path — the ticket queue, parkedassess/analyzereview items, the GitHub outbox, cloned repos, PR-flow worktrees, transcripts, and watchlist/spend/metrics/log files — now resolves under. The tree is materialized eagerly at daemon startup — every directory exceptclones/external/andworktrees/(still created on demand, since a legacy override can point them outside the root) — and the root gets a self-.gitignore(*, written only when absent) so pointingdataDirinside a git checkout — including junco's own — can never dirty a commit. junco data [--json]— a pure, read-only view of the resolved tree: live counts per node, legacy-override provenance, pending migrations, and config deprecations.junco data migrate [--dry-run|--force]— the opt-in full unification for a config still carrying legacy path keys: moves the queue into<dataDir>/queue, normalizes the state tree, rewritesconfig.jsonto drop the legacy keys, and prints a receipt. Refuses while the daemon looks like it's running (--forceto override).- An in-place state-tree migration (old directory names → new ones, e.g.
assess-review→review/assess) runs automatically at every daemon startup — journaled, idempotent, and never destructive on a name conflict. - Daemon startup logs a one-line warning per deprecated legacy config key set, plus a warning for any state-tree migration conflict it had to skip;
junco doctorandjunco dataadditionally report pending (not-yet-run) migrations as informational findings, pointing atjunco data migrate. - Update notification: the dashboard header,
junco status, andjunco doctornow surface a newer npm release (best-effort daily check against the npm registry, cached in<dataDir>/update-check.json; opt out with"updateCheck": false). junco update— install the latest release and drain-restart the supervised daemon.junco --version.- Tickets can request a bot-created tracking issue:
github_request: { create_issue: true }makes the worker create the issue at claim time (own gh identity — bot account when configured) and link the PR (Closes owner/repo#N), so merging closes it. Best-effort; fork-push tickets are skipped.
Changed
- Bare
junconow ensures the supervised daemon is up, then opens the dashboard. On an interactive terminal, if the daemon is down and a launchd/systemd service references your config, it starts that service and waits briefly for it to come up before landing in the dashboard; with no service installed it opens the dashboard and points you atjunco service. Previously barejuncowith a config ran the daemon in the foreground (blocking the terminal). Never starts an unsupervised daemon, and non-interactive invocations (pipes/CI) skip the pre-flight entirely.junco start(explicit foreground daemon) andjunco dashboard(observe without starting anything) are unchanged. - Dashboard mouse protocol upgraded to SGR any-motion tracking (hover); click targets now resolve via a render-time hit-region registry.
Default on-disk locations moved under the unified
dataDirroot (existing configs are unaffected — see Deprecated below):| What | Old default | New default | | ------------------------- | ------------------------------------- | ---------------------------- | | Ticket queue | `<vaultRoot>/<juncoSubdir>/{inbox,…}` | `<dataDir>/queue/{inbox,…}` | | Assess review parking | `<stateDir>/assess-review/` | `<dataDir>/review/assess/` | | Analyze review parking | `<stateDir>/comment-review/` | `<dataDir>/review/comments/` | | GitHub outbox | `<stateDir>/github-outbox/` | `<dataDir>/outbox/` | | Dashboard-cloned repos | `<stateDir>/repos/` | `<dataDir>/clones/watched/` | | External (fork-PR) clones | `<stateDir>/external/` | `<dataDir>/clones/external/` | | PR-flow worktrees | `~/junco/worktrees` | `<dataDir>/worktrees/` | | Watchlist | `<stateDir>/github-watchlist.json` | `<dataDir>/watchlist.json` |worker.log,transcripts/, andspend.jsonwere already understateDirand move with it todataDir.metrics.jsonis a reserved, forward-looking path — listed in the tree (and byjunco data) now, written by a planned metrics-persistence follow-up.- The setup wizard's Workspace question now scaffolds
dataDirinto a freshconfig.json, but only when it differs from the default — a fully-default fresh config still carries no path keys at all.
Deprecated
- Four legacy, single-purpose path keys are now optional per-subtree overrides — each still works exactly as before, but logs a one-line deprecation warning at daemon startup and is flagged by
junco doctor/junco data:vaultRoot+juncoSubdir(queue root),observability.stateDir(the whole data root),git.worktreeRoot(worktrees root), andgithub.externalReposRoot(external-clones root). Runjunco data migrateto drop them and unify ontodataDir.
Fixed
- Dashboard rail: the
▌selection bar could be squeezed to zero width by a longowner/reponame, leaving no visible selection — and no fallback onNO_COLORterminals, where▌is the only selection cue. The rail row now pins the selection bar and the assess column and truncates the repo name between them.
Removed
- Breaking: the
junco initsubcommand. Interactive setup →junco dashboard; scripted scaffold →junco config init.
0.7.0 — 2026-07-12
Added
- Hosted catalog model resolution: a provider-prefixed
model.id(e.g.<provider>/<model-name>) with no explicitmodel.baseUrlnow resolves from the embedded SDK's builtin provider catalog (real endpoint, cost, and context-window metadata).model.source(auto/catalog/inline) pins the behavior explicitly. model.apiKeymay be omitted (the provider's environment variable, e.g.<PROVIDER>_API_KEY, applies at request time) or set to an"$ENV_VAR"reference;"!command"values are rejected.model.retry.maxRetries/model.retry.baseDelayMs— SDK auto-retry levers.- Endpoint probing (startup wait, readiness, doctor) is skipped for hosted catalog models.
- Provider gate: infrastructure failures from the inference endpoint now pause ticket claiming instead of quietly retrying against a provider that will keep saying no. Seven states —
ok,auth_error,quota_exhausted,misconfig,rate_limited,outage_backoff,budget_exhausted— are latched or backed off based on the failure text (auth/quota/model-not-found/ rate-limit/outage/daily-budget); any successful session, a config hot-reload apply, or a daemon restart clears the gate, andrate_limited/outage_backoffalso expire on their own once their backoff window passes (budget_exhaustedis the one exception — see below). worker.endpointProbe("auto"/"always"/"never") controls whether the inference endpoint is probed for reachability, overriding the catalog-skip default; probe results are cached for ~10 seconds and shared across the claim gate,/health, and/ready./healthgains agate: {state, reason, since, until}field (nullwhen no gate is wired);/readyreturns its 503 body with the gate's reason whenever the gate is latched or backed off.- The interactive dashboard (
junco dashboard) shows the provider gate's state as a colored dot on the daemon panel (red for a latch, yellow for a backoff) plus a reason line when the gate isn'tok. - Cost accounting. Every completed session (main run, critic pass, corrective re-dispatch) records its resolved USD cost to a per-day spend ledger. A Q&A/assess/analyze ticket's
## Resultfooter gains acost=$X.XXXXfield alongside elapsed time and tokens;/healthgains aspend: {todayUsd, dailyBudgetUsd} | nullfield; the dashboard's daemon panel prints aspend $X.XX todayline (/ $Y.YY budgetonce a cap is configured). worker.dailyBudgetUsd(default0, disabled): once today's spend reaches the cap, the provider gate entersbudget_exhaustedand pauses ticket claiming until local midnight or an operator's config hot-reload raises the cap — unlike every other gate state, a successful session does NOT clear it, since finishing a session doesn't un-spend money.junco doctorruns a hosted-aware preflight for a catalog-resolved model: echoes the resolved provider and base URL, reports the api-key source (a$ENV_VARreference, a config literal, or the provider's own environment variable), and — when a key is configured — runs a live per-provider auth check against the resolved endpoint.junco initgains a "hosted provider from the built-in catalog" model-source option alongside the inline-endpoint and models.json paths: pick a provider then a model straight from the embedded catalog, then the same shared api-key step (a blank key defers to the provider's own environment variable at runtime).
Changed
- Behavior: auth, quota, model-not-found, and rate-limit failures no longer consume the ticket's
retry_countbudget. Previously every infrastructure failure went through the same budgeted transient-retry path; now these four classes are recognized as the provider's fault, not the ticket's — the ticket is stamped with a freshnot_beforeand returned to the inbox withretry_countuntouched. Outage (network/5xx) and unclassified failures keep the existing budgeted transient-retry path. junco initis now a full-screen guided walkthrough (Ink): chapter rail, machine preflight, live model discovery, repo-containment and GitHub-bridge setup, an extras multiselect, a review-before-write step, and a post-write flight check. Re-runningjunco initon an existing config enters a tune-up mode that pre-fills current values and writes only what changed (all other keys preserved).--yesstill scaffolds the same minimal default config non-interactively.- Behavior: a provider-prefixed
model.idwithout an explicitmodel.baseUrlpreviously bound to the local default endpoint (http://127.0.0.1:1234/v1); it now resolves from the builtin catalog. Explicitly setmodel.baseUrl(ormodel.source: "inline") to keep the old binding. A provider-prefixed id that is NOT actually in the builtin catalog falls through to inline resolution — an explicitmodel.baseUrl+model.apiKeyare then required, and the session build fails with an actionable error if the key is missing; endpoint probing is still skipped for any catalog-eligible config, including this fall-through case. - The agent session no longer reads or creates
~/.pi/agent/auth.json,~/.pi/agent/settings.json, or a target repo's.pi/settings.json— auth and settings are fully injected from junco config.
Removed
@clack/promptsdependency — the old prompt-based wizard is gone, replaced by the Ink walkthrough above.
0.6.0 — 2026-07-11
Added
junco config path|list|get|setand an in-dashboard config editor (press,), backed by a single lever registry that also powersjunco config list(#161).- Daemon hot-reload: live-safe settings apply at the next poll; structural changes surface
pendingRestartFieldsinjunco status//health(#161). - Agent execution sandbox (
sandbox). Native OS isolation of the Pi agent's tool execution — Seatbelt on macOS, bubblewrap on Linux, no container runtime, works fully offline. Confines tool writes to the worktree + a per-session scratch dir, denies network by default (per-ticketnetwork: truefrontmatter opts one ticket in), scrubs credentials (GH_TOKEN/API keys) from the agent's environment, applies a read deny-list over~/.ssh/~/.config/gh/etc., and freezes ambient~/.piextension loading. On by default; fails closed when a required backend binary is unavailable — never a silent unsandboxed run.junco doctorpreflights availability. Toggle it (andbackend/network) live from the in-dashboard config editor (,→ sandbox section) orjunco config set sandbox.enabled false; changes apply to the next ticket with no restart. Pair with a dedicated GitHub identity for full credential separation (seedocs/operations.md§ Security model) (#160). - Two-phase assess with a review queue.
junco assessno longer files issues straight from the audit — the daemon parks every finding for review, and nothing lands on a tracker until a human confirms the batch (junco assess reviewto inspect,junco assess file <id> --all | --only <fingerprints>to file). Filing runs under your ownghauth and works on any watched repo, owned or not — owned repos getjunco:finding+severity/<level>labels best-effort; repos you don't own get label-free issues. An authoritative dedup re-runs at file time so a finding filed by hand in the interim is skipped, not duplicated (#95). - Dashboard assess review view. Press
vin the dashboard to open a per-finding checklist with the same select-and-confirm-to-file flow as the CLI (#96). junco analyze owner/repo#N— a read-only issue investigation that parks a comment draft for review and never posts without operator confirmation. Shares the issue-target resolution (gh issue view, then watched-repo lookup or auto-provision) that dispatch uses (#98).- Issue-scoped assess:
junco assess owner/repo#N. Steers the audit to the code an issue implicates, auto-provisions an unwatched repo (fork, clone, watchlist add), and stamps each filed finding's body with aContext: owner/repo#Ncross-reference so it shows up on the original issue's timeline. Dedup stays shared, not scoped — an issue-scoped run and a whole-repo run never double-file the same defect (#99). - Two-mode dashboard with an actionable LOCAL runtime-visibility mode alongside the GitHub-integrated view (#97).
Changed
- The execution sandbox is now ON by default (
sandbox.enableddefaults totrue). On macOS this is transparent (Seatbelt is always available). On Linux withoutbwrapinstalled, tickets fail closed with a clear error — install bubblewrap, or setsandbox.enabled: false/sandbox.backend: "none"(viajunco config setor the,config editor). Runjunco doctorto preflight. Seedocs/operations.md§ Sandboxing the agent (#166). - BREAKING: configuration is now
config.json(camelCase) instead ofconfig.toml; thesmol-tomldependency is removed. Convert existingconfig.tomlfiles by hand (see docs/configuration.md); junco errors with a pointer if it finds a leftoverconfig.toml. The legacy[pi]and inference-endpoint sections are gone — setmodel.*directly; the tool allowlist is now top-leveltools, andcommit_leftoversisworker.commitLeftovers.
Fixed
- Transcripts: every flow's transcript path routes through one slugifying helper, so a frontmatter id with path-unsafe characters can't escape the transcripts directory (#94, #100).
- Daemon & queue: the scheduler drains in-flight tasks in a
finallyon a claim error and the third-signal hard-exit is testable (#142); malformed-frontmatter tickets route tofailed/instead of looping, with a multi-level requeue collision suffix (#143); claims are guarded against a same-minute overwrite andsubmituses a unique temp with an exclusive-create fallback (#144);repoKeyis canonicalized so aliased repo paths serialize (#147). - Health & CLI: IPv6 health URLs are bracketed,
junco listtolerates a missing queue-box dir (ENOENT), and the health server keeps a persistent error handler (#152). - PR flow & GitHub bridge: an offline soft-abort that made commits now routes to
done/like its online twin (#146); the outbox embeds its idempotency marker on live comment posts and author-scopes the dedup scan (#148); the bridge vouches issue-body edits,junco prsincludes external repos, ticket ids are unambiguous, plans are CRLF-normalized, and a null author is guarded (#150); managed external clones + syncs are asserted contained withinexternalReposRoot(#151). - Assess & analyze: a review batch is preserved when every filing fails and an empty
--onlyselection is rejected (#149); assess/analyze issue numbers are bounded and branch/base names pattern-checked, with refreshed docs (#154, #155). - Agent runtime: dropped a phantom nudge on an output-budget kill, gated nudge-ignored on delivery, and injected the transcript sink (#153).
- Service: non-blocking systemd restart with the stop timeout sized to the max ticket timeout (#145).
- TUI:
aliveRefguards extended to the remaining async handlers, plus a non-TTYuseTerminalSizefallback (#156). - Config hot-reload follow-ups:
github.triggerLabel/askLabelreclassifiedrestart(and frozen so the bridge and reporter can't drift); the watcher single-parses and re-applieslogLevelonly when it changed; stale JSDoc dropped (#167, closes #162–#164). - Migration follow-ups: analyze threads its client's
gh/gitdeps; the analyze branch uses a no-op reporter so it can never post; the unconsumedissue_titleis dropped; read-only assess clones fork-lessly (no stray fork); the review store gains read-time shape validation + ENOENT-safe archiving; a transcript-slugify CI flake is fixed; TUI review-view scroll/badge polish (#168, closes #101–#106, #157).
Security
- Agent execution sandbox fs-tool path jail hardened against a symlink-swap TOCTOU (#169, closes #159): bash execution is serialized against the in-process fs tools (only bash can plant a symlink) and bash's process group is reaped, so a compromised agent can't win a check→syscall swap race against the jail. The OS sandbox backend (Seatbelt/bwrap) remains the primary containment; a
setsid-escaping background process on macOS is a documented residual.
0.5.0 — 2026-07-09
Added
junco-dispatchskill now recognizes repo-audit requests. Phrases like "assess this repo" or "have junco audit this repo" route to a new Assess mode that runsjunco assess(a read-only audit that files one GitHub issue per finding) instead of authoring a plan ticket.- Guard & requeue observability. Guard decisions (nudges, escalations, kills) are logged, recorded in the per-ticket transcript, and counted in run metrics;
junco statusand/healthsurface the requeue and guard counters.
Changed
- README restructured GitHub-first. New tagline (Issues in. Pull requests out.), a "The loop" walkthrough (label → plan → approve → PR, with the lifecycle labels and a real
junco logs -ftranscript), an assess section, fork-PR mode in the CLI table, and the dashboard mock up front.package.json#homepageand the README now point at the project's one-page site, junco.ironforgesoftware.com.
Fixed
- Bridge: the plan is recovered from the whole planning-session text when the junco-ticket fence isn't the last message (#86).
- PR flow: fork PRs are recovered via
gh pr list --head owner:branch(#75); only network/transientgh pr createfailures requeue (#73); a pushed branch with no PR is recovered instead of stranded; an offline amend's push reports as queued, not unqualified success;ls-remoteselects the exact ref sha, never a sibling ref (#72); fresh-mode resume is gated on crash-recovery provenance (#70); the fresh-mode fallback branch force-resets toorigin/<base>. - Locking: one shared, hardened pidfile-lock helper — atomic stale-steal via rename-aside with post-move verify, ABA-steal and pid-reuse protection, a locale-stable
psstart-time discriminator, and a fallback for filesystems without hard-link support. - Outbox: the finalize tail survives when a created-PR op dead-letters, and the flush lock is hardened against ABA steal and pid reuse.
- Assess: code findings are fingerprinted by line bucket with a normalized title — dedup survives code drift and retitles — and findings parse from the whole run's text, not just the last message.
- Agent sessions: a fallback grace deadline aborts wedged sessions; repetition-guard buffers clear after a nudge;
RunResult.finalTextis the last assistant message, not the whole run; transcript paths slugify the frontmatter id; subscribe-callback observability is guarded against throws. - Queue & dispatch: ticket placement is atomic (
linkSyncEEXIST, not check-then-act), and same-named terminal records uniquify instead of overwriting. - Service & config: systemd units escape
$and%per field-expansion rules and double-quoteExecStart/Environmentvalues; an emptyhealth_hostnormalizes to loopback, and non-loopback binds warn at startup and injunco doctor. - Logging & verification:
worker.logrotates mid-run as a single-writer, lock-holder concern; verification blocks are capped in count, bounded in aggregate wall clock, and run with a scrubbed child environment. - Daemon & schema: the stop flag is re-checked before claiming in the serial poll loop; the ticket contract bounds
timeout_minutes(> 0) andamends_pr(≥ 1).
0.4.0 — 2026-07-06
Added
- GitHub-integrated inbox mode. Trigger-labeled GitHub issues are planned first, then executed: the daemon sweeps watched repos (
[github]config section, default off — zero GitHub calls when disabled), verifies the labeler has write access (fail-closed), and dispatches a daemon-authored planning ticket built from thejunco-dispatchskill's template (read-only session at the mapped clone;planner_model_idoptionally plans with a different model). The plan is posted back as one editable issue comment for review; a write+ collaborator appliesjunco:approvedto authorize execution (or setrequire_approval = falseto auto-execute — recommended only for trusted private repos), and Junco then reads the (possibly edited) plan back out of the comment to build the execution ticket. Silent lifecycle labels track every step (junco:planning/plan-ready/approved/queued/working/done/failed/denied), and exactly one finalize comment lands per hop (the plan, or the PR link + summary / Q&A answer / failure reason). PR bodies gain a deterministicCloses owner/repo#N; an ask label skips planning entirely and routes straight to the read-only Q&A path; sub-issue parents are attached as background context for the planner.junco doctorvalidates repo mappings (clone exists, origin matches, reachable viagh) and that the planner template is readable;junco statusand/healthreport bridge sweeps. - Ticket schema (additive): worker-managed
githubprovenance block andworkdir(Q&A session cwd, validated againstallowed_repo_roots). junco dashboard— an interactive terminal UI for GitHub-integrated mode: a repos pane with per-state issue counts, an issues pane with lifecycle glyphs, and a status bar with daemon health, plus in-terminal plan review before dispatch/approve/re-plan. Repos can be added or removed at runtime through a hot-reloaded watchlist file (no daemon restart) that lives alongside, and defers to,config.toml's repo mappings.junco restart— restart the supervised daemon (picks up config + code changes): discovers the launchd/systemd user unit whose invocation references your config path, kicks it with the platform-correct verb (launchctl kickstart -k/systemctl --user restart), validates the config first (never bounces onto an unparseable config), and verifies the pid changed.- Dashboard command palette + focus keys —
:opens a palette that runs junco CLI subcommands from inside the dashboard (spawns the real CLI against the same config; output + exit code in a scrollable pane; args field forlist/retry/submit/logs/service;logsbounded;init/start/dashboardexcluded with reasons).wopens add-repo (the watchlist key);ijumps to the issues pane; a persistent context-aware shortcut bar shows the full key set at all times. Add-repo can auto-clone: leave the path empty and the repo is cloned into a managed directory under the state dir, then validated and watched as usual.
0.3.0 — 2026-06-10
Added
- Self-healing retries. Transient failures (endpoint errors, truncated streams) with no commits requeue the ticket with backoff (
[worker].max_transient_retries, default 2; worker-managedretry_count/not_beforefrontmatter). Crashed tickets found inprocessing/at startup requeue under the same budget instead of failing. - Endpoint-aware claiming. The daemon probes readiness before every claim — an endpoint outage queues work instead of burning the inbox into
failed/. - Timeout salvage. Sessions that hit the ticket timeout after committing get their commits pushed and a draft PR opened (new terminal status
timeout_partial, routed todone/) with a partial-run banner. - Force-stop. Second SIGTERM/SIGINT aborts the in-flight session and salvages commits; third hard-exits. Rendered service units now set
ExitTimeOut/TimeoutStopSecsized to the ticket timeout so supervisors don't SIGKILL a draining worker. - Day-2 CLI:
junco status,junco list [box],junco retry <name…|--all>,junco doctor,junco logs [-f] [-n N] [--json]. - Concurrency.
[worker].max_concurrent(default 1) runs tickets in parallel with per-repo serialization and graceful drain;/healthreportscurrentTickets. - Observability. Structured logs tee to
<state_dir>/worker.log(10 MB rotation) with a human-readable TTY format; per-ticket transcripts under<state_dir>/transcripts/; live progress (turns, last tool, output tokens) in/health(currentProgress). - Per-ticket
tools:override — Q&A tickets stay read-only by default and can opt into more (e.g.tools: [read, grep, bash]). [git].allowed_repo_rootsconfines PR-flow tickets to approved repo roots; the README now documents the inbox trust model.not_beforefrontmatter — schedule a ticket for later (also the worker's retry-backoff mechanism).- Plain (non-Obsidian) ticket templates under
templates/plain/; CI test workflow on push/PR; prettier + eslint (no-floating-promises).
Changed
- User-level config discovery:
--config→./config.toml→~/.config/junco/config.toml(the wizard writes the user-level path by default, sojuncoworks from any directory). - Stack-agnostic naming: daemon logs say "inference endpoint"; bare model ids default to the
localprovider (previously a server-specific provider name). - The diff-vs-spec critic is told when its diff was truncated, preventing false MISSING verdicts on very large diffs.
- The Pi event stream is typed at the session boundary (
AgentEvent).
Fixed
- README troubleshooting referenced the legacy inference-endpoint section's
urlkey instead of[model].base_url. - Stale-worktree cleanup failures now surface as a clear
GitOpErrorinstead of a raw fs error.
0.2.2 — 2026-06-01
Added
- Colorized
junco initwizard (via@clack/prompts): boxed prompts and an arrow-key model picker that discovers models from your endpoint (GET /v1/models) or lists the entries in a Pimodels.json, with a spinner while it fetches. Falls back to manual entry when the endpoint is unreachable. - Graceful cancel: Ctrl-C / Ctrl-D during setup exits cleanly (exit 130, no stack trace).
Changed
- The wizard now writes
junco_subdir = "", so the queue lives directly under the chosen directory (default~/Junco/{inbox,…}) — no redundantJunco/subfolder. Existing configs are unaffected (the schema default staysJunco). - Removed personal-stack strings from the shipped surface — wizard prompts, the legacy
[pi].model_idfallback default, and doc-comment examples now use neutral placeholders. The wizard infers the provider label from the endpoint host.
0.2.1 — 2026-06-01
Changed
- Pinned all dependencies to exact versions (removed
^ranges) for fully reproducible installs. - CI: bumped
actions/checkout→ v6 andactions/setup-node→ v6, and the publish runner to Node 24 — clears the Node-20 runner deprecation warning.
0.2.0 — 2026-06-01
Added
- Interactive setup wizard.
junco initnow prompts for the vault directory and model (an OpenAI-compatible endpoint, or a Pimodels.json), writesconfig.toml, and creates the queue directories — no more hand-writing the config.--yesscaffolds defaults non-interactively (for CI/scripts). - First-run-aware bare invocation.
junco(ornpx @ironforgesoftware/junco) with no config present runs the setup wizard; with a config present it starts the daemon as before. A non-TTY guard prints guidance instead of hanging on a prompt in pipes/CI.
Changed
junco initno longer requires a pre-existingconfig.toml; when one already exists it just ensures the queue dirs and never overwrites it.
0.1.0 — 2026-05-31
Added
- Configurable model + inference provider via a
[model]config section — the API style, context window, max tokens, thinking format, and the rest of the model's capabilities are configurable, so junco can drive any model on any Pi-supported provider (OpenAI-compatible or any hosted catalog provider). Two modes: point[model].models_jsonat a Pi-stylemodels.jsonto load the provider+model from that file, or describe it inline with[model]fields. The legacy[pi].model_idkey and the legacy inference-endpoint section still work as fallbacks forid/base_url/api_key. - Daemon (
junco start) with configurable poll loop, single-instance lock via PID file, orphan recovery on restart, and graceful SIGTERM/SIGINT shutdown. - PR flow: per-ticket git worktree isolation, plan-lint gating (validates frontmatter + discipline rules before the agent runs), loop guards via the supervisor (per-kind budgets, escalation-window turn cap, per-turn and post-commit output budgets),
## Verificationbash-block runner (executed in the worktree after the agent session, results surfaced in the PR body), diff-vs-spec critic pass with one configurable corrective re-dispatch,gh pr createintegration, and amend mode (amends_pr) for adding commits to existing PRs. - Q&A ticket mode: tickets without a
repo:field are answered in-place by the agent with no git operations. - Embedded coding agent over any OpenAI-compatible inference endpoint (the legacy inference-endpoint config section); agent driven via the
piSDK with a configurable tool allowlist and per-ticket timeout. - Observability:
/live,/ready, and/healthHTTP endpoints; per-run metrics (turn count, output tokens, elapsed time); structured JSON logs; configurable log level (debug|info|warn|error). - Dispatch CLI:
junco submit <ticket>(enqueue a ticket),junco inbox-path(print the inbox directory),junco schema(print the ticket-frontmatter JSON Schema),junco init(scaffold~/junco/config.toml),junco service(render a launchd plist or systemd unit for the daemon). - Typed ticket-frontmatter contract validated with Zod; all fields documented in the schema subcommand output.
- Harness-agnostic
junco-dispatchcoding-agent skill for scaffolding plan-lint-clean tickets from natural-language prompts. - Service rendering for launchd (macOS) and systemd (Linux) via
junco service.