diff --git a/apps/presentation/dashboard/src/data/status.ts b/apps/presentation/dashboard/src/data/status.ts
index 301fcf817f..f655b87a8d 100644
--- a/apps/presentation/dashboard/src/data/status.ts
+++ b/apps/presentation/dashboard/src/data/status.ts
@@ -539,6 +539,7 @@ export const runRecordSchema = z.object({
});
export const runGoalSchema = z.object({
+ goal_objective: z.string().nullable().optional().catch(null),
zcode_goal_eligible_agent_ids: z.array(z.string()).optional().default([]).catch([]),
acceptance_observation: goalAcceptanceObservationSchema.optional().nullable().catch(null),
id: z.string(),
diff --git a/apps/presentation/dashboard/src/features/personal-workspace/goal-acceptance-contract.tsx b/apps/presentation/dashboard/src/features/personal-workspace/goal-acceptance-contract.tsx
index 51c63ccc94..51043aa448 100644
--- a/apps/presentation/dashboard/src/features/personal-workspace/goal-acceptance-contract.tsx
+++ b/apps/presentation/dashboard/src/features/personal-workspace/goal-acceptance-contract.tsx
@@ -81,7 +81,11 @@ export function GoalAcceptanceContractSection({goalId, contract: retained, copy,
} finally {if (token === generation.current) {flight.current = false; setBusy(false);}}
}
const unavailable = read.isError || actions.isError || !!error;
- if (!management && !enabled && !plan && !unavailable) return <>{children}{notice ?
{notice}
: null}>;
+ if (!management && !enabled && !plan && !unavailable) return <>{children}
+ {overview ? {contract?.enabled === false
+ ? (zh ? "验收标准未配置(可选)。" : "Acceptance criteria are not configured (optional).")
+ : (zh ? "此来源未提供验收标准(可选)。" : "This source has not provided acceptance criteria (optional).")}
: null}
+ {notice ? {notice}
: null}>;
const readOnly = !canManage;
const criterionText = (item: typeof criteria[number]) => item.oracle
? ({current: zh ? "当前版本已核对" : "Current versions checked", recorded: zh ? "历史结果,当前版本未核对" : "Recorded; current versions not checked", stale: zh ? "来源或版本已变化,请重新检查" : "Source or versions changed; check again", failed: zh ? "检查失败;修复交付物后可再次检查" : "Checks failed; repair the delivery before checking again", unverified: zh ? "尚未检查" : "Not checked", unavailable: zh ? "证据不可用" : "Evidence unavailable"})[fresh ? item.oracle.state : "recorded"]
diff --git a/apps/presentation/dashboard/src/features/personal-workspace/goal-masthead.tsx b/apps/presentation/dashboard/src/features/personal-workspace/goal-masthead.tsx
index 9759c4b859..235cee8c40 100644
--- a/apps/presentation/dashboard/src/features/personal-workspace/goal-masthead.tsx
+++ b/apps/presentation/dashboard/src/features/personal-workspace/goal-masthead.tsx
@@ -35,6 +35,7 @@ export function useGoalWorkspaceDigest(goal: WorkspaceGoal | null, userTodos: Wo
const byTodo = new Map(map?.nodes.flatMap(node => (node.refs.todo_ids ?? []).map(id => [id, node] as const)));
return compileGoalWorkspaceDigest({
observedAt: new Date().toISOString(),
+ objective: goal?.objective,
activation: goal?.activationState ?? "active",
todos: (goal?.agentTodos ?? []).map(todo => ({
todoId: todo.todoId, done: todo.done, status: todo.status, taskClass: todo.taskClass,
diff --git a/apps/presentation/dashboard/src/features/personal-workspace/goal-overview.tsx b/apps/presentation/dashboard/src/features/personal-workspace/goal-overview.tsx
index 5a06729b75..e5b32c3b13 100644
--- a/apps/presentation/dashboard/src/features/personal-workspace/goal-overview.tsx
+++ b/apps/presentation/dashboard/src/features/personal-workspace/goal-overview.tsx
@@ -1,4 +1,4 @@
-import {useEffect, useState, type ReactNode} from "react";
+import {type ReactNode} from "react";
import {type DeliveryReviewRead} from "./delivery-review-state";
import {GoalWorkspaceWork} from "./goal-workspace-work";
import { ArrowRight, Hand, Info } from "lucide-react";
@@ -40,23 +40,15 @@ export function GoalOverview({ active, goal, items, userTodos, readOnly, onOpenD
readOnly: boolean; onOpenDetails: () => void; onSelect: (selection: WorkspaceDrawerSelection | null) => void; onView: (view: WorkspaceGoalTab) => void;
}) {
const { t, locale } = useWorkspaceI18n();
- const [narrow, setNarrow] = useState(() => typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(max-width: 640px)").matches);
- const [directionExpanded, setDirectionExpanded] = useState(false);
- useEffect(() => {
- const media = window.matchMedia("(max-width: 640px)");
- const update = () => setNarrow(media.matches);
- update(); media.addEventListener("change", update);
- return () => media.removeEventListener("change", update);
- }, []);
const copy = locale === "zh-CN" ? {
direction: "方向", current: "当前推进", agentSummary: "Agent 的进展摘要", attention: "需要你", details: "Goal 信息",
tasks: "查看任务", execution: "执行记录", optional: "{count} 项可选",
- noTarget: "尚未声明可衡量目标。目标通过 Goal 的验收配置声明。",
+ objectiveUnavailable: "此来源尚未提供 Goal 的目标声明。",
remote: "此来源仅提供同步的状态与验收观察,交付链需要实时本机来源。",
} : {
direction: "Direction", current: "Current work", agentSummary: "Agent's progress summary", attention: "Needs you", details: "Goal information",
tasks: "View tasks", execution: "Execution", optional: "{count} optional",
- noTarget: "No measurable target declared. Targets are declared through the Goal's acceptance configuration.",
+ objectiveUnavailable: "This source has not provided the Goal's declared objective.",
remote: "This source provides synchronized status and acceptance observations. The delivery chain requires the live local source.",
};
const attention = userTodos.filter(item => item.goalId === goal.goalId);
@@ -68,7 +60,6 @@ export function GoalOverview({ active, goal, items, userTodos, readOnly, onOpenD
const moreAttention = needs.availability !== "ok" || needs.coverage !== "complete" || (totalAttention ?? attention.length) > 3;
const run = items.find((item): item is Extract => item.kind === "run" && item.run.goalId === goal.goalId);
const observation = review.snapshot?.acceptance ?? (goal.acceptanceObservation?.goal_id === goal.goalId ? goal.acceptanceObservation : null);
- const contract = observation?.goal_acceptance_contract?.enabled === true ? observation.goal_acceptance_contract : null;
const acceptanceCopy = goalAcceptanceContractCopy[locale === "zh-CN" ? "zh-CN" : "en"];
const sinceParts = sinceYouLooked ? [
sinceYouLooked.newlyDone ? t("since.newlyDone", { count: sinceYouLooked.newlyDone }) : null,
@@ -86,19 +77,15 @@ export function GoalOverview({ active, goal, items, userTodos, readOnly, onOpenD
- {contract ? copy.direction : copy.current}
+ {copy.direction}
-
-
-
-
{goal.nextSentence}
+
{digest.objective ?? copy.objectiveUnavailable}
+
{copy.current}
+ {goal.nextSentence}
{goal.agentSentence && goal.agentSentence !== goal.nextSentence ? {copy.agentSummary}
{goal.agentSentence}
: null}
- {copy.noTarget}
-
-
+
+
{run ? : null}
{showAttention ?
diff --git a/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-model.ts b/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-model.ts
index 107b1d5a45..469c91573a 100644
--- a/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-model.ts
+++ b/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-model.ts
@@ -158,6 +158,8 @@ export type WorkspaceGoalSubagentConfiguration = {
};
export type WorkspaceGoal = {
+ /** Bounded, safe text from this Goal's declared source document. */
+ objective?: string | null;
acceptanceObservation?: GoalAcceptanceObservation | null;
loadState?: "loading" | "error";
loadError?: WorkspaceLoadError;
diff --git a/apps/presentation/dashboard/src/views/dashboard-page.tsx b/apps/presentation/dashboard/src/views/dashboard-page.tsx
index f301bc5962..bbdcc13ee7 100644
--- a/apps/presentation/dashboard/src/views/dashboard-page.tsx
+++ b/apps/presentation/dashboard/src/views/dashboard-page.tsx
@@ -1060,6 +1060,7 @@ function buildPersonalHomeModel(
.find((value) => value !== "" && value !== "暂无") ?? t("projection.nextUpdatePending");
return [{
activationState: goal.activation_state,
+ objective: goal.goal_objective ?? null,
agentId: agentRow?.agentId ?? registeredAgentIds[0] ?? "codex",
agentLaneCount: goalAgentLanes.length,
agentLanes: goalAgentLanes,
diff --git a/docs/architecture/rfcs/goal-workspace-surface-v0.md b/docs/architecture/rfcs/goal-workspace-surface-v0.md
index 064a30028e..5a299fbac9 100644
--- a/docs/architecture/rfcs/goal-workspace-surface-v0.md
+++ b/docs/architecture/rfcs/goal-workspace-surface-v0.md
@@ -133,7 +133,7 @@ Facts at `5c62c5c03`:
| Tasks | `GoalTasksView` | Lanes: awaiting confirmation, pending/running, scheduled, completed; agent-lane filter; completed history from `/api/chat/completed-todos` | Not grouped by responsibility; text assignee; no observed elapsed time |
| Usage | `usage_summary` | 24h/7d tokens, `cost_usd`, `duration_ms`; fields absent when unmeasured; `sample_run_count`, `available` | No lifetime aggregate; no explicit per-runtime coverage |
| Acceptance | Goal authority via `goal-acceptance` (preview, provider CAS, exact replay); Dashboard reads only | Objective, criteria, coverage scope, bindings; observation projection is bounded and partial by design | No App declaration path; target absence is silent |
-| Goal brief | Status Goal snapshot | No general objective field; the Agent's progress sentence and, when enabled, the acceptance contract objective | The page has no stable brief for Goals without a contract |
+| Goal brief | Status Goal snapshot | Bounded declared Goal-document objective; progress and optional contract stay separate | Missing source content stays unavailable |
| Todo identity | Status `todoItemSchema` | Native Todos are addressed by `todo_id` and intentionally carry no synthetic index; legacy Markdown Todos keep `index` | A numbered rail would invent identity |
| Activity | `goal-activity.ts`, agent-lane `lastActivityAt`, host-thread activity, run timeline | Work-kind chip and per-lane observation | Not summarized as one observed last-activity fact |
| Themes | `workspace-theme.ts` (`loopx`, `paper`, `brutal`) | Light-first presets | No dark inverse of the LoopX theme |
@@ -154,7 +154,7 @@ owns cross-region page semantics, so this RFC owns composition only.
| --- | --- | --- |
| Masthead | Goal registry and lifecycle activation | `goal_lifecycle` typed action |
| Vitals | Todo projection, attention queue, `usage_summary`, execution observation | None; each cell focuses its region |
-| Direction | Acceptance contract and observation; progress summary (Section 12 D6) | Acceptance owner configuration (M3, Section 12 D3) |
+| Direction | Recorded Goal-document objective; separate acceptance contract and progress observation (Section 12 D6) | Acceptance owner configures checks, not the Goal's recorded objective (M3, Section 12 D3) |
| Needs you | Attention and gate owners; action review plan | Existing gate decision actions |
| Now, rail | Todo projection, execution observation, run timeline, `goal_task_map_v0` relations | Todo create dry-run/apply; existing drawer actions |
| Results | Files and Goal-results owner; acceptance owner for badges | Existing open/read actions |
@@ -178,7 +178,7 @@ screen.*
| 1 | **Masthead**: breadcrumb (Workspace / Goals / Goal), title, team line (Agent label or count, repository), Stop/Resume, settings | Always, on every Goal view; compacts on scroll | Identity and the one consequential control stay one glance away |
| 2 | **Vitals strip**: at most six cells; each cell is a button that focuses its region | Always | Answers moving / waiting on me / how far / what cost in one line |
| 3 | **Delta line**: "Since you looked at 09:12: 2 done · 1 new decision" | Only for a material delta; cost omitted until its owner qualifies the interval | The return moment the product promises |
-| 4 | **Direction**: objective, acceptance criteria with verification state | Always; criteria only with a contract | What "done" means and how close it is |
+| 4 | **Direction**: recorded Goal objective, separate optional acceptance criteria and verification state | Always; criteria only with a contract | What the Goal is for; what its configured checks establish |
| 5 | **Needs you**: decision cards with recommendation, consequence and actions | Only when non-empty | Decisions are the owner's actual work |
| 6 | **Now**: lens switch (By owner / Board / Map) and Add task | Open work exists | Who is doing what, who is blocked |
| 7 | **Results**: returned outputs, with acceptance shown separately | At least one result | What came back and whether it is trusted |
@@ -320,18 +320,20 @@ qualification remain required.
### 5.8 Direction and measurable target
-- When an acceptance contract is enabled, show its objective and criteria.
- Otherwise show the current progress sentence, labeled as the Agent's
- summary. Status projects no general Goal objective today; adding one is an
- owner decision (Section 12 D6), not a frontend inference from chat or Todo
- text.
+- Always show the Goal's recorded objective independently of acceptance. Status
+ transports the existing Goal document's `objective` metadata through the
+ typed workspace digest, with the existing public-safety filter and a 1,000
+ character bound. It performs no additional source read. Missing or unreadable
+ content stays unavailable; a title, Todo, progress sentence, Agent Vision or
+ acceptance contract cannot fill it. Current progress is a separate disclosure.
+ An enabled acceptance contract keeps its own scoped objective and criteria.
- Each criterion shows its owner state: verified (with time), failing, not yet
verified, or waiting on work. A summary reads "1 of 3 verified". Observation
partiality stays visible; an absence of gaps is not acceptance.
-- **No contract:** a quiet line, "No measurable target declared", with a
- "Declare target" action once M3 admits an App path. Until then, the line
- explains plainly that targets are declared through the Goal owner's
- acceptance configuration. A command-copy button is not offered as the action.
+- **No contract:** a quiet "Acceptance criteria are not configured (optional)"
+ line when that absence is observed; an older or incomplete source says that
+ criteria were not provided. Existing Goal settings own configuration. This
+ never denies the Goal's objective, enables a contract or creates a new gate.
- **M3:** declaration goes through the acceptance owner: objective, criteria
descriptions, coverage scope (`selected_work` or `all_advancement`) and
bindings. It follows preview, one confirmation, provider CAS and readback, and
@@ -394,7 +396,7 @@ separate identity.
- At 1200px and wider: sidebar, one content frame and the rail.
- From 768px to 1199px: the rail becomes a drawer toggled from the masthead.
- At 640px and narrower: one column in this order: masthead with an icon Stop,
- vitals in a 3×2 grid, the delta line, Needs you and Now. Direction collapses,
+ vitals in a 3×2 grid, the delta line, Needs you and Now. Current progress collapses; the recorded Goal objective stays visible,
and All work opens from the top bar.

@@ -654,7 +656,7 @@ local packaged rendering, not cross-host latency or W2 live scale. V6 desktop/mo
have been inspected; the PR's final validation record owns the full regression result. Activity covers
material observations available in the existing timeline, not a complete audit or independent acceptance.
Results keep their existing readback validators; run summaries cannot prove adoption.
-The approved narrow-screen presentation folds Direction on phones and opens All work from the
+The narrow-screen presentation keeps the Goal objective visible, folds current progress and opens All work from the
masthead in a native dialog. Escape restores launcher focus; selecting a task returns to the existing
inspector. History keeps its loaded state when reads are inactive, and missing remote Todo links
show an unavailable notice without querying local owners.
@@ -754,7 +756,7 @@ introduced; six uncalibrated process judgments retain their existing role.
| D3 | App authoring scope for acceptance targets | Goal acceptance authority | Descriptions, scope and bindings with checks kept local; full check authoring; none | The first option | M3 |
| D4 | Usage coverage signal | Usage RFC owner | Sample counts only; explicit measured versus observed runtimes | An explicit field before "partial" means more than sampling | M1 "partial" beyond sampling |
| D5 | Dark theme default | Presentation owner | Opt-in; follow system preference | Opt-in until AA review on real data | M4 |
-| D6 | Goal objective in status | Goal registry / Vision owner | Contract objective or progress summary only; an additive projected objective from the Goal's recorded creation or Vision text | The additive field, public-safe and length-bounded | A brief for Goals without a contract |
+| D6 | Goal objective in status | Goal registry / Vision owner | Recorded Goal-document objective, independent of acceptance and progress; unavailable remains explicit | The additive field, public-safe and length-bounded | A brief for Goals without a contract |
| D7 | Since-last-look spend | Usage RFC owner | Omit cost; owner-qualified interval aggregate or compatible monotonic counter | Omit cost until source/scope/currency/generation, interval coverage and retention are qualified; no rolling-total subtraction | Any cost in the M3 delta line |
## Appendix A: Execution ledger
diff --git a/docs/architecture/rfcs/goal-workspace-surface-v0.zh-CN.md b/docs/architecture/rfcs/goal-workspace-surface-v0.zh-CN.md
index 9f54f5eba9..039b9dd0aa 100644
--- a/docs/architecture/rfcs/goal-workspace-surface-v0.zh-CN.md
+++ b/docs/architecture/rfcs/goal-workspace-surface-v0.zh-CN.md
@@ -102,7 +102,7 @@ owner 隔了几个小时回到一个 Goal([GQ09](../../product/use-cases/stewa
| 任务 | `GoalTasksView` | 分栏:待确认、待处理/运行中、已安排、已完成;按 Agent 筛选;已完成历史来自 `/api/chat/completed-todos` | 未按负责人分组;负责人为纯文本;无观察到的耗时 |
| 用量 | `usage_summary` | 24h/7d token、`cost_usd`、`duration_ms`;未测量时字段缺失;`sample_run_count`、`available` | 无累计聚合;无明确的分运行时覆盖度 |
| 验收 | Goal 权限经 `goal-acceptance`(预览、provider CAS、精确重放);Dashboard 只读 | 目标、标准、覆盖范围、绑定;观测投影按设计有界且不完整 | 无 App 声明路径;缺少目标时界面无提示 |
-| Goal 简报 | 状态 Goal 快照 | 无通用目标字段;只有 Agent 进展句,以及启用时的验收合同目标 | 无合同的 Goal 没有稳定简报 |
+| Goal 简报 | 状态 Goal 快照 | 限长的 Goal 文档目标;进展与可选验收合同独立 | 来源缺失保持未提供 |
| Todo 身份 | 状态 `todoItemSchema` | 原生 Todo 以 `todo_id` 寻址,刻意不带合成序号;旧 Markdown Todo 保留 `index` | 编号侧栏会凭空制造身份 |
| 活动 | `goal-activity.ts`、Agent 分道 `lastActivityAt`、宿主线程活动、运行时间线 | 工作类型标记与分道观察 | 未汇总为一个“最近观察到的活动”事实 |
| 主题 | `workspace-theme.ts`(`loopx`、`paper`、`brutal`) | 浅色优先预设 | LoopX 主题没有深色反色 |
@@ -121,7 +121,7 @@ owner 隔了几个小时回到一个 Goal([GQ09](../../product/use-cases/stewa
| --- | --- | --- |
| 页头 | Goal registry 与生命周期激活状态 | `goal_lifecycle` 类型化动作 |
| 指标 | Todo 投影、attention 队列、`usage_summary`、执行观察 | 无;每格聚焦到对应区域 |
-| 方向 | 验收合同与观测;进展摘要(第 12 节 D6) | 验收 owner 配置(M3,第 12 节 D3) |
+| 方向 | 已记录 Goal 文档目标;独立的验收合同与进展观测(第 12 节 D6) | 验收 owner 配置检查,不改写已记录 Goal 目标(M3,第 12 节 D3) |
| 等你决定 | attention 与 Gate owner;动作审阅计划 | 既有 Gate 决定动作 |
| 当前工作、侧栏 | Todo 投影、执行观察、运行时间线、`goal_task_map_v0` 关系 | Todo 新建 dry-run/apply;既有抽屉动作 |
| 成果 | 文件与 Goal-results owner;验收标记来自验收 owner | 既有打开/阅读动作 |
@@ -142,7 +142,7 @@ owner 隔了几个小时回到一个 Goal([GQ09](../../product/use-cases/stewa
| 1 | **页头**:面包屑(工作区 / 目标 / Goal)、标题、团队行(Agent 名称或数量、仓库)、停止/恢复、设置 | 始终,在每个 Goal 视图;滚动时收紧 | 身份与唯一有后果的控制始终一眼可及 |
| 2 | **指标条**:最多六格,每格是聚焦对应区域的按钮 | 始终 | 一行回答在动吗/等我吗/到哪了/花多少 |
| 3 | **变化行**:“自你 09:12 查看以来:完成 2 项 · 新决定 1 项” | 仅有实质变化时;成本由其 owner 核验区间后才可加入 | 产品承诺的“回来看结果”时刻 |
-| 4 | **方向**:目标与带核验状态的验收标准 | 始终;有合同时显示标准 | “完成”指什么、离它多远 |
+| 4 | **方向**:已记录的 Goal 目标,独立且可选的验收标准与核验状态 | 始终;有合同时显示标准 | Goal 为了什么;已配置检查能证明什么 |
| 5 | **等你决定**:带建议、后果与动作的决定卡 | 仅非空时 | 决定才是 owner 真正的工作 |
| 6 | **当前工作**:镜头切换(按负责人 / 看板 / 地图)与新建任务 | 有未完成工作时 | 谁在做什么、谁被阻塞 |
| 7 | **成果**:已交回的产出,验收状态单独显示 | 至少一项成果时 | 交回了什么、是否可信 |
@@ -249,13 +249,14 @@ attention 条目、用量、执行观察、生命周期激活、验收摘要、
### 5.8 方向与可衡量目标
-- 启用验收合同时显示其目标与标准;否则显示当前进展句,并标明是 Agent 的摘要。状态当前
- 不投影通用 Goal 目标;新增该字段是 owner 决定(第 12 节 D6),不能由前端从对话或 Todo
- 文本推断。
+- 始终独立于验收显示 Goal 已记录的目标。状态将既有 Goal 文档的 `objective` 元数据经
+ 类型化工作区 digest 投影,复用公开安全过滤与 1,000 字符上限,不增加来源读取。
+ 内容缺失或不可读时明确未提供,不能用标题、Todo、进展句、Agent Vision 或验收合同
+ 补齐。当前进展另行折叠;启用的验收合同继续显示其自身覆盖范围内的目标与标准。
- 每条标准显示 owner 给出的状态:已核验(含时间)、未通过、尚未核验或等待工作。摘要写作
“3 项中 1 项已核验”。观测的不完整性保持可见:没有缺口不等于已验收。
-- **无合同:** 安静显示“未声明可衡量目标”;M3 提供 App 路径后附“声明目标”动作。在此之前,
- 该行用平实语言说明目标经由 Goal owner 的验收配置声明,不以复制命令按钮作为动作。
+- **无合同:** 已观察到未配置时安静显示“验收标准未配置(可选)”;旧来源或读取不完整时
+ 说明标准尚未提供。配置复用既有 Goal 设置;这不否定 Goal 目标、不启用合同或新增门禁。
- **M3:** 声明经由验收 owner:目标、标准描述、覆盖范围(`selected_work` 或
`all_advancement`)与绑定。流程为预览、一次确认、provider CAS 与回读,冲突时显示更新的
修订。可执行检查仍留在 owner 本地文件中(第 12 节 D3)。
@@ -303,7 +304,7 @@ attention 条目、用量、执行观察、生命周期激活、验收摘要、
- 1200px 及以上:侧边栏、一个内容框和侧栏。
- 768px 至 1199px:侧栏变为从页头切换的抽屉。
- 640px 及以下:单列,顺序为带图标停止的页头、3×2 指标、变化行、等你决定、当前工作;
- 方向折叠,全部工作从顶栏打开。
+ 当前推进折叠,已记录的 Goal 目标保持可见,全部工作从顶栏打开。

@@ -460,7 +461,7 @@ Agent/执行 owner 准备并完成分析。不要求用户填写来源 id、选
| M4 实时与表现力 | 由执行观察 owner 在行与地图上显示 W2 实时状态;事件动效;快捷键与命令面板;深色反色 | M2;W2 观察可用 | V6(动效、深色);团队实时 W2 验收 | 关闭动效/主题 |
**检查点 2026-10-12(M2 候选,#6255)。**
-已批准的窄屏呈现在手机默认折叠方向,通过顶部「全部工作」打开原生对话框;Escape 返回入口焦点,选择任务沿用已有检查器。非活动视图暂停读取而保留已载入历史;未知远端任务链接明确显示不可用,不查询本机 owner。
+窄屏呈现在手机保持 Goal 目标可见、默认折叠当前推进,通过顶部「全部工作」打开原生对话框;Escape 返回入口焦点,选择任务沿用已有检查器。非活动视图暂停读取而保留已载入历史;未知远端任务链接明确显示不可用,不查询本机 owner。
按负责人、未分配与决策关联任务由既有 TypeScript digest 分类;页头、Now、工作侧栏和看板
共用该结果。Map 与交付区域共用既有快照读取,只有当前可用的 typed dependency 才影响等待分类;
@@ -537,7 +538,7 @@ CLI 准备复用 producer 既有接口;本切片未验收新增飞书交互入
| D3 | App 编写验收目标的范围 | Goal 验收权限 | 只含描述、范围与绑定,检查留在本地;完整编写检查;不提供 | 第一项 | M3 |
| D4 | 用量覆盖度信号 | 用量 RFC owner | 仅采样计数;明确区分已测量与已观察的运行时 | 在“不完整”超出采样含义之前新增明确字段 | M1 中超出采样含义的“不完整” |
| D5 | 深色主题默认值 | 展示 owner | 手动开启;跟随系统 | 在真实数据上完成 AA 评审前保持手动开启 | M4 |
-| D6 | 状态中的 Goal 目标 | Goal registry / Vision owner | 只用合同目标或进展摘要;由 Goal 创建时记录或 Vision 文本投影的增量目标字段 | 采用可公开、限长的增量字段 | 为无合同 Goal 提供简报 |
+| D6 | 状态中的 Goal 目标 | Goal registry / Vision owner | 独立于验收和进展投影已记录 Goal 文档目标;来源缺失保持明确 | 采用可公开、限长的增量字段 | 为无合同 Goal 提供简报 |
| D7 | 自上次查看的花费 | 用量 RFC owner | 省略成本;owner 核验的区间聚合或兼容单调计数 | 来源/范围/币种/代际、区间覆盖度和保留期核验前省略;不减滚动总额 | M3 变化行中的任何成本 |
## 附录 A:执行账本
diff --git a/examples/personal-workspace-browser/assets/goal-objective-closeout/after-desktop.png b/examples/personal-workspace-browser/assets/goal-objective-closeout/after-desktop.png
new file mode 100644
index 0000000000..f018eccf0e
Binary files /dev/null and b/examples/personal-workspace-browser/assets/goal-objective-closeout/after-desktop.png differ
diff --git a/examples/personal-workspace-browser/assets/goal-objective-closeout/after-phone.png b/examples/personal-workspace-browser/assets/goal-objective-closeout/after-phone.png
new file mode 100644
index 0000000000..9a3aefac45
Binary files /dev/null and b/examples/personal-workspace-browser/assets/goal-objective-closeout/after-phone.png differ
diff --git a/examples/personal-workspace-browser/assets/goal-objective-closeout/before-desktop.png b/examples/personal-workspace-browser/assets/goal-objective-closeout/before-desktop.png
new file mode 100644
index 0000000000..708419cb85
Binary files /dev/null and b/examples/personal-workspace-browser/assets/goal-objective-closeout/before-desktop.png differ
diff --git a/examples/personal-workspace-browser/assets/goal-objective-closeout/before-phone.png b/examples/personal-workspace-browser/assets/goal-objective-closeout/before-phone.png
new file mode 100644
index 0000000000..c181bb8762
Binary files /dev/null and b/examples/personal-workspace-browser/assets/goal-objective-closeout/before-phone.png differ
diff --git a/examples/personal-workspace-browser/assets/goal-objective-closeout/enabled-phone.png b/examples/personal-workspace-browser/assets/goal-objective-closeout/enabled-phone.png
new file mode 100644
index 0000000000..aaac49968e
Binary files /dev/null and b/examples/personal-workspace-browser/assets/goal-objective-closeout/enabled-phone.png differ
diff --git a/loopx/control_plane/presentation/goal_workspace_digest.ts b/loopx/control_plane/presentation/goal_workspace_digest.ts
index 035bfe4980..7d788d5121 100644
--- a/loopx/control_plane/presentation/goal_workspace_digest.ts
+++ b/loopx/control_plane/presentation/goal_workspace_digest.ts
@@ -52,6 +52,8 @@ export type DigestActivity = { actor: string; at: string | null };
export type GoalWorkspaceDigestInput = {
observedAt: string;
+ /** Declared Goal document content; never an acceptance-contract or progress fallback. */
+ objective?: string | null;
activation: "active" | "stopped";
todos: readonly DigestTodo[];
/** Owner-projected done count; the item list carries only recent completions. */
@@ -76,6 +78,7 @@ export type DigestCoverage = "complete" | "partial" | "unknown";
export type GoalWorkspaceDigest = {
schema: typeof GOAL_WORKSPACE_DIGEST_SCHEMA;
observedAt: string;
+ objective: string | null;
lifecycle: { activation: "active" | "stopped"; finishingTurn: boolean };
work: {
open: number;
@@ -235,6 +238,7 @@ export function compileGoalWorkspaceDigest(input: GoalWorkspaceDigestInput): Goa
return {
schema: GOAL_WORKSPACE_DIGEST_SCHEMA,
observedAt: input.observedAt,
+ objective: input.objective?.trim() || null,
lifecycle: { activation: input.activation, finishingTurn: input.activation === "stopped" && executionObserved },
work: {
open: openWork.length,
diff --git a/loopx/control_plane/runtime/run_history.py b/loopx/control_plane/runtime/run_history.py
index 3213a4f38b..ba441ff946 100644
--- a/loopx/control_plane/runtime/run_history.py
+++ b/loopx/control_plane/runtime/run_history.py
@@ -95,6 +95,7 @@ def build_run_history(
"activation_state": goal.get("activation_state") or "active",
"registry_goal_digest": goal.get("registry_goal_digest"),
"display_name": goal.get("display_name"),
+ "goal_objective": goal.get("goal_objective"),
"domain": goal.get("domain"),
"status": goal.get("status"),
"lifecycle_phase": lifecycle_fields["lifecycle_phase"],
diff --git a/loopx/control_plane/todos/active_state_todos.py b/loopx/control_plane/todos/active_state_todos.py
index 273b4f1df3..a23b196c8d 100644
--- a/loopx/control_plane/todos/active_state_todos.py
+++ b/loopx/control_plane/todos/active_state_todos.py
@@ -9,6 +9,8 @@
canonical_todo_summary_fields,
read_canonical_todos_if_promoted,
)
+from ..goals.active_state_metadata import parse_state_frontmatter
+from ..runtime.public_safety import public_safe_compact_text
from .succession_warning import public_todo_summary
from .resume_condition import TodoResumeRolloutEvents
@@ -145,6 +147,12 @@ def active_state_todo_fields(
preferred_todo_ids=preferred_todo_ids,
rollout_events=events,
)
+ # Transport the Goal document's declared objective, independently of the
+ # optional acceptance contract. This is display content, never a write basis
+ # or a target inferred from Todos, progress, model output or a contract.
+ fields["goal_objective"] = public_safe_compact_text(
+ parse_state_frontmatter(state_text).get("objective"), limit=1000,
+ )
issue_meta_surface = parse_issue_meta_surface(state_text)
if issue_meta_surface:
fields["issue_meta_surface"] = issue_meta_surface
diff --git a/loopx/control_plane/work_items/attention_queue.py b/loopx/control_plane/work_items/attention_queue.py
index e264c9048f..b28ca6ef2b 100644
--- a/loopx/control_plane/work_items/attention_queue.py
+++ b/loopx/control_plane/work_items/attention_queue.py
@@ -144,6 +144,18 @@ def _reconcile_active_state_item(
return reconciled["item"], reconciled["warning"]
+def _active_state_goal_context(
+ goal: dict[str, Any],
+ current_run: dict[str, Any] | None,
+ context: AttentionQueueContext,
+ runtime_root: Path | None,
+) -> tuple[dict[str, Any], dict[str, Any] | None]:
+ fields = context.active_state_todo_fields(goal, runtime_root=runtime_root)
+ # Goal content survives even when no work creates an attention item.
+ goal["goal_objective"] = fields.get("goal_objective")
+ return fields, context.active_state_todo_attention_item(goal, fields, current_run)
+
+
def build_attention_queue(
*,
contract: dict[str, Any],
@@ -190,7 +202,9 @@ def build_attention_queue(
goal_latest_runs = goal.get("latest_runs") if isinstance(goal.get("latest_runs"), list) else []
if goal.get("registry_member"):
try:
- active_state_fields = context.active_state_todo_fields(goal, runtime_root=runtime_root)
+ active_state_fields, active_state_item = _active_state_goal_context(
+ goal, current_status_run, context, runtime_root,
+ )
except RetiredTodoEventSourceError as error:
# Refuse this source without hiding independent Goals or turning
# stale Todo/run projections into executable fallback work.
@@ -206,11 +220,6 @@ def build_attention_queue(
item["activation_state"] = str(goal.get("activation_state") or "active")
history_items.append(item)
continue
- active_state_item = context.active_state_todo_attention_item(
- goal,
- active_state_fields,
- current_status_run,
- )
if active_state_item and active_state_item.get("waiting_on") in {"controller", "user_or_controller"}:
item = active_state_item
else:
@@ -292,7 +301,8 @@ def build_attention_queue(
if goal.get("registry_member"):
if active_state_fields is None:
active_state_fields = context.active_state_todo_fields(goal, runtime_root=runtime_root)
- item.update(active_state_fields)
+ item.update({key: value for key, value in active_state_fields.items()
+ if key != "goal_objective"})
context.sync_connected_attention_action_from_todos(item)
external_progress_review = (
context.external_progress_review_context(goal, runtime_root)
diff --git a/tests/control_plane_ts/goal_workspace_digest.test.ts b/tests/control_plane_ts/goal_workspace_digest.test.ts
index 21e7e003cd..7813ef80a2 100644
--- a/tests/control_plane_ts/goal_workspace_digest.test.ts
+++ b/tests/control_plane_ts/goal_workspace_digest.test.ts
@@ -14,6 +14,16 @@ const base: GoalWorkspaceDigestInput = { observedAt, activation: "active", todos
const running = (observed: boolean, quiet = false, lastActivityAt: string | null = "2026-10-11T09:58:00.000Z") =>
({ kind: "running" as const, observed, quiet, lastActivityAt });
+test("a declared Goal objective is independent of work, progress and optional acceptance", () => {
+ assert.equal(compileGoalWorkspaceDigest({ ...base, objective: " Preserve the original research question. " }).objective,
+ "Preserve the original research question.");
+ for (const objective of [undefined, null, "", " "]) {
+ const digest = compileGoalWorkspaceDigest({ ...base, objective,
+ todos: [{todoId: "work", done: false}], execution: running(true) });
+ assert.equal(digest.objective, null, "Missing objective content is not inferred from activity or work");
+ }
+});
+
test("absent usage is unknown, never zero, and a measured zero stays zero", () => {
for (const usage of [undefined, null, {}]) {
const digest = compileGoalWorkspaceDigest({ ...base, usage });
diff --git a/tests/presentation/test_goal_objective.py b/tests/presentation/test_goal_objective.py
new file mode 100644
index 0000000000..e8e7d322e2
--- /dev/null
+++ b/tests/presentation/test_goal_objective.py
@@ -0,0 +1,112 @@
+"""Declared Goal content survives the real status/HTTP path without a contract."""
+
+from __future__ import annotations
+
+import http.client
+import json
+import runpy
+import threading
+from pathlib import Path
+
+import pytest
+
+from loopx.chat_server import ChatHTTPServer, ChatRequestHandler
+from loopx.control_plane.coordination.runtime_shadow import (
+ build_todo_runtime_shadow_projection,
+)
+from loopx.status import active_state_todo_fields
+
+# Reuse the provider fixture across test directories without duplicating a
+# File/SQLite initializer or depending on pytest's collection/import order.
+_providers = runpy.run_path(
+ str(Path(__file__).parents[1] / "control_plane/canonical_authority_fixture.py")
+)
+
+
+@pytest.fixture(params=["file", "sqlite"])
+def objective_source(tmp_path, monkeypatch, request):
+ _providers["isolate_sqlite_runtime"](tmp_path, monkeypatch)
+ state = tmp_path / "state.md"
+ state.write_text(
+ '---\nobjective: "Answer the original research question."\n---\n\n# Goal\n'
+ )
+ runtime = tmp_path / "runtime"
+ goal = {
+ "id": "research",
+ "display_name": "A different display title",
+ "repo": str(tmp_path),
+ "state_file": "state.md",
+ "adapter": {"kind": "read_only_project_map_v0"},
+ }
+ projection = build_todo_runtime_shadow_projection(goal_id="research", todos=[])
+ _providers["initialize_canonical_authority"](
+ runtime, "research", projection, state_path=state, provider=request.param
+ )
+ registry = tmp_path / "registry.json"
+ registry.write_text(
+ json.dumps(
+ {"schema_version": 1, "common_runtime_root": str(runtime), "goals": [goal]}
+ )
+ )
+ return goal, registry, runtime, state
+
+
+def test_declared_objective_is_not_an_acceptance_contract_or_task(objective_source):
+ goal, _, runtime, state = objective_source
+ assert (
+ active_state_todo_fields(goal, runtime_root=runtime)["goal_objective"]
+ == "Answer the original research question."
+ )
+ for content in [
+ "# Goal\n## Next Action\nInfer nothing from this progress.\n",
+ '---\nobjective: ""\n---\n',
+ ]:
+ state.write_text(content)
+ assert (
+ active_state_todo_fields(goal, runtime_root=runtime)["goal_objective"]
+ is None
+ )
+ state.write_text('---\nobjective: "' + "Declared text. " * 200 + '"\n---\n')
+ assert (
+ len(active_state_todo_fields(goal, runtime_root=runtime)["goal_objective"])
+ <= 1000
+ )
+ state.unlink()
+ assert (
+ active_state_todo_fields(goal, runtime_root=runtime)["goal_objective"] is None
+ )
+
+
+def test_real_status_http_keeps_an_empty_goals_declared_objective(objective_source):
+ _, registry, runtime, state = objective_source
+ before = registry.read_bytes(), state.read_bytes()
+ server = ChatHTTPServer(("127.0.0.1", 0), ChatRequestHandler)
+ server.registry_path, server.runtime_root, server.runtime_root_override = (
+ registry,
+ runtime,
+ str(runtime),
+ )
+ server.selected_goal_id, server.scan_roots, server.limit, server.verbose = (
+ None,
+ [],
+ 10,
+ False,
+ )
+ thread = threading.Thread(target=server.serve_forever, daemon=True)
+ thread.start()
+ try:
+ connection = http.client.HTTPConnection(*server.server_address, timeout=45)
+ connection.request("GET", "/status.json?goal_id=research")
+ response = connection.getresponse()
+ payload = json.loads(response.read())
+ connection.close()
+ assert response.status == 200, payload
+ assert (
+ payload["run_history"]["goals"][0]["goal_objective"]
+ == "Answer the original research question."
+ )
+ assert (registry.read_bytes(), state.read_bytes()) == before
+ finally:
+ server.shutdown()
+ thread.join(timeout=5)
+ server.server_close()