Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,15 @@ export const goalAcceptanceObservationSchema = z.object({
missing_sources: z.array(z.string()),
truncated: z.boolean(),
historical_progress: z.array(z.object({ kind: z.string(), observed_at: nullableText, source: z.string(), evidence_refs: z.array(z.string()) })),
acceptance_gaps: z.array(z.object({ kind: z.string(), owner: nullableText, reason: nullableText, evidence_required: nullableText, observed_at: nullableText, source: z.string() })),
acceptance_gaps: z.array(z.object({
kind: z.string(), owner: nullableText, reason: nullableText, evidence_required: nullableText, observed_at: nullableText, source: z.string(),
reason_code: z.string().optional(), resolution_hint: z.string().optional(),
component_checks: z.object({
checkpoint_satisfied: z.boolean(), checkpoint_fresh: z.boolean(),
path_outcome_valid: z.boolean(), evidence_refs_present: z.boolean(),
final_outcome_claim_present: z.boolean(), no_reported_outcome_gap: z.boolean(),
}).optional(),
})),
guards: z.array(z.object({ kind: z.string(), todo_id: nullableText, blocks_agent: nullableText, owner: nullableText, reason: nullableText, evidence_required: nullableText, decision_scope: nullableText })),
next_action: nullableText,
next_action_source: nullableText,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,16 @@ import type { WorkspaceGoal } from "./personal-workspace-model";
import { useWorkspaceI18n } from "./i18n";

export function GoalAcceptanceObservationCard({ goal }: { goal: WorkspaceGoal }) {
const { t } = useWorkspaceI18n();
const { t, locale } = useWorkspaceI18n();
const checkLabels = locale === "zh-CN" ? {
checkpoint_satisfied: "检查点满足", checkpoint_fresh: "检查点有效",
path_outcome_valid: "路径决策有效", evidence_refs_present: "证据引用齐全",
final_outcome_claim_present: "最终成果声明齐全", no_reported_outcome_gap: "无已报告成果缺口",
} : {
checkpoint_satisfied: "Checkpoint satisfied", checkpoint_fresh: "Checkpoint current",
path_outcome_valid: "Valid path decision", evidence_refs_present: "Evidence refs present",
final_outcome_claim_present: "Final outcome claim present", no_reported_outcome_gap: "No reported outcome gap",
};
const labels = {
connected: t("acceptance.connected"), mapped: t("acceptance.mapped"), refreshed: t("acceptance.refreshed"),
adapter_inspected: t("acceptance.inspected"), run_recorded: t("acceptance.recorded"), reward_judged: t("acceptance.judged"),
Expand All @@ -20,6 +29,10 @@ export function GoalAcceptanceObservationCard({ goal }: { goal: WorkspaceGoal })
<h4>{t("acceptance.gaps")}</h4>
{projection.acceptance_gaps.length ? projection.acceptance_gaps.map((gap, index) => <div className="personal-acceptance-observation" key={`${gap.kind}:${gap.owner}:${index}`}>
<p>{gap.reason ?? t("acceptance.reasonUnknown")}</p>
{gap.resolution_hint ? <p>{gap.resolution_hint}</p> : null}
{gap.component_checks ? <div>{Object.entries(gap.component_checks).map(([key, passed]) =>
<p key={key}>{checkLabels[key as keyof typeof checkLabels]}: {locale === "zh-CN" ? (passed ? "通过" : "未通过") : (passed ? "Passed" : "Failed")}</p>
)}</div> : null}
<dl><div><dt>{t("common.owner")}</dt><dd>{gap.owner ?? t("acceptance.unknown")}</dd></div>
<div><dt>{t("acceptance.required")}</dt><dd>{gap.evidence_required ?? t("acceptance.unknown")}</dd></div>
<div><dt>{t("acceptance.observed")}</dt><dd>{gap.observed_at ?? t("acceptance.unknown")}</dd></div></dl>
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
29 changes: 29 additions & 0 deletions docs/reference/protocols/goal-vision-replan-contract-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,35 @@ source references with the typed observation.

## Write / Correction Mechanism

After a material milestone, `vision_outcome_checkpoint_required` remains a
completion guard. When the checkpoint is satisfied and current, the path outcome
is `continue`, `no_change`, or `replan`, evidence refs are present, and no
`outcome_gap` was reported, an absent or blank `vision_patch.acceptance_summary`
is diagnosed as `final_outcome_claim_missing`. Add or restore a bounded claim
supported by those evidence refs; a learning milestone alone does not prove
the final outcome. Keep the valid route and evidence in the correction. The
existing durable-field write gate still requires `path_delta.outcome=replan`
when changing the claim against an open replan obligation; record that bounded
claim correction rather than repeating a generic path investigation.

The gap and replan trigger carry `reason_code`, `component_checks`, and
`resolution_hint`. The same diagnostic appears in the compact CLI audit,
managed Turn contract capsule, Goal acceptance observations, and Lark projection
rows. Component checks distinguish checkpoint satisfaction and freshness, path
validity, evidence presence, claim presence, and reported outcome gaps. Other
incomplete combinations retain the guard and use
`final_outcome_checkpoint_incomplete`. Diagnostics are read-only: retrying
`quota should-run` neither spends quota nor supplies acceptance evidence, and
the added explanation does not change the obligation's identity.

Synthetic Goal acceptance views (read-only fixture, no connected execution
service) show the missing-claim diagnosis and passed/failed components:

| View | Before | After |
| --- | --- | --- |
| Desktop, Chinese | [Before](../../assets/personal-workspace/final-outcome-claim-before-desktop.png) | [After](../../assets/personal-workspace/final-outcome-claim-after-desktop.png) |
| Mobile, English | [Before](../../assets/personal-workspace/final-outcome-claim-before-mobile.png) | [After](../../assets/personal-workspace/final-outcome-claim-after-mobile.png) |

Vision correction is a normal state-machine transition, not only a
self-repair fallback. Agents should write a bounded vision patch when:

Expand Down
7 changes: 5 additions & 2 deletions examples/dashboard-goal-acceptance-browser-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const port = Number(process.env.LOOPX_GOAL_ACCEPTANCE_PORT ?? 5291);
const packaged = process.env.LOOPX_GOAL_ACCEPTANCE_PACKAGED === "1";
const python = process.env.LOOPX_PYTHON ?? "python3";
const payload = JSON.parse(execFileSync(python, ["-c", "import runpy,tempfile,json; from pathlib import Path; m=runpy.run_path('tests/control_plane/test_goal_acceptance_observation.py'); t=tempfile.TemporaryDirectory(); print(json.dumps(m['collect_fixture'](Path(t.name))))"], { cwd: root, encoding: "utf8" }));
const payload = JSON.parse(execFileSync(python, ["-c", "import runpy,tempfile,json; from pathlib import Path; m=runpy.run_path('tests/control_plane/test_goal_acceptance_observation.py'); t=tempfile.TemporaryDirectory(); print(json.dumps(m['collect_fixture'](Path(t.name), missing_claim=True)))"], { cwd: root, encoding: "utf8" }));
const dashboardDir = resolve(root, "apps/presentation/dashboard");
const server = packaged
? spawn(python, ["-m", "http.server", String(port), "--bind", "127.0.0.1", "--directory", resolve(root, "loopx/web")], { stdio: "ignore" })
Expand All @@ -38,9 +38,12 @@ try {
const card = page.locator(".personal-goal-acceptance");
await card.getByText("Independent verification report", {exact: true}).waitFor();
assert.match(await card.innerText(), /agent-a/);
assert.match(await card.innerText(), /vision_patch.acceptance_summary/);
assert.match(await card.innerText(), language === "en" ? /Final outcome claim present: Failed/ : /最终成果声明齐全: 未通过/);
assert.match(await card.innerText(), language === "en" ? /Valid path decision: Passed/ : /路径决策有效: 通过/);
assert.match(await card.innerText(), language === "en" ? /do not prove Goal acceptance/ : /不能证明 Goal 已通过验收/);
await card.locator("summary").click();
await card.scrollIntoViewIfNeeded();
await card.locator(".personal-detail-card-title").scrollIntoViewIfNeeded();
assert.equal(await card.evaluate(el => el.scrollWidth > el.clientWidth + 2), false, "Acceptance content must wrap on mobile");
await page.screenshot({path: resolve(output, `${packaged ? "packaged" : "development"}-${language}.png`)});
if (language === "en") {
Expand Down
19 changes: 16 additions & 3 deletions loopx/control_plane/goals/acceptance_observation.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
acceptance_gaps_from_agent_vision,
latest_agent_vision_from_runs,
)
from .goal_frontier.outcome_continuity import (
acceptance_gaps_from_outcome_checkpoint,
latest_outcome_vision_checkpoint_from_status_payload,
)

GOAL_ACCEPTANCE_OBSERVATION_SCHEMA_VERSION = "goal_acceptance_observation_projection_v0"
OBSERVATION_LIMIT = 12
Expand Down Expand Up @@ -130,9 +134,13 @@ def build_goal_acceptance_observation(
agent_runs, goal_id=goal_id, agent_id=agent or None
)
patch = _dict(_dict(vision).get("vision_patch"))
for gap in acceptance_gaps_from_agent_vision(
checkpoint = latest_outcome_vision_checkpoint_from_status_payload(
{"run_history": {"goals": [goal]}}, goal_id=goal_id, agent_id=agent or None,
)
agent_gaps = acceptance_gaps_from_agent_vision(
vision, goal_status=goal.get("status")
):
) + acceptance_gaps_from_outcome_checkpoint(vision, checkpoint)
for gap in agent_gaps:
gaps.append(
{
"kind": _text(gap.get("kind")) or "acceptance_gap",
Expand All @@ -145,7 +153,12 @@ def build_goal_acceptance_observation(
if gap.get("kind") == "vision_acceptance_gap"
else _text(gap.get("acceptance_summary")),
"observed_at": _text(gap.get("generated_at")),
"source": "latest_agent_vision",
"source": gap.get("source") or "latest_agent_vision",
**{
key: gap[key]
for key in ("reason_code", "component_checks", "resolution_hint")
if key in gap
},
}
)
guards: list[dict[str, Any]] = []
Expand Down
17 changes: 17 additions & 0 deletions loopx/control_plane/goals/goal_frontier/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,13 @@ def build_vision_continuation_audit(
}
if acceptance_requirements:
audit["acceptance_requirements"] = acceptance_requirements[:5]
diagnostics = [
{key: gap[key] for key in ("reason_code", "component_checks", "resolution_hint")}
for gap in compact_acceptance_gaps
if gap.get("reason_code") and gap.get("component_checks") and gap.get("resolution_hint")
]
if diagnostics:
audit["outcome_checkpoint_diagnostics"] = diagnostics[:5]
return audit


Expand Down Expand Up @@ -1146,6 +1153,9 @@ def derive_goal_frontier_replan_obligation_from_summaries(
"completed_todo_count",
"completed_todo_threshold",
"completed_todo_ids",
"reason_code",
"component_checks",
"resolution_hint",
)
if gap.get(key) is not None
},
Expand Down Expand Up @@ -1174,6 +1184,13 @@ def derive_goal_frontier_replan_obligation_from_summaries(
"production actions, or owner-only decisions"
),
recommended_action=(
" ".join(
str(gap["resolution_hint"])
for gap in compact_acceptance_gaps
if gap.get("resolution_hint")
)
if all(gap.get("resolution_hint") for gap in compact_acceptance_gaps)
else
"run a bounded vision-gap replan before another quiet poll: create "
"successor work, update the agent vision, record evidence gap, or "
"record no-follow-up"
Expand Down
47 changes: 38 additions & 9 deletions loopx/control_plane/goals/goal_frontier/outcome_continuity.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

from datetime import UTC, datetime
from enum import StrEnum
from typing import Any

from ...agents.agent_scope import agent_scope_item_claimed_by
Expand All @@ -21,6 +22,13 @@
"no_change",
"replan",
}


class OutcomeCheckpointReason(StrEnum):
CLAIM_MISSING = "final_outcome_claim_missing"
INCOMPLETE = "final_outcome_checkpoint_incomplete"


def _compact_text(value: Any, *, limit: int) -> str | None:
text = " ".join(str(value or "").strip().split())
return text[:limit] if text else None
Expand Down Expand Up @@ -267,22 +275,43 @@ def acceptance_gaps_from_outcome_checkpoint(
== qualification_vision.get("generated_at")
)
outcome_gap_reported = "outcome_gap" in delivery_outcomes
checkpoint_complete = bool(
checkpoint.get("satisfied") is True
and (fresh_vision_patch or unchanged_vision_revision)
and claim
and evidence_refs
and path_outcome in VISION_OUTCOME_CHECKPOINT_CONTINUATION_OUTCOMES
and not outcome_gap_reported
)
if checkpoint_complete:
# The same component facts own both qualification and its explanation.
component_checks = {
"checkpoint_satisfied": checkpoint.get("satisfied") is True,
"checkpoint_fresh": fresh_vision_patch or unchanged_vision_revision,
"path_outcome_valid": path_outcome in VISION_OUTCOME_CHECKPOINT_CONTINUATION_OUTCOMES,
"evidence_refs_present": bool(evidence_refs),
"final_outcome_claim_present": bool(claim),
"no_reported_outcome_gap": not outcome_gap_reported,
}
failed_components = [key for key, passed in component_checks.items() if not passed]
if not failed_components:
return []
claim_only_missing = failed_components == ["final_outcome_claim_present"]
reason = (
OutcomeCheckpointReason.CLAIM_MISSING
if claim_only_missing else OutcomeCheckpointReason.INCOMPLETE
)

gap: dict[str, Any] = {
"kind": VISION_OUTCOME_CHECKPOINT_REQUIRED_TRIGGER,
"source": "latest_vision_checkpoint",
"agent_id": checkpoint.get("agent_id") or agent_vision.get("agent_id"),
"reason_code": reason.value,
"component_checks": component_checks,
"resolution_hint": (
"Add or restore a bounded vision_patch.acceptance_summary stating the "
"final outcome supported by the existing evidence. Retain that route "
"and its refs in the required path_delta.outcome=replan correction. "
"A learning milestone alone is not completion."
if claim_only_missing else
"Repair the failed checkpoint components with a fresh evidence-linked "
"final-outcome claim and a valid continuation path decision."
),
"replan_trigger_summary": (
"the checkpoint, path decision, and evidence are valid, but the "
"final-outcome claim (vision_patch.acceptance_summary) is missing"
if claim_only_missing else
"a material milestone closed without a fresh evidence-linked final-outcome "
"path decision"
),
Expand Down
1 change: 1 addition & 0 deletions loopx/control_plane/quota/cli_projection.py
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,7 @@ def _compact_vision_continuation_audit(
"trigger_kinds",
"required_before_closeout",
"recommended_action",
"outcome_checkpoint_diagnostics",
)
compact = {key: audit[key] for key in retained_fields if key in audit}
judge = audit.get("vision_gap_judge")
Expand Down
19 changes: 19 additions & 0 deletions loopx/control_plane/quota/turn_envelope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,25 @@ function contractCapsule(
);
if (Object.keys(compact).length > 0) capsule[sourceKey] = compact;
}
// Preserve the bounded source diagnosis; the generic capsule list path is
// for scalar lists and must not stringify structured component checks.
const diagnostics = object(payload.vision_continuation_audit).outcome_checkpoint_diagnostics;
if (Array.isArray(diagnostics) && diagnostics.length > 0) {
const audit = object(capsule.vision_continuation_audit);
audit.outcome_checkpoint_diagnostics = diagnostics.slice(0, 5).map((value) => {
const source = object(value);
const checks = object(source.component_checks);
return {
reason_code: text(source.reason_code, 80),
resolution_hint: text(source.resolution_hint, 420),
component_checks: Object.fromEntries([
"checkpoint_satisfied", "checkpoint_fresh", "path_outcome_valid",
"evidence_refs_present", "final_outcome_claim_present", "no_reported_outcome_gap",
].filter((key) => typeof checks[key] === "boolean").map((key) => [key, checks[key]])),
};
});
capsule.vision_continuation_audit = audit;
}
const taskScope = text(payload.task_scope, 80);
if (taskScope) capsule.task_scope = taskScope;
const workLane = object(payload.work_lane_contract);
Expand Down
4 changes: 4 additions & 0 deletions loopx/control_plane/status/agent_lane_projection.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ def _compact_vision_audit(value: Any) -> dict[str, Any] | None:
"selected_todo_is_goal_completion",
"closeout_allowed_without_evidence",
"recommended_action",
"outcome_checkpoint_diagnostics",
)
if value.get(key) is not None
}
Expand Down Expand Up @@ -263,6 +264,9 @@ def _compact_goal_frontier(value: Any) -> dict[str, Any] | None:
"source",
"agent_id",
"replan_trigger_source",
"reason_code",
"component_checks",
"resolution_hint",
)
if gap.get(key) is not None
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,10 @@ def project_replan_writeback_rejection(
compact_triggers = [
{
key: trigger.get(key)
for key in ("kind", "todo_id", "completion_turn_key")
for key in (
"kind", "todo_id", "completion_turn_key",
"reason_code", "component_checks", "resolution_hint",
)
if trigger.get(key) is not None
}
for trigger in obligation.get("triggers") or []
Expand Down
27 changes: 27 additions & 0 deletions loopx/extensions/lark/presentation/projection_rows.py
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,33 @@ def add_row(row: dict[str, Any]) -> None:
)
)

frontier = _as_mapping(
projection.get("goal_frontier_projection")
or project_asset.get("goal_frontier_projection")
)
audit = _as_mapping(
projection.get("vision_continuation_audit")
or frontier.get("vision_continuation_audit")
)
for diagnostic in _as_mapping_list(audit.get("outcome_checkpoint_diagnostics")):
checks = _as_mapping(diagnostic.get("component_checks"))
check_summary = ", ".join(
f"{key}={'pass' if passed else 'fail'}" for key, passed in checks.items()
)
add_row(_projection_row(
source_id=source_id, goal_id=resolved_goal_id,
kind="outcome_checkpoint", identity=diagnostic.get("reason_code"),
role="agent", projection_agent_id=audit.get("agent_id") or payload_agent_id,
item={
**diagnostic,
"claimed_by": audit.get("agent_id") or payload_agent_id,
"text": diagnostic.get("resolution_hint"),
"evidence": f"{diagnostic.get('reason_code')}: {check_summary}",
"status": "open", "priority": "P0",
},
fallback_text="Resolve the final-outcome checkpoint gap",
))

next_action = _as_mapping(projection.get("agent_lane_next_action"))
if not next_action and projection.get("next_action"):
next_action = {
Expand Down
9 changes: 9 additions & 0 deletions loopx/presentation/renderers/quota_markdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -664,6 +664,15 @@ def render_quota_should_run_markdown(payload: dict[str, Any]) -> str:
f"trigger_count={replan_obligation.get('trigger_count')} "
f"triggers={','.join(trigger_kinds)}"
)
for trigger in as_list(replan_obligation.get("triggers")):
if not isinstance(trigger, dict) or not trigger.get("reason_code"):
continue
lines.append(f" - reason_code: {trigger['reason_code']}")
checks = as_dict(trigger.get("component_checks"))
lines.append(" - component_checks: " + ", ".join(
f"{key}={'pass' if passed else 'fail'}" for key, passed in checks.items()
))
lines.append(f" - resolution: {trigger.get('resolution_hint')}")
required_reads = as_list(payload.get("required_reads"))
for read in required_reads[:3]:
if not isinstance(read, dict):
Expand Down
Loading