fix(isolation): preserve task runtime selection and step caps

Problem: worktree task dispatch silently discarded effort, step limits and configured subagent models; parent model aliases also lost live session effort overrides.

Root cause: isolation branched before ordinary task runtime resolution, and alias resolution reloaded config instead of retaining the canonical parent runtime entry.

Fix: reuse shared task/model/effort/budget resolution, pass ephemeral options to the unattended Build, and canonicalize parent aliases to their runtime entry. Existing unsupported isolation restrictions remain refused; best-of keeps its prior policy. No persisted, wire, tool-definition or default changes.

Verification: complete 238-package root inventory passes (217 pass/21 no tests; 23289 test/subtest passes, 215 existing conditional skips, 198 cached packages). Owning delegation/isolation race has 400 passes; real boot focus has 39 passes, no skips. Compiling old owners fail 18 runtime scenarios plus parent, with two automatic-budget controls passing. First-Finish composition race has 32 passes. Gofmt, vet, full lint, make check and diff checks pass. Supplemental required builtin/boot is still running; hosted/native results remain pending.
This commit is contained in:
SivanCola committed 2026-10-25 09:46:07 +08:00
1 parent 5b2b297e6e
commit cbac083536
11 files changed
+411 -29

No files matched your search

+115
View File
@@ -0,0 +1,115 @@
---
owner: @SivanCola
backup: @esengine
status: active
reviewed: 2026-10-25
---
# Isolated runtime options / 隔离任务运行参数
## Cause and scope / 根因与范围
The task tool dispatched `isolation: "worktree"` before ordinary task-spec
resolution. That branch forwarded the prompt and explicit model, but silently
dropped `effort`, `max_steps` and the configured subagent model.
Its separate
kernel could therefore run with a different model/depth and without the requested
step cap. This is parameter loss, not evidence of a permission bypass.
An adjacent shared model-resolution defect reloaded a parent-model alias from
configuration instead of preserving the actual parent entry, losing a session
effort override. Canonical references already preserved that entry.
任务工具在普通任务规格解析前分流到 worktree,只转发提示与显式模型,遗漏 effort、
max_steps 和已配置的子代理模型。隔离子内核因此可能使用错误模型、思考深度或步数上限。
这属于参数丢失,不能据此声称权限绕过。相邻的模型别名解析还会重新读取配置,丢失父
会话已生效的 effort 覆盖;规范模型引用原本会保留该覆盖。
## Source, owner and sink / 来源、所有者与执行端
- Task arguments and existing task defaults enter the same model/effort and
child-budget resolution owners used by ordinary tasks. Vision routing remains
in the existing child-runtime resolver.
- The isolated runner receives ephemeral `Run.Effort` and `Run.MaxSteps` values.
Boot resolves a `provider.Selection` through the shared subagent model/effort
validation owner, then supplies it and the cap to the unattended child's Build.
No extra provider is constructed merely to validate a selection.
- Parent aliases resolving to the same canonical model reuse the runtime parent
entry. A different model keeps its own defaults; explicit effort still wins.
- Unsupported explicit model/effort combinations remain errors. Existing
inherited-effort policy may drop an incompatible inherited setting on a
call-selected model, while a configured model/effort pair remains strict.
A custom provider resolver retains its existing selection authority.
- Best-of uses the same private unattended builder. It continues to pass its
model with no effort override or explicit cap, preserving its prior policy.
显式参数和默认值通过普通任务已有的解析所有者,执行端将结果传入子内核 Build。
新增字段只在内存中的 runner 调用传递,不增加额外 provider 构造。
父模型别名保留
实际会话值,其他模型使用自身默认值;显式 effort 优先。不兼容显式参数继续报错,
继承值沿用既有逐模型策略,自定义 resolver 的权限不变。共享 unattended builder
的 best-of 调用继续使用原先无显式 effort/上限的策略。
## Authority and compatibility / 权限与兼容
This does not enable isolation in a session that did not offer it. Profiles,
tool restrictions, write paths, background execution and continuation/fork
arguments remain explicitly refused by this isolation path before execution.
They are not silently removed or treated as implemented. Existing approvals,
workspace selection, sandbox policy and apply/discard behavior are unchanged.
Only the in-process runner and private builder signatures change. Persisted
result records, config files, wire messages, public tool schemas and tool order
do not change. The Store Execute signature remains compatible.
Zero MaxSteps
retains the existing automatic behavior; a positive explicit cap wins, otherwise
the ordinary child-budget rule derives it from the parent cap. This is a local
step cap, not a shared root resource or concurrency budget.
不会为原本关闭的会话启用隔离。profile、工具限制、write_paths、后台和续跑/fork
仍在执行前明确拒绝,不能视为已支持或静默删除。原审批、沙箱、应用/丢弃行为不变。
只改进程内调用,不改持久格式、配置、wire、工具定义及顺序。零上限沿用自动行为,
显式正值优先,否则复用普通子任务预算规则;这不是根任务共享预算或并发限额。
## Regression boundary / 回归边界
Real `boot.Build` regressions observe actual provider construction and requests
through both direct task and deferred capability dispatch.
They cover explicit
and configured models, supported/incompatible effort, parent runtime overrides,
parent aliases, explicit/derived/automatic step caps and unchanged system/tool
prefixes over the child run. Finite scripted providers bound the negative
control even when the old implementation ignores its cap.
The predecessor owners can be compiled with the unchanged public-assembly
behavior regression. The additional private-helper unit is excluded from that
overlay because its helper did not exist before this repair; compilation errors
are not counted as behavior failures.
真实装配测试观察 provider 构造和请求,覆盖直接与动态 capability 入口、模型与 effort
及三类步数策略,并比较子任务各回合的系统/工具前缀。反向测试保留真实装配行为测试,
只排除旧版本不存在的私有 helper 单测;编译错误不能算行为反例。
## Remaining work / 后续边界
Automatic isolation, durable Begin/running identity, background/continuation,
profile/tool/write-path support, nested/root budgets, environment preparation,
cross-process settlement, recovery journals, snapshot-bound verification and
automatic integration remain independent series work.
Worktrees do not isolate
network services, databases, credentials, ports or all shared Git metadata.
This author assessment is not independent security approval or native-platform
qualification, and does not resolve historical findings from the original large
PR. Native and hosted evidence must name the tested revision separately.
自动隔离、持久运行身份、后台/续跑、profile/工具/路径支持、根预算、环境准备、
整合日志和快照验证仍独立推进。worktree 不隔离网络、数据库、凭证、端口及全部
Git 共享元数据。作者分析不能替代独立安全审查或原生平台验证。
+5 -3
View File
@@ -73,7 +73,7 @@ func (b *builder) addBestOf() {
func candidateRunner(parent Options, sink event.Sink, approval *candidateApproval) bestof.Runner {
return func(ctx context.Context, run bestof.Run) (bestof.Outcome, error) {
cs := &candidateSink{parent: sink}
answer, unverified, err := runUnattended(ctx, parent, cs, approval.mode(), run.WorkspaceRoot, run.Prompt, run.Model)
answer, unverified, err := runUnattended(ctx, parent, cs, approval.mode(), run.WorkspaceRoot, run.Prompt, provider.Selection{Ref: run.Model}, 0)
return bestof.Outcome{Answer: answer, Unverified: unverified, Host: cs.summary()}, err
}
}
@@ -82,7 +82,7 @@ func candidateRunner(parent Options, sink event.Sink, approval *candidateApprova
// its end. Its transcript lives in a temp dir removed with it; only what sink
// forwards reaches the parent session. A run that stopped without proving its
// work is not an error: its readiness reason comes back as unverified.
func runUnattended(ctx context.Context, parent Options, sink event.Sink, mode, root, prompt, model string) (answer, unverified string, err error) {
func runUnattended(ctx context.Context, parent Options, sink event.Sink, mode, root, prompt string, selection provider.Selection, maxSteps int) (answer, unverified string, err error) {
sessDir, err := os.MkdirTemp("", "reasonix-candidate-")
if err != nil {
return "", "", err
@@ -92,7 +92,9 @@ func runUnattended(ctx context.Context, parent Options, sink event.Sink, mode, r
ComputerDriver: parent.ComputerDriver,
WorkspaceRoot: root,
Home: parent.Home,
Model: model,
Model: selection.Ref,
EffortOverride: selection.Effort,
MaxSteps: maxSteps,
AgentPreset: parent.AgentPreset,
ProviderResolver: parent.ProviderResolver,
Sink: sink,
@@ -50,7 +50,7 @@ func TestComputerDriverReachesStudioUnattendedChildSchema(t *testing.T) {
approveWorkspace(t, root)
driver := computer.NewDriver("/unused", nil)
defer driver.Close()
_, _, err := runUnattended(t.Context(), Options{ComputerDriver: driver}, event.Discard, "deny", root, "answer briefly", "")
_, _, err := runUnattended(t.Context(), Options{ComputerDriver: driver}, event.Discard, "deny", root, "answer briefly", provider.Selection{}, 0)
if err != nil {
t.Fatal(err)
}
@@ -0,0 +1,196 @@
package boot
import (
"context"
"encoding/json"
"fmt"
"os/exec"
"strings"
"sync"
"testing"
"reasonix/internal/base/testenv"
"reasonix/internal/contract/event"
"reasonix/internal/contract/provider"
"reasonix/internal/session/control"
)
const isolationRuntimePrompt = "ISOLATED-RUNTIME: inspect a.txt"
type isolationRuntimeRequest struct {
model, effort string
request provider.Request
}
type isolationRuntimeRecorder struct {
mu sync.Mutex
requests []isolationRuntimeRequest
args map[string]any
deferred bool
result string
}
type isolationRuntimeProvider struct {
recorder *isolationRuntimeRecorder
model, effort string
}
func (*isolationRuntimeProvider) Name() string { return "boot-isolation-runtime" }
func (p *isolationRuntimeProvider) Stream(_ context.Context, req provider.Request) (<-chan provider.Chunk, error) {
r := p.recorder
r.mu.Lock()
defer r.mu.Unlock()
ch := make(chan provider.Chunk, 2)
call := func(id, name string, args any) {
raw, _ := json.Marshal(args)
ch <- provider.Chunk{Type: provider.ChunkToolCall, ToolCall: &provider.ToolCall{ID: id, Name: name, Arguments: string(raw)}}
}
if strings.Contains(firstUser(req), isolationRuntimePrompt) {
r.requests = append(r.requests, isolationRuntimeRequest{model: p.model, effort: p.effort, request: req})
// A finite scripted ceiling bounds the negative control even without a cap.
if len(r.requests) >= 10 {
ch <- provider.Chunk{Type: provider.ChunkText, Text: "inspection finished"}
} else {
call(fmt.Sprintf("read-%d", len(r.requests)), "read_file", map[string]string{"path": fmt.Sprintf("a-%d.txt", len(r.requests))})
}
} else if hasToolResult(req) {
r.result = lastToolResult(req)
ch <- provider.Chunk{Type: provider.ChunkText, Text: "parent done"}
} else if r.deferred {
call("delegate", "use_capability", map[string]any{"action": "call", "capability_id": "task:subagent", "arguments": r.args})
} else {
call("delegate", "task", r.args)
}
ch <- provider.Chunk{Type: provider.ChunkDone}
close(ch)
return ch, nil
}
func TestEffectIsolatedTaskHonorsRuntimeSelectionAndBudget(t *testing.T) {
if _, err := exec.LookPath("git"); err != nil {
t.Skip("git not installed")
}
cases := []struct {
name, configuredModel, inheritedEffort, callModel, callEffort, parentEffort string
wantModel, wantEffort string
unsupported, deriveBudget, noParentCap, shared bool
wantRequests int
}{
{name: "explicit", configuredModel: "child-model", callModel: "alternate-model", callEffort: "low", wantModel: "alternate", wantEffort: "low"},
{name: "configured-model", configuredModel: "child-model", callEffort: "low", wantModel: "child", wantEffort: "low"},
{name: "inherited-effort", configuredModel: "child-model", inheritedEffort: "low", wantModel: "child", wantEffort: "low"},
{name: "parent-runtime-effort", parentEffort: "low", wantModel: "parent", wantEffort: "low"},
{name: "unsupported-inherited-effort", callModel: "child-model", inheritedEffort: "max", wantModel: "child", wantEffort: "high"},
{name: "unsupported-explicit-effort", configuredModel: "child-model", callEffort: "max", unsupported: true},
{name: "unsupported-configured-pair", configuredModel: "child-model", inheritedEffort: "max", unsupported: true},
{name: "derived-budget", configuredModel: "child-model", wantModel: "child", wantEffort: "high", deriveBudget: true, wantRequests: 7},
{name: "automatic-budget", wantModel: "parent", wantEffort: "high", deriveBudget: true, noParentCap: true, wantRequests: 10},
{name: "shared-parent-alias", callModel: "parent-model", parentEffort: "low", wantModel: "parent", wantEffort: "low", shared: true},
}
for _, tc := range cases {
for _, deferred := range []bool{false, true} {
t.Run(fmt.Sprintf("%s/deferred=%t", tc.name, deferred), func(t *testing.T) {
home, root := statedBootHome(t), testenv.TempDir(t)
kind := uniqueKind("isolation-runtime")
rec := &isolationRuntimeRecorder{deferred: deferred, args: map[string]any{
"prompt": isolationRuntimePrompt, "isolation": "worktree", "max_steps": 1,
}}
if tc.deriveBudget {
delete(rec.args, "max_steps")
}
if tc.shared {
delete(rec.args, "isolation")
}
if tc.callModel != "" {
rec.args["model"] = tc.callModel
}
if tc.callEffort != "" {
rec.args["effort"] = tc.callEffort
}
provider.Register(kind, func(cfg provider.Config) (provider.Provider, error) {
effort, _ := cfg.Extra["effort"].(string)
return &isolationRuntimeProvider{recorder: rec, model: cfg.Model, effort: effort}, nil
})
var config strings.Builder
fmt.Fprintf(&config, `
default_model = "parent-model"
[agent]
system_prompt = "BASE"
worktree_isolation = true
subagent_model = %q
subagent_effort = %q
[codegraph]
enabled = false
`, tc.configuredModel, tc.inheritedEffort)
for _, model := range []string{"parent", "child", "alternate"} {
fmt.Fprintf(&config, `
[[providers]]
name = %q
kind = %q
model = %q
supported_efforts = ["low", "high"]
default_effort = "high"
`, model+"-model", kind, model)
}
writeFile(t, root, "reasonix.toml", config.String())
writeFile(t, root, "a.txt", "read me\n")
for i := 1; i <= 10; i++ {
writeFile(t, root, fmt.Sprintf("a-%d.txt", i), fmt.Sprintf("round %d\n", i))
}
gitInDir(t, root, "init", "-q")
mirrorToUserConfigAt(t, home, root)
gitInDir(t, root, "add", "-A")
gitInDir(t, root, "commit", "-q", "-m", "init")
opts := Options{Home: home, WorkspaceRoot: root, MaxSteps: 12, Sink: event.Discard}
if tc.noParentCap {
opts.MaxSteps = 0
}
if tc.parentEffort != "" {
opts.EffortOverride = &tc.parentEffort
}
ctrl, err := Build(t.Context(), opts)
if err != nil {
t.Fatal(err)
}
defer ctrl.Close()
ctrl.SetToolApprovalMode(control.ToolApprovalYolo)
if err := ctrl.Run(t.Context(), "delegate the inspection"); err != nil {
t.Fatal(err)
}
rec.mu.Lock()
defer rec.mu.Unlock()
if tc.unsupported {
if len(rec.requests) != 0 || !strings.Contains(rec.result, "max") {
t.Fatalf("unsupported effort silently executed: requests=%d result=%q", len(rec.requests), rec.result)
}
return
}
wantRequests := tc.wantRequests
if wantRequests == 0 {
wantRequests = 2
}
if len(rec.requests) != wantRequests {
t.Errorf("child request count = %d, want %d; result=%q", len(rec.requests), wantRequests, rec.result)
}
for _, got := range rec.requests {
if got.model != tc.wantModel || got.effort != tc.wantEffort {
t.Errorf("child runtime = model %q effort %q; want %q/%q", got.model, got.effort, tc.wantModel, tc.wantEffort)
}
}
if len(rec.requests) > 1 {
first, _ := json.Marshal(rec.requests[0].request.Tools)
last, _ := json.Marshal(rec.requests[len(rec.requests)-1].request.Tools)
if string(first) != string(last) || len(rec.requests[0].request.Tools) == 0 {
t.Fatal("child tool definitions changed at finalization")
}
initial := rec.requests[0].request.Messages[0]
final := rec.requests[len(rec.requests)-1].request.Messages[0]
if initial.Role != provider.RoleSystem || final.Role != provider.RoleSystem || initial.Content != final.Content {
t.Fatal("child system prefix changed during the run")
}
}
})
}
}
}
+5 -1
View File
@@ -56,7 +56,11 @@ func (b *builder) addIsolation() {
b.tools.reg.Add(isolation.NewDiscardTool(store))
parent, sink := b.opts, b.sink
b.tools.taskTool.SetIsolation(store, b.root, func(ctx context.Context, run isolation.Run) (isolation.Outcome, error) {
answer, unverified, err := runUnattended(ctx, parent, isolatedUsage{sink}, posture.mode(), run.WorkspaceRoot, run.Prompt, run.Model)
_, selection, err := subagentProviderSelection(b.cfg, b.providers.effective, b.model.entry, run.Model, run.Effort)
if err != nil {
return isolation.Outcome{}, err
}
answer, unverified, err := runUnattended(ctx, parent, isolatedUsage{sink}, posture.mode(), run.WorkspaceRoot, run.Prompt, selection, run.MaxSteps)
return isolation.Outcome{Answer: answer, Unverified: unverified}, err
})
// The registry canonicalizes a schema when a tool is added, and task was
+31 -20
View File
@@ -99,29 +99,11 @@ func newSubagentConfig(opts Options, cfg *config.Config, entry *config.ProviderE
inherited := resolveInheritedSubagentEffort(cfg, entry)
return subagentConfig{
resolveProvider: func(modelRef, effort string) (provider.Provider, *provider.Pricing, int, error) {
me, selectedRef, err := subagentModelEntry(cfg, resolver, entry, modelRef)
me, selection, err := subagentProviderSelection(cfg, resolver, entry, modelRef, effort)
if err != nil {
return nil, nil, 0, err
}
var effortOverride *string
if selectedRef == modelRefFromEntry(entry) {
effortOverride = &me.Effort
}
if strings.TrimSpace(effort) != "" {
normalized, err := config.NormalizeEffort(&me, effort)
if err != nil {
if resolver == nil {
return nil, nil, 0, fmt.Errorf("subagent effort %q on %s: %w", effort, selectedRef, err)
}
normalized = effort
}
me.Effort = normalized
effortOverride = &normalized
if me.Kind == "anthropic" && strings.TrimSpace(me.Effort) != "" && strings.TrimSpace(me.Thinking) == "" {
me.Thinking = "adaptive"
}
}
p, err := resolveProvider(resolver, cfg, proxy, provider.Selection{Ref: selectedRef, Effort: effortOverride})
p, err := resolveProvider(resolver, cfg, proxy, selection)
if err != nil {
return nil, nil, 0, err
}
@@ -148,6 +130,32 @@ func newSubagentConfig(opts Options, cfg *config.Config, entry *config.ProviderE
}
}
func subagentProviderSelection(cfg *config.Config, resolver provider.Resolver, parent *config.ProviderEntry, modelRef, effort string) (config.ProviderEntry, provider.Selection, error) {
entry, selectedRef, err := subagentModelEntry(cfg, resolver, parent, modelRef)
if err != nil {
return entry, provider.Selection{}, err
}
selection := provider.Selection{Ref: selectedRef}
if selectedRef == modelRefFromEntry(parent) {
selection.Effort = &entry.Effort
}
if strings.TrimSpace(effort) != "" {
normalized, err := config.NormalizeEffort(&entry, effort)
if err != nil {
if resolver == nil {
return entry, selection, fmt.Errorf("subagent effort %q on %s: %w", effort, selectedRef, err)
}
normalized = effort
}
entry.Effort = normalized
selection.Effort = &normalized
if entry.Kind == "anthropic" && strings.TrimSpace(entry.Effort) != "" && strings.TrimSpace(entry.Thinking) == "" {
entry.Thinking = "adaptive"
}
}
return entry, selection, nil
}
func subagentModelEntry(cfg *config.Config, resolver provider.Resolver, parent *config.ProviderEntry, ref string) (config.ProviderEntry, string, error) {
ref = childModelRef(cfg, parent, ref)
if parent != nil && (ref == "" || ref == modelRefFromEntry(parent)) {
@@ -155,6 +163,9 @@ func subagentModelEntry(cfg *config.Config, resolver provider.Resolver, parent *
}
if cfg != nil {
if resolved, ok := cfg.ResolveModel(ref); ok {
if parent != nil && modelRefFromEntry(resolved) == modelRefFromEntry(parent) {
return *parent, modelRefFromEntry(parent), nil
}
return *resolved, modelRefFromEntry(resolved), nil
}
}
@@ -0,0 +1,34 @@
package boot
import (
"testing"
"reasonix/internal/contract/config"
)
func TestSubagentSelectionPreservesRuntimeForParentAliases(t *testing.T) {
cfg := config.Default()
cfg.Providers = []config.ProviderEntry{{Name: "parent", Kind: "openai", Models: []string{"p", "q"}, Default: "p",
SupportedEfforts: []string{"low", "high"}, DefaultEffort: "high"}}
parent, ok := cfg.ResolveModel("parent/p")
if !ok {
t.Fatal("parent model missing")
}
parent.Effort = "low"
for _, ref := range []string{"", "parent", "parent/p", "p"} {
t.Run(ref, func(t *testing.T) {
_, selection, err := subagentProviderSelection(cfg, nil, parent, ref, "")
if err != nil || selection.Ref != "parent/p" || selection.Effort == nil || *selection.Effort != "low" {
t.Fatalf("parent runtime selection for %q = %+v, %v", ref, selection, err)
}
})
}
_, other, err := subagentProviderSelection(cfg, nil, parent, "parent/q", "")
if err != nil || other.Ref != "parent/q" || other.Effort != nil {
t.Fatalf("another model inherited the parent's override: %+v, %v", other, err)
}
_, explicit, err := subagentProviderSelection(cfg, nil, parent, "parent", "high")
if err != nil || explicit.Effort == nil || *explicit.Effort != "high" {
t.Fatalf("explicit effort did not narrow the inherited selection: %+v, %v", explicit, err)
}
}
+14 -3
View File
@@ -35,8 +35,9 @@ func (t *TaskTool) SetIsolation(store *isolation.Store, root string, runner isol
// isolatedCall is the part of a task call an isolated run reads, and the
// arguments it has to refuse rather than silently drop.
type isolatedCall struct {
mode, prompt, model string
unsupported []string
mode, prompt, model, effort string
maxSteps int
unsupported []string
}
func (t *TaskTool) executeIsolated(ctx context.Context, call isolatedCall) (string, error) {
@@ -50,7 +51,17 @@ func (t *TaskTool) executeIsolated(ctx context.Context, call isolatedCall) (stri
return "", tool.Refusal{Code: codeIsolationUnsupported, Message: "an isolated task runs as its own agent and cannot take " +
strings.Join(call.unsupported, ", ") + "; drop them or run without isolation"}
}
return t.isolated.store.Execute(ctx, t.isolated.runner, t.isolated.root, call.prompt, strings.TrimSpace(call.model))
model, effort := t.resolveModelEffort("", "", call.model, call.effort, "", "")
if model == "" {
model = t.baseModel
}
model, effort = t.childRuntimeRef(ctx, model, effort)
steps := t.childMaxStepsForContext(ctx, call.maxSteps)
runner := func(ctx context.Context, run isolation.Run) (isolation.Outcome, error) {
run.Effort, run.MaxSteps = effort, steps
return t.isolated.runner(ctx, run)
}
return t.isolated.store.Execute(ctx, runner, t.isolated.root, call.prompt, model)
}
// isolatedUnsupported names the task arguments that were set and that an
+1 -1
View File
@@ -418,7 +418,7 @@ func (t *TaskTool) executeTask(ctx context.Context, args json.RawMessage) (strin
return "", fmt.Errorf("prompt is required")
}
if p.Isolation != "" {
return t.executeIsolated(ctx, isolatedCall{mode: p.Isolation, prompt: p.Prompt, model: p.Model,
return t.executeIsolated(ctx, isolatedCall{mode: p.Isolation, prompt: p.Prompt, model: p.Model, effort: p.Effort, maxSteps: p.MaxSteps,
unsupported: isolatedUnsupported(p.Profile, p.WritePaths, p.Tools, p.ContinueFrom, p.ForkFrom, p.RunInBackground)})
}
+2
View File
@@ -12,6 +12,8 @@ type Run struct {
WorkspaceRoot string
Prompt string
Model string
Effort string
MaxSteps int
}
// Outcome is what the run ended with. Unverified carries the readiness reason
@@ -0,0 +1,7 @@
## 修复
- worktree 隔离任务遵守显式或继承的模型、effort 与步数上限;同模型别名保留
父会话实际 effort。原本不支持的工具/路径限制、后台和续跑仍明确拒绝。Refs #14025
- Worktree tasks honor model, effort and step caps. Parent aliases preserve
session effort; unsupported restrictions and lifecycle arguments remain
refused. Refs #14025