diff --git a/apps/presentation/dashboard/smoke/capability-configuration-smoke.ts b/apps/presentation/dashboard/smoke/capability-configuration-smoke.ts index ceedb155dc..08bcbb6e2d 100644 --- a/apps/presentation/dashboard/smoke/capability-configuration-smoke.ts +++ b/apps/presentation/dashboard/smoke/capability-configuration-smoke.ts @@ -171,3 +171,10 @@ assert.deepEqual( { wait_for_ci: false, review_priority: "other-developers-first" }, "an explicit Goal CI opt-out must survive default projection and omit envelope fields", ); + +// Capability defaults may enable Stride without changing saved legacy choices. +const enabledStrideDefault = {...cadenceDefault, stride_strategy: "evidence_conditioned_v1"}; +assert.deepEqual(goalReplanCadenceEditorValue(null, enabledStrideDefault), enabledStrideDefault); +assert.equal(goalReplanCadenceEditorValue({stride_strategy: "off"}, enabledStrideDefault).stride_strategy, "off"); +assert.equal(goalReplanCadenceEditorValue({count_unit: "effective_turns", count: 3}, enabledStrideDefault).stride_strategy, "off"); +assert.equal(projectEditableCapabilityConfiguration(cadenceEditor, {}, enabledStrideDefault).stride_strategy, "evidence_conditioned_v1"); diff --git a/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.md b/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.md index 76c4b8a0bf..12283ce21c 100644 --- a/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.md +++ b/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.md @@ -695,7 +695,7 @@ times separately. Check whether later work implemented the proposed mechanism; a decision record, saved snapshot or new identifier alone does not establish adoption, restoration or improved results. These are refinements of the existing Agent questions, not new decision fields, score thresholds or settlement gates; -the strategy remains default-off. Identify the remaining authoritative goal gap, its limiting +the inherited capability default enables the strategy; explicit off and saved legacy configurations retain their meaning. Identify the remaining authoritative goal gap, its limiting component and plausible headroom before choosing the next mechanism. Compare continuation with plausible alternatives that change the limiting assumption or mechanism, including complete cost. A stalled route calls for a discriminating @@ -870,7 +870,8 @@ source-bound stride decision. Typed progress, successor and terminal results remain valid evidence/outcomes, while a bare Todo successor transition cannot stand in for the review. An evidenced path decision can retain unfinished useful work without a fabricated new progress identifier or Todo. Explicit acceptance -and capability outcome restrictions still apply; default-off and other sources +and capability outcome restrictions still apply; explicit off, saved legacy +cadence-only configurations and other sources retain their prior exit policies. No new detector, threshold or action is added. The frozen [EdgeBench diagnostic window](https://huggingface.co/datasets/songwells/loopxbench/blob/c5dd30bfaa2f5af42ccd7d0ca9ed59b865a81d42/public/edgebench-20261018-v1/diagnostic_windows.csv) @@ -888,23 +889,25 @@ This proves the scripted control path, not model adoption or EdgeBench score gai Existing negative-result and later-work followthrough coverage remains necessary. The effective review cadence remains owned by the [Goal Vision/replan contract](../../reference/protocols/goal-vision-replan-contract-v0.md#default-review-cadence) -and Goal/device configuration. Six effective work Turns is the default; enabling -the stride strategy for one Goal inherits that configured baseline. For the -enabled strategy with receipt-backed effective-Turn cadence, the latest settled -fresh stride review through the current Agent's last autonomous ACK adjusts the -next interval: `grow` with `supported_progress` and an evidence-linked previous -`goal_gain=improved` uses 1.5 times the baseline; `verify` or an uncertainty/gap/ -failure basis uses 0.5 times; `switch` takes precedence and keeps the baseline. -Round up to at least one Turn, without compounding repeated reads. An initial, -unsettled, historical-v1 or missing decision keeps the baseline; do not reach -through a newer ACK to reuse older feedback. The settled writer validates citation -provenance; the gain remains an Agent claim, not independent outcome certification. -Off mode, legacy counting, stall triggers, authority, per-Turn settlement and -frozen workers retain their behavior. The inline review questions and syntax -example remain delivered together; shortening repeated prose adds no decision -fields or gates. Cadence is not Todo size, an experiment boundary or a mandatory -route switch. Matched continuation must qualify actual -adoption, target benefit and full cost before promotion or synergy claims. +and Goal/device configuration; six effective work Turns is the default. Stride +no longer maps grow/verify to fixed cadence multipliers. It governs work shape: +apply the three independent plans to actual Todo scope and acceptance through +existing authoring/update commands, record verified IDs in `todo_delta`, and +execute the retained or revised work under normal admission. Supported grow +consolidates fragmented setup, implementation, integration and decisive validation +into a coherent accepted deliverable or complete interpretable experiment. +Keep dependencies, acceptance, risk, authority and per-Turn settlement boundaries. +The next review must distinguish declared scope from actual execution, outcome +quality and full cost; preserving a plan is not adoption or benefit. + +Evidence-conditioned stride is the capability default for inherited configuration. +Explicit off, saved legacy cadence-only configurations and historical records +retain their semantics. Clear overrides to inherit, or select off to roll back; +no installed configuration, frozen worker or historical receipt is rewritten. +The codec's missing historical strategy remains off. Review questions and syntax +examples remain together; no extra decision fields, strategy versions or +machine-enforced semantic classifier are added. Matched continuation must still +qualify target benefit and full cost before promotion or synergy claims. Exploratory iteration may run only strategy-enabled test arms and reuse historical baselines as descriptive references. A fresh concurrent baseline is not required diff --git a/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.zh-CN.md b/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.zh-CN.md index d8085ce686..ffd6cdd226 100644 --- a/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.zh-CN.md +++ b/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.zh-CN.md @@ -587,7 +587,7 @@ switch(尝试不同机制)或 verify(解决决定扩大或切换的具体 比较须绑定产物版本、输入条件和原预算,区分提交时刻与反馈实际到达时刻,并检查 后续工作是否执行了提议的机制变化。决定记录、快照或新标识本身不证明采用、恢复 或结果改善。这些是既有Agent问题的细化,不增加决策字段、分数阈值或结算门槛; -策略仍默认关闭。先识别仍未满足的 +继承能力配置时策略默认开启,显式关闭和已保存的旧配置保留原语义。先识别仍未满足的 权威目标、当前瓶颈分量和可能的收益空间,再选择下一机制。比较原路线接续与 改变该瓶颈假设或机制的可行替代路线,并计入完整成本。路线停滞时,应以能区分 这些解释的完整实验推进,不能只继续局部调参。区分干预未执行、局部改善未迁移, @@ -705,7 +705,8 @@ Benchmark 的显式 `loopx-planned` provider 在未修改的规划 packet 外附 任何结算路径都须留下绑定本次来源的 stride 决定。 类型化进展、后续任务和终止结果仍可作为证据与结果,但单独的 Todo 后续转换不能 代替复盘。有证据的路径决定可以保留尚未完成的有用工作,不必编造新进展标识或 -Todo。验收和能力 owner 显式限定的结果仍须遵守;默认关闭和其他来源保持原有 +Todo。验收和能力 owner 显式限定的结果仍须遵守;显式关闭、已保存的旧版仅周期 +配置和其他来源保持原有 出口规则。不增加检测器、阈值或动作。 冻结的 [EdgeBench 诊断窗口](https://huggingface.co/datasets/songwells/loopxbench/blob/c5dd30bfaa2f5af42ccd7d0ca9ed59b865a81d42/public/edgebench-20261018-v1/diagnostic_windows.csv) @@ -719,17 +720,19 @@ Todo。验收和能力 owner 显式限定的结果仍须遵守;默认关闭和 机制选择,改名本身不算换路。这证明脚本化控制链路,不证明模型采用或 EdgeBench 分数提升;既有负结果与后续实际工作跟进验证仍有必要。 有效复核周期仍由[Goal Vision/replan 契约](../../reference/protocols/goal-vision-replan-contract-v0.md#default-review-cadence) -及 Goal/设备配置负责。六个有效工作 Turn 是默认值;对单个 Goal 启用步幅策略时 -沿用该配置基准。启用策略且按结算回执计有效 Turn 时,读取当前 Agent 最近一次 -自主 ACK 之前(含该 ACK)的最新已结算 fresh stride 复盘,调整下一周期: -`grow`、`supported_progress` 且前次 `goal_gain=improved` 有证据关联时取基准的 -1.5 倍;`verify` 或不确定/差距/失败依据取 0.5 倍;`switch` 优先并保留基准。 -向上取整且至少一个 Turn,多次读取不连乘。首次、未结算、历史 v1 或缺失决定 -保留基准;不能越过更新的 ACK 重用旧反馈。已结算写回校验引用来源,收益仍是 -Agent 声明,不是独立结果认证。关闭状态、旧计数方式、停滞触发器、权限、每 -Turn 结算和冻结 worker 行为不变。复盘问题和语法示例仍在同一 inline 包中交付; -压缩重复文字不新增决定字段或 gate。周期不是 Todo 大小、实验边界或强制换路信号。 -策略采用、目标收益、完整成本和交互增益须通过匹配接续等实验验证后再作结论。 +及 Goal/设备配置负责,默认六个有效工作 Turn。Stride 不再把 grow/verify 映射为 +固定周期倍数,而是调整工作形态:通过既有 Todo 创建/更新命令,把三个轴的独立 +计划落实到真实 Todo 范围和验收,向 `todo_delta` 写入已核验的 ID,并经正常准入 +执行保留或调整后的工作。有证据支持的 grow 将碎片化准备、实现、集成及决定性 +验证组织为完整可验收交付或完整可解释实验;保留依赖、验收、风险、权限与每 Turn +结算边界。下次复盘区分计划范围与实际执行、结果质量和完整成本;保存计划不等于 +采用或收益。 + +继承能力配置时,依据证据的步幅策略默认开启。显式关闭、已保存的旧版仅周期配置 +及历史记录保持原语义。清除覆盖可恢复继承,显式选择 off 可回退;不重写已安装 +配置、冻结 worker 或历史回执。历史解码中缺失的策略仍按 off 处理。复盘问题和 +语法示例仍一起交付,不新增决定字段、策略版本或机器强制语义分类器。目标收益、 +完整成本与交互增益仍须通过匹配接续等实验验证后再作结论。 敏捷探索可只跑启用策略的测试组,以历史基线作描述性参照;不要求每次迭代都 重启并发基线。这类运行用于观察路线采用与迭代收益,不单独证明因果净收益或 diff --git a/docs/reference/protocols/goal-vision-replan-contract-v0.md b/docs/reference/protocols/goal-vision-replan-contract-v0.md index 8ec71bebbf..40fc1cf60f 100644 --- a/docs/reference/protocols/goal-vision-replan-contract-v0.md +++ b/docs/reference/protocols/goal-vision-replan-contract-v0.md @@ -38,15 +38,22 @@ no TurnEnvelope opt-in is needed. Quota and writeback resolve the same live configuration. See [Goal review cadence](../../quota-allocation.md#goal-review-cadence) for configuration precedence, rollback and independent review triggers. -When `stride_strategy=evidence_conditioned_v1` is enabled, receipt-backed cadence -uses the configured count as a baseline for the next interval. A settled fresh -review can scale it by 1.5 (supported grow with evidence-linked improved goal -gain) or 0.5 (verify or uncertainty/gap/failure); switch keeps the baseline. -Counts round up to at least one, never compound across reads, and do not cross a -newer autonomous ACK to reuse older feedback. Missing/unsettled/historical -decisions and off mode keep the baseline. Stall triggers and settlement remain -independent. This feedback uses source-validated Agent claims, not certified gain; -see the [strategy boundary](../../architecture/rfcs/hierarchical-agent-stride-control-v0.md#76-research-scope-and-evidence-conditioned-control). +Evidence-conditioned Stride is the capability default when no Goal or saved +machine configuration overrides it. Explicit `off`, legacy saved cadence-only +configuration and historical decisions keep their original meaning. Clear the +cadence/strategy override to inherit the current default, or explicitly select +`off` for rollback; this does not rewrite prior evidence or frozen workers. + +Stride sizes the next coherent Todo/delivery or interpretable experiment, route +change and validation scope through the existing Todo and Vision owners. Apply +an evidence-supported `grow` plan to actual Todo scope and acceptance, retain +unfinished useful work, and record verified Todo IDs in `todo_delta`. Subsequent +normal admission, execution and evidence-linked review qualify adoption; prose +and a new Todo alone do not. `verify` resolves a decision-relevant uncertainty +and need not shrink work. All actions preserve the configured review count, +stall triggers, dependencies, authority and per-Turn settlement. There are no +fixed grow/verify cadence multipliers. See the +[strategy boundary](../../architecture/rfcs/hierarchical-agent-stride-control-v0.md#76-research-scope-and-evidence-conditioned-control). ## Replan ACK freshness @@ -432,8 +439,10 @@ is required before typed progress, a successor, blocker or terminal outcome can settle the obligation. Enabled fresh-path decisions also retain their existing structured-decision validation. The [structured contract](#structured-stride-control) owns those field, reference and review requirements; the shared planning -guidance does not replace or add to them. `off` remains the default, including -legacy configurations, and unknown versions fail before mutation. No new +guidance does not replace or add to them. The inherited capability default is +`evidence_conditioned_v1`; explicit `off` and saved legacy cadence-only +configurations retain their meaning, as described in [Default review cadence](#default-review-cadence). +Unknown versions fail before mutation. No new permission, scheduler or automatic route actuator is introduced here. The [captured reader](turn-envelope-v0.md#captured-replan-reads--捕获后的重规划读取) @@ -478,7 +487,9 @@ Todo 顺序和 Agent 暂定筛选规则可调整;到期复核可以继续、 fresh-path 决定仍受既有结构校验约束。这些是机器执行的字段、引用与复核要求, 不能一概称为建议文字,也不是本段新增的门禁。 -默认仍为 `off`,旧配置兼容,未知版本在写入前拒绝。捕获后的 reader 在开关两种 +继承能力配置时默认启用 `evidence_conditioned_v1`;显式 `off` 和已保存的旧版仅周期 +配置保留原语义,详见[默认复盘周期](#default-review-cadence)。未知版本在写入前拒绝。 +捕获后的 reader 在开关两种 情况下都先交付复盘事实、再交付写回格式;它改善信息交付,不是首次引入证据驱动 重规划。关闭 Stride 时观察到的读取/结算改善不能归为 Stride 收益。权限、调度器与 自动路线执行权不变;投递与隔离验证不证明实际决策采用、有用成果或可比完整成本。 diff --git a/examples/autonomous-replan-obligation-smoke.py b/examples/autonomous-replan-obligation-smoke.py index 66f9480bb2..5512834286 100644 --- a/examples/autonomous-replan-obligation-smoke.py +++ b/examples/autonomous-replan-obligation-smoke.py @@ -173,6 +173,8 @@ def write_fixture( { "id": GOAL_ID, "domain": "autonomous-replan-fixture", + # Isolate progress/cadence semantics from optional structured work-shape review. + "execution_profile": {"stride_strategy": "off"}, "status": "active", "repo": str(project), "state_file": state_file, diff --git a/examples/control_plane/refresh-state-shared-runtime-projection-smoke.py b/examples/control_plane/refresh-state-shared-runtime-projection-smoke.py index 4458078370..71aa6e5abf 100644 --- a/examples/control_plane/refresh-state-shared-runtime-projection-smoke.py +++ b/examples/control_plane/refresh-state-shared-runtime-projection-smoke.py @@ -93,6 +93,8 @@ def write_fixture(root: Path) -> tuple[Path, Path, Path, Path]: goal = { "id": GOAL_ID, "domain": "refresh-state-shared-runtime-smoke", + # This fixture qualifies routing/writeback, not a Stride review. + "execution_profile": {"stride_strategy": "off"}, "status": "active", "repo": str(project), "state_file": str(state_file.relative_to(project)), diff --git a/loopx/capabilities/todo_replan_cadence/machine_defaults.py b/loopx/capabilities/todo_replan_cadence/machine_defaults.py index db3b324104..3c022f1a8f 100644 --- a/loopx/capabilities/todo_replan_cadence/machine_defaults.py +++ b/loopx/capabilities/todo_replan_cadence/machine_defaults.py @@ -104,6 +104,7 @@ def default_replan_cadence_configuration() -> dict[str, Any]: "count_unit": ReplanCadenceUnit.EFFECTIVE_TURNS.value, "count": DEFAULT_EFFECTIVE_TURN_REPLAN_THRESHOLD, "planning_horizon_goal": DEFAULT_PLANNING_HORIZON_GOAL, + "stride_strategy": "evidence_conditioned_v1", } @@ -126,14 +127,13 @@ def todo_replan_cadence_machine_configuration_namespace() -> ( description=( "Live review threshold for Goals without an explicit cadence override. " "Defaults to six settled work Turns; completed Todos remain an explicit option. " - "The optional versioned stride strategy is disabled by default; enabled fresh Vision reviews require a bounded structured proposal. " + "Evidence-conditioned stride is enabled by default; fresh Vision reviews require a bounded structured work-shape proposal. Explicit off and legacy saved configurations retain their meaning. " "It does not create " "turns, spend quota, or grant authority." ), default_configuration={ "schema_version": TODO_REPLAN_CADENCE_MACHINE_DEFAULTS_V2, **default_replan_cadence_configuration(), - "stride_strategy": "off", }, ) diff --git a/loopx/control_plane/testing/cli_output_budget.py b/loopx/control_plane/testing/cli_output_budget.py index 7fb5d117da..e0b972394d 100644 --- a/loopx/control_plane/testing/cli_output_budget.py +++ b/loopx/control_plane/testing/cli_output_budget.py @@ -154,7 +154,12 @@ class CliOutputCommandClassification: # Native Windows paired base/head both emit 40,701 JSON chars. # Retain complete routes/context and bounded host-path headroom; # line, per-Todo/fixed growth and all other ceilings stay unchanged. - "crowded": {"json": 42_000, "markdown": 10_500}, + # Inherited Stride adds its complete decision/axis/source contract: + # the same native fixture measures 35,597 -> 42,281 characters. + # Preserve the original 42,000 failure and the required semantics; + # calibrate this regression allowance only, with 219 chars headroom. + # No production hard limit or line/collection-growth guard changes. + "crowded": {"json": 42_500, "markdown": 10_500}, # The unchanged 18-Todo / 3-Agent fixture on main ffc90182b and # this candidate both emit 25,836 JSON characters (96 matched # surface/mode measurements, no semantic or route differences). @@ -191,11 +196,13 @@ class CliOutputCommandClassification: markdown_anchor="# LoopX Turn Plan", max_chars={ "small": {"json": 14_000, "markdown": 600}, - # Pinned formatter base/head both emit 21,238 chars / 477 lines - # with complete vision authoring on the unchanged 36-Todo fixture. - # Keep executable evidence/continuation rules; 1,262 chars of - # headroom covers presentation variation, not extra Todo growth. - "crowded": {"json": 22_500, "markdown": 600}, + # With an existing vision, the same 36-Todo fixture grows from + # 18,955 to 25,319 chars when inheriting the complete Stride contract + # (+6,364, identical to the no-vision route). Preserve the 22,500 + # failure; 26,000 leaves 681 chars without relaxing collection, + # line or other-route limits. The earlier formatter observation + # was 21,238 chars / 477 lines, before these semantic changes. + "crowded": {"json": 26_000, "markdown": 600}, "multi_agent": {"json": 15_500, "markdown": 600}, }, max_lines={ @@ -214,7 +221,11 @@ class CliOutputCommandClassification: # 35*60 = 2,100 as the per-Todo allowance; the advice adds 673 fixed # chars, so 5,400 leaves 154 of headroom without widening per-Todo growth. # This is a regression budget, not authority. - max_json_fixed_semantic_growth_chars=5_400, + # Inherited Stride adds a fixed required-review decision contract; + # matched small/crowded output is 10,560/15,492 -> 10,560/21,856. + # Keep 35*60 collection growth; 9,300 covers 9,196 fixed chars with + # 104 headroom. Retain the earlier 5,400 failure, not a size reduction. + max_json_fixed_semantic_growth_chars=9_300, ), CliOutputBudgetSpec( surface_id="status", diff --git a/loopx/control_plane/testing/cli_output_differential.py b/loopx/control_plane/testing/cli_output_differential.py index a2aea89c1a..53e292e4b8 100644 --- a/loopx/control_plane/testing/cli_output_differential.py +++ b/loopx/control_plane/testing/cli_output_differential.py @@ -892,6 +892,49 @@ def _turn_projection_migration( ) -> tuple[dict[str, int], list[str]]: old_paths = set(base.get("json_shape_paths") or []) new_paths = set(candidate.get("json_shape_paths") or []) + # Inherited Stride now supplies the existing bounded decision contract on + # the required-review route. Only absence -> complete projection receives + # this one-time allowance; later growth, removed facts and other paths do not. + stride_routes = { + "surface/quota_should_run/crowded/json": ("$", 6_800), + "surface/loopx_turn_plan/crowded/json": ("$.turn_envelope", 6_500), + } + stride_route = stride_routes.get(str(base.get("row_id"))) + if stride_route and candidate.get("row_id") == base.get("row_id"): + root, bound = stride_route + control = f"{root}.replan_action_packet.stride_control" + strategy = f"{root}.autonomous_replan_obligation.replan_context.stride_strategy" + required = {f"{control}.{key}" for key in ( + "schema_version", "strategy_version", "reference_context_id", + "reference_obligation_id", "required_fields", "rule", + "minimal_example.schema_version", "minimal_example.strategy_version", + "minimal_example.reference_context_id", "minimal_example.reference_obligation_id", + "minimal_example.basis", "minimal_example.action", + *[f"minimal_example.axes.{axis}.{field}" + for axis in ("work_slice", "route_distance", "validation_scope") + for field in ("plan", "reason")], + *[f"minimal_example.next_probe.{field}" for field in ( + "scope", "prediction", "adoption_criterion", "cost_bound", "minimum_evidence", + )], + )} + # Optional review blocks are required in full once projected. Presence + # of representative leaves cannot qualify a truncated decision example. + for review, fields in ( + ("previous_review", ("reference_decision_ref", "adoption", "information_gain", + "goal_gain", "evidence_refs", "lesson")), + ("explore_review", ("revision", "disposition", "considered_refs", "reason")), + ): + prefix = f"{control}.minimal_example.{review}" + if any(path == prefix or path.startswith(prefix + ".") for path in new_paths): + required.update(f"{prefix}.{field}" for field in fields) + added = new_paths - old_paths + if (base.get("format") == candidate.get("format") == "json" + and control not in old_paths and required <= new_paths and old_paths <= new_paths + and all(path == control or path.startswith(control + ".") or path == strategy + for path in added)): + return {metric: bound for metric in ("chars", "utf8_bytes", "compact_payload_chars")}, [ + "inherited Stride decision context enabled; bounded one-time JSON growth" + ] profile = "$.turn_envelope.contract_capsule.execution_profile" restored = {f"{profile}.{field}" for field in ( "small_scale_streak_threshold", "outcome_floor", "outcome_floor.configured", diff --git a/loopx/control_plane/work_items/replan_history.ts b/loopx/control_plane/work_items/replan_history.ts index a97bc2684c..53f30c0ccf 100644 --- a/loopx/control_plane/work_items/replan_history.ts +++ b/loopx/control_plane/work_items/replan_history.ts @@ -204,41 +204,6 @@ function historyWindow(request: Request): Run[] { return result; } -/** Feedback changes only the next periodic count, never work/stall/authority. - * Decisions come from the settled writeback, not projected history or Vision's - * carried-forward value. Include the last ACK itself; otherwise a decision made - * at that boundary could never affect its following interval. Never cross it. - */ -function periodicThreshold(request: Request, decisions: ReadonlyMap): number { - const lane = historyLane(request); - if (request.stride === "off" || !lane) return request.periodic; - for (const run of request.runs) { - if (run.agent && run.agent !== lane) continue; - // A later observation can reuse an older Turn id without being its - // writeback. Only the current ACK may select its settled feedback. - if (!run.ack) continue; - const decision = !request.neutral.has(run.classification) && run.agent === lane && run.turn - ? decisions.get(JSON.stringify([lane, run.turn])) : undefined; - if (decision) { - // Switching starts a new route at the configured baseline even if its - // basis names a gap. Uncertainty does not silently veto that action. - if (decision.action === "switch") return request.periodic; - if (decision.action === "verify" || - ["uncertain_evidence", "unexplained_gap", "decision_failure"].includes(String(decision.basis))) { - return Math.max(1, Math.ceil(request.periodic / 2)); - } - const review = jsonObject(decision.previous_review); - if (decision.action === "grow" && decision.basis === "supported_progress" && - review?.goal_gain === "improved" && Array.isArray(review.evidence_refs) && review.evidence_refs.length) { - return Math.min(Number.MAX_SAFE_INTEGER, Math.ceil(request.periodic * 1.5)); - } - return request.periodic; - } - return request.periodic; - } - return request.periodic; -} - /** De-duplicate accepted evidence, not arbitrary records sharing its turn. * A leading untyped row cannot erase an older typed observation from that turn. * Null legacy identities remain independent; neutral rows were already removed. @@ -389,10 +354,9 @@ function monitorStreak(request: Request): Trigger | null { }; } -export function projectReplanHistory(value: unknown, qualified?: ReadonlySet, - settledDecisions: ReadonlyMap = new Map()): JsonObject { - const decoded = decode(value); - const request = {...decoded, periodic: periodicThreshold(decoded, settledDecisions)}; +export function projectReplanHistory(value: unknown, qualified?: ReadonlySet): JsonObject { + // Work-shape decisions do not silently reconfigure the review interval. + const request = decode(value); const runs = historyWindow(request); let trigger: Trigger | null; switch (request.operation) { diff --git a/loopx/control_plane/work_items/replan_history_settlement.ts b/loopx/control_plane/work_items/replan_history_settlement.ts index cd8104787a..b09652f49e 100644 --- a/loopx/control_plane/work_items/replan_history_settlement.ts +++ b/loopx/control_plane/work_items/replan_history_settlement.ts @@ -11,7 +11,6 @@ import { readGoalRolloutEventSnapshot, strictGoalRolloutEvents } from "../rollou import { parseQuotaAccountingOwner, withQuotaAccountingOwner, withBorrowedQuotaAccountingOwner, quotaOwnerOwnsProjection } from "../quota/source_admission.ts"; import { projectReplanHistory } from "./replan_history.ts"; -import { normalizeRecordedStrideDecision, STRIDE_DECISION_SCHEMA } from "./stride_policy.ts"; export async function projectSettledReplanHistory(value: unknown): Promise { const request = requireJsonObject(value, "replan history"); @@ -52,7 +51,6 @@ export async function projectSettledReplanHistory(value: unknown): Promise(); - const decisions = new Map(); const seen = new Set(); for (const raw of settlementRuns) { const row = jsonObject(raw); @@ -72,22 +70,8 @@ export async function projectSettledReplanHistory(value: unknown): Promise None: + from loopx.capabilities.todo_replan_cadence.machine_defaults import ( + apply_todo_replan_cadence_machine_default, default_replan_cadence_configuration, + todo_replan_cadence_machine_configuration_namespace, + ) + from loopx.capabilities.goal_inspection import inspect_goal_capabilities + _repo, registry, runtime = _fixture(tmp_path) + from loopx.capabilities.machine_configuration.contract import remove_machine_configuration_namespace + owner = build_builtin_machine_configuration_registry() + removed = remove_machine_configuration_namespace(_machine_configuration(), + namespace="todo_replan_cadence", registry=owner) + preview = configure_machine_configuration(runtime_root=runtime, registry=owner, configuration=removed) + configure_machine_configuration(runtime_root=runtime, registry=owner, configuration=removed, + execute=True, expected_plan_revision=preview["plan_revision"]) + original = registry.read_bytes() + assert default_replan_cadence_configuration()["stride_strategy"] == "evidence_conditioned_v1" + assert todo_replan_cadence_machine_configuration_namespace().default_configuration["stride_strategy"] == "evidence_conditioned_v1" + inherited = _history_goal(registry, runtime) + assert inherited["execution_profile"]["stride_strategy"] == "evidence_conditioned_v1" + assert registry.read_bytes() == original + catalog = inspect_goal_capabilities(registry_path=registry, runtime_root=runtime, + goal_id=GOAL_ID)["configuration"]["capability_catalog"]["capabilities"] + effective = next(c for c in catalog if c["capability_id"] == "todo_replan_cadence")["effective_configuration"] + assert effective["source"] == "capability_default" + assert effective["configuration"]["stride_strategy"] == "evidence_conditioned_v1" + off = apply_todo_replan_cadence_machine_default({"execution_profile": {"stride_strategy": "off"}}, None) + assert off["execution_profile"]["stride_strategy"] == "off" + for saved in ({"schema_version": "todo_replan_cadence_machine_defaults_v0", "completed_todos": 2}, + {"schema_version": "todo_replan_cadence_machine_defaults_v1", "count_unit": "effective_turns", "count": 4}, + {"schema_version": "todo_replan_cadence_machine_defaults_v2", "count_unit": "effective_turns", "count": 4, "stride_strategy": "off"}): + machine = _machine_configuration() + machine["namespaces"]["todo_replan_cadence"] = saved + projected = apply_todo_replan_cadence_machine_default({}, machine) + assert projected["execution_profile"].get("stride_strategy", "off") == "off" + legacy_goal = {"execution_profile": {"replan_after_effective_turns": 3}} + assert apply_todo_replan_cadence_machine_default(legacy_goal, None) == legacy_goal diff --git a/tests/control_plane/test_cli_output_budget.py b/tests/control_plane/test_cli_output_budget.py index 2d3566357a..a6a35c3c4c 100644 --- a/tests/control_plane/test_cli_output_budget.py +++ b/tests/control_plane/test_cli_output_budget.py @@ -1298,6 +1298,67 @@ def test_crowded_quota_preserves_long_chain_decision_clauses( assert "do not rewrite criteria after failure" in guidance +@pytest.mark.parametrize("surface", ["quota_should_run", "loopx_turn_plan"]) +@pytest.mark.parametrize("include_agent_vision", [False, True]) +def test_native_stride_projection_rejects_each_missing_decision_field( + tmp_path: Path, surface: str, include_agent_vision: bool, +) -> None: + import copy + + from loopx.control_plane.testing.cli_output_differential import _turn_projection_migration + + with _stable_budget_fixture_root(tmp_path / "stride-projection") as stable_root: + project, runtime, registry_path, state_file = _write_fixture( + stable_root, SCENARIOS[1], include_agent_vision=include_agent_vision, + ) + import_fixture_authority(registry_path, GOAL_ID) + command = _surface_commands( + project=project, runtime=runtime, registry_path=registry_path, + state_file=state_file, output_format="json", + )[surface] + code, text = _invoke_cli(command) + assert code == 0, text + payload = json.loads(text) + + def envelope(value): + return value["turn_envelope"] if surface == "loopx_turn_plan" else value + + def row(value): + return { + **measure_cli_output(json.dumps(value, ensure_ascii=False), output_format="json"), + "row_id": f"surface/{surface}/crowded/json", + } + + base_payload = copy.deepcopy(payload) + del envelope(base_payload)["replan_action_packet"]["stride_control"] + envelope(base_payload).get("autonomous_replan_obligation", {}).get("replan_context", {}).pop("stride_strategy", None) + base = row(base_payload) + assert _turn_projection_migration(base, row(payload))[0] + + # Walk the actual native example rather than a hand-written projection. + # Each delivered leaf is required for a usable decision (including each + # axis reason and all five probe fields). Arrays are atomic fields here. + def leaves(value, prefix=()): + for key, child in value.items(): + path = (*prefix, key) + if isinstance(child, dict): + yield from leaves(child, path) + else: + yield path + + example = envelope(payload)["replan_action_packet"]["stride_control"]["minimal_example"] + paths = list(leaves(example)) + assert ("next_probe", "cost_bound") in paths + assert len(paths) >= 17 + for path in paths: + partial = copy.deepcopy(payload) + target = envelope(partial)["replan_action_packet"]["stride_control"]["minimal_example"] + for key in path[:-1]: + target = target[key] + del target[path[-1]] + assert not _turn_projection_migration(base, row(partial))[0], path + + def test_quota_cli_keeps_full_user_todo_diagnostics_on_explicit_cold_path( tmp_path: Path, ) -> None: diff --git a/tests/control_plane/test_cli_output_differential.py b/tests/control_plane/test_cli_output_differential.py index c0690526da..4c3000e1fb 100644 --- a/tests/control_plane/test_cli_output_differential.py +++ b/tests/control_plane/test_cli_output_differential.py @@ -2045,3 +2045,47 @@ def test_objective_replan_cost_is_complete_once_and_carrier_scoped(): assert _compare_row(base, {**head, "chars": 1769})["failures"] assert not observe(prior + " " + " ".join(reversed(clauses)))["objective_replan_after"] assert not observe(json.dumps({"prior": prior, "clauses": clauses}))["objective_replan_after"] + + +@pytest.mark.parametrize("surface,root,growth", [ + ("quota_should_run", "$", 6684), + ("loopx_turn_plan", "$.turn_envelope", 6364), +]) +def test_inherited_stride_projection_growth_is_one_time_and_bounded(surface, root, growth): + from loopx.control_plane.testing.cli_output_differential import _compare_row + + control = f"{root}.replan_action_packet.stride_control" + paths = [control, *[f"{control}.{key}" for key in ( + "schema_version", "strategy_version", "reference_context_id", "reference_obligation_id", + "required_fields", "rule", "minimal_example.schema_version", "minimal_example.strategy_version", + "minimal_example.reference_context_id", "minimal_example.reference_obligation_id", + "minimal_example.basis", "minimal_example.action", + *[f"minimal_example.axes.{axis}.{field}" + for axis in ("work_slice", "route_distance", "validation_scope") + for field in ("plan", "reason")], + *[f"minimal_example.next_probe.{field}" for field in ( + "scope", "prediction", "adoption_criterion", "cost_bound", "minimum_evidence", + )], + )]] + base = _row(row_id=f"surface/{surface}/crowded/json") + candidate = {**base, "json_shape_paths": [*base["json_shape_paths"], *paths]} + for metric in ("chars", "utf8_bytes", "compact_payload_chars"): + candidate[metric] += growth + result = _compare_row(base, candidate) + assert not result["failures"] and result["review_signals"] + assert _compare_row({**base, "json_shape_paths": candidate["json_shape_paths"]}, candidate)["failures"] + assert _compare_row(base, {**candidate, "json_shape_paths": candidate["json_shape_paths"][:-1]})["failures"] + assert _compare_row(base, {**candidate, "json_shape_paths": [*candidate["json_shape_paths"], "$.unrelated"]})["failures"] + assert _compare_row(base, {**candidate, "json_shape_paths": paths})["failures"] + for metric in ("chars", "utf8_bytes", "lines", "compact_payload_chars"): + assert _compare_row(base, {**candidate, metric: base[metric] + result["allowances"][metric] + 1})["failures"] + for review, fields in ( + ("previous_review", ("reference_decision_ref", "adoption", "information_gain", "goal_gain", "evidence_refs", "lesson")), + ("explore_review", ("revision", "disposition", "considered_refs", "reason")), + ): + review_paths = [f"{control}.minimal_example.{review}.{field}" for field in fields] + complete = {**candidate, "json_shape_paths": [*candidate["json_shape_paths"], *review_paths]} + assert not _compare_row(base, complete)["failures"] + for missing in review_paths: + partial = {**complete, "json_shape_paths": [p for p in complete["json_shape_paths"] if p != missing]} + assert _compare_row(base, partial)["failures"], missing diff --git a/tests/control_plane/test_effective_turn_replan.py b/tests/control_plane/test_effective_turn_replan.py index 2234bcf6fd..abbbd002b9 100644 --- a/tests/control_plane/test_effective_turn_replan.py +++ b/tests/control_plane/test_effective_turn_replan.py @@ -70,7 +70,11 @@ def test_open_todo_settled_turn_cadence_and_evidence_linked_review( "configure-goal", "--goal-id", GOAL_ID, - *(["--execution-replan-after-turns", str(threshold_override), "--execute"] + # This matrix isolates cadence and typed-delta authority from the + # optional structured Stride decision. Inherited-on behavior is + # covered by machine-default and actual Stride followthrough tests. + "--execution-stride-strategy", "off", "--execute", + *(["--execution-replan-after-turns", str(threshold_override)] if threshold_override else []), ) assert rc == 0, configured @@ -333,7 +337,7 @@ def periodic(): @pytest.mark.parametrize("provider", ["file", "sqlite"]) @pytest.mark.parametrize("changed_context", [False, True]) -@pytest.mark.parametrize("stride_action,followthrough_count", [("verify", 1), ("switch", 2)]) +@pytest.mark.parametrize("stride_action,followthrough_count", [("verify", 2), ("switch", 2), ("grow", 2)]) def test_due_stride_review_survives_successor_transition_until_decision( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, provider: str, changed_context: bool, stride_action: str, followthrough_count: int, @@ -596,12 +600,21 @@ def call(*args: str, expected_code: int = 0) -> dict: ordinary_ref = delivered_result["evidence_ref"] # The optional typed-progress attachment is not required for the Agent to # discover a declared experiment's ordinary result in its next review. - assert follow["ordinary_work"]["recorded_count"] == 1 - assert follow["ordinary_work"]["evidence"] == [{ + ordinary = follow["ordinary_work"] + # Switch's first Turn has typed progress; every other settled Turn is an + # ordinary result. Fixed cadence must retain both verify/grow results. + assert ordinary["recorded_count"] == followthrough_count - (stride_action == "switch") + assert ordinary["evidence"][0] == { "evidence_ref": ordinary_ref, "generated_at": last_work["generated_at"], "todo_id": successor["todo_id"], - }] - assert planned[0]["ordinary_evidence_refs"] == [ordinary_ref] + } + refs = [row["evidence_ref"] for row in ordinary["evidence"]] + assert len(set(refs)) == ordinary["recorded_count"] + assert planned[0]["ordinary_evidence_refs"] == refs + for ref in refs: + result = call("history", "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, + "--evidence-ref", ref)["evidence"] + assert "regressed (error 5 versus reference 2)" in result["recommended_action"] from loopx.control_plane.quota.turn_envelope import build_turn_envelope envelope = build_turn_envelope(next_guard) assert envelope["replan_action_packet"]["stride_control"]["decision_followthrough"] == follow diff --git a/tests/control_plane/test_quota_settlement_cli.py b/tests/control_plane/test_quota_settlement_cli.py index 3e9586671c..712a185aff 100644 --- a/tests/control_plane/test_quota_settlement_cli.py +++ b/tests/control_plane/test_quota_settlement_cli.py @@ -2404,6 +2404,12 @@ def test_same_todo_writeback_reentry_only_settles_original_turn( _initialize_git_checkout(project) todo_id = SELECTED_REPLAN_TODO_ID _initialize_fixture_authority(project, runtime, provider=provider) + if writeback_kind == "replan": + # This replay fixture supplies the ordinary typed replan delta. Keep + # its strategy explicit; Stride decision followthrough is tested separately. + rc, configured = _run_cli(registry, runtime, "configure-goal", "--goal-id", GOAL_ID, + "--execution-stride-strategy", "off", "--execute") + assert rc == 0, configured turn_id = "turn-same-todo-writeback" host_args = ("--codex-app",) if host == "codex_app" else ("--runtime-profile", "generic_cli") guard_args = ( @@ -6404,6 +6410,11 @@ def test_todoless_autonomous_replan_settles_quota_refresh_spend_chain( ) -> None: project, runtime, registry_path = _write_fixture(tmp_path) _configure_autonomous_replan_fixture(project, runtime, registry_path) + # This case owns ordinary settlement/recovery, independently of Stride. + # Enabled decision adoption has separate real native coverage. + rc, configured = _run_cli(registry_path, runtime, "configure-goal", "--goal-id", GOAL_ID, + "--execution-stride-strategy", "off", "--execute") + assert rc == 0, configured registry = json.loads(registry_path.read_text(encoding="utf-8")) registry["goals"][0]["control_plane"] = { "periodic_report": { @@ -6645,6 +6656,11 @@ def test_blocked_replan_keeps_material_checkpoint_in_native_readback(tmp_path, m isolate_sqlite_runtime(tmp_path, monkeypatch) project, runtime, registry_path = _write_fixture(tmp_path) _configure_autonomous_replan_fixture(project, runtime, registry_path) + # This case owns ordinary settlement/recovery, independently of Stride. + # Enabled decision adoption has separate real native coverage. + rc, configured = _run_cli(registry_path, runtime, "configure-goal", "--goal-id", GOAL_ID, + "--execution-stride-strategy", "off", "--execute") + assert rc == 0, configured _initialize_fixture_authority(project, runtime, provider=provider) stamp = "2026-01-01T00:00:00+00:00" prior = { @@ -6721,6 +6737,11 @@ def test_todoless_blocked_replan_settles_read_only_external_evidence_without_wor ) -> None: project, runtime, registry_path = _write_fixture(tmp_path) _configure_autonomous_replan_fixture(project, runtime, registry_path) + # This case owns ordinary settlement/recovery, independently of Stride. + # Enabled decision adoption has separate real native coverage. + rc, configured = _run_cli(registry_path, runtime, "configure-goal", "--goal-id", GOAL_ID, + "--execution-stride-strategy", "off", "--execute") + assert rc == 0, configured _initialize_git_checkout(project) registry = json.loads(registry_path.read_text(encoding="utf-8")) registry["goals"][0]["coordination"]["registered_agents"].append( diff --git a/tests/control_plane/test_replan_progress_native_cli.py b/tests/control_plane/test_replan_progress_native_cli.py index 751699eac0..f15f4dfefd 100644 --- a/tests/control_plane/test_replan_progress_native_cli.py +++ b/tests/control_plane/test_replan_progress_native_cli.py @@ -65,6 +65,8 @@ def make_progress_cli_fixture(root: Path, provider: str, scenario: str) -> dict: registry.write_text(json.dumps({"common_runtime_root": str(runtime), "goals": [{ "id": GOAL, "status": "active", "repo": str(project), "state_file": state.name, "domain": "synthetic-typed-progress", + # Individual comparison journeys explicitly enable Stride below. + "execution_profile": {"stride_strategy": "off"}, "adapter": {"kind": "fixture_connected_delivery_v0", "status": "connected-delivery"}, "quota": {"compute": 1.0, "window_hours": 24}, "coordination": {"agent_model": "peer_v1", "registered_agents": [AGENT]}, @@ -293,6 +295,11 @@ def spend(turn: str, *, todo: str | None = TODO, obligation: str | None = None) "--tag", "writeback-result", "--evidence-ref", "evidence-existing") call("todo", "update", "--goal-id", GOAL, "--agent-id", AGENT, "--todo-id", TODO, "--explore-result-node-ref", "recorded-local-route") + # The proposal must reach the real Todo owner, not only a cadence label. + complete_scope = ("Implement local, reverse-priority and joint assignment; compare their costs " + "under the unchanged one-use-per-column constraint and retain the best valid artifact.") + call("todo", "update", "--goal-id", GOAL, "--agent-id", AGENT, "--todo-id", TODO, + "--text", complete_scope) guard(TURN, todo=None) admitted = guard(TURN) obligation = admitted["autonomous_replan_obligation"] @@ -369,6 +376,7 @@ def spend(turn: str, *, todo: str | None = TODO, obligation: str | None = None) work_turn = "turn-executed-route-comparison" selected = guard(work_turn) assert selected["selected_todo"]["todo_id"] == TODO + assert next(row for row in _authority(fixture)["todos"] if row["todo_id"] == TODO)["text"].endswith(complete_scope) artifact = fixture["project"] / "assignment-comparison.json" subprocess.run([sys.executable, "-c", """ import itertools, json, sys diff --git a/tests/control_plane_ts/replan_history.test.ts b/tests/control_plane_ts/replan_history.test.ts index 83a8f6bfb7..9f251af020 100644 --- a/tests/control_plane_ts/replan_history.test.ts +++ b/tests/control_plane_ts/replan_history.test.ts @@ -35,88 +35,25 @@ function trigger(runs: JsonObject[], patch: JsonObject = {}): JsonObject | null } const many = (count: number, build = run): JsonObject[] => Array.from({ length: count }, (_, i) => build(count - i)); -test("settled stride feedback scales only the next periodic interval from its configured baseline", () => { - const ack = run(0, {accepted_ack: true}); - const key = JSON.stringify(["worker-a", "turn-0"]); - const project = (count: number, decision: JsonObject, patch: JsonObject = {}) => { - const rows = [...many(count), ack]; - return projectReplanHistory(request(rows, {operation: "periodic", periodic_threshold: 6, - stride_strategy: "evidence_conditioned_v1", ...patch}), - new Set(rows.map(row => JSON.stringify([row.agent_id, row.turn_id]))), - new Map([[key, decision]])).trigger as JsonObject | null; - }; - const grow = {action: "grow", basis: "supported_progress", previous_review: { - goal_gain: "improved", evidence_refs: ["evidence:subsequent-work"]}}; - assert.equal(project(8, grow), null); - assert.equal(project(9, grow)?.threshold, 9); - assert.equal(project(20, grow)?.threshold, 9, "feedback does not compound on every read"); - for (const decision of [{action: "verify", basis: "supported_progress"}, - ...["uncertain_evidence", "unexplained_gap", "decision_failure"].map(basis => ({action: "grow", basis}))]) { - assert.equal(project(2, decision), null); - assert.equal(project(3, decision)?.threshold, 3); - assert.equal(project(3, decision, {periodic_threshold: 5})?.threshold, 3, "round fractional counts up"); - assert.equal(project(1, decision, {periodic_threshold: 1})?.threshold, 1); - } - for (const decision of [{action: "switch", basis: "unexplained_gap"}, - {...grow, previous_review: {goal_gain: "flat", evidence_refs: ["evidence:negative"]}}, - {...grow, previous_review: {goal_gain: "improved", evidence_refs: []}}, - {...grow, previous_review: undefined}, {...grow, basis: "useful_new_route"}]) { - assert.equal(project(5, decision), null); - assert.equal(project(6, decision)?.threshold, 6); - } - assert.equal(project(6, grow, {stride_strategy: "off"})?.threshold, 6); - assert.equal(project(6, grow, {stride_strategy: undefined})?.threshold, 6); -}); - -test("cadence feedback cannot borrow a peer, unsettled decision or a decision before the current ACK", () => { - const verify = {action: "verify", basis: "uncertain_evidence"}; - const key = (agent: string, turn: string) => JSON.stringify([agent, turn]); - const old = run(0, {accepted_ack: true}); - const rows = [...many(3), old]; - const qualified = new Set(rows.map(row => key(String(row.agent_id), String(row.turn_id)))); - const project = (history: JsonObject[], decisions: Map) => - projectReplanHistory(request(history, {periodic_threshold: 6, stride_strategy: "evidence_conditioned_v1"}), - qualified, decisions).trigger as JsonObject | null; - assert.equal(project(rows, new Map()), null, "a projected decision is not settlement"); - assert.equal(project(rows, new Map([[key("peer", "turn-0"), verify]])), null); - assert.equal(project([...many(3), run(4, {accepted_ack: true}), old], - new Map([[key("worker-a", "turn-0"), verify]])), null, "do not cross a newer ACK without stride feedback"); - const decisions = new Map([[key("worker-a", "turn-0"), verify]]); - assert.equal(project([run(9, {agent_id: "peer", accepted_ack: true}), ...rows], decisions)?.threshold, 3); - const stalled = [run(2, {progress: observation}), run(1, {progress: observation}), old]; - const before = projectReplanHistory(request(stalled), qualified).trigger; - assert.deepEqual(project(stalled, decisions), before, "typed stalls retain their original priority and threshold"); -}); - -test("late observations cannot revive an older settled stride decision by reusing its Turn", () => { - const key = (turn: string) => JSON.stringify(["worker-a", turn]); - const old = run(0, {accepted_ack: true}); - const current = run(4, {accepted_ack: true}); - const decisions = new Map([ - [key("turn-0"), {action: "verify", basis: "uncertain_evidence"}], - [key("turn-4"), {action: "switch", basis: "useful_new_route"}], - ]); - const qualified = new Set([key("turn-1"), key("turn-2")]); - const project = (rows: JsonObject[], feedback = decisions, patch: JsonObject = {}) => - projectReplanHistory(request(rows, {operation: "periodic", periodic_threshold: 2, - stride_strategy: "evidence_conditioned_v1", ...patch}), qualified, feedback).trigger as JsonObject | null; - // These are request-local history observations, not synthetic settlements. - // Neutral inventory omitted by an older caller must retain the same rule. - for (const classification of ["replan_noop", "repair_noop", ...neutral, "status_readback"]) { - const alias = run(9, {turn_id: "turn-0", classification}); - assert.equal(project([alias, run(1), current, old]), null, classification); - assert.equal(project([alias, run(2), run(1), current, old])?.threshold, 2, classification); - assert.equal(project([alias, run(1), current, old], - new Map([[key("turn-0"), decisions.get(key("turn-0"))!]])), null, - "the newer ACK stops lookup even when it has no settled decision"); - assert.equal(project([alias, run(1), current, old], decisions, {stride_strategy: "off"}), null); +test("work-shape decisions never silently change the configured review count", () => { + for (const strategy of ["off", "evidence_conditioned_v1"]) { + for (const action of ["grow", "verify", "switch"]) { + for (const threshold of [1, 3, 6]) { + const ack = run(0, {accepted_ack: true, agent_vision: {stride_decision: { + action, basis: "supported_progress", previous_review: {goal_gain: "improved"}, + }}}); + const project = (count: number) => { + const rows = [...many(count), ack]; + return projectReplanHistory(request(rows, {operation: "periodic", + periodic_threshold: threshold, stride_strategy: strategy}), + new Set(rows.map(row => JSON.stringify([row.agent_id, row.turn_id])))).trigger as JsonObject | null; + }; + assert.equal(project(threshold - 1), null); + assert.equal(project(threshold)?.threshold, threshold); + assert.equal(project(threshold + 2)?.threshold, threshold); + } + } } - // A neutral ACK still cuts the existing window, but cannot supply feedback. - assert.equal(project([run(1), {...old, classification: "replan_noop"}], - new Map([[key("turn-0"), decisions.get(key("turn-0"))!]])), null); - assert.equal(project([run(1), old], - new Map([[key("turn-0"), decisions.get(key("turn-0"))!]]))?.threshold, 1, - "the genuine latest ACK remains inclusive"); }); test("effective cadence uses qualified lane identities, independent of Todo completion", () => {