From 1b13f97377bc1e844b1182cea93131b33b30222d Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Sat, 7 Nov 2026 01:25:32 +0800 Subject: [PATCH 1/3] feat(research): carry scoped yardstick decisions across work turns Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../smoke/capability-configuration-smoke.ts | 11 + .../src/data/capability-configuration.ts | 3 + apps/presentation/dashboard/src/data/chat.ts | 5 + .../capability-localization.ts | 2 + .../goal-capability-settings.tsx | 16 +- .../goal-explore-results.tsx | 2 + .../goal-research-history.tsx | 3 +- .../work-yardstick-readback.tsx | 24 +++ .../target-yardstick-stride-decisions-v0.md | 72 ++++++- ...get-yardstick-stride-decisions-v0.zh-CN.md | 56 ++++- .../replan-cadence.mjs | 53 ++++- .../research-finding-reader.mjs | 24 ++- loopx/capabilities/configuration_ui.py | 11 +- loopx/capabilities/explore/result_log.py | 11 +- .../capabilities/explore/result_writeback.py | 28 ++- loopx/capabilities/explore/turn_context.py | 7 +- .../todo_replan_cadence/goal_configuration.py | 2 +- .../todo_replan_cadence/machine_defaults.py | 34 ++- loopx/chat/goal_configuration_api.py | 15 +- loopx/cli_commands/registry_admin.py | 2 + .../cli_commands/registry_admin_configure.py | 8 + loopx/configure_goal.py | 4 + .../capabilities/explore_result_writeback.ts | 8 +- .../capabilities/explore_turn_context.ts | 26 ++- .../control_plane/effect_runtime_handlers.ts | 2 + .../presentation/explore_history.ts | 5 + .../work_items/yardstick_decision.py | 23 +++ .../work_items/yardstick_decision.ts | 131 ++++++++++++ loopx/execution_profile.py | 17 +- .../presentation/explore_record_values.py | 99 +++++++++ .../lark/presentation/explore_results.py | 75 +------ .../test_goal_capability_inspection.py | 2 +- ...est_machine_configuration_goal_defaults.py | 82 ++++++++ .../control_plane/test_todo_replan_cadence.py | 20 ++ .../control_plane/test_work_yardstick_cli.py | 195 ++++++++++++++++++ .../yardstick_decision.test.ts | 95 +++++++++ tests/test_chat_goal_configuration_api.py | 1 + tests/test_chat_machine_configuration_api.py | 1 + 38 files changed, 1053 insertions(+), 122 deletions(-) create mode 100644 apps/presentation/dashboard/src/features/personal-workspace/work-yardstick-readback.tsx create mode 100644 loopx/control_plane/work_items/yardstick_decision.py create mode 100644 loopx/control_plane/work_items/yardstick_decision.ts create mode 100644 loopx/extensions/lark/presentation/explore_record_values.py create mode 100644 tests/control_plane/test_work_yardstick_cli.py create mode 100644 tests/control_plane_ts/yardstick_decision.test.ts diff --git a/apps/presentation/dashboard/smoke/capability-configuration-smoke.ts b/apps/presentation/dashboard/smoke/capability-configuration-smoke.ts index 08bcbb6e2d..9033ddff70 100644 --- a/apps/presentation/dashboard/smoke/capability-configuration-smoke.ts +++ b/apps/presentation/dashboard/smoke/capability-configuration-smoke.ts @@ -178,3 +178,14 @@ assert.deepEqual(goalReplanCadenceEditorValue(null, enabledStrideDefault), enabl 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"); + +// Independent Yardstick inheritance must not be pinned by editing the interval. +const yardstickEffective = {...inheritedReview, yardstick_enabled: true}; +assert.equal(goalReplanCadenceEditorValue({count_unit: "effective_turns", count: 2}, yardstickEffective).yardstick_enabled, true); +assert.deepEqual(goalReplanCadenceWriteValue({...yardstickEffective, count: 2}, true, false, false), + {...inheritedReview, count: 2}, "an unchanged inherited measurement choice stays live"); +for (const enabled of [true, false]) { + assert.deepEqual(goalReplanCadenceWriteValue({...yardstickEffective, yardstick_enabled: enabled}, false, false, true), + {stride_strategy: "off", yardstick_enabled: enabled}, "explicit measurement opt-in and opt-out survive form saving"); +} +assert.equal(replanCadenceEditorDefaults(yardstickEffective, {...cadenceDefault, yardstick_enabled: false}).yardstick_enabled, true); diff --git a/apps/presentation/dashboard/src/data/capability-configuration.ts b/apps/presentation/dashboard/src/data/capability-configuration.ts index c87d8431bb..6c61e3d091 100644 --- a/apps/presentation/dashboard/src/data/capability-configuration.ts +++ b/apps/presentation/dashboard/src/data/capability-configuration.ts @@ -78,6 +78,7 @@ export function replanCadenceEditorDefaults(effective: unknown, fallback: unknow if (Object.hasOwn(live, "count_unit")) defaults.count_unit = live.count_unit; if (Object.hasOwn(live, "count")) defaults.count = live.count; if (Object.hasOwn(live, "planning_horizon_goal")) defaults.planning_horizon_goal = live.planning_horizon_goal; + if (Object.hasOwn(live, "yardstick_enabled")) defaults.yardstick_enabled = live.yardstick_enabled; // Strategy omission keeps the existing default-off meaning. return defaults; } @@ -102,6 +103,7 @@ export function goalReplanCadenceWriteValue( draft: Record, cadenceOverride: boolean, horizonOverride = Object.hasOwn(draft, "planning_horizon_goal"), + yardstickOverride = Object.hasOwn(draft, "yardstick_enabled"), ): Record { const value = { ...draft }; if (!cadenceOverride) { @@ -109,6 +111,7 @@ export function goalReplanCadenceWriteValue( delete value.count; } if (!horizonOverride) delete value.planning_horizon_goal; + if (!yardstickOverride) delete value.yardstick_enabled; return value; } diff --git a/apps/presentation/dashboard/src/data/chat.ts b/apps/presentation/dashboard/src/data/chat.ts index 5d278ab127..dc51d6bb5a 100644 --- a/apps/presentation/dashboard/src/data/chat.ts +++ b/apps/presentation/dashboard/src/data/chat.ts @@ -1,3 +1,4 @@ +import {normalizeWorkYardstickCapture} from "../../../../../loopx/control_plane/work_items/yardstick_decision.js"; import {sourceReadApiUrl} from "./local-status-query"; import type {OracleEvidence} from "./goal-acceptance-observation"; import { HANDOFF_MODES, EXECUTION_HANDOFF_MODES, type ExecutionHandoffMode } from "../../../../../loopx/control_plane/coordination/handoff_mode_vocabulary.js"; @@ -3008,6 +3009,7 @@ const exploreResultPageSchema = z.object({ evidence_refs: z.array(z.string()), last_updated_at: z.string(), measurements: z.unknown().transform(value => normalizeExploreMeasurements(value)).optional(), measurement_error: z.string().optional(), + work_yardstick: z.unknown().transform(capture => normalizeWorkYardstickCapture({capture})).optional(), linked_todos_available: z.boolean().optional(), linked_todos: z.array(z.object({ todo_id: z.string(), text: z.string(), status: z.string(), claimed_by: z.string(), @@ -3019,5 +3021,8 @@ export async function fetchExploreResults(goalId: string, cursor?: string, readS const query = new URLSearchParams({goal_id: goalId, view: "findings", ...(cursor ? {cursor} : {})}); const page = exploreResultPageSchema.parse(await requestJson(sourceReadApiUrl(readSourceUrl, `/api/chat/explore-results?${query}`))); if (page.goal_id !== goalId) throw new Error("Explore result Goal mismatch"); + for (const item of page.items) if (item.work_yardstick) normalizeWorkYardstickCapture({ + capture: item.work_yardstick, goal_id: goalId, agent_id: item.agent_id, evidence_refs: item.evidence_refs, + }); return page; } diff --git a/apps/presentation/dashboard/src/features/personal-workspace/capability-localization.ts b/apps/presentation/dashboard/src/features/personal-workspace/capability-localization.ts index c344ded2c2..bb4d4f76dc 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/capability-localization.ts +++ b/apps/presentation/dashboard/src/features/personal-workspace/capability-localization.ts @@ -166,6 +166,7 @@ const fieldCopy: Record = { executor_reasoning_effort: { label: "Reasoning effort", description: "Optional reasoning effort for the selected executor. Leave blank to keep the executor's own default." }, count_unit: { label: "Review after", description: "Work Turns require accepted settlement; polls and retries do not count.", options: { completed_todos: "Completed Todos", effective_turns: "Settled work Turns" } }, planning_horizon_goal: { label: "Show Goal in planning horizon", description: "Default on. Keep the original Goal and acceptance visible before choosing work. Turn off for rollback or comparisons; replan, stride and execution authority are unchanged." }, + yardstick_enabled: { label: "Review measurement criteria", description: "Default off. Review measurement scope and feedback using available Explore evidence. Requires Explore evidence; does not enable it or authorize execution." }, stride_strategy: { label: "Stride strategy", description: "Structured work scope, route and validation proposals are required for enabled fresh Vision reviews. Does not grant execution authority.", options: { off: "Off", evidence_conditioned_v1: "Evidence-conditioned · v1" } }, count: { label: "Number between reviews", description: "Settled work Turns: 1–6; completed Todos: 1–5. Goal overrides take precedence over device defaults." }, allowed_domains: { label: "Allowed responsibility domains", description: "Enter one bounded, public-safe domain per line." }, @@ -203,6 +204,7 @@ const fieldCopy: Record = { executor_reasoning_effort: { label: "推理档位", description: "所选执行器使用的推理档位,可留空;留空表示沿用执行器自身的默认档位。" }, count_unit: { label: "复核计数依据", description: "有效工作 Turn 须完成结算;轮询和重复重试不计数。", options: { completed_todos: "已完成 Todo", effective_turns: "已结算工作 Turn" } }, planning_horizon_goal: { label: "在规划视野中显示 Goal", description: "默认开启,选择工作前可见原目标与验收要求。可为回滚或对照实验关闭;不改变复盘、步幅策略或执行权限。" }, + yardstick_enabled: { label: "复核衡量标准", description: "默认关闭。使用已有 Explore 证据复核测量范围与反馈;需有可用的 Explore 证据,不会自动启用它或授予执行权限。" }, stride_strategy: { label: "步幅策略", description: "启用后,新的 Vision 路线复盘须提交工作大小、路线及验证范围的结构化提案,不授予执行权限。", options: { off: "关闭", evidence_conditioned_v1: "依据证据调整 · v1" } }, count: { label: "两次复核间的数量", description: "已结算工作 Turn 为 1–6,已完成 Todo 为 1–5;Goal 显式设置优先于设备默认值。" }, allowed_domains: { label: "允许的职责域", description: "每行填写一个有边界、可公开的职责域。" }, diff --git a/apps/presentation/dashboard/src/features/personal-workspace/goal-capability-settings.tsx b/apps/presentation/dashboard/src/features/personal-workspace/goal-capability-settings.tsx index 2a971c736b..8c90f09797 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/goal-capability-settings.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/goal-capability-settings.tsx @@ -40,13 +40,14 @@ type CapabilityMutationState = Readonly<{ draft: Record; cadenceOverride: boolean; horizonOverride: boolean; + yardstickOverride: boolean; exploreModeOverride: boolean; partialWrite: GoalConfigurationPartialWrite | null; readbackPending: boolean; preview: GoalConfigurationPreview | null; }>; -function goalWriteDraft(selected: CapabilityConfigurationCatalog["capabilities"][number], draft: Record, editorMode: "guided" | "json", cadenceOverride: boolean, horizonOverride: boolean, exploreModeOverride: boolean) { +function goalWriteDraft(selected: CapabilityConfigurationCatalog["capabilities"][number], draft: Record, editorMode: "guided" | "json", cadenceOverride: boolean, horizonOverride: boolean, exploreModeOverride: boolean, yardstickOverride: boolean) { const defaults = selected.capability_id === "todo_replan_cadence" ? editorMode === "json" ? undefined : replanCadenceEditorDefaults(selected.effective_configuration?.configuration, selected.default) : selected.default; @@ -54,7 +55,7 @@ function goalWriteDraft(selected: CapabilityConfigurationCatalog["capabilities"] if (selected.capability_id === "reward_memory") return rewardMemoryWriteValue(writable); if (selected.capability_id === "explore_harness") return goalExploreWriteValue(writable, exploreModeOverride); if (selected.capability_id === "todo_replan_cadence" && editorMode === "guided") { - return goalReplanCadenceWriteValue(writable, cadenceOverride, horizonOverride); + return goalReplanCadenceWriteValue(writable, cadenceOverride, horizonOverride, yardstickOverride); } if (selected.capability_id === "pull_request_review" && !Object.hasOwn(selected.current ?? {}, "owner_logins") && JSON.stringify(writable.owner_logins ?? []) === JSON.stringify(selected.effective_configuration?.configuration?.owner_logins ?? [])) { @@ -83,6 +84,7 @@ function useCapabilityMutation({ goalId, loading, onApplied, selected, t }: Read draft: {}, cadenceOverride: false, horizonOverride: false, + yardstickOverride: false, exploreModeOverride: false, partialWrite: null, readbackPending: false, @@ -122,6 +124,7 @@ function useCapabilityMutation({ goalId, loading, onApplied, selected, t }: Read ), cadenceOverride: hasReplanCadenceOverride(selected?.current), horizonOverride: Object.hasOwn(selected?.current ?? {}, "planning_horizon_goal"), + yardstickOverride: Object.hasOwn(selected?.current ?? {}, "yardstick_enabled"), exploreModeOverride: Object.hasOwn(selected?.current ?? {}, "mode"), partialWrite: null, readbackPending: false, @@ -137,7 +140,7 @@ function useCapabilityMutation({ goalId, loading, onApplied, selected, t }: Read setMutation((current) => ({ ...current, busy: "preview", partialWrite: null, readbackPending: false })); setError(null); try { - const nextPreview = await previewGoalConfiguration(goalId, selected.capability_id, configuration === null ? null : goalWriteDraft(selected, configuration, editorMode, mutation.cadenceOverride, mutation.horizonOverride, mutation.exploreModeOverride)); + const nextPreview = await previewGoalConfiguration(goalId, selected.capability_id, configuration === null ? null : goalWriteDraft(selected, configuration, editorMode, mutation.cadenceOverride, mutation.horizonOverride, mutation.exploreModeOverride, mutation.yardstickOverride)); if (generation === editorGeneration.current) setMutation((current) => ({ ...current, preview: nextPreview })); } catch (reason) { if (generation === editorGeneration.current) setError(reason instanceof Error ? reason.message : t("capabilities.previewFailed")); @@ -152,7 +155,7 @@ function useCapabilityMutation({ goalId, loading, onApplied, selected, t }: Read setMutation((current) => ({ ...current, busy: "apply" })); setError(null); try { - const writableDraft = goalWriteDraft(selected, mutation.draft, editorMode, mutation.cadenceOverride, mutation.horizonOverride, mutation.exploreModeOverride); + const writableDraft = goalWriteDraft(selected, mutation.draft, editorMode, mutation.cadenceOverride, mutation.horizonOverride, mutation.exploreModeOverride, mutation.yardstickOverride); const result = await applyGoalConfiguration( goalId, selected.capability_id, @@ -190,6 +193,7 @@ function useCapabilityMutation({ goalId, loading, onApplied, selected, t }: Read : { ...current.draft, [key]: value }, cadenceOverride: current.cadenceOverride || (selected?.capability_id === "todo_replan_cadence" && (key === "count_unit" || key === "count")), horizonOverride: current.horizonOverride || (selected?.capability_id === "todo_replan_cadence" && key === "planning_horizon_goal"), + yardstickOverride: current.yardstickOverride || (selected?.capability_id === "todo_replan_cadence" && key === "yardstick_enabled"), exploreModeOverride: current.exploreModeOverride || (selected?.capability_id === "explore_harness" && key === "mode"), preview: null, })); @@ -212,6 +216,8 @@ function useCapabilityMutation({ goalId, loading, onApplied, selected, t }: Read ? hasReplanCadenceOverride(parsed) : current.cadenceOverride, horizonOverride: selected.capability_id === "todo_replan_cadence" && parsed ? Object.hasOwn(parsed, "planning_horizon_goal") : current.horizonOverride, + yardstickOverride: selected.capability_id === "todo_replan_cadence" && parsed + ? Object.hasOwn(parsed, "yardstick_enabled") : current.yardstickOverride, exploreModeOverride: selected.capability_id === "explore_harness" && parsed ? Object.hasOwn(parsed, "mode") : current.exploreModeOverride, })); @@ -222,7 +228,7 @@ function useCapabilityMutation({ goalId, loading, onApplied, selected, t }: Read if (loading || mutation.busy || !jsonValid) return; if (editorMode === "guided" && selected) { const jsonValue = ["todo_replan_cadence", "explore_harness"].includes(selected.capability_id) - ? goalWriteDraft(selected, mutation.draft, editorMode, mutation.cadenceOverride, mutation.horizonOverride, mutation.exploreModeOverride) + ? goalWriteDraft(selected, mutation.draft, editorMode, mutation.cadenceOverride, mutation.horizonOverride, mutation.exploreModeOverride, mutation.yardstickOverride) : mutation.draft; setJsonDraft(JSON.stringify(jsonValue, null, 2)); } diff --git a/apps/presentation/dashboard/src/features/personal-workspace/goal-explore-results.tsx b/apps/presentation/dashboard/src/features/personal-workspace/goal-explore-results.tsx index a69fbebc54..590a267d8a 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/goal-explore-results.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/goal-explore-results.tsx @@ -1,3 +1,4 @@ +import {WorkYardstickReadback} from "./work-yardstick-readback"; import {exploreFindingCopy} from "./explore-finding-copy"; import {useEffect, useRef, useState} from "react"; import {AlertTriangle, ChevronRight, RefreshCw} from "lucide-react"; @@ -127,6 +128,7 @@ export function GoalExploreResults({goalId, zh, active = true, readSourceUrl}: {
    {sections.decision![key].map((text, index) =>
  • {text}
  • )}
: null)} : null} :

{selected.summary}

} + {selected.work_yardstick ? : null}
{zh ? "来源与范围" : "Sources and scope"}
{zh ? "记录时间" : "Recorded at"}
{zh ? "记录者" : "Recorded by"}
{selected.agent_id || "—"}
diff --git a/apps/presentation/dashboard/src/features/personal-workspace/goal-research-history.tsx b/apps/presentation/dashboard/src/features/personal-workspace/goal-research-history.tsx index 2dc89422f2..3ba8c18b2b 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/goal-research-history.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/goal-research-history.tsx @@ -1,3 +1,4 @@ +import {WorkYardstickReadback} from "./work-yardstick-readback"; import {exploreFindingCopy} from "./explore-finding-copy"; import {AlertCircle, ArrowLeft, ArrowRight, Circle, CircleHelp, FlaskConical, GitBranch, RefreshCw, Search} from "lucide-react"; import {Fragment, useEffect, useId, useLayoutEffect, useMemo, useRef, useState} from "react"; @@ -143,7 +144,7 @@ export function GoalResearchHistory({goalId, active, zh, refreshRevision = 0, re const findingBody = (finding: ExploreHistoryEvent) => { const execution = finding.execution_observation; const measured = (value: unknown) => value === null ? (zh ? "未测量" : "Not measured") : String(value); - return <>{finding.measurements ? : null}{findingNarrative(finding)}{execution ?
{zh ? "实际执行与资源观测" : "Execution and resource observations"}

{zh ? "来源记录的执行观测;不等于方法有效或当前候选已通过验收。" : "Source-recorded execution observations; not proof of method benefit or current-candidate acceptance."}

+ return <>{finding.measurements ? : null}{findingNarrative(finding)}{finding.work_yardstick ? : null}{execution ?
{zh ? "实际执行与资源观测" : "Execution and resource observations"}

{zh ? "来源记录的执行观测;不等于方法有效或当前候选已通过验收。" : "Source-recorded execution observations; not proof of method benefit or current-candidate acceptance."}

{zh ? "计划方法 → 实际方法" : "Planned → executed method"}
{String(execution.planned_method_id)} → {String(execution.executed_method_id)}
{execution.change_reason ? <>
{zh ? "改向原因" : "Reason for change"}
{String(execution.change_reason)}
: null} diff --git a/apps/presentation/dashboard/src/features/personal-workspace/work-yardstick-readback.tsx b/apps/presentation/dashboard/src/features/personal-workspace/work-yardstick-readback.tsx new file mode 100644 index 0000000000..017cc0b494 --- /dev/null +++ b/apps/presentation/dashboard/src/features/personal-workspace/work-yardstick-readback.tsx @@ -0,0 +1,24 @@ +import type {WorkYardstickCapture, WorkYardstickProfile} from "../../../../../../loopx/control_plane/work_items/yardstick_decision.js"; + +/** A source record, never an acceptance or permission indicator. */ +export function WorkYardstickReadback({capture, zh}: {capture: WorkYardstickCapture; zh: boolean}) { + const {decision, binding} = capture; + const profile = (value: WorkYardstickProfile) => <>{value.metric} ({value.unit}) {value.higher_is_better ? "↑" : "↓"} +
{value.comparison_scope} · {value.input_revision} · {value.evaluation_ref}; + return
+ {zh ? "目标与衡量标准" : "Target and yardstick"} +
+
{zh ? "目标" : "Target"}
{decision.target.summary}
+
{zh ? "适用范围" : "Scope"}
{decision.target.scope}
+
{zh ? "成果指标" : "Outcome metric"}
{profile(decision.authoritative)}
+
{zh ? "代理指标" : "Proxy metric"}
{decision.proxy ? profile(decision.proxy) : zh ? "直接评估" : "Direct evaluation"}
+ {decision.counterevidence_refs.length ? <>
{zh ? "代理反证" : "Proxy counterevidence"}
{decision.counterevidence_refs.map(ref => {ref})}
: null} +
{zh ? "下一步" : "Next action"}
{decision.next_action.reason}
{decision.next_action.method_id}
+
{zh ? "成本边界" : "Cost bound"}
{decision.next_action.cost_bound}
+
{zh ? "验收来源" : "Acceptance source"}
{decision.target.acceptance_ref}
+
{zh ? "任务" : "Task"}
{binding.todo_id}
+ {decision.prior_finding_id ? <>
{zh ? "上次判断" : "Previous decision"}
{decision.prior_finding_id}
: null} +
+

{zh ? "这是记录者的研究判断;代理有效性与成果验收仍需独立证据。" : "This records the author's research decision; proxy fidelity and outcome acceptance still require independent evidence."}

+
; +} diff --git a/docs/architecture/rfcs/target-yardstick-stride-decisions-v0.md b/docs/architecture/rfcs/target-yardstick-stride-decisions-v0.md index 0870c96ec1..8e9ba32112 100644 --- a/docs/architecture/rfcs/target-yardstick-stride-decisions-v0.md +++ b/docs/architecture/rfcs/target-yardstick-stride-decisions-v0.md @@ -1,11 +1,11 @@ # RFC: Joint Target, Yardstick and Stride Decisions (v0) -- **RFC status:** Accepted (effective on merge; this PR is a proposal) +- **RFC status:** Accepted - **Supersedes / closes:** none -- **Delivery maturity:** Proposal +- **Delivery maturity:** M1 implementation proposal; M2/M3 unqualified - **Authors / owners:** LoopX maintainers - **Created:** 2026-11-05 -- **Last normative revision:** 2026-11-05 +- **Last normative revision:** 2026-11-07 - **Implementation baseline:** `aa82591fb0a129f3ea9cc5e67343e742899c7ccd` - **Related contracts:** [Hierarchical stride](hierarchical-agent-stride-control-v0.md), [Research exploration](research-exploration-control-plane-v0.md), [Research measurement](long-horizon-harness-benchmark-research-program-v0.md), [Controller/executor composition](controller-executor-unified-contract-v0.md) - **Semantic mirror:** [中文版](target-yardstick-stride-decisions-v0.zh-CN.md) @@ -18,7 +18,8 @@ Appendix A identifies sources and the limits of their evidence. Merge accepts a design basis, not implementation, deployment, a default change or measured benefit. Keep both language versions semantically synchronized. Later execution evidence belongs in the existing RFC ledger convention, with explicit normative revisions -when a decision changes. No runtime changes or experiments are delivered here. +when a decision changes. Section 11 records the bounded M1 implementation boundary; +no long-horizon effectiveness gain or default promotion is established. ## 1. Decision summary @@ -160,7 +161,28 @@ Two entry bindings are a tagged union: **work Turn with Todo** or **real replan obligation**. Reject mixed/stale bindings. Preserve existing replan decision references and add an ordinary-work reference form through the same owner; never synthesize a replan obligation to satisfy the current regex. Exact -serialization/migration is an M1 design task, not an implicit v2 relaxation. +serialization uses the additive `work_yardstick_decision_v0` attachment below, +not an implicit v2 relaxation. + +The M1 attachment is optional `work_yardstick_decision` inside the existing +`explore_result_attachment_v0` (including the existing path-delta authoring +adapter). Its owner is `control_plane/work_items/yardstick_decision.ts`. +It records a bounded target and acceptance-source reference, an outcome +measurement profile, an optional proxy profile, source-linked counterevidence, +next method/reason/cost bound and the prior same-Todo finding. Profiles reuse +Explore's exact metric, unit, direction, comparison scope, input revision and +evaluator reference; they do not merge proxy and authoritative measurements. +`authoritative` names the Agent's declared outcome profile, not verified authority. + +The original settlement supplies Goal/Agent/Todo/Turn identity. The existing run +journal, Explore event log and additive Todo link own persistence and retry; +there is no extra journal, metadata note, planner or mandatory writeback. +A changed decision must cite the current same-Todo finding. Current context +selects by original committed time, so late ingestion cannot replace a newer +choice. Ordinary work preserves its original settlement type and never creates +an obligation to satisfy Stride v2. Replans retain their existing v2 contract. +An absent attachment preserves the current decision; an explicit `proxy: null` +chooses direct evaluation while historical profiles remain readable. ### 5.3 Co-discovery: invest in a pair @@ -186,7 +208,10 @@ narrowing/changing target and direct evaluation. An untested proxy may support bounded exploration while confirmations accumulate. An easy proxy is not a reason to remain on a low-impact target; no reliable proxy is not a reason to idle. -Retain a promising incumbent while testing alternatives. Switching must name +Retaining a promising incumbent can provide a matched control or recovery point; +it is not a universal final-delivery requirement. Follow the accepted outcome: +a best-seen valid-score objective does not require the final workspace to contain +the best candidate, whereas a deliverable-quality objective may. Switching must name what changes: only yardstick, only target, or both. Replacing a broken yardstick is usually `verify`; it is not automatically a mechanism-changing `switch`. @@ -490,11 +515,44 @@ existing Stride/Explore owners and downstream callers; remove prototype duplicates when adopted. Do not add a registry, second journal or speculative provider abstraction to make these milestones appear complete. +### M1 implementation checkpoint + +`execution_profile.yardstick_enabled` is an independent, default-off setting of +the existing Goal review/stride capability. Machine and Goal editors share that +owner; an explicit Goal false wins. Explore evidence must be available before a +new capture; enabling this setting does not launch work or activate another +capability. Disabling removes the extra ordinary-Turn cue and rejects new +captures before primary commit, while historical findings and successful exact +replays remain readable. + +The existing Research finding reader exposes the recorded target, outcome/proxy +metrics, source, next action and cost bound. The ordinary task-scoped Explore +context projects only that Todo's current target, a short next-choice cue and +counterevidence references. Its existing question-scoped read command exposes +the full decision, exact-profile feedback and prior execution attribution; the +default projection does not repeat that full record. +Counterevidence requests reassessment at the next concrete choice; it is not a +new hard gate, fidelity certification, permission to waive correctness or an +instruction to replan every Turn. At a later meaningful capture, existing +execution observations can bind the predecessor's proposed method to the actual +method, candidate and evidence, including a justified different method. + +Source validation includes a real isolated SQLite CLI → ordinary guard → +concrete synthetic artifact/evaluation → same-Todo result journey, off-mode +rejection/readback and recovery checks. Its deterministic actor demonstrates +transport and attribution only. The packaged configuration journey qualifies +machine inheritance, Goal overrides and failed-read recovery; the populated +Research reader qualifies the original HTTP/SQLite history on desktop and narrow +screens. Lark uses its existing finding-summary projection, with no external send +in qualification. Actual model adoption, a numerical fidelity profile, complete +marginal cost and matched benefit remain untested; schema survival and synthetic +followthrough do not close C13 or M2/M3. + ## 12. Open decisions | Decision | Owner / recommendation | Evidence needed / deadline | | --- | --- | --- | -| Exact attachment schema and configuration field | Work-items and cadence maintainers: additive tagged work/replan binding, independent opt-in; preserve v2. | Producer/reader inventory and C8–C12 before M1 implementation. | +| Attachment and configuration seam | Work-items owns `work_yardstick_decision_v0`; cadence owns independent `yardstick_enabled`; Explore owns ingestion and readback. Preserve replan v2. | Keep exact-head C8–C12 and packaged journey evidence with the implementation proposal. | | First fidelity profile and evidence floor | Measurement provider maintainer: rank-based profile for a numerical task; no universal six-state rule. | Noise, selection/re-testing risks and affordable true confirmations before M2. | | Automatic triggers and context budget | Stride/context maintainers: typed evidence deltas and reached probe conditions; no prose classifier or fixed per-Turn form. | Full-path overhead and missed-decision counterexamples in M1/M2; thresholds fixed before M3. | | Default promotion | Product owner: remain off until matched evidence justifies promotion. | M3 including regressions, uncertainty and actual user journey. | diff --git a/docs/architecture/rfcs/target-yardstick-stride-decisions-v0.zh-CN.md b/docs/architecture/rfcs/target-yardstick-stride-decisions-v0.zh-CN.md index 6f2a6a0154..0b5d7432f1 100644 --- a/docs/architecture/rfcs/target-yardstick-stride-decisions-v0.zh-CN.md +++ b/docs/architecture/rfcs/target-yardstick-stride-decisions-v0.zh-CN.md @@ -1,11 +1,11 @@ # RFC:Target、Yardstick 与 Stride 联合决策(v0) -- **RFC 状态:** Accepted(合并后生效;本 PR 仍是提案) +- **RFC 状态:** Accepted - **替代 / 关闭:** none -- **交付成熟度:** Proposal +- **交付成熟度:** M1 实现提案;M2/M3 未资格确认 - **作者 / 归属:** LoopX maintainers - **创建日期:** 2026-11-05 -- **最近规范修订:** 2026-11-05 +- **最近规范修订:** 2026-11-07 - **实现基线:** `aa82591fb0a129f3ea9cc5e67343e742899c7ccd` - **相关契约:** [分层 Stride](hierarchical-agent-stride-control-v0.zh-CN.md)、[研究探索](research-exploration-control-plane-v0.zh-CN.md)、[研究测量](long-horizon-harness-benchmark-research-program-v0.zh-CN.md)、[Controller/Executor 组合](controller-executor-unified-contract-v0.zh-CN.md) - **语义镜像(语言镜像):** [English](target-yardstick-stride-decisions-v0.md) @@ -16,7 +16,7 @@ 第 12 节列出尚待决定的实现选择。附录 A 标明来源及证据边界。 合并只接受设计依据,不证明实现、部署、默认启用或效果收益。 双语版本保持语义一致。后续执行证据沿用 RFC ledger 约定;改变设计时显式修订规范。 -本 PR 不交付运行时改动,也不启动实验。 +第 11 节记录有界 M1 实现边界;尚未证明长程效果收益,也不提升默认配置。 ## 1. 决策摘要 @@ -130,8 +130,23 @@ replan 和普通 Turn adapter 组合它;Python adapter 不另写一套策略 入口绑定是 tagged union:**有 Todo 的工作 Turn**,或**真实 replan obligation**。 拒绝混合/过期绑定。保留现有 replan decision reference,并通过同一 owner 添加普通工作引用形式。 -绝不为满足当前 regex 伪造 replan obligation。精确序列化/迁移属于 M1 设计, -不能隐式放宽 v2。 +绝不为满足当前 regex 伪造 replan obligation。精确序列化采用下述追加的 +`work_yardstick_decision_v0` 附件,不能隐式放宽 v2。 + +M1 在既有 `explore_result_attachment_v0` 中添加可选 `work_yardstick_decision`, +也支持既有 path-delta authoring 适配。owner 为 +`control_plane/work_items/yardstick_decision.ts`。它记录有界 target 与验收来源引用、 +成果 measurement profile、可选 proxy profile、有来源的反证、下一方法/原因/成本边界 +及同 Todo 的前次 finding。Profile 复用 Explore 的精确指标、单位、方向、比较范围、 +输入版本和 evaluator 引用,不合并代理与权威测量。 +`authoritative` 表示 Agent 声明的成果 profile,不证明权威性已被验证。 + +原 settlement 提供 Goal/Agent/Todo/Turn 身份。既有 run journal、Explore event log +和 Todo 追加关联管理持久化与重试;不新建 journal、metadata note、planner 或必填写回。 +决策变化必须引用同 Todo 当前 finding;按原提交时间选择当前决策,迟到接收不能 +覆盖更新的选择。普通工作保留原 settlement 类型,不为 Stride v2 伪造 obligation; +replan 沿用既有 v2 契约。省略附件保留当前决策;显式 `proxy: null` 选择直接评测, +历史 profile 仍可读取。 ### 5.3 Co-discovery:投资于组合 @@ -151,7 +166,9 @@ replan 和普通 Turn adapter 组合它;Python adapter 不另写一套策略 收窄/改变 target、直接评测。Untested 尺子可以支持有界探索,同时积累真实确认。 容易测不是留在低收益方向的理由;找不到可靠尺子也不是空转的理由。 -试替代方案时保护有前景的 incumbent。切换明确改的是尺子、方向还是两者。 +保留有前景的 incumbent 可以提供同期对照或恢复点,不是通用终态交付要求。 +应服从实际验收:最高有效已见分数目标不要求最终工作区保留最佳候选,而交付物质量 +目标可能要求。切换明确改的是尺子、方向还是两者。 只替换坏尺子通常属于 `verify`,不自动意味着研究机制发生 `switch`。 ### 5.4 Fidelity 与重新验证 @@ -383,11 +400,34 @@ CLI、App、Lark 复用同一投影:当前方向、尺子或直接评测、fid M1/M2 是交付切片,不代表长程收益。复用既有 Stride/Explore owner 和下游调用方, 采用后移除原型重复实现;不为凑齐里程碑加入 registry、第二个 journal 或假想 provider 抽象。 +### M1 实现 checkpoint + +`execution_profile.yardstick_enabled` 是既有 Goal 复盘/Stride capability 的独立、 +默认关闭设置。设备和 Goal 编辑器共用 owner,Goal 显式 false 优先。新 capture 需要 +Explore evidence 可用;开启该设置不启动工作或自动开启其他 capability。 +关闭后移除普通 Turn 的新增提示,并在主提交前拒绝新 capture;历史 finding +及成功的精确重放仍可读。 + +既有 Research finding reader 展示记录的目标、成果/代理指标、来源、下一步和成本边界。 +普通 task-scoped Explore context 只投影该 Todo 当前目标、下一选择短提示和反证引用。 +既有 question-scoped 扩读命令返回完整决策、精确 profile 的反馈和前次执行归属; +默认投影不重复完整记录。 +反证要求在下一个具体选择处重新判断,不构成新的硬门、fidelity 认证、放松正确性约束的 +权限或每轮重规划指令。后续有意义的 capture 可复用 execution observation,把前次 +提议方法连到实际方法、候选和证据,也允许有据采用不同方法。 + +源码验证包括真实隔离 SQLite CLI → 普通 guard → 具体合成产物/评估 → 同 Todo +结果链、关闭时拒写/回读与恢复检查。确定性 actor 只证明通路和归属。 +打包配置旅程验证设备继承、Goal 覆盖与读取失败后的恢复;有密集内容的 Research reader +在桌面和窄屏上验证原始 HTTP/SQLite 历史读回。Lark 复用 finding summary 投影, +资格验证没有对外发送。真实模型采用、数值 fidelity profile、完整边际成本和匹配收益 +仍未测试;schema 存活和合成 followthrough 不能关闭 C13 或 M2/M3。 + ## 12. 待决事项 | 决策 | Owner / 建议 | 需要的证据 / 截止边界 | | --- | --- | --- | -| 精确附件 schema 和配置字段 | Work-items/cadence maintainer:追加 tagged work/replan binding,独立 opt-in,保留 v2。 | M1 实现前核对 producer/reader 及 C8–C12。 | +| 附件与配置接缝 | Work-items 管理 `work_yardstick_decision_v0`,cadence 管理独立 `yardstick_enabled`,Explore 管理接收与回读;保留 replan v2。 | 实现提案保留 exact-head C8–C12 及打包 journey 证据。 | | 首个 fidelity profile/证据下限 | Measurement provider maintainer:先用于数值任务的排序 profile,不全局规定六状态。 | M2 前核对噪声、选择/重复检验风险及可负担真实确认。 | | 自动触发与 context 预算 | Stride/context maintainer:typed evidence delta 和 probe 到边界,不用 prose classifier 或每轮固定表格。 | M1/M2 实测完整路径开销和漏决策反例;M3 前固定阈值。 | | 默认推广 | Product owner:有配对证据前保持关闭。 | M3,包含回归、不确定性和实际用户流程。 | diff --git a/examples/personal-workspace-browser/replan-cadence.mjs b/examples/personal-workspace-browser/replan-cadence.mjs index ee1b471553..5eda91d8ed 100644 --- a/examples/personal-workspace-browser/replan-cadence.mjs +++ b/examples/personal-workspace-browser/replan-cadence.mjs @@ -85,6 +85,8 @@ export const replanCadenceScenario = { await waitForInputValue(count, "6"); if (await strategy.inputValue() !== "evidence_conditioned_v1") throw new Error("Stride must default to evidence_conditioned_v1"); if (await unit.inputValue() !== "effective_turns") throw new Error("Product default is not settled work Turns"); + const yardstick = page.getByLabel(/^复核衡量标准/); + assert.equal(await yardstick.isChecked(), false, "Yardstick remains a separate explicit opt-in"); const horizonGoal = page.getByLabel(/^在规划视野中显示 Goal/); const advanced = page.getByText("高级设置", {exact: true}); assert.equal(await horizonGoal.isVisible(), false, "ordinary configuration does not require a Goal-context opt-in"); @@ -271,6 +273,8 @@ export const replanCadenceScenario = { await page.keyboard.press("Tab"); if (!(await strategy.evaluate((element) => element === document.activeElement))) throw new Error("Strategy is not keyboard reachable"); await page.keyboard.press("Tab"); + assert.equal(await yardstick.evaluate((element) => element === document.activeElement), true, "Measurement review is keyboard reachable"); + await page.keyboard.press("Tab"); assert.equal(await advanced.evaluate((element) => element === document.activeElement), true, "Advanced settings is keyboard reachable"); await page.keyboard.press("Enter"); await page.keyboard.press("Tab"); @@ -346,7 +350,54 @@ export const replanCadenceScenario = { await page.waitForFunction(element => !element.disabled, await englishCount.elementHandle()); assert.equal(goalApplyCount, committedApplies + 1); assert.equal((await readGoalCadence()).current.count, 4, "A fresh preview can be applied after readback recovery"); - return { coverageEntries: context.coverageEntries, note: "Packaged default six effective Turns, machine v0→v2 migration, strategy-only JSON/form inheritance across later machine changes, equal-value explicit pin and interval-only clear, default-off/versioned strategy, invalid preview, Goal override/clear, device removal, mobile keyboard and English labels through real handlers. Delayed post-apply readback keeps editing closed across capability changes; failed readback retains the disabled draft and invalidates its preview, recovers through GET only, then admits a new explicitly previewed change." }; + // A pinned interval does not suppress the independent live Yardstick + // preference. Editing that interval does not silently pin the switch. + const measurementPreference = {namespace: "todo_replan_cadence", namespace_configuration: { + schema_version: "todo_replan_cadence_machine_defaults_v2", count_unit: "effective_turns", count: 6, + stride_strategy: "evidence_conditioned_v1", yardstick_enabled: true, + }}; + const measurementPreview = await request(base + "/preview", measurementPreference); + await request(base + "/apply", {...measurementPreference, expected_plan_revision: measurementPreview.plan_revision}); + await reopenGoalCadence(); + const englishYardstick = page.getByLabel(/^Review measurement criteria/); + await waitForInputValue(englishCount, "4"); + assert.equal(await englishYardstick.isChecked(), true); + assert.equal((await readGoalCadence()).current.yardstick_enabled, undefined); + await englishCount.fill("3"); + await page.getByRole("button", {name: "Preview changes", exact: true}).click(); + await applyGoalPreview("Apply preview"); + await reopenGoalCadence(); + assert.equal((await readGoalCadence()).current.yardstick_enabled, undefined, + "saving a different option must preserve live measurement inheritance"); + assert.equal(await englishYardstick.isChecked(), true); + await englishYardstick.uncheck(); + await page.getByRole("button", {name: "Preview changes", exact: true}).click(); + await applyGoalPreview("Apply preview"); + await reopenGoalCadence(); + assert.equal(await englishYardstick.isChecked(), false); + assert.equal((await readGoalCadence()).current.yardstick_enabled, false); + assert.equal((await request(base)).machine_configuration.namespaces.todo_replan_cadence.yardstick_enabled, true); + await page.screenshot({path: resolve(outputDir, "yardstick-goal-explicit-off-desktop.png"), animations: "disabled"}); + await page.setViewportSize({width: 390, height: 844}); + await englishYardstick.scrollIntoViewIfNeeded(); + await page.screenshot({path: resolve(outputDir, "yardstick-goal-explicit-off-mobile.png"), animations: "disabled"}); + assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth + 1), false); + await page.setViewportSize({width: 1512, height: 982}); + await englishYardstick.check(); + await page.getByRole("button", {name: "Preview changes", exact: true}).click(); + await applyGoalPreview("Apply preview"); + await reopenGoalCadence(); + assert.equal((await readGoalCadence()).current.yardstick_enabled, true); + await page.getByRole("button", {name: "Edit JSON", exact: true}).click(); + await page.locator("#goal-configuration-json").fill(JSON.stringify({count_unit: "effective_turns", count: 3, stride_strategy: "off"})); + await page.getByRole("button", {name: "Back to form", exact: true}).click(); + await page.getByRole("button", {name: "Preview changes", exact: true}).click(); + await applyGoalPreview("Apply preview"); + await reopenGoalCadence(); + assert.equal((await readGoalCadence()).current.yardstick_enabled, undefined); + assert.equal(await englishYardstick.isChecked(), true, "clearing only the switch restores independent device inheritance"); + await page.screenshot({path: resolve(outputDir, "yardstick-goal-inherited-desktop.png"), animations: "disabled"}); + return { coverageEntries: context.coverageEntries, note: "Packaged default six effective Turns, machine v0→v2 migration, strategy-only JSON/form inheritance across later machine changes, equal-value explicit pin and interval-only clear, default-off/versioned strategy, invalid preview, Goal override/clear, device removal, mobile keyboard and English labels through real handlers. Independent default-off Yardstick inheritance, explicit false/true and sparse JSON/form clear reuse the same revision-checked handler. Delayed post-apply readback keeps editing closed across capability changes; failed readback retains the disabled draft and invalidates its preview, recovers through GET only, then admits a new explicitly previewed change." }; } catch (error) { if (context) { await context.page.screenshot({ path: resolve(outputDir, "replan-cadence-failure.png"), animations: "disabled" }); diff --git a/examples/personal-workspace-browser/research-finding-reader.mjs b/examples/personal-workspace-browser/research-finding-reader.mjs index 4f8654d2ec..9ab0439246 100644 --- a/examples/personal-workspace-browser/research-finding-reader.mjs +++ b/examples/personal-workspace-browser/research-finding-reader.mjs @@ -15,6 +15,11 @@ import json, sys from pathlib import Path from loopx.capabilities.explore.result_log import build_explore_node_event, build_explore_finding_event goal='synthetic-finding-reading' +profile=dict(metric='quality',unit='points',higher_is_better=True,comparison_scope='validation:fixed',input_revision='inputs-v1',evaluation_ref='evaluator:outcome-v1') +capture=dict(binding=dict(goal_id=goal,agent_id='synthetic-researcher',todo_id='todo_comparison',turn_instance_id='turn-comparison'), + decision=dict(schema_version='work_yardstick_decision_v0',target=dict(summary='在相同成本下提高可验证质量',scope='固定输入与原有正确性约束',acceptance_ref='acceptance:quality-v1'), + authoritative=profile,proxy={**profile,'metric':'screening','evaluation_ref':'evaluator:local-v1'},counterevidence_refs=['result:synthetic-19'],prior_finding_id=None, + next_action=dict(method_id='method:direct-check',reason='代理顺序与实际结果相反;下一次直接核对成果指标',cost_bound='一次独立检查,不绕过正确性要求'))) events=[build_explore_node_event(goal_id=goal,node_id='question',title='合成研究:质量、延迟和未知成本应如何一起判断?',recorded_at='2026-01-01T00:00:00Z')] for index in range(20): status='refuted' if index%7==0 else 'tentative' if index%4==0 else 'confirmed' @@ -25,7 +30,8 @@ for index in range(20): summary=f'Input revision: input:{"a"*64} Applicability: 固定合成样本;不是生产评测。 Observation: {observation} Interpretation: Outcome: {outcome} changed: - {change} stopped: - 不把局部改善当整体收益 retained: - 保留负结果与原始版本' events.append(build_explore_finding_event(goal_id=goal,node_id='question',finding_id=f'finding-{index}', title=f'比较 {index+1}:质量、延迟与采用版本的核对 · quality, latency and retained candidate', - status=status,summary=summary,evidence_refs=['result:synthetic-'+str(index)],recorded_at=f'2026-01-01T{index+1:02}:00:00Z')) + status=status,summary=summary,evidence_refs=['result:synthetic-'+str(index)],recorded_at=f'2026-01-01T{index+1:02}:00:00Z', + **(dict(agent_id='synthetic-researcher',work_yardstick=capture) if index==19 else {}))) events.append(build_explore_node_event(goal_id=goal,node_id='blocked',title='来源暂不可用的研究问题',status='blocked',blocked_reason='无法读取评测来源,当前结果未知。',recorded_at='2026-01-02T00:00:00Z')) events.append(build_explore_node_event(goal_id=goal,node_id='single',title='仅有一条结论的问题',recorded_at='2026-01-02T01:00:00Z')) events.append(build_explore_finding_event(goal_id=goal,node_id='single',finding_id='single-finding',title='原始观测',summary='一条完整的原始结论。',status='tentative',recorded_at='2026-01-02T01:00:00Z')) @@ -67,6 +73,22 @@ export const researchFindingReaderScenario = { await page.locator(".personal-channel-scroll").evaluate(element => element.scrollTop = 0); await page.screenshot({path: resolve(outputDir, "research-finding-reader-first-viewport.png"), animations: "disabled"}); + const yardstick = article.locator(".work-yardstick-readback"); + assert.equal(await yardstick.count(), 1, "The production SQLite history retains the optional decision"); + assert.equal(await yardstick.evaluate(element => element.open), false, "Detailed scope does not crowd the first viewport"); + await yardstick.locator(":scope > summary").click(); + assert.match(await yardstick.innerText(), /代理顺序与实际结果相反.*下一次直接核对成果指标/s); + assert.match(await yardstick.innerText(), /evaluator:outcome-v1.*evaluator:local-v1/s); + assert.match(await yardstick.innerText(), /result:synthetic-19.*todo_comparison/s); + for (const width of [1440, 390]) { + await page.setViewportSize({width, height: 900}); + await yardstick.scrollIntoViewIfNeeded(); + assert((await reader.evaluate(element => element.scrollWidth - element.clientWidth)) <= 1, "Scoped decision fits the reading width"); + await page.screenshot({path: resolve(outputDir, `work-yardstick-readback-${width}.png`), animations: "disabled"}); + } + await yardstick.locator(":scope > summary").click(); + await page.setViewportSize({width: 1440, height: 900}); + await picker.selectOption("finding-0"); assert.match(await article.innerText(), /已反驳/); assert.match(await article.innerText(), /观测 1:质量下降而延迟改善/); diff --git a/loopx/capabilities/configuration_ui.py b/loopx/capabilities/configuration_ui.py index c46d5c96a6..9be1689c3d 100644 --- a/loopx/capabilities/configuration_ui.py +++ b/loopx/capabilities/configuration_ui.py @@ -144,6 +144,8 @@ def capability_configuration_editor( _field("stride_strategy", "Stride strategy", "select", options=list(stride_strategy_options()), description="Off retains ordinary review. Evidence-conditioned v1 independently reviews work-slice size, route changes and validation scope; fresh Vision path reviews require a structured proposal, without automatic execution."), + _field("yardstick_enabled", "Review measurement criteria", "boolean", + description="Default off. Use available Explore evidence to review measurement scope and feedback before choosing work. Requires Explore evidence; does not enable it or authorize execution."), ], }, "periodic_report": { @@ -655,7 +657,7 @@ def _attach_effective_configuration(entry: dict[str, Any]) -> None: entry["capability_id"] == "todo_replan_cadence" and goal_override is not None and bool(goal_override) - and set(goal_override) <= {"stride_strategy", "planning_horizon_goal"} + and set(goal_override) <= {"stride_strategy", "planning_horizon_goal", "yardstick_enabled"} ): from .todo_replan_cadence.machine_defaults import ( cadence_configuration_value, @@ -679,9 +681,12 @@ def _attach_effective_configuration(entry: dict[str, Any]) -> None: # values and atomic Goal cadence overrides. Keep stored/current values # sparse: reading Settings must never pin or migrate an override. goal_override = None if goal_override is None else { - "planning_horizon_goal": DEFAULT_PLANNING_HORIZON_GOAL, **goal_override} + "planning_horizon_goal": DEFAULT_PLANNING_HORIZON_GOAL, + "yardstick_enabled": (machine_default or {}).get("yardstick_enabled", False), + **goal_override} machine_default = None if machine_default is None else { - "planning_horizon_goal": DEFAULT_PLANNING_HORIZON_GOAL, **machine_default} + "planning_horizon_goal": DEFAULT_PLANNING_HORIZON_GOAL, + "yardstick_enabled": False, **machine_default} if entry["capability_id"] == "explore_harness": from .explore.machine_defaults import resolve_explore_configuration from ..explore_graph import plan_explore_configuration diff --git a/loopx/capabilities/explore/result_log.py b/loopx/capabilities/explore/result_log.py index 09cf6ca6b8..d93c0b8427 100644 --- a/loopx/capabilities/explore/result_log.py +++ b/loopx/capabilities/explore/result_log.py @@ -352,6 +352,7 @@ def build_explore_finding_event( recorded_at: str | None = None, execution_observation: Mapping[str, Any] | None = None, measurements: Sequence[Mapping[str, Any]] | None = None, + work_yardstick: Mapping[str, Any] | None = None, _check_execution_measurements: bool = True, ) -> dict[str, Any]: safe_goal_id = _safe_goal_id(goal_id) @@ -402,6 +403,12 @@ def build_explore_finding_event( if any(item["evidence_ref"] not in event.get("evidence_refs", []) for item in measured): raise ValueError("measurement evidence must be included in the finding's evidence_refs") event["measurements"] = measured + if work_yardstick is not None: + from ...control_plane.work_items.yardstick_decision import normalize_work_yardstick_capture + event["work_yardstick"] = normalize_work_yardstick_capture( + work_yardstick, goal_id=safe_goal_id, agent_id=str(agent_id or ""), + evidence_refs=event.get("evidence_refs", []), + ) event["event_id"] = _event_id(event) return event @@ -524,6 +531,7 @@ def validate_explore_result_event( supersedes=payload.get("supersedes"), execution_observation=payload.get("execution_observation"), measurements=payload.get("measurements"), + work_yardstick=payload.get("work_yardstick"), # Canonical history predates the stricter new-write rule. Keep # original bytes/identities auditable; current views qualify the # folded result separately, and append rechecks new writes below. @@ -794,7 +802,7 @@ def explore_history_rows(events: Sequence[Mapping[str, Any]], *, goal_id: str | "schema_version", "event_id", "goal_id", "event_kind", "result_id", "recorded_at", "title", "summary", "status", "node_kind", "blocked_reason", "parent_id", "node_id", "from_node", "to_node", - "edge_type", "agent_id", "run_id", "evidence_refs", "supersedes", "confidence", "execution_observation", "measurements", + "edge_type", "agent_id", "run_id", "evidence_refs", "supersedes", "confidence", "execution_observation", "measurements", "work_yardstick", ) return [{key: event[key] for key in fields if key in event} for event in events] @@ -906,6 +914,7 @@ def _finding_view(event: Mapping[str, Any]) -> dict[str, Any]: else: measured.update(measurements=event["measurements"], recorded_order=event["_recorded_order"]) return { + **({"work_yardstick": event["work_yardstick"], "recorded_order": event["_recorded_order"]} if "work_yardstick" in event else {}), **({"execution_observation": event["execution_observation"]} if "execution_observation" in event else {}), **measured, "finding_id": str(event.get("result_id") or ""), diff --git a/loopx/capabilities/explore/result_writeback.py b/loopx/capabilities/explore/result_writeback.py index f2beabd9ab..d0dc5f1859 100644 --- a/loopx/capabilities/explore/result_writeback.py +++ b/loopx/capabilities/explore/result_writeback.py @@ -99,6 +99,7 @@ def _evolution_plan(attachment, projection, *, agent_id): def _events(attachment, *, goal_id, agent_id, source_id, recorded_at, existing_node=None, + todo_id=None, turn_instance_id=None, _check_execution_measurements=True): previous = existing_node or {} update = attachment.get("question_update") or {} @@ -136,6 +137,10 @@ def _events(attachment, *, goal_id, agent_id, source_id, recorded_at, existing_n tags=["writeback-result"], execution_observation=attachment.get("execution_observation"), measurements=attachment.get("measurements"), + work_yardstick=({"decision": attachment["work_yardstick_decision"], "binding": { + "goal_id": goal_id, "agent_id": agent_id, "todo_id": todo_id, + "turn_instance_id": turn_instance_id, + }} if "work_yardstick_decision" in attachment else None), _check_execution_measurements=_check_execution_measurements, recorded_at=recorded_at, ) @@ -193,6 +198,7 @@ def prepare_result_attachment( agent_id, todo_id, turn_instance_id, + _committed_replay=False, ): """Validate before primary commit; this path never appends graph evidence.""" result = normalize_result_attachment(attachment) @@ -200,7 +206,7 @@ def prepare_result_attachment( raise ValueError( "Explore result attachment requires agent, Todo and Turn identity" ) - _, graph, gate = _policy(registry_path, goal_id) + goal, graph, gate = _policy(registry_path, goal_id) if not (graph or gate["enabled"]): raise ValueError( "Explore result attachment requires enabled Explore evidence or planning mode" @@ -221,11 +227,23 @@ def prepare_result_attachment( agent_id=agent_id, source_id="validation", recorded_at="2000-01-01T00:00:00Z", + todo_id=todo_id, turn_instance_id=turn_instance_id, ) current_events = load_explore_result_events_strict( explore_result_log_path(runtime_root, goal_id), goal_id=goal_id ) - existing = build_explore_result_projection(current_events, goal_id=goal_id) + existing = build_explore_result_projection(current_events, goal_id=goal_id, + **({"finding_limit": len(current_events)} if "work_yardstick_decision" in result else {})) + if "work_yardstick_decision" in result: + from ..todo_replan_cadence.machine_defaults import work_yardstick_enabled + effect_runtime_result("work_item.yardstick.continuation", { + # Only replay of the original committed attachment may finish after + # disable; it cannot grant a new capture or change its contents. + "enabled": _committed_replay or work_yardstick_enabled(goal, runtime_root), + "decision": result["work_yardstick_decision"], "evidence_refs": result["evidence_refs"], + "binding": {"goal_id": goal_id, "agent_id": agent_id, "todo_id": todo_id}, + "findings": existing["findings"], + }) if _evolves_question(result): _evolution_plan(result, existing, agent_id=agent_id) for node in existing.get("nodes", []): @@ -324,6 +342,7 @@ def deliver_result_attachment( agent_id=agent_id, source_id=intent["idempotency_key"], recorded_at=committed_at, + todo_id=todo_id, turn_instance_id=turn_instance_id, # A durable terminal intent can describe an old writer's event. # Structural reconstruction is only for exact canonical readback; # a missing event still goes through strict preparation below. @@ -366,6 +385,7 @@ def deliver_result_attachment( agent_id=agent_id, todo_id=todo_id, turn_instance_id=turn_instance_id, + _committed_replay=payload.get("idempotent_replay") is True, ) current_events = load_explore_result_events_strict( explore_result_log_path(runtime_root, goal_id), goal_id=goal_id @@ -379,12 +399,14 @@ def deliver_result_attachment( attachment, goal_id=goal_id, agent_id=agent_id, source_id=intent["idempotency_key"], recorded_at=committed_at, existing_node=plan.get("existing_node"), + todo_id=todo_id, turn_instance_id=turn_instance_id, ) result["graph"] = append_explore_result_events( explore_result_log_path(runtime_root, goal_id), events, expected_goal_id=goal_id, create_only_node_ids=[] if evolves else [attachment["node_id"]], - expected_event_ids=([row["event_id"] for row in current_events] if evolves else None), + expected_event_ids=([row["event_id"] for row in current_events] + if evolves or "work_yardstick_decision" in attachment else None), ) linked = update_goal_todo( **_link_arguments( diff --git a/loopx/capabilities/explore/turn_context.py b/loopx/capabilities/explore/turn_context.py index fd2485e9a1..46af6028e3 100644 --- a/loopx/capabilities/explore/turn_context.py +++ b/loopx/capabilities/explore/turn_context.py @@ -79,6 +79,8 @@ def explore_turn_context( require_registered_agent_id( registry_path=registry_path, goal_id=goal_id, agent_id=agent_id ) + from ..todo_replan_cadence.machine_defaults import work_yardstick_enabled + yardstick = work_yardstick_enabled(goal, runtime_root) projection, plan, focus, todos = {}, {}, {}, {} page_tasks = {row["todo_id"] for row in review_pages if isinstance(row, Mapping) and isinstance(row.get("todo_id"), str)} if isinstance(review_pages, list) else set() @@ -92,7 +94,7 @@ def explore_turn_context( events, goal_id=goal_id, finding_limit=len(events), mermaid_node_limit=3, ) - if gate["enabled"] or graph and (todo_id is not None or page_tasks): + if gate["enabled"] or graph and (todo_id is not None or page_tasks or yardstick): todos = list_goal_todos( registry_path=registry_path, goal_id=goal_id, @@ -137,6 +139,9 @@ def explore_turn_context( "harness_gate": gate, "projection": projection, "plan": plan, + **({"yardstick_enabled": True, "work_todos": [{key: row[key] for key in ( + "todo_id", "role", "status", "done", "claimed_by", "explore_result_node_refs") if key in row} + for row in todos.get("todos", [])]} if yardstick else {}), **focus, **({"review_todos": [scopes[key] for key in sorted(page_tasks) if key in scopes]} if page_tasks else {}), "route": route, diff --git a/loopx/capabilities/todo_replan_cadence/goal_configuration.py b/loopx/capabilities/todo_replan_cadence/goal_configuration.py index 2290e46489..80de6d1107 100644 --- a/loopx/capabilities/todo_replan_cadence/goal_configuration.py +++ b/loopx/capabilities/todo_replan_cadence/goal_configuration.py @@ -12,7 +12,7 @@ def configuration_summary(goal: Mapping[str, Any]) -> dict[str, Any] | None: profile = goal.get("execution_profile") if not isinstance(profile, Mapping): return None - strategy = {key: profile[key] for key in ("stride_strategy", "planning_horizon_goal") if key in profile} + strategy = {key: profile[key] for key in ("stride_strategy", "planning_horizon_goal", "yardstick_enabled") if key in profile} if "replan_after_effective_turns" in profile: return { "count_unit": "effective_turns", diff --git a/loopx/capabilities/todo_replan_cadence/machine_defaults.py b/loopx/capabilities/todo_replan_cadence/machine_defaults.py index 3c022f1a8f..68faa90efb 100644 --- a/loopx/capabilities/todo_replan_cadence/machine_defaults.py +++ b/loopx/capabilities/todo_replan_cadence/machine_defaults.py @@ -37,7 +37,7 @@ def normalize_replan_cadence_configuration(raw: Mapping[str, Any]) -> dict[str, "count_unit": ReplanCadenceUnit.COMPLETED_TODOS.value, "count": normalize_completed_todo_replan_threshold(raw["completed_todos"]), } - if not {"count_unit", "count"} <= set(raw) or set(raw) - {"count_unit", "count", "stride_strategy", "planning_horizon_goal"}: + if not {"count_unit", "count"} <= set(raw) or set(raw) - {"count_unit", "count", "stride_strategy", "planning_horizon_goal", "yardstick_enabled"}: raise ValueError("review cadence requires count_unit and count") try: unit = ReplanCadenceUnit(raw["count_unit"]) @@ -58,6 +58,10 @@ def normalize_replan_cadence_configuration(raw: Mapping[str, Any]) -> dict[str, if not isinstance(raw["planning_horizon_goal"], bool): raise ValueError("planning_horizon_goal must be a boolean") result["planning_horizon_goal"] = raw["planning_horizon_goal"] + if "yardstick_enabled" in raw: + if not isinstance(raw["yardstick_enabled"], bool): + raise ValueError("yardstick_enabled must be a boolean") + result["yardstick_enabled"] = raw["yardstick_enabled"] return result @@ -81,6 +85,8 @@ def normalize_todo_replan_cadence_machine_defaults( expected.add("stride_strategy") if "planning_horizon_goal" in raw: expected.add("planning_horizon_goal") + if "yardstick_enabled" in raw: + expected.add("yardstick_enabled") if set(raw) != expected: raise ValueError("review configuration fields do not match its schema version") return { @@ -105,6 +111,7 @@ def default_replan_cadence_configuration() -> dict[str, Any]: "count": DEFAULT_EFFECTIVE_TURN_REPLAN_THRESHOLD, "planning_horizon_goal": DEFAULT_PLANNING_HORIZON_GOAL, "stride_strategy": "evidence_conditioned_v1", + "yardstick_enabled": False, } @@ -168,15 +175,18 @@ def apply_todo_replan_cadence_machine_default( projected = deepcopy(dict(goal)) raw_profile = goal.get("execution_profile") - if isinstance(raw_profile, Mapping) and ( - "replan_after_completed_todos" in raw_profile - or "replan_after_effective_turns" in raw_profile - ): - return projected cadence = _machine_default(machine_configuration) if cadence is None: cadence = default_replan_cadence_configuration() profile = dict(raw_profile) if isinstance(raw_profile, Mapping) else {} + # Yardstick is independent of the legacy cadence/stride override. Keep + # absence sparse and default-off; explicit false always wins inheritance. + if "yardstick_enabled" not in profile and cadence.get("yardstick_enabled") is True: + profile["yardstick_enabled"] = True + if "replan_after_completed_todos" in profile or "replan_after_effective_turns" in profile: + if profile != raw_profile: + projected["execution_profile"] = profile + return projected field = ( "replan_after_effective_turns" if cadence["count_unit"] == ReplanCadenceUnit.EFFECTIVE_TURNS.value @@ -197,10 +207,12 @@ def resolve_todo_replan_cadence_goal( """Resolve live cadence for raw-registry writeback callers, without writes. History already composes machine defaults before calling the history codec. - Explicit Goal cadence remains independent of device configuration IO. + Explicit cadence and Yardstick overrides remain independent. A cadence-only + Goal still reads the live device Yardstick preference; explicit choices + for both can skip device configuration IO. """ profile = goal.get("execution_profile") - if isinstance(profile, Mapping) and ( + if isinstance(profile, Mapping) and "yardstick_enabled" in profile and ( "replan_after_effective_turns" in profile or "replan_after_completed_todos" in profile ): @@ -215,3 +227,9 @@ def resolve_todo_replan_cadence_goal( ) if runtime_root is not None else None ) return apply_todo_replan_cadence_machine_default(goal, machine) + + +def work_yardstick_enabled(goal: dict[str, Any], runtime_root) -> bool: + """Independent opt-in resolved through the existing review configuration.""" + resolved = resolve_todo_replan_cadence_goal(goal, runtime_root) + return (resolved.get("execution_profile") or {}).get("yardstick_enabled") is True diff --git a/loopx/chat/goal_configuration_api.py b/loopx/chat/goal_configuration_api.py index 6e892004a5..64dcfb383c 100644 --- a/loopx/chat/goal_configuration_api.py +++ b/loopx/chat/goal_configuration_api.py @@ -201,7 +201,7 @@ def _goal_capability_options( if capability_id == "periodic_report": return {"clear_periodic_report_configuration": True} if capability_id == "todo_replan_cadence": - return {"clear_execution_replan_after_todos": True, "clear_execution_replan_after_turns": True, "clear_execution_stride_strategy": True, "clear_execution_planning_horizon_goal": True} + return {"clear_execution_replan_after_todos": True, "clear_execution_replan_after_turns": True, "clear_execution_stride_strategy": True, "clear_execution_planning_horizon_goal": True, "clear_execution_yardstick_enabled": True} if capability_id == "pull_request_review": return {"clear_pull_request_review_configuration": True} if capability_id == "change_quality_qualification": @@ -214,7 +214,7 @@ def _goal_capability_options( config = dict(configuration) allowed: dict[str, set[str]] = { "goal_capability_organization": {"mode", "discovery_budget_minutes", "max_trials"}, - "todo_replan_cadence": {"completed_todos", "count_unit", "count", "stride_strategy", "planning_horizon_goal"}, + "todo_replan_cadence": {"completed_todos", "count_unit", "count", "stride_strategy", "planning_horizon_goal", "yardstick_enabled"}, "multi_subagent": { "enabled", "max_children", @@ -255,12 +255,17 @@ def _goal_capability_options( if not isinstance(config["planning_horizon_goal"], bool): raise ValueError("planning_horizon_goal must be a boolean") horizon = {"execution_planning_horizon_goal": config["planning_horizon_goal"]} - if config and set(config) <= {"stride_strategy", "planning_horizon_goal"}: + yardstick = {"clear_execution_yardstick_enabled": True} + if "yardstick_enabled" in config: + if not isinstance(config["yardstick_enabled"], bool): + raise ValueError("yardstick_enabled must be a boolean") + yardstick = {"execution_yardstick_enabled": config["yardstick_enabled"]} + if config and set(config) <= {"stride_strategy", "planning_horizon_goal", "yardstick_enabled"}: # The capability editor submits the complete sparse value. Unlike # the CLI's single-field strategy patch, omission restores live # interval inheritance even when this Goal was previously pinned. return { - **horizon, + **horizon, **yardstick, **({"execution_stride_strategy": normalize_stride_strategy(config["stride_strategy"])} if "stride_strategy" in config else {}), "clear_execution_replan_after_turns": True, @@ -269,7 +274,7 @@ def _goal_capability_options( cadence = normalize_replan_cadence_configuration(config) option = ("execution_replan_after_turns" if cadence["count_unit"] == "effective_turns" else "execution_replan_after_todos") - return {option: cadence["count"], **horizon, **({"execution_stride_strategy": cadence["stride_strategy"]} + return {option: cadence["count"], **horizon, **yardstick, **({"execution_stride_strategy": cadence["stride_strategy"]} if "stride_strategy" in cadence else {})} if capability_id == "periodic_report": from ..capabilities.periodic_report.goal_configuration import normalize_configuration diff --git a/loopx/cli_commands/registry_admin.py b/loopx/cli_commands/registry_admin.py index f5f85031c1..070ddafc36 100644 --- a/loopx/cli_commands/registry_admin.py +++ b/loopx/cli_commands/registry_admin.py @@ -454,6 +454,8 @@ def handle_registry_admin_command( execution_replan_after_turns=args.execution_replan_after_turns, execution_planning_horizon_goal=args.execution_planning_horizon_goal, clear_execution_planning_horizon_goal=args.clear_execution_planning_horizon_goal, + execution_yardstick_enabled=args.execution_yardstick_enabled, + clear_execution_yardstick_enabled=args.clear_execution_yardstick_enabled, execution_stride_strategy=args.execution_stride_strategy, clear_execution_stride_strategy=args.clear_execution_stride_strategy, clear_execution_replan_after_turns=args.clear_execution_replan_after_turns, diff --git a/loopx/cli_commands/registry_admin_configure.py b/loopx/cli_commands/registry_admin_configure.py index eee24a94ed..39c958fb60 100644 --- a/loopx/cli_commands/registry_admin_configure.py +++ b/loopx/cli_commands/registry_admin_configure.py @@ -83,6 +83,14 @@ def register_configure_goal_command(subparsers: argparse._SubParsersAction) -> N "--clear-execution-planning-horizon-goal", action="store_true", help="Remove the Goal horizon override and restore review-configuration inheritance.", ) + configure_goal_parser.add_argument( + "--execution-yardstick-enabled", action=BooleanOptionalAction, default=None, + help="Enable Yardstick measurement review for this Goal (default off). Requires available Explore evidence; does not enable Explore or authorize execution.", + ) + configure_goal_parser.add_argument( + "--clear-execution-yardstick-enabled", action="store_true", + help="Remove the Goal Yardstick choice and restore the live device preference.", + ) from ..control_plane.work_items.stride_policy_codec import stride_strategy_options configure_goal_parser.add_argument( "--execution-stride-strategy", choices=stride_strategy_options(), diff --git a/loopx/configure_goal.py b/loopx/configure_goal.py index 1385d36907..4ac5f6c14b 100644 --- a/loopx/configure_goal.py +++ b/loopx/configure_goal.py @@ -442,6 +442,8 @@ def configure_goal( clear_execution_stride_strategy: bool = False, execution_planning_horizon_goal: bool | None = None, clear_execution_planning_horizon_goal: bool = False, + execution_yardstick_enabled: bool | None = None, + clear_execution_yardstick_enabled: bool = False, clear_execution_replan_after_turns: bool = False, clear_execution_replan_after_todos: bool = False, self_repair_enabled: bool | None = None, @@ -838,6 +840,8 @@ def configure_goal( stride_strategy=execution_stride_strategy, clear_stride_strategy=clear_execution_stride_strategy, planning_horizon_goal=execution_planning_horizon_goal, clear_planning_horizon_goal=clear_execution_planning_horizon_goal, + yardstick_enabled=execution_yardstick_enabled, + clear_yardstick_enabled=clear_execution_yardstick_enabled, clear_replan_after_effective_turns=clear_execution_replan_after_turns, clear_replan_after_completed_todos=clear_execution_replan_after_todos, ) diff --git a/loopx/control_plane/capabilities/explore_result_writeback.ts b/loopx/control_plane/capabilities/explore_result_writeback.ts index 5592206590..c90ea88bc5 100644 --- a/loopx/control_plane/capabilities/explore_result_writeback.ts +++ b/loopx/control_plane/capabilities/explore_result_writeback.ts @@ -7,6 +7,7 @@ import {contextSourceIdentity} from "../quota/diagnostic_projection.ts"; import {normalizeGoalPathDelta} from "../goals/vision_checkpoint.ts"; import {normalizeExploreExecutionObservation} from "./explore_execution_observation.ts"; import {isExploreEvidenceRef} from "./explore_evidence_ref.ts"; +import {normalizeWorkYardstickDecision} from "../work_items/yardstick_decision.ts"; import {normalizeExploreMeasurements} from "./explore_measurements.ts"; export const EXPLORE_RESULT_ATTACHMENT_SCHEMA = "explore_result_attachment_v0"; @@ -107,7 +108,7 @@ export function exploreResultWritebackAffordance(): JsonObject { } const FIELDS = ["schema_version", "node_id", "question", "applicability", "input_revision", - "observation", "interpretation", "status", "evidence_refs", "question_update", "relations", "execution_observation", "measurements", "measurement_context"]; + "observation", "interpretation", "status", "evidence_refs", "question_update", "relations", "execution_observation", "measurements", "measurement_context", "work_yardstick_decision"]; // Reuse the persisted Explore node/edge vocabulary, distinct from finding status. const QUESTION_STATUSES = ["open", "exploring", "blocked", "resolved", "dead_end"] as const; const RELATION_TYPES = ["subtopic_of", "depends_on", "answers", "supports", "refutes", "leads_to"] as const; @@ -216,11 +217,12 @@ function normalizeAttachment(value: unknown, visionPacket?: unknown, linkedScope status: requireStringLiteral(row.status, ["tentative", "confirmed", "refuted"], "finding status"), evidence_refs: [...new Set(refs)], ...evolutionFields(row, node), ...(execution ? {execution_observation: execution} : {}), - ...(measurements ? {measurements} : {})}; + ...(measurements ? {measurements} : {}), + ...(row.work_yardstick_decision !== undefined ? {work_yardstick_decision: normalizeWorkYardstickDecision(row.work_yardstick_decision, refs)} : {})}; } /** Explicit reference, not inferred evidence. Preserve every route item verbatim. */ function attachmentFromPathDelta(row: JsonObject, visionPacket: unknown, linkedScope: unknown): JsonObject { - const fields = ["schema_version", "node_id", "question", "applicability", "input_revision", "status", "evidence_refs", "question_update", "relations", "execution_observation", "measurements", "measurement_context"]; + const fields = ["schema_version", "node_id", "question", "applicability", "input_revision", "status", "evidence_refs", "question_update", "relations", "execution_observation", "measurements", "measurement_context", "work_yardstick_decision"]; if (Object.keys(row).some(key => !fields.includes(key))) { throw new EffectRuntimeRequestError("Explore path_delta attachment contains unknown fields"); } diff --git a/loopx/control_plane/capabilities/explore_turn_context.ts b/loopx/control_plane/capabilities/explore_turn_context.ts index 5b77bc3a5f..b6a4fa911e 100644 --- a/loopx/control_plane/capabilities/explore_turn_context.ts +++ b/loopx/control_plane/capabilities/explore_turn_context.ts @@ -9,6 +9,7 @@ import {PLANNING_TODO_ID} from "../work_items/planning_relations.ts"; import {EXPLORE_WRITEBACK_SUMMARY_LIMIT, exploreBeforeWorkGuidance, exploreResultWritebackAffordance} from "./explore_result_writeback.ts"; import {EXPLORE_RESULT_PAGE_MAX, normalizeExploreResultPages} from "./explore_result_page.ts"; +import {projectWorkYardstickCue, WORK_YARDSTICK_DECISION_SCHEMA} from "../work_items/yardstick_decision.ts"; import {projectExploreMeasurementFeedback} from "./explore_measurements.ts"; /** Reusable evidence and instructions in this owner's read model. Current @@ -17,7 +18,7 @@ import {projectExploreMeasurementFeedback} from "./explore_measurements.ts"; * provenance and applicability; no timestamp or revision is normalized away. */ export function exploreReusableContextPaths(content: JsonObject = {}): (string | number)[][] { return [ - ...["recent_nodes", "recent_findings", "writeback_results", "measurement_feedback", + ...["recent_nodes", "recent_findings", "writeback_results", "measurement_feedback", "work_yardsticks", "work_yardstick_capture", "path_delta_attachment_template", "linked_question_attachment_template", "optional_question_evolution_example", "result_writeback_guidance", "result_attachment_template", "optional_measurement_example", "measurement_guidance", @@ -336,12 +337,35 @@ export function projectExploreTurnContext(params: JsonObject): JsonObject { ...(resultNode == null ? [] : ["--result-node", resultNode]), ) : null; const writeback = exploreResultWritebackAffordance(); + const workCues = params.yardstick_enabled === true ? rows(params.work_todos) + .filter(todo => todo.role === "agent" && !todo.done && todo.claimed_by === agent + && ["open", "pending", "in_progress", "blocked"].includes(String(todo.status)) + && (todoId === null || todo.todo_id === todoId)) + .map(todo => projectWorkYardstickCue({enabled: true, goal_id: goal, agent_id: agent, todo_id: todo.todo_id, + findings: findings.filter(row => (todo.explore_result_node_refs as unknown[] ?? []).includes(row.node_id)), + measurement_feedback: measurementFeedback})) + .filter((cue): cue is JsonObject => cue !== null && (resultNode === null || cue.node_id === resultNode)) : []; return { ok: true, goal_id: goal, agent_id: agent, graph_enabled: graph, harness_enabled: harness, ...(todoId === null ? {} : {todo_id: todoId}), ...(reviewPages ? {review_pages: reviewPages} : {}), graph: graph ? { counts: projection.counts ?? {}, + ...(params.yardstick_enabled === true ? { + work_yardstick_capture: {optional: true, field: "work_yardstick_decision", schema_version: WORK_YARDSTICK_DECISION_SCHEMA, + guidance: "At a meaningful target, yardstick or next-method change, optionally attach the current decision to the existing Explore result. No change needs no attachment. Cite prior_finding_id from this Todo's current decision (null initially). Distinguish the accepted outcome's evaluation profile from an unqualified proxy; record counterevidence and bounded next work without changing user constraints. Read the ordinary task-scoped context for omitted decisions.", + template: {schema_version: WORK_YARDSTICK_DECISION_SCHEMA, + target: {summary: "", scope: "", acceptance_ref: ""}, + authoritative: {metric: "", unit: "", higher_is_better: true, comparison_scope: "", input_revision: "", evaluation_ref: ""}, + proxy: null, counterevidence_refs: [], next_action: {method_id: "", reason: "", cost_bound: ""}, prior_finding_id: null}}, + ...(workCues.length ? {work_yardsticks: workCues.slice(0, 3).map(cue => resultNode !== null ? cue : { + source_finding_id: cue.source_finding_id, node_id: cue.node_id, + todo_id: (cue.binding as JsonObject).todo_id, target: (cue.target as JsonObject).summary, + cue: (cue.next_action as JsonObject).reason, counterevidence_refs: cue.counterevidence_refs, + details_command: resultRead("--result-node", String(cue.node_id)), + }), omitted_work_yardsticks: Math.max(0, workCues.length - 3), + work_yardstick_command: command("turn-context", "--agent-id", agent, "--todo-id", "")} : {}), + } : {}), recent_nodes: nodes.slice(0, 3).map(row => compact(row, ["node_id", "title", "status", "blocked_reason"])), // The canonical evidence projection calls the finding's title `finding`. recent_findings: findings.slice(0, 3).map(row => ({...compact({...row, title: row.finding}, ["finding_id", "node_id", "title", "status", "measurement_error"]), diff --git a/loopx/control_plane/effect_runtime_handlers.ts b/loopx/control_plane/effect_runtime_handlers.ts index f9d7223893..b3765822f4 100644 --- a/loopx/control_plane/effect_runtime_handlers.ts +++ b/loopx/control_plane/effect_runtime_handlers.ts @@ -795,6 +795,8 @@ export function createEffectRuntimeHandlers( ["work_item.replan_semantics.project", lazyHandler(() => import("./work_items/replan_semantics.ts"), ({projectReplanWriteback}) => projectReplanWriteback)], ["work_item.replan_context.project", lazyHandler(() => import("./work_items/replan_context.ts"), ({projectReplanContext}) => projectReplanContext)], ["work_item.replan_context.project_snapshot", lazyHandler(() => import("./work_items/replan_context.ts"), ({projectReplanContextSnapshot}) => projectReplanContextSnapshot)], + ["work_item.yardstick.capture", lazyHandler(() => import("./work_items/yardstick_decision.ts"), ({normalizeWorkYardstickCapture}) => normalizeWorkYardstickCapture)], + ["work_item.yardstick.continuation", lazyHandler(() => import("./work_items/yardstick_decision.ts"), ({validateWorkYardstickContinuation}) => validateWorkYardstickContinuation)], ["explore.configuration.resolve", lazyHandler(() => import("./capabilities/explore_configuration.ts"), ({resolveExploreConfiguration}) => resolveExploreConfiguration)], ["explore.configuration.goal", lazyHandler(() => import("./capabilities/explore_configuration.ts"), ({resolveExploreGoalConfiguration}) => resolveExploreGoalConfiguration)], ["explore.configuration.plan", lazyHandler(() => import("./capabilities/explore_configuration.ts"), ({planExploreConfiguration}) => planExploreConfiguration)], diff --git a/loopx/control_plane/presentation/explore_history.ts b/loopx/control_plane/presentation/explore_history.ts index 622cd1f0bb..954b6c7dd0 100644 --- a/loopx/control_plane/presentation/explore_history.ts +++ b/loopx/control_plane/presentation/explore_history.ts @@ -4,6 +4,7 @@ */ import {normalizeExploreExecutionObservation} from "../capabilities/explore_execution_observation.ts"; import {normalizeExploreMeasurements, projectExploreMeasurementFeedback, type ExploreMeasurement} from "../capabilities/explore_measurements.ts"; +import {normalizeWorkYardstickCapture, type WorkYardstickCapture} from "../work_items/yardstick_decision.ts"; import type {JsonObject} from "../runtime_decode.ts"; export type ExploreHistoryEvent = { schema_version: string; event_id: string; goal_id: string; event_kind: string; @@ -13,6 +14,7 @@ export type ExploreHistoryEvent = { agent_id: string; run_id: string; evidence_refs: string[]; supersedes: string; confidence: number | null; execution_observation?: JsonObject; measurements?: ExploreMeasurement[]; + work_yardstick?: WorkYardstickCapture; }; export type ExploreHistoryPage = {goal_id: string; items: ExploreHistoryEvent[]; total: number; next_cursor: string | null}; export type ExploreHistoryNode = {id: string; latest: ExploreHistoryEvent; updates: ExploreHistoryEvent[]; findings: ExploreHistoryEvent[]; revisions: ExploreHistoryEvent[]}; @@ -66,6 +68,9 @@ export function parseExploreHistoryPage(input: unknown, goalId: string): Explore ...(raw.execution_observation === undefined ? {} : { execution_observation: normalizeExploreExecutionObservation({observation: raw.execution_observation}), }), + ...(raw.work_yardstick === undefined ? {} : {work_yardstick: normalizeWorkYardstickCapture({ + capture: raw.work_yardstick, goal_id: goalId, agent_id: raw.agent_id ?? "", evidence_refs: raw.evidence_refs ?? [], + })}), ...(raw.measurements === undefined ? {} : {measurements: normalizeExploreMeasurements(raw.measurements)}), } as ExploreHistoryEvent; }); diff --git a/loopx/control_plane/work_items/yardstick_decision.py b/loopx/control_plane/work_items/yardstick_decision.py new file mode 100644 index 0000000000..d9c649203e --- /dev/null +++ b/loopx/control_plane/work_items/yardstick_decision.py @@ -0,0 +1,23 @@ +"""Transport for the typed current-work Yardstick owner; no policy or store.""" +from __future__ import annotations + +from functools import lru_cache +import json +from typing import Any + +from ..effect_runtime import EffectRuntimeRejected, effect_runtime_result +from ..runtime.public_safety import validate_public_safe_value + + +@lru_cache(maxsize=256) +def _capture(encoded: str) -> str: + try: + return json.dumps(effect_runtime_result("work_item.yardstick.capture", json.loads(encoded)), sort_keys=True) + except EffectRuntimeRejected as error: + raise ValueError(str(error)) from error + + +def normalize_work_yardstick_capture(capture: Any, *, goal_id: str, agent_id: str, evidence_refs: Any) -> dict[str, Any]: + params = dict(capture=capture, goal_id=goal_id, agent_id=agent_id, evidence_refs=evidence_refs) + validate_public_safe_value(params, path="work_yardstick") + return json.loads(_capture(json.dumps(params, sort_keys=True, allow_nan=False))) diff --git a/loopx/control_plane/work_items/yardstick_decision.ts b/loopx/control_plane/work_items/yardstick_decision.ts new file mode 100644 index 0000000000..796a820131 --- /dev/null +++ b/loopx/control_plane/work_items/yardstick_decision.ts @@ -0,0 +1,131 @@ +/** Optional current-work decision carried by Explore's existing result writeback. + * The declaration records a research method, not acceptance or execution authority. + * Stride's replan-only v2 contract remains unchanged. */ +import {parseTodoTimestampMicros} from "../runtime_timestamp.ts"; +import {EffectRuntimeRequestError} from "../effect_runtime_errors.ts"; +import {requireJsonObject, type JsonObject} from "../runtime_decode.ts"; +import {isExploreEvidenceRef} from "../capabilities/explore_evidence_ref.ts"; + +export const WORK_YARDSTICK_DECISION_SCHEMA = "work_yardstick_decision_v0"; +export type WorkYardstickProfile = {metric: string; unit: string; higher_is_better: boolean; + comparison_scope: string; input_revision: string; evaluation_ref: string}; +export type WorkYardstickDecision = {schema_version: typeof WORK_YARDSTICK_DECISION_SCHEMA; + target: {summary: string; scope: string; acceptance_ref: string}; + authoritative: WorkYardstickProfile; proxy: WorkYardstickProfile | null; + counterevidence_refs: string[]; prior_finding_id: string | null; + next_action: {method_id: string; reason: string; cost_bound: string}}; +export type WorkYardstickCapture = {decision: WorkYardstickDecision; + binding: {goal_id: string; agent_id: string; todo_id: string; turn_instance_id: string}}; +function object(value: unknown, fields: string[], label: string): JsonObject { + const row = requireJsonObject(value, label); + if (Object.keys(row).some(key => !fields.includes(key)) || fields.some(key => !Object.hasOwn(row, key))) { + throw new EffectRuntimeRequestError(`${label} fields are missing or unsupported`); + } + return row; +} +function text(value: unknown, label: string, limit = 240): string { + if (typeof value !== "string" || !value.trim() || value.length > limit || /[\u0000-\u001f]/.test(value)) { + throw new EffectRuntimeRequestError(`${label} requires bounded single-line text`); + } + return value.trim(); +} +function ref(value: unknown, label: string): string { + const result = text(value, label, 128); + if (!isExploreEvidenceRef(result) || result.startsWith("file:") || result.split("/").includes("..")) { + throw new EffectRuntimeRequestError(`${label} requires an opaque reference`); + } + return result; +} +function refs(value: unknown, label: string): string[] { + if (!Array.isArray(value) || value.length > 8) throw new EffectRuntimeRequestError(`${label} requires at most eight references`); + return [...new Set(value.map(item => ref(item, label)))].sort(); +} +/** The scope keys deliberately reuse Explore measurement comparison identity. */ +function profile(value: unknown): WorkYardstickProfile { + const row = object(value, ["metric", "unit", "higher_is_better", "comparison_scope", "input_revision", "evaluation_ref"], "yardstick profile"); + if (typeof row.higher_is_better !== "boolean") throw new EffectRuntimeRequestError("yardstick requires explicit metric direction"); + return {metric: text(row.metric, "metric", 80), unit: text(row.unit, "unit", 40), + higher_is_better: row.higher_is_better, comparison_scope: ref(row.comparison_scope, "comparison_scope"), + input_revision: text(row.input_revision, "input_revision", 160), evaluation_ref: ref(row.evaluation_ref, "evaluation_ref")}; +} +export function normalizeWorkYardstickDecision(value: unknown, evidenceRefs?: unknown): WorkYardstickDecision { + const row = object(value, ["schema_version", "target", "authoritative", "proxy", "counterevidence_refs", "next_action", "prior_finding_id"], "work yardstick decision"); + if (row.schema_version !== WORK_YARDSTICK_DECISION_SCHEMA) throw new EffectRuntimeRequestError("unsupported work yardstick decision schema"); + const target = object(row.target, ["summary", "scope", "acceptance_ref"], "research target"); + const next = object(row.next_action, ["method_id", "reason", "cost_bound"], "next research action"); + const counter = refs(row.counterevidence_refs, "counterevidence_refs"); + if (evidenceRefs !== undefined && (!Array.isArray(evidenceRefs) || counter.some(item => !evidenceRefs.includes(item)))) { + throw new EffectRuntimeRequestError("counterevidence must reference evidence captured by this finding"); + } + const authoritative = profile(row.authoritative), proxy = row.proxy === null ? null : profile(row.proxy); + if (proxy && JSON.stringify(proxy) === JSON.stringify(authoritative)) throw new EffectRuntimeRequestError("proxy and authoritative profiles must be distinct"); + if (!proxy && counter.length) throw new EffectRuntimeRequestError("proxy counterevidence requires the affected proxy profile"); + return {schema_version: WORK_YARDSTICK_DECISION_SCHEMA, + target: {summary: text(target.summary, "target summary"), scope: text(target.scope, "target scope"), acceptance_ref: ref(target.acceptance_ref, "acceptance_ref")}, + authoritative, proxy, counterevidence_refs: counter, + next_action: {method_id: ref(next.method_id, "method_id"), reason: text(next.reason, "next action reason", 320), cost_bound: text(next.cost_bound, "cost_bound", 160)}, + prior_finding_id: row.prior_finding_id === null ? null : ref(row.prior_finding_id, "prior_finding_id")}; +} +/** Binding is supplied by the committed settlement, never by an attachment. */ +export function normalizeWorkYardstickCapture(params: JsonObject): WorkYardstickCapture { + const capture = object(params.capture, ["decision", "binding"], "yardstick capture"); + const binding = object(capture.binding, ["goal_id", "agent_id", "todo_id", "turn_instance_id"], "work binding"); + const normalized = Object.fromEntries(Object.entries(binding).map(([key, value]) => [key, ref(value, key)])); + if (!String(normalized.todo_id).startsWith("todo_")) throw new EffectRuntimeRequestError("yardstick capture requires a work Todo"); + for (const key of ["goal_id", "agent_id"]) { + if (params[key] !== undefined && normalized[key] !== params[key]) throw new EffectRuntimeRequestError("yardstick capture cannot cross Goal or Agent identity"); + } + return {decision: normalizeWorkYardstickDecision(capture.decision, params.evidence_refs), binding: normalized as WorkYardstickCapture["binding"]}; +} +function currentFinding(findings: JsonObject[], binding: JsonObject): JsonObject | undefined { + return findings.filter(row => { + const capture = row.work_yardstick as JsonObject | undefined; + const source = capture?.binding as JsonObject | undefined; + return source && ["goal_id", "agent_id", "todo_id"].every(key => source[key] === binding[key]); + }).sort((a, b) => { + // Late delivery reuses the original committed time, not append order. + const left = parseTodoTimestampMicros(String(a.last_updated_at ?? "")); + const right = parseTodoTimestampMicros(String(b.last_updated_at ?? "")); + if (left !== null && right !== null && left !== right) return left > right ? -1 : 1; + return Number(b.recorded_order ?? 0) - Number(a.recorded_order ?? 0); + })[0]; +} +export function validateWorkYardstickContinuation(params: JsonObject): JsonObject { + if (params.enabled !== true) throw new EffectRuntimeRequestError("work yardstick capture requires explicit yardstick_enabled configuration"); + const binding = requireJsonObject(params.binding, "work binding"); + const decision = normalizeWorkYardstickDecision(params.decision, params.evidence_refs); + const current = currentFinding((params.findings ?? []) as JsonObject[], binding); + if (decision.prior_finding_id !== (current?.finding_id ?? null)) throw new EffectRuntimeRequestError("yardstick basis changed; read the current same-Todo decision before revising it"); + return {decision}; +} +export function projectWorkYardstickCue(params: JsonObject): JsonObject | null { + if (params.enabled !== true || !params.todo_id) return null; + const source = currentFinding((params.findings ?? []) as JsonObject[], params); + if (!source) return null; + const capture = normalizeWorkYardstickCapture({capture: source.work_yardstick, evidence_refs: source.evidence_refs, + goal_id: params.goal_id, agent_id: params.agent_id}); + const decision = capture.decision as JsonObject; + const counter = decision.counterevidence_refs as string[]; + const previous = ((params.findings ?? []) as JsonObject[]).find(row => row.finding_id === decision.prior_finding_id); + const priorCapture = previous?.work_yardstick as JsonObject | undefined; + const priorBinding = priorCapture?.binding as JsonObject | undefined; + const priorDecision = priorCapture?.decision as JsonObject | undefined; + const execution = source.execution_observation as JsonObject | undefined; + const followthrough = priorBinding && execution && ["goal_id", "agent_id", "todo_id"].every(key => priorBinding[key] === params[key]) + ? {source_finding_id: previous!.finding_id, result_finding_id: source.finding_id, + proposed_method_id: (priorDecision!.next_action as JsonObject).method_id, + executed_method_id: execution.executed_method_id, change_reason: execution.change_reason, + candidate_sha256: execution.candidate_sha256, evidence_refs: execution.evidence_refs, + recorded_planned_method_matches: execution.planned_method_id === (priorDecision!.next_action as JsonObject).method_id} + : null; + const profiles = [decision.authoritative, decision.proxy].filter(Boolean) as JsonObject[]; + const feedback = ((params.measurement_feedback ?? []) as JsonObject[]).filter(row => row.node_id === source.node_id && profiles.some(p => + Object.entries(p).every(([key, value]) => row[key] === value))); + return {source_finding_id: source.finding_id, node_id: source.node_id, binding: capture.binding, ...decision, + measurement_feedback: feedback, + ...(followthrough ? {previous_decision_followthrough: followthrough} : {}), + guidance: counter.length + ? "Counterevidence is recorded for this scoped proxy. Reassess its use at the next concrete choice; a local screening heuristic cannot veto an informative authoritative check by itself. Preserve accepted correctness, authorization and budget constraints." + : "Use this current target and scoped yardstick when choosing the next useful action. Proxy fidelity remains unqualified here; preserve accepted constraints and bound confirmation cost.", + authority: "Recorded method and source references are Agent declarations, not independently verified acceptance or permission. Omission preserves this decision; revise it only when work or evidence changes. No extra Turn, replan or mandatory capture is required."}; +} diff --git a/loopx/execution_profile.py b/loopx/execution_profile.py index ff5919431c..f503a65d7c 100644 --- a/loopx/execution_profile.py +++ b/loopx/execution_profile.py @@ -160,6 +160,11 @@ def compact_execution_profile(value: Any) -> dict[str, Any]: raise ValueError("planning_horizon_goal must be a boolean") profile["planning_horizon_goal"] = value["planning_horizon_goal"] + if "yardstick_enabled" in value: + if not isinstance(value["yardstick_enabled"], bool): + raise ValueError("yardstick_enabled must be a boolean") + profile["yardstick_enabled"] = value["yardstick_enabled"] + if "stride_strategy" in value: from .control_plane.work_items.stride_policy_codec import normalize_stride_strategy profile["stride_strategy"] = normalize_stride_strategy(value["stride_strategy"]) @@ -257,6 +262,8 @@ def apply_goal_execution_profile_change( clear_stride_strategy: bool = False, planning_horizon_goal: bool | None = None, clear_planning_horizon_goal: bool = False, + yardstick_enabled: bool | None = None, + clear_yardstick_enabled: bool = False, ) -> None: if ( clear_replan_after_completed_todos @@ -274,10 +281,14 @@ def apply_goal_execution_profile_change( raise ValueError("choose a stride strategy override or clear operation") if planning_horizon_goal is not None and clear_planning_horizon_goal: raise ValueError("choose a planning horizon Goal override or clear operation") + if yardstick_enabled is not None and clear_yardstick_enabled: + raise ValueError("choose a Yardstick override or clear operation") raw = goal.get("execution_profile") profile = dict(raw) if isinstance(raw, dict) else {} if ( - planning_horizon_goal is None + yardstick_enabled is None + and not (clear_yardstick_enabled and "yardstick_enabled" in profile) + and planning_horizon_goal is None and not clear_planning_horizon_goal and stride_strategy is None and not clear_stride_strategy @@ -288,6 +299,10 @@ def apply_goal_execution_profile_change( and not (clear_replan_after_completed_todos and "replan_after_completed_todos" in profile) ): return + if clear_yardstick_enabled: + profile.pop("yardstick_enabled", None) + if yardstick_enabled is not None: + profile["yardstick_enabled"] = yardstick_enabled if clear_planning_horizon_goal: profile.pop("planning_horizon_goal", None) if planning_horizon_goal is not None: diff --git a/loopx/extensions/lark/presentation/explore_record_values.py b/loopx/extensions/lark/presentation/explore_record_values.py new file mode 100644 index 0000000000..f6577a2f3e --- /dev/null +++ b/loopx/extensions/lark/presentation/explore_record_values.py @@ -0,0 +1,99 @@ +"""Pure values for the existing Explore Base tables; no transport or policy. + +Row identity and lifecycle are unchanged by optional research readback. +""" +from typing import Any, Mapping + +_GOAL_ID_FIELD = "LoopX Goal ID" +_RESULT_ID_FIELD = "LoopX Result ID" + + +def _joined(values: Any) -> str: + if isinstance(values, list): + return ", ".join(str(item) for item in values if str(item or "").strip()) + return str(values or "") + + +def _lifecycle_values(item: Mapping[str, Any], *, source_id: str) -> dict[str, Any]: + return { + "Source ID": source_id, + "Row Lifecycle": "superseded" if str(item.get("superseded_by") or "") else "current", + "Supersedes": _joined(item.get("supersedes")), + "Superseded By": str(item.get("superseded_by") or ""), + } + + +def _node_record_values(node: Mapping[str, Any], *, goal_id: str, source_id: str) -> dict[str, Any]: + return { + "Title": str(node.get("title") or ""), + "Kind": str(node.get("node_kind") or ""), + "Status": str(node.get("status") or ""), + "Summary": "\n".join(part for part in [str(node.get("summary") or ""), str(node.get("research_summary") or "")] if part), + "Blocked Reason": str(node.get("blocked_reason") or ""), + "Parent Node": str(node.get("parent_id") or ""), + "Findings": node.get("finding_count"), + "Evidence Refs": _joined(node.get("evidence_refs")), + "Tags": _joined(node.get("tags")), + "Agent ID": str(node.get("agent_id") or ""), + "First Recorded At": str(node.get("first_recorded_at") or ""), + "Last Updated At": str(node.get("last_updated_at") or ""), + _GOAL_ID_FIELD: goal_id, + _RESULT_ID_FIELD: str(node.get("node_id") or ""), + **_lifecycle_values(node, source_id=source_id), + } + + +def _edge_record_values(edge: Mapping[str, Any], *, goal_id: str, source_id: str) -> dict[str, Any]: + return { + "From Node": str(edge.get("from_node") or ""), + "To Node": str(edge.get("to_node") or ""), + "Type": str(edge.get("edge_type") or ""), + "Confidence": edge.get("confidence"), + "Condition": str(edge.get("summary") or ""), + "State Transition": str(edge.get("edge_type") or ""), + "Summary": str(edge.get("summary") or ""), + "Last Updated At": str(edge.get("last_updated_at") or ""), + _GOAL_ID_FIELD: goal_id, + _RESULT_ID_FIELD: str(edge.get("edge_id") or ""), + "Source ID": source_id, + } + + +def _finding_summary(finding: Mapping[str, Any]) -> str: + summary = str(finding.get("summary") or "") + capture = finding.get("work_yardstick") + if not isinstance(capture, Mapping): + return summary + # Projection only: the canonical event/typed owner has validated this + # declaration. A Lark row does not upgrade it to acceptance or authority. + decision = capture["decision"] + target, outcome, proxy = decision["target"], decision["authoritative"], decision["proxy"] + next_action = decision["next_action"] + details = [f"Target: {target['summary']}", f"Scope: {target['scope']}", + f"Outcome metric: {outcome['metric']} ({outcome['unit']}); {outcome['comparison_scope']}; {outcome['input_revision']}; {outcome['evaluation_ref']}", + f"Proxy: {proxy['metric']} ({proxy['unit']}); {proxy['comparison_scope']}; {proxy['input_revision']}; {proxy['evaluation_ref']}" if proxy else "Proxy: none (direct evaluation)", + f"Acceptance source: {target['acceptance_ref']}", + f"Next action: {next_action['reason']} ({next_action['method_id']})", f"Cost bound: {next_action['cost_bound']}", + f"Task: {capture['binding']['todo_id']}"] + if decision["counterevidence_refs"]: + details.append("Proxy counterevidence: " + ", ".join(decision["counterevidence_refs"])) + details.append("Recorded research decision; fidelity and acceptance remain independently assessed.") + return "\n".join([summary, *details]) + + +def _finding_record_values(finding: Mapping[str, Any], *, goal_id: str, source_id: str) -> dict[str, Any]: + return { + "Finding": str(finding.get("finding") or ""), + "Summary": _finding_summary(finding), + "Status": str(finding.get("status") or ""), + "Confidence": finding.get("confidence"), + "Node": str(finding.get("node_id") or ""), + "Evidence Refs": _joined(finding.get("evidence_refs")), + "Tags": _joined(finding.get("tags")), + "Agent ID": str(finding.get("agent_id") or ""), + "First Recorded At": str(finding.get("first_recorded_at") or ""), + "Last Updated At": str(finding.get("last_updated_at") or ""), + _GOAL_ID_FIELD: goal_id, + _RESULT_ID_FIELD: str(finding.get("finding_id") or ""), + **_lifecycle_values(finding, source_id=source_id), + } diff --git a/loopx/extensions/lark/presentation/explore_results.py b/loopx/extensions/lark/presentation/explore_results.py index fe0316152d..132c2df5d5 100644 --- a/loopx/extensions/lark/presentation/explore_results.py +++ b/loopx/extensions/lark/presentation/explore_results.py @@ -59,6 +59,10 @@ now_lark_datetime, parse_lark_base_url, ) +from .explore_record_values import ( + _GOAL_ID_FIELD, _RESULT_ID_FIELD, + _node_record_values, _edge_record_values, _finding_record_values, +) from .explore_singleflight import ( default_lark_explore_config_path, singleflight_issue_fix_material_sync, @@ -105,8 +109,6 @@ TABLE_FINDINGS: "Findings", } -_GOAL_ID_FIELD = "LoopX Goal ID" -_RESULT_ID_FIELD = "LoopX Result ID" def _number_field(name: str, *, precision: int) -> dict[str, Any]: @@ -1478,75 +1480,6 @@ def failure(error: str | None = None) -> dict[str, Any]: } -def _joined(values: Any) -> str: - if isinstance(values, list): - return ", ".join(str(item) for item in values if str(item or "").strip()) - return str(values or "") - - -def _lifecycle_values(item: Mapping[str, Any], *, source_id: str) -> dict[str, Any]: - return { - "Source ID": source_id, - "Row Lifecycle": "superseded" if str(item.get("superseded_by") or "") else "current", - "Supersedes": _joined(item.get("supersedes")), - "Superseded By": str(item.get("superseded_by") or ""), - } - - -def _node_record_values(node: Mapping[str, Any], *, goal_id: str, source_id: str) -> dict[str, Any]: - return { - "Title": str(node.get("title") or ""), - "Kind": str(node.get("node_kind") or ""), - "Status": str(node.get("status") or ""), - "Summary": "\n".join(part for part in [str(node.get("summary") or ""), str(node.get("research_summary") or "")] if part), - "Blocked Reason": str(node.get("blocked_reason") or ""), - "Parent Node": str(node.get("parent_id") or ""), - "Findings": node.get("finding_count"), - "Evidence Refs": _joined(node.get("evidence_refs")), - "Tags": _joined(node.get("tags")), - "Agent ID": str(node.get("agent_id") or ""), - "First Recorded At": str(node.get("first_recorded_at") or ""), - "Last Updated At": str(node.get("last_updated_at") or ""), - _GOAL_ID_FIELD: goal_id, - _RESULT_ID_FIELD: str(node.get("node_id") or ""), - **_lifecycle_values(node, source_id=source_id), - } - - -def _edge_record_values(edge: Mapping[str, Any], *, goal_id: str, source_id: str) -> dict[str, Any]: - return { - "From Node": str(edge.get("from_node") or ""), - "To Node": str(edge.get("to_node") or ""), - "Type": str(edge.get("edge_type") or ""), - "Confidence": edge.get("confidence"), - "Condition": str(edge.get("summary") or ""), - "State Transition": str(edge.get("edge_type") or ""), - "Summary": str(edge.get("summary") or ""), - "Last Updated At": str(edge.get("last_updated_at") or ""), - _GOAL_ID_FIELD: goal_id, - _RESULT_ID_FIELD: str(edge.get("edge_id") or ""), - "Source ID": source_id, - } - - -def _finding_record_values(finding: Mapping[str, Any], *, goal_id: str, source_id: str) -> dict[str, Any]: - return { - "Finding": str(finding.get("finding") or ""), - "Summary": str(finding.get("summary") or ""), - "Status": str(finding.get("status") or ""), - "Confidence": finding.get("confidence"), - "Node": str(finding.get("node_id") or ""), - "Evidence Refs": _joined(finding.get("evidence_refs")), - "Tags": _joined(finding.get("tags")), - "Agent ID": str(finding.get("agent_id") or ""), - "First Recorded At": str(finding.get("first_recorded_at") or ""), - "Last Updated At": str(finding.get("last_updated_at") or ""), - _GOAL_ID_FIELD: goal_id, - _RESULT_ID_FIELD: str(finding.get("finding_id") or ""), - **_lifecycle_values(finding, source_id=source_id), - } - - def _public_safe_values(values: dict[str, Any]) -> dict[str, Any]: return {key: _public_safe_text(value) if isinstance(value, str) else value for key, value in values.items()} diff --git a/tests/capabilities/test_goal_capability_inspection.py b/tests/capabilities/test_goal_capability_inspection.py index 8123d711df..af9107c8a8 100644 --- a/tests/capabilities/test_goal_capability_inspection.py +++ b/tests/capabilities/test_goal_capability_inspection.py @@ -305,7 +305,7 @@ def cadence(packet): overridden = inspect() assert cadence(overridden)["source"] == "goal_override" assert cadence(overridden)["configuration"] == { - "count_unit": "completed_todos", "count": 3, "planning_horizon_goal": True, + "count_unit": "completed_todos", "count": 3, "planning_horizon_goal": True, "yardstick_enabled": False, } assert overridden["revision"] != inherited["revision"] assert overridden == SettingsHandler( diff --git a/tests/capabilities/test_machine_configuration_goal_defaults.py b/tests/capabilities/test_machine_configuration_goal_defaults.py index ae262b5282..bb3c022810 100644 --- a/tests/capabilities/test_machine_configuration_goal_defaults.py +++ b/tests/capabilities/test_machine_configuration_goal_defaults.py @@ -423,6 +423,7 @@ def test_goal_stride_only_uses_live_review_interval(tmp_path: Path) -> None: }) assert options == { "clear_execution_planning_horizon_goal": True, + "clear_execution_yardstick_enabled": True, "execution_stride_strategy": "evidence_conditioned_v1", "clear_execution_replan_after_turns": True, "clear_execution_replan_after_todos": True, @@ -453,6 +454,7 @@ def review() -> dict[str, object]: "count_unit": "completed_todos", "count": 2, "stride_strategy": "evidence_conditioned_v1", "planning_horizon_goal": True, + "yardstick_enabled": False, } machine_registry = build_builtin_machine_configuration_registry() @@ -595,3 +597,83 @@ def test_stride_default_preserves_explicit_off_and_saved_legacy_configuration(tm 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 + + +def test_yardstick_inherits_independently_of_a_pinned_goal_cadence(tmp_path): + from loopx.capabilities.goal_inspection import inspect_goal_capabilities + from loopx.chat.goal_configuration_api import _goal_capability_options + + _repo, registry_path, runtime_root = _fixture(tmp_path) + configure_goal(registry_path=registry_path, goal_id=GOAL_ID, + execution_replan_after_turns=3, execute=True) + original = registry_path.read_bytes() + assert "yardstick_enabled" not in _history_goal(registry_path, runtime_root)["execution_profile"] + machine = _machine_configuration() + machine["namespaces"]["todo_replan_cadence"] = { + "schema_version": "todo_replan_cadence_machine_defaults_v2", + "count_unit": "effective_turns", "count": 5, + "stride_strategy": "evidence_conditioned_v1", "yardstick_enabled": True, + } + owner = build_builtin_machine_configuration_registry() + plan = configure_machine_configuration(runtime_root=runtime_root, registry=owner, configuration=machine) + configure_machine_configuration(runtime_root=runtime_root, registry=owner, configuration=machine, + execute=True, expected_plan_revision=plan["plan_revision"]) + raw = json.loads(registry_path.read_text())["goals"][0] + effective = resolve_todo_replan_cadence_goal(raw, runtime_root)["execution_profile"] + assert effective["replan_after_effective_turns"] == 3 + assert effective["yardstick_enabled"] is True + assert "stride_strategy" not in effective, "legacy cadence-only strategy semantics remain off" + assert registry_path.read_bytes() == original, "inheritance must not migrate stored Goal state" + projected = _history_goal(registry_path, runtime_root) + assert projected["execution_profile"]["yardstick_enabled"] is True + assert not (projected.get("explore_graph") or {}).get("enabled"), "preference never enables Explore" + + def readback(): + inspection = inspect_goal_capabilities(registry_path=registry_path, runtime_root=runtime_root, + goal_id=GOAL_ID)["configuration"]["capability_catalog"]["capabilities"] + return next(x for x in inspection if x["capability_id"] == "todo_replan_cadence") + assert readback()["effective_configuration"]["configuration"]["yardstick_enabled"] is True + for enabled in (False, True): + options = _goal_capability_options("todo_replan_cadence", { + "count_unit": "effective_turns", "count": 3, "yardstick_enabled": enabled}) + configure_goal(registry_path=registry_path, goal_id=GOAL_ID, execute=True, **options) + assert _history_goal(registry_path, runtime_root)["execution_profile"]["yardstick_enabled"] is enabled + entry = readback() + assert entry["current"]["yardstick_enabled"] is enabled + assert entry["effective_configuration"]["configuration"]["yardstick_enabled"] is enabled + configure_goal(registry_path=registry_path, goal_id=GOAL_ID, + clear_execution_yardstick_enabled=True, execute=True) + assert readback()["effective_configuration"]["configuration"]["yardstick_enabled"] is True + assert "yardstick_enabled" not in readback()["current"] + + +@pytest.mark.parametrize("invalid", [None, 0, 1, "false", "true", [], {}]) +def test_yardstick_rejects_non_boolean_at_machine_goal_and_profile_boundaries(invalid, tmp_path): + from loopx.execution_profile import compact_execution_profile + from loopx.chat.goal_configuration_api import _goal_capability_options + + with pytest.raises(ValueError, match="yardstick_enabled"): + compact_execution_profile({"yardstick_enabled": invalid}) + with pytest.raises(ValueError, match="yardstick_enabled"): + _goal_capability_options("todo_replan_cadence", {"yardstick_enabled": invalid}) + with pytest.raises(ValueError, match="yardstick_enabled"): + normalize_todo_replan_cadence_machine_defaults({ + "schema_version": "todo_replan_cadence_machine_defaults_v2", + "count_unit": "effective_turns", "count": 6, + "stride_strategy": "off", "yardstick_enabled": invalid}) + + +@pytest.mark.parametrize("version", [0, 1, 2]) +def test_legacy_cadence_keeps_yardstick_absent_and_default_off(version): + from loopx.capabilities.todo_replan_cadence.machine_defaults import apply_todo_replan_cadence_machine_default + + legacy = {"schema_version": f"todo_replan_cadence_machine_defaults_v{version}"} + legacy.update({"completed_todos": 2} if version == 0 else {"count_unit": "effective_turns", "count": 6}) + if version == 2: + legacy["stride_strategy"] = "off" + assert normalize_todo_replan_cadence_machine_defaults(legacy) == legacy + machine = {"schema_version": "loopx_machine_configuration_v0", "namespaces": {"todo_replan_cadence": legacy}} + assert "yardstick_enabled" not in apply_todo_replan_cadence_machine_default({}, machine)["execution_profile"] + if version < 2: + with pytest.raises(ValueError): + normalize_todo_replan_cadence_machine_defaults({**legacy, "yardstick_enabled": True}) diff --git a/tests/control_plane/test_todo_replan_cadence.py b/tests/control_plane/test_todo_replan_cadence.py index 6413a6f91b..898e4200f8 100644 --- a/tests/control_plane/test_todo_replan_cadence.py +++ b/tests/control_plane/test_todo_replan_cadence.py @@ -234,3 +234,23 @@ def test_quota_uses_cadence_without_bypassing_a_goal_pause(threshold, paused): if completed_gaps: assert completed_gaps[0]["completed_todo_threshold"] == threshold assert result["goal_frontier_projection"]["replan_required"] is True + + +def test_cli_yardstick_boolean_and_clear_preserve_the_independent_cadence(tmp_path): + registry = tmp_path / "registry.json" + registry.write_text(json.dumps({"goals": [{"id": "example", "status": "active", + "execution_profile": {"replan_after_effective_turns": 3}}]})) + prefix = ["--registry", str(registry), "--format", "json", "--runtime-root", str(tmp_path / "runtime"), + "configure-goal", "--goal-id", "example"] + for flag, expected in [("--execution-yardstick-enabled", True), ("--no-execution-yardstick-enabled", False), + ("--clear-execution-yardstick-enabled", None)]: + preview = run_source_cli([*prefix, flag], check=True) + assert json.loads(preview.stdout)["ok"] + run_source_cli([*prefix, flag, "--execute"], check=True) + profile = json.loads(registry.read_text())["goals"][0]["execution_profile"] + assert profile["replan_after_effective_turns"] == 3 + assert profile.get("yardstick_enabled") is expected + before = registry.read_bytes() + result = run_source_cli([*prefix, "--execution-yardstick-enabled", "--clear-execution-yardstick-enabled", "--execute"], check=False) + assert result.returncode != 0 + assert registry.read_bytes() == before diff --git a/tests/control_plane/test_work_yardstick_cli.py b/tests/control_plane/test_work_yardstick_cli.py new file mode 100644 index 0000000000..26f7039e6b --- /dev/null +++ b/tests/control_plane/test_work_yardstick_cli.py @@ -0,0 +1,195 @@ +"""Same-work SQLite journey: evidence → ordinary Turn → real action → readback. + +The deterministic actor proves transport/adoption attribution, not model quality +or a causal benchmark gain. No evaluator, credentials or active Goal are used. +""" +import hashlib +import json + + +from tests.control_plane.test_quota_settlement_cli import ( + _write_fixture, _run_cli, GOAL_ID, AGENT_ID, TODO_ID, +) +from loopx.capabilities.explore.result_log import explore_result_log_path, load_explore_result_events +from loopx.capabilities.explore.turn_context import explore_turn_context +from loopx.presentation.explore_results_api import _result_rows + + +def decision(): + outcome = dict(metric="quality", unit="points", higher_is_better=True, + comparison_scope="validation:fixed", input_revision="inputs-v1", evaluation_ref="evaluator:outcome-v1") + return dict(schema_version="work_yardstick_decision_v0", + target=dict(summary="Improve the best accepted score", scope="Same public input and budget", + acceptance_ref="acceptance:task-v1"), + authoritative=outcome, proxy={**outcome, "metric": "screen", "evaluation_ref": "evaluator:screen-v1"}, + counterevidence_refs=["result:reversal"], prior_finding_id=None, + next_action=dict(method_id="method:direct", reason="Use one informative direct check after screening reversed the ordering", + cost_bound="One available evaluation, with the original correctness check")) + + +def attachment(): + return dict(schema_version="explore_result_attachment_v0", question="Does this screen predict the outcome?", + applicability="Fixed inputs and budget only", input_revision="inputs-v1", + observation="The screen ranked the better validated candidate lower", + interpretation="Reassess the screen; retain the original correctness requirement", status="confirmed", + evidence_refs=["result:reversal"], work_yardstick_decision=decision()) + + +def setup(tmp_path, enabled=True): + project, runtime, path = _write_fixture(tmp_path, provider="sqlite") + config = json.loads(path.read_text()) + config["goals"][0]["explore_graph"] = {"enabled": True} + config["goals"][0]["quota"]["allowed_slots"] = 4 + config["goals"][0].setdefault("execution_profile", {})["yardstick_enabled"] = enabled + path.write_text(json.dumps(config)) + return project, runtime, path + + +def run(project, runtime, path, *command): + rc, packet = _run_cli(path, runtime, *command, cwd=project) + assert rc == 0, json.dumps({key: packet.get(key) for key in ("error", "reason", "classification", "decision", "admission", "quota")}, ensure_ascii=False, indent=2) + return packet + + +def binding(turn): + return ["--goal-id", GOAL_ID, "--agent-id", AGENT_ID, "--todo-id", TODO_ID, "--turn-instance-id", turn] + + +def refresh(project, runtime, path, turn, source): + run(project, runtime, path, "quota", "should-run", "--codex-app", *binding(turn)) + return run(project, runtime, path, "refresh-state", *binding(turn), "--classification", "validated_change", + "--delivery-batch-scale", "implementation", "--delivery-outcome", "outcome_progress", + "--explore-result-json", str(source), "--no-global-sync", "--suppress-external-sinks") + + +def context(runtime, path, *, detail=True): + params = dict(registry_path=path, runtime_root=runtime, goal_id=GOAL_ID, agent_id=AGENT_ID, todo_id=TODO_ID) + compact = explore_turn_context(**params) + cues = compact["graph"].get("work_yardsticks", []) + if detail and cues: + return explore_turn_context(**params, result_node=cues[0]["node_id"]) + return compact + + +def test_sqlite_current_work_to_ordinary_turn_to_concrete_action_and_readback(tmp_path): + project, runtime, path = setup(tmp_path) + source = tmp_path / "capture.json" + source.write_text(json.dumps(attachment())) + first = refresh(project, runtime, path, "turn-yardstick-1", source) + assert first["explore_result_delivery"]["ok"], first + assert not first.get("autonomous_replan_ack", {}).get("recorded") + run(project, runtime, path, "quota", "spend-slot", *binding("turn-yardstick-1"), "--slots", "1", "--source", "heartbeat", "--execute") + short = context(runtime, path, detail=False)["graph"]["work_yardsticks"][0] + assert "authoritative" not in short and "measurement_feedback" not in short + assert "result:reversal" in short["counterevidence_refs"] and short["details_command"] + cue = context(runtime, path)["graph"]["work_yardsticks"][0] + assert cue["binding"]["todo_id"] == TODO_ID + assert cue["source_finding_id"] == first["explore_result_delivery"]["finding_id"] + # The ordinary guard delivers the same current decision, without introducing + # a replan obligation or changing its task identity. + guard = run(project, runtime, path, "quota", "should-run", "--codex-app", *binding("turn-yardstick-2")) + assert cue["source_finding_id"] in json.dumps(guard) + assert "work_yardstick_decision_v0" in json.dumps(guard) + # Independent synthetic workload: screening picked A, the accepted score + # prefers B. The actor uses the cited action, verifies a hard constraint, + # evaluates B and writes an actual result artifact (not an adoption boolean). + candidate = {"correct": True, "accepted_score": 8, "screen": 3} + assert candidate["correct"] and cue["next_action"]["method_id"] == "method:direct" + artifact = project / "accepted-result.json" + artifact.write_text(json.dumps(candidate, sort_keys=True)) + digest = hashlib.sha256(artifact.read_bytes()).hexdigest() + follow = attachment() + follow["evidence_refs"] = ["result:direct-1"] + follow["observation"] = "Direct evaluation returned 8 points for the correct candidate" + follow["work_yardstick_decision"].update(prior_finding_id=cue["source_finding_id"], counterevidence_refs=[]) + follow["work_yardstick_decision"]["proxy"] = None + follow["work_yardstick_decision"]["next_action"] = dict(method_id="method:next", reason="Retain the direct result and compare a distinct candidate", cost_bound="One further check within the original budget") + follow["execution_observation"] = dict(schema_version="explore_execution_observation_v0", + candidate_sha256=digest, input_revision="inputs-v1", planned_method_id="method:direct", executed_method_id="method:direct", + change_reason=None, elapsed_seconds=0.01, limit_seconds=1, search_iterations=1, stop_reason="evaluation returned", + evidence_refs=["result:direct-1"], result_ref="result:direct-1", result_candidate_sha256=digest, exit_code=0) + follow["measurements"] = [{**decision()["authoritative"], "value": 8, "candidate_ref": f"sha256:{digest}", "evidence_ref": "result:direct-1"}] + source2 = tmp_path / "next.json" + source2.write_text(json.dumps(follow)) + second = run(project, runtime, path, "refresh-state", *binding("turn-yardstick-2"), "--classification", "validated_change", + "--delivery-batch-scale", "implementation", "--delivery-outcome", "outcome_progress", + "--explore-result-json", str(source2), "--no-global-sync", "--suppress-external-sinks") + assert second["explore_result_delivery"]["ok"], second + run(project, runtime, path, "quota", "spend-slot", *binding("turn-yardstick-2"), "--slots", "1", "--source", "heartbeat", "--execute") + cue2 = context(runtime, path)["graph"]["work_yardsticks"][0] + observed = cue2["previous_decision_followthrough"] + assert observed["source_finding_id"] == cue["source_finding_id"] + assert observed["executed_method_id"] == "method:direct" and observed["recorded_planned_method_matches"] + assert observed["candidate_sha256"] == hashlib.sha256(artifact.read_bytes()).hexdigest() + assert cue2["measurement_feedback"][0]["best"]["value"] == 8 + # Ordinary unchanged work has no extra finding or decision write. + log = explore_result_log_path(runtime, GOAL_ID) + unchanged = log.read_bytes() + run(project, runtime, path, "quota", "should-run", "--codex-app", *binding("turn-yardstick-3")) + third = run(project, runtime, path, "refresh-state", *binding("turn-yardstick-3"), "--classification", "validated_change", + "--delivery-batch-scale", "implementation", "--delivery-outcome", "outcome_progress", + "--no-global-sync", "--suppress-external-sinks") + assert log.read_bytes() == unchanged and "explore_result" not in third + assert context(runtime, path)["graph"]["work_yardsticks"][0]["source_finding_id"] == cue2["source_finding_id"] + run(project, runtime, path, "quota", "spend-slot", *binding("turn-yardstick-3"), "--slots", "1", "--source", "heartbeat", "--execute") + # Disable removes the cue; canonical historical evidence remains readable. + config = json.loads(path.read_text()) + config["goals"][0]["execution_profile"]["yardstick_enabled"] = False + path.write_text(json.dumps(config)) + assert "work_yardsticks" not in context(runtime, path)["graph"] + rows = _result_rows(runtime, GOAL_ID, path) + assert rows[0]["work_yardstick"]["binding"]["todo_id"] == TODO_ID + from loopx.extensions.lark.presentation.explore_results import _finding_record_values + original = _finding_record_values({"summary": "legacy"}, goal_id=GOAL_ID, source_id="synthetic") + assert original["Summary"] == "legacy" + projected = _finding_record_values(rows[0], goal_id=GOAL_ID, source_id="synthetic") + assert "Target:" in projected["Summary"] and "Acceptance source:" in projected["Summary"] + assert "fidelity and acceptance remain independently assessed" in projected["Summary"] + # Successful replay is exact readback even after disabling the feature. + log = explore_result_log_path(runtime, GOAL_ID) + before = log.read_bytes() + replay = run(project, runtime, path, "refresh-state", *binding("turn-yardstick-2"), "--classification", "validated_change", + "--delivery-batch-scale", "implementation", "--delivery-outcome", "outcome_progress", + "--explore-result-json", str(source2), "--no-global-sync", "--suppress-external-sinks") + assert replay["explore_result_delivery"]["idempotent_replay"] and log.read_bytes() == before + + +def test_disabled_new_attachment_rejects_before_primary_commit(tmp_path): + project, runtime, path = setup(tmp_path, enabled=False) + source = tmp_path / "capture.json" + source.write_text(json.dumps(attachment())) + run(project, runtime, path, "quota", "should-run", "--codex-app", *binding("turn-off")) + rc, rejected = _run_cli(path, runtime, "refresh-state", *binding("turn-off"), "--classification", "validated_change", + "--delivery-batch-scale", "implementation", "--delivery-outcome", "outcome_progress", + "--explore-result-json", str(source), "--no-global-sync", "--suppress-external-sinks", cwd=project) + assert rc == 1 and not rejected.get("appended"), rejected + assert "yardstick_enabled" in json.dumps(rejected) + assert not explore_result_log_path(runtime, GOAL_ID).exists() + + +def test_committed_pending_capture_recovers_after_disable_without_new_authority(tmp_path, monkeypatch, capsys): + from loopx.cli import main + import loopx.capabilities.explore.result_writeback as writer + project, runtime, path = setup(tmp_path) + source = tmp_path / "capture.json" + source.write_text(json.dumps(attachment())) + turn = "turn-interrupted" + run(project, runtime, path, "quota", "should-run", "--codex-app", *binding(turn)) + command = ["refresh-state", *binding(turn), "--classification", "validated_change", + "--delivery-batch-scale", "implementation", "--delivery-outcome", "outcome_progress", + "--explore-result-json", str(source), "--no-global-sync", "--suppress-external-sinks"] + def unavailable(*args, **kwargs): + raise OSError("injected graph write interruption") + with monkeypatch.context() as patch: + patch.chdir(project) + patch.setattr(writer, "append_explore_result_events", unavailable) + rc = main(["--registry", str(path), "--runtime-root", str(runtime), "--format", "json", *command]) + failed = json.loads(capsys.readouterr().out) + assert rc == 1 and failed["appended"] and failed["explore_result_delivery"]["primary_committed"] + config = json.loads(path.read_text()) + config["goals"][0]["execution_profile"]["yardstick_enabled"] = False + path.write_text(json.dumps(config)) + recovered = run(project, runtime, path, *command) + assert recovered["idempotent_replay"] and recovered["explore_result_delivery"]["ok"] + assert len(load_explore_result_events(explore_result_log_path(runtime, GOAL_ID))) == 2 + assert "work_yardsticks" not in context(runtime, path)["graph"] diff --git a/tests/control_plane_ts/yardstick_decision.test.ts b/tests/control_plane_ts/yardstick_decision.test.ts new file mode 100644 index 0000000000..bdba64d458 --- /dev/null +++ b/tests/control_plane_ts/yardstick_decision.test.ts @@ -0,0 +1,95 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import {normalizeWorkYardstickDecision, normalizeWorkYardstickCapture, + validateWorkYardstickContinuation, projectWorkYardstickCue} from "../../loopx/control_plane/work_items/yardstick_decision.ts"; +import {normalizeExploreResultAttachment} from "../../loopx/control_plane/capabilities/explore_result_writeback.ts"; +import {projectExploreTurnContext} from "../../loopx/control_plane/capabilities/explore_turn_context.ts"; + +const authoritative = {metric: "quality", unit: "points", higher_is_better: true, + comparison_scope: "validation:fixed", input_revision: "inputs-v1", evaluation_ref: "evaluator:outcome-v1"}; +const proxy = {...authoritative, metric: "screening", evaluation_ref: "evaluator:local-v1"}; +const decision = {schema_version: "work_yardstick_decision_v0", target: { + summary: "Improve the best accepted outcome", scope: "Same task and resource allowance", acceptance_ref: "acceptance:task-v1"}, + authoritative, proxy, counterevidence_refs: ["result:reversal"], prior_finding_id: null, + next_action: {method_id: "method:direct", reason: "Use a bounded direct check after the proxy reversed the ordering", cost_bound: "One available evaluation; preserve required correctness"}}; +const binding = {goal_id: "research", agent_id: "worker", todo_id: "todo_search", turn_instance_id: "turn-one"}; +const finding = {finding_id: "finding-one", node_id: "question", evidence_refs: ["result:reversal"], + last_updated_at: "2026-01-01T00:00:01Z", recorded_order: 1, + work_yardstick: {decision, binding}}; + +test("ordinary decision is bounded, source-linked and distinct from replan v2", () => { + const output = normalizeWorkYardstickDecision(decision, finding.evidence_refs); + assert.deepEqual(output, decision); + assert.equal(Object.hasOwn(output, "replan_obligation_id"), false); + assert.throws(() => normalizeWorkYardstickDecision({...decision, replan_obligation_id: "fake"}), /unsupported/); + assert.throws(() => normalizeWorkYardstickDecision(decision, ["result:unrelated"]), /counterevidence/); + assert.throws(() => normalizeWorkYardstickDecision({...decision, proxy: authoritative}), /distinct/); + assert.throws(() => normalizeWorkYardstickDecision({...decision, target: {...decision.target, acceptance_ref: "file:/private/source"}}), /opaque/); + assert.throws(() => normalizeWorkYardstickCapture({capture: finding.work_yardstick, goal_id: "foreign"}), /cross Goal/); +}); + +test("changed decisions require their actual same-Todo predecessor, not the most recent other task", () => { + assert.deepEqual(validateWorkYardstickContinuation({enabled: true, decision, binding, findings: []}), {decision}); + assert.throws(() => validateWorkYardstickContinuation({enabled: true, decision, binding, findings: [finding]}), /basis changed/); + const next = {...decision, prior_finding_id: "finding-one"}; + assert.deepEqual(validateWorkYardstickContinuation({enabled: true, decision: next, binding, findings: [finding]}), {decision: next}); + assert.throws(() => validateWorkYardstickContinuation({enabled: false, decision, binding, findings: []}), /explicit/); + assert.equal(projectWorkYardstickCue({...binding, enabled: false, findings: [finding]}), null); + assert.equal(projectWorkYardstickCue({...binding, todo_id: "todo_other", enabled: true, findings: [finding]}), null); +}); + +test("a late replay cannot replace a newer committed choice; exact profiles select feedback", () => { + const old = {...finding, finding_id: "late-old", recorded_order: 99, + last_updated_at: "2026-01-01T00:00:00Z"}; + const feedback = [{...authoritative, node_id: "question", improvement: 2}, {...proxy, node_id: "question", improvement: -1}, + {...proxy, node_id: "question", input_revision: "different-inputs", improvement: 10}, + {...authoritative, node_id: "other-question", improvement: 100}]; + const cue = projectWorkYardstickCue({...binding, enabled: true, findings: [old, finding], measurement_feedback: feedback})!; + assert.equal(cue.source_finding_id, "finding-one"); + assert.deepEqual(cue.measurement_feedback, feedback.slice(0, 2)); + assert.match(String(cue.guidance), /cannot veto/); + assert.match(String(cue.guidance), /correctness, authorization and budget/); + assert.equal(Object.hasOwn(cue, "required"), false); + assert.equal(Object.hasOwn(cue, "fidelity_validated"), false); +}); + +test("one optional attachment reaches an ordinary task context without creating a planner or off-mode cue", () => { + const result = normalizeExploreResultAttachment({attachment: { + schema_version: "explore_result_attachment_v0", question: "Does the screen rank useful candidates?", + applicability: "Same task only", input_revision: "inputs-v1", observation: "The paired result reversed the screen's ranking", + interpretation: "Reassess screening before repeating it", status: "confirmed", evidence_refs: finding.evidence_refs, + work_yardstick_decision: decision, + }}); + assert.deepEqual(result.work_yardstick_decision, decision); + const todo = {todo_id: binding.todo_id, role: "agent", claimed_by: "worker", status: "open", explore_result_node_refs: ["question"]}; + const params = {goal_id: "research", agent_id: "worker", graph_enabled: true, harness_gate: {enabled: false}, route: ["loopx"], + plan: {}, projection: {nodes: [], findings: [finding]}, todo_id: binding.todo_id, focus_todo: todo}; + const off = projectExploreTurnContext(params); + assert.equal(Object.hasOwn(off.graph, "work_yardsticks"), false); + const on = projectExploreTurnContext({...params, yardstick_enabled: true, work_todos: [todo]}); + assert.equal(on.graph.work_yardsticks[0].source_finding_id, finding.finding_id); + assert.equal(on.graph.work_yardsticks[0].cue, decision.next_action.reason); + assert.equal(Object.hasOwn(on.graph.work_yardsticks[0], "authoritative"), false); + assert.deepEqual(on.graph.work_yardsticks[0].counterevidence_refs, finding.evidence_refs); + const detail = projectExploreTurnContext({...params, yardstick_enabled: true, work_todos: [todo], result_node: "question"}); + assert.deepEqual(detail.graph.work_yardsticks[0].authoritative, authoritative); + assert.deepEqual(on.harness, off.harness); + assert.equal(on.graph.work_yardstick_capture.optional, true); + // Unknown or unowned work cannot adopt a shared question's decision. + assert.equal(Object.hasOwn(projectExploreTurnContext({...params, yardstick_enabled: true, + work_todos: [{...todo, claimed_by: "peer"}]}).graph, "work_yardsticks"), false); +}); + + +test("history readback preserves scoped declarations and rejects foreign source bindings", async () => { + const {parseExploreHistoryPage} = await import("../../loopx/control_plane/presentation/explore_history.ts"); + const row = {schema_version: "loopx_explore_result_event_v0", event_id: "capture-event", result_id: "capture-finding", + goal_id: "research", agent_id: "worker", event_kind: "finding", recorded_at: "2026-01-01T00:00:00Z", title: "Recorded decision", + evidence_refs: finding.evidence_refs, work_yardstick: finding.work_yardstick}; + const page = {ok: true, goal_id: "research", total: 1, next_cursor: null, items: [row]}; + assert.deepEqual(parseExploreHistoryPage(page, "research").items[0]!.work_yardstick, finding.work_yardstick); + assert.throws(() => parseExploreHistoryPage({...page, items: [{...row, agent_id: "different"}]}, "research"), /cross Goal or Agent/); + assert.throws(() => parseExploreHistoryPage({...page, items: [{...row, evidence_refs: []}]}, "research"), /counterevidence/); + assert.deepEqual(normalizeWorkYardstickDecision(decision, [...finding.evidence_refs, ...Array.from({length: 12}, (_, i) => `result:extra-${i}`)]), decision, + "The bounded counterevidence list does not shrink Explore's finding evidence allowance"); +}); diff --git a/tests/test_chat_goal_configuration_api.py b/tests/test_chat_goal_configuration_api.py index 3470a06b84..4734f1fdd7 100644 --- a/tests/test_chat_goal_configuration_api.py +++ b/tests/test_chat_goal_configuration_api.py @@ -325,6 +325,7 @@ def test_machine_inheritable_goal_capabilities_can_clear_their_overrides() -> No "clear_execution_replan_after_turns": True, "clear_execution_stride_strategy": True, "clear_execution_planning_horizon_goal": True, + "clear_execution_yardstick_enabled": True, } assert _goal_capability_options("change_quality_qualification", None) == { "clear_change_quality_configuration": True diff --git a/tests/test_chat_machine_configuration_api.py b/tests/test_chat_machine_configuration_api.py index 061fc75355..c36aa0dab5 100644 --- a/tests/test_chat_machine_configuration_api.py +++ b/tests/test_chat_machine_configuration_api.py @@ -365,6 +365,7 @@ def test_inspection_lists_registered_namespaces_without_local_refs( "count": 6, "stride_strategy": "evidence_conditioned_v1", "planning_horizon_goal": True, + "yardstick_enabled": False, } assert namespace_catalog["change_quality_qualification"][ "configuration_template" From 385263c2af586636d8caa319ae8c85e8039289d4 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Sat, 7 Nov 2026 01:35:43 +0800 Subject: [PATCH 2/3] fix(research): preserve yardstick CAS and delayed capture history Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../target-yardstick-stride-decisions-v0.md | 6 +- ...get-yardstick-stride-decisions-v0.zh-CN.md | 7 ++- .../capabilities/explore/result_writeback.py | 20 +++--- .../work_items/yardstick_decision.ts | 11 +++- .../control_plane/test_work_yardstick_cli.py | 62 ++++++++++++++++++- .../yardstick_decision.test.ts | 12 ++++ 6 files changed, 106 insertions(+), 12 deletions(-) diff --git a/docs/architecture/rfcs/target-yardstick-stride-decisions-v0.md b/docs/architecture/rfcs/target-yardstick-stride-decisions-v0.md index 8e9ba32112..0aeffb0e98 100644 --- a/docs/architecture/rfcs/target-yardstick-stride-decisions-v0.md +++ b/docs/architecture/rfcs/target-yardstick-stride-decisions-v0.md @@ -177,7 +177,11 @@ evaluator reference; they do not merge proxy and authoritative measurements. The original settlement supplies Goal/Agent/Todo/Turn identity. The existing run journal, Explore event log and additive Todo link own persistence and retry; there is no extra journal, metadata note, planner or mandatory writeback. -A changed decision must cite the current same-Todo finding. Current context +A changed decision must cite the current same-Todo finding; validation and append +CAS use the same graph snapshot. An exact durable replay with an original commit +strictly older than the current choice may restore its missing historical +capture. This does not relax predecessor checks for equal-time or newer work, +and attachment callers cannot supply the replay timestamp. Current context selects by original committed time, so late ingestion cannot replace a newer choice. Ordinary work preserves its original settlement type and never creates an obligation to satisfy Stride v2. Replans retain their existing v2 contract. diff --git a/docs/architecture/rfcs/target-yardstick-stride-decisions-v0.zh-CN.md b/docs/architecture/rfcs/target-yardstick-stride-decisions-v0.zh-CN.md index 0b5d7432f1..9aee8eeefa 100644 --- a/docs/architecture/rfcs/target-yardstick-stride-decisions-v0.zh-CN.md +++ b/docs/architecture/rfcs/target-yardstick-stride-decisions-v0.zh-CN.md @@ -143,8 +143,11 @@ M1 在既有 `explore_result_attachment_v0` 中添加可选 `work_yardstick_deci 原 settlement 提供 Goal/Agent/Todo/Turn 身份。既有 run journal、Explore event log 和 Todo 追加关联管理持久化与重试;不新建 journal、metadata note、planner 或必填写回。 -决策变化必须引用同 Todo 当前 finding;按原提交时间选择当前决策,迟到接收不能 -覆盖更新的选择。普通工作保留原 settlement 类型,不为 Stride v2 伪造 obligation; +决策变化必须引用同 Todo 当前 finding,校验与 append CAS 使用同一 graph 快照。 +原提交时间严格早于当前选择的 durable 精确重放可以补回缺失的历史 capture, +但不放宽同刻或较新工作的前驱检查,附件调用方也不能提供重放时间戳。 +按原提交时间选择当前决策,迟到接收不能覆盖更新的选择。 +普通工作保留原 settlement 类型,不为 Stride v2 伪造 obligation; replan 沿用既有 v2 契约。省略附件保留当前决策;显式 `proxy: null` 选择直接评测, 历史 profile 仍可读取。 diff --git a/loopx/capabilities/explore/result_writeback.py b/loopx/capabilities/explore/result_writeback.py index d0dc5f1859..020d613264 100644 --- a/loopx/capabilities/explore/result_writeback.py +++ b/loopx/capabilities/explore/result_writeback.py @@ -198,7 +198,8 @@ def prepare_result_attachment( agent_id, todo_id, turn_instance_id, - _committed_replay=False, + _replay_committed_at=None, + _current_events=None, ): """Validate before primary commit; this path never appends graph evidence.""" result = normalize_result_attachment(attachment) @@ -229,7 +230,7 @@ def prepare_result_attachment( recorded_at="2000-01-01T00:00:00Z", todo_id=todo_id, turn_instance_id=turn_instance_id, ) - current_events = load_explore_result_events_strict( + current_events = _current_events if _current_events is not None else load_explore_result_events_strict( explore_result_log_path(runtime_root, goal_id), goal_id=goal_id ) existing = build_explore_result_projection(current_events, goal_id=goal_id, @@ -239,7 +240,8 @@ def prepare_result_attachment( effect_runtime_result("work_item.yardstick.continuation", { # Only replay of the original committed attachment may finish after # disable; it cannot grant a new capture or change its contents. - "enabled": _committed_replay or work_yardstick_enabled(goal, runtime_root), + "enabled": _replay_committed_at is not None or work_yardstick_enabled(goal, runtime_root), + "replay_committed_at": _replay_committed_at, "decision": result["work_yardstick_decision"], "evidence_refs": result["evidence_refs"], "binding": {"goal_id": goal_id, "agent_id": agent_id, "todo_id": todo_id}, "findings": existing["findings"], @@ -377,6 +379,12 @@ def deliver_result_attachment( result["graph"] = {"ok": True, "appended_event_count": 0, "idempotent_replay": True} else: + # The continuation check, evolution plan and append CAS must use + # one snapshot. A second read after validation could silently admit + # a predecessor made stale by a concurrent same-work delivery. + current_events = load_explore_result_events_strict( + explore_result_log_path(runtime_root, goal_id), goal_id=goal_id + ) attachment = prepare_result_attachment( intent["payload"]["attachment"], registry_path=registry_path, @@ -385,10 +393,8 @@ def deliver_result_attachment( agent_id=agent_id, todo_id=todo_id, turn_instance_id=turn_instance_id, - _committed_replay=payload.get("idempotent_replay") is True, - ) - current_events = load_explore_result_events_strict( - explore_result_log_path(runtime_root, goal_id), goal_id=goal_id + _replay_committed_at=(committed_at if payload.get("idempotent_replay") is True else None), + _current_events=current_events, ) evolves = _evolves_question(attachment) plan = (_evolution_plan(attachment, diff --git a/loopx/control_plane/work_items/yardstick_decision.ts b/loopx/control_plane/work_items/yardstick_decision.ts index 796a820131..f236ab6281 100644 --- a/loopx/control_plane/work_items/yardstick_decision.ts +++ b/loopx/control_plane/work_items/yardstick_decision.ts @@ -95,7 +95,16 @@ export function validateWorkYardstickContinuation(params: JsonObject): JsonObjec const binding = requireJsonObject(params.binding, "work binding"); const decision = normalizeWorkYardstickDecision(params.decision, params.evidence_refs); const current = currentFinding((params.findings ?? []) as JsonObject[], binding); - if (decision.prior_finding_id !== (current?.finding_id ?? null)) throw new EffectRuntimeRequestError("yardstick basis changed; read the current same-Todo decision before revising it"); + // Only the settlement adapter supplies the original timestamp of an exact + // committed replay. Preserve a strictly older missing capture as history; + // it cannot displace the current choice or admit a newer stale predecessor. + const replayTime = typeof params.replay_committed_at === "string" + ? parseTodoTimestampMicros(params.replay_committed_at) : null; + const currentTime = current ? parseTodoTimestampMicros(String(current.last_updated_at ?? "")) : null; + const historicalReplay = replayTime !== null && currentTime !== null && replayTime < currentTime; + if (decision.prior_finding_id !== (current?.finding_id ?? null) && !historicalReplay) { + throw new EffectRuntimeRequestError("yardstick basis changed; read the current same-Todo decision before revising it"); + } return {decision}; } export function projectWorkYardstickCue(params: JsonObject): JsonObject | null { diff --git a/tests/control_plane/test_work_yardstick_cli.py b/tests/control_plane/test_work_yardstick_cli.py index 26f7039e6b..8868d472e9 100644 --- a/tests/control_plane/test_work_yardstick_cli.py +++ b/tests/control_plane/test_work_yardstick_cli.py @@ -186,10 +186,70 @@ def unavailable(*args, **kwargs): rc = main(["--registry", str(path), "--runtime-root", str(runtime), "--format", "json", *command]) failed = json.loads(capsys.readouterr().out) assert rc == 1 and failed["appended"] and failed["explore_result_delivery"]["primary_committed"] + run(project, runtime, path, "quota", "spend-slot", *binding(turn), "--slots", "1", "--source", "heartbeat", "--execute") + # Work continues while the old graph delivery is pending. Its later exact + # replay must retain history without replacing this newer target. + newer_source = tmp_path / "newer.json" + newer = attachment() + newer["work_yardstick_decision"]["target"]["summary"] = "Use the updated current target" + newer_source.write_text(json.dumps(newer)) + latest = refresh(project, runtime, path, "turn-newer", newer_source) + assert latest["explore_result_delivery"]["ok"] config = json.loads(path.read_text()) config["goals"][0]["execution_profile"]["yardstick_enabled"] = False path.write_text(json.dumps(config)) recovered = run(project, runtime, path, *command) assert recovered["idempotent_replay"] and recovered["explore_result_delivery"]["ok"] - assert len(load_explore_result_events(explore_result_log_path(runtime, GOAL_ID))) == 2 + assert len(load_explore_result_events(explore_result_log_path(runtime, GOAL_ID))) == 3 assert "work_yardsticks" not in context(runtime, path)["graph"] + config["goals"][0]["execution_profile"]["yardstick_enabled"] = True + path.write_text(json.dumps(config)) + cue = context(runtime, path)["graph"]["work_yardsticks"][0] + assert cue["source_finding_id"] == latest["explore_result_delivery"]["finding_id"] + assert cue["target"]["summary"] == "Use the updated current target" + # Exact replay is not permission to substitute an older timestamp or a + # different method into the canonical committed attachment. + changed = attachment() + changed["work_yardstick_decision"]["next_action"]["method_id"] = "method:substituted" + source.write_text(json.dumps(changed)) + rc, rejected = _run_cli(path, runtime, *command, cwd=project) + assert rc == 1 and not rejected.get("explore_result_delivery", {}).get("ok"), rejected + + +def test_predecessor_validation_and_append_cas_share_the_same_snapshot(tmp_path, monkeypatch): + from tests.control_plane.test_explore_result_writeback import fixture, payload + import loopx.capabilities.explore.result_writeback as writer + args = fixture(tmp_path) + config = json.loads(args["registry_path"].read_text()) + config["goals"][0].setdefault("execution_profile", {})["yardstick_enabled"] = True + args["registry_path"].write_text(json.dumps(config)) + + def capture(turn, when): + value = payload() + value["generated_at"] = when + value["settlement_identity"]["effect_id"] = f"research:worker:{turn}" + value["explore_result"].update(evidence_refs=["result:reversal"], work_yardstick_decision=decision()) + return value + + first = capture("turn-one", "2026-01-02T00:00:00Z") + concurrent = capture("turn-two", "2026-01-01T00:00:00Z") + prepare = writer.prepare_result_attachment + inserted = False + + def insert_after_validation(*a, **kw): + nonlocal inserted + result = prepare(*a, **kw) + if not inserted: + inserted = True + other = writer.deliver_result_attachment(payload=concurrent, + **{**args, "turn_instance_id": "turn-two"}) + assert other["ok"], other + return result + + with monkeypatch.context() as patch: + patch.setattr(writer, "prepare_result_attachment", insert_after_validation) + result = writer.deliver_result_attachment(payload=first, **args) + assert not result["ok"] and result["primary_committed"] and result["retryable"], result + assert "graph changed" in result["error"] + events = load_explore_result_events(explore_result_log_path(args["runtime_root"], args["goal_id"])) + assert len([row for row in events if row["event_kind"] == "finding"]) == 1 diff --git a/tests/control_plane_ts/yardstick_decision.test.ts b/tests/control_plane_ts/yardstick_decision.test.ts index bdba64d458..f63a665c61 100644 --- a/tests/control_plane_ts/yardstick_decision.test.ts +++ b/tests/control_plane_ts/yardstick_decision.test.ts @@ -53,6 +53,18 @@ test("a late replay cannot replace a newer committed choice; exact profiles sele assert.equal(Object.hasOwn(cue, "fidelity_validated"), false); }); +test("only strictly older committed replay preserves a stale predecessor as history", () => { + const params = {enabled: true, decision, binding, findings: [finding]}; + assert.deepEqual(validateWorkYardstickContinuation({...params, + replay_committed_at: "2026-01-01T00:00:00.999999Z"}), {decision}); + for (const time of [undefined, "invalid", finding.last_updated_at, "2026-01-01T00:00:01.000001Z"]) { + assert.throws(() => validateWorkYardstickContinuation({...params, replay_committed_at: time}), /basis changed/); + } + assert.throws(() => normalizeWorkYardstickDecision({...decision, + replay_committed_at: "2026-01-01T00:00:00Z"}), /unsupported/, + "The attachment cannot supply replay provenance"); +}); + test("one optional attachment reaches an ordinary task context without creating a planner or off-mode cue", () => { const result = normalizeExploreResultAttachment({attachment: { schema_version: "explore_result_attachment_v0", question: "Does the screen rank useful candidates?", From b1428c3acc12307a27b54c2ab9181d501be78cf1 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Sat, 7 Nov 2026 11:27:33 +0800 Subject: [PATCH 3/3] fix(research): complete Yardstick transport type annotations Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- loopx/capabilities/todo_replan_cadence/machine_defaults.py | 2 +- loopx/control_plane/work_items/yardstick_decision.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/loopx/capabilities/todo_replan_cadence/machine_defaults.py b/loopx/capabilities/todo_replan_cadence/machine_defaults.py index 68faa90efb..b15113e49c 100644 --- a/loopx/capabilities/todo_replan_cadence/machine_defaults.py +++ b/loopx/capabilities/todo_replan_cadence/machine_defaults.py @@ -229,7 +229,7 @@ def resolve_todo_replan_cadence_goal( return apply_todo_replan_cadence_machine_default(goal, machine) -def work_yardstick_enabled(goal: dict[str, Any], runtime_root) -> bool: +def work_yardstick_enabled(goal: dict[str, Any], runtime_root: Path | None) -> bool: """Independent opt-in resolved through the existing review configuration.""" resolved = resolve_todo_replan_cadence_goal(goal, runtime_root) return (resolved.get("execution_profile") or {}).get("yardstick_enabled") is True diff --git a/loopx/control_plane/work_items/yardstick_decision.py b/loopx/control_plane/work_items/yardstick_decision.py index d9c649203e..7522de2800 100644 --- a/loopx/control_plane/work_items/yardstick_decision.py +++ b/loopx/control_plane/work_items/yardstick_decision.py @@ -3,7 +3,7 @@ from functools import lru_cache import json -from typing import Any +from typing import Any, cast from ..effect_runtime import EffectRuntimeRejected, effect_runtime_result from ..runtime.public_safety import validate_public_safe_value @@ -20,4 +20,5 @@ def _capture(encoded: str) -> str: def normalize_work_yardstick_capture(capture: Any, *, goal_id: str, agent_id: str, evidence_refs: Any) -> dict[str, Any]: params = dict(capture=capture, goal_id=goal_id, agent_id=agent_id, evidence_refs=evidence_refs) validate_public_safe_value(params, path="work_yardstick") - return json.loads(_capture(json.dumps(params, sort_keys=True, allow_nan=False))) + normalized = json.loads(_capture(json.dumps(params, sort_keys=True, allow_nan=False))) + return cast(dict[str, Any], normalized)