@tea-agent/loop-agent 0.33.7-beta.0 → 0.34.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/CHANGELOG.md +67 -16
  2. package/dist/application/task-lifecycle/advance.js +309 -9
  3. package/dist/application/task-lifecycle/gates.js +50 -0
  4. package/dist/application/task-lifecycle/observe.js +11 -2
  5. package/dist/application/task-lifecycle/plan-transitions.js +13 -21
  6. package/dist/commands/init-upgrade.js +32 -1
  7. package/dist/commands/init.js +94 -3
  8. package/dist/executors/dag-pi-executor.js +18 -3
  9. package/dist/executors/shell-executor.js +4 -91
  10. package/dist/executors/shell-write-guard.js +26 -8
  11. package/dist/shared/operator/capabilities.js +98 -44
  12. package/dist/shared/resilient-git.js +133 -0
  13. package/dist/task/source-prepare/artifact-meta.js +137 -0
  14. package/dist/task/source-prepare/index.js +2 -0
  15. package/dist/task/source-prepare/parse-intent.js +58 -10
  16. package/dist/task/source-prepare/prepare.js +229 -18
  17. package/dist/task/source-prepare/reference-integrity.js +18 -2
  18. package/dist/task/source-prepare/semantic-intake.js +404 -0
  19. package/dist/worker/console/app-data.js +2 -0
  20. package/dist/worker/console/chat/chat-event-store.js +190 -25
  21. package/dist/worker/console/chat/model-resolver.js +17 -0
  22. package/dist/worker/console/chat/pi-console-config.js +250 -32
  23. package/dist/worker/console/chat/pi-runtime.js +1007 -188
  24. package/dist/worker/console/chat/resource-loader.js +5 -4
  25. package/dist/worker/console/chat/routes.js +495 -157
  26. package/dist/worker/console/chat/runtime-context.js +48 -12
  27. package/dist/worker/console/chat/runtime-selection.js +59 -0
  28. package/dist/worker/console/chat/session-store.js +39 -0
  29. package/dist/worker/console/chat/shortcuts.js +1 -0
  30. package/dist/worker/console/chat/tool-adapter.js +9 -3
  31. package/dist/worker/console/chat/tools.js +5 -1
  32. package/dist/worker/console/dag-execution-receipt.js +380 -0
  33. package/dist/worker/console/interview/grill-me.js +7 -6
  34. package/dist/worker/console/operator-actions.js +785 -85
  35. package/dist/worker/console/prd-intake-bridge.js +393 -0
  36. package/dist/worker/console/recovery-cta.js +35 -6
  37. package/dist/worker/console/server.js +8 -15
  38. package/dist/worker/console/static/assets/index-BQkhJpV8.css +1 -0
  39. package/dist/worker/console/static/assets/index-BpuHmlSP.js +29 -0
  40. package/dist/worker/console/static/index.html +2 -2
  41. package/dist/worker/console/static-src/app/console-types.js +1 -0
  42. package/dist/worker/console/static-src/app/usePrdImport.js +2 -1
  43. package/dist/worker/console/static-src/app/useRecoveryActions.js +24 -1
  44. package/dist/worker/console/static-src/app/useRecoveryConsole.js +19 -1
  45. package/dist/worker/console/static-src/app/useTaskWizard.js +57 -2
  46. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +45 -8
  47. package/dist/worker/console/static-src/operator-chat/landing-density.js +23 -0
  48. package/dist/worker/console/static-src/operator-chat/refs.js +9 -0
  49. package/dist/worker/console/static-src/operator-chat/runtime-snapshot-store.js +257 -0
  50. package/dist/worker/console/static-src/operator-chat/session-title-watcher.js +128 -0
  51. package/dist/worker/console/static-src/operator-chat/sidebar-split.js +90 -0
  52. package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +37 -0
  53. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +125 -22
  54. package/dist/worker/console/static-src/operator-chat/useChatStream.js +215 -184
  55. package/dist/worker/console/static-src/operator-chat/useChatThread.js +49 -5
  56. package/dist/worker/console/static-src/operator-chat/useComposer.js +17 -0
  57. package/dist/worker/console/static-src/operator-chat/useOverlayFocus.js +84 -0
  58. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +225 -74
  59. package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +196 -0
  60. package/dist/worker/console/static-src/operator-chat/useWorkspaceLayout.js +58 -0
  61. package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +31 -0
  62. package/dist/worker/delivery/final-verification.js +13 -5
  63. package/dist/worker/delivery/package.js +31 -19
  64. package/dist/worker/delivery/verification-bundle.js +6 -4
  65. package/dist/worker/observability/read-model.js +3 -0
  66. package/dist/worker/observe/static/operator-chrome.css +5 -2
  67. package/dist/worker/observe/static/operator-chrome.js +6 -1
  68. package/dist/worker/observe/static/styles.css +39 -9
  69. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +33 -462
  70. package/dist/workflows/dag/backend-test-case-manifest.js +0 -4
  71. package/dist/workflows/dag/backend-test-markdown-workflow.js +1 -25
  72. package/dist/workflows/dag/backend-test-module-stem.js +0 -5
  73. package/dist/workflows/dag/backend-test-pytest-collection.js +24 -345
  74. package/dist/workflows/dag/backend-test-scenario-param.js +82 -269
  75. package/dist/workflows/dag/backend-test-writer-completeness.js +16 -47
  76. package/dist/workflows/dag/dynamic-runtime/map.js +8 -24
  77. package/dist/workflows/dag/failure-category.js +3 -0
  78. package/dist/workflows/dag/frontend-worktree-diff.js +12 -27
  79. package/dist/workflows/dag/init-hybrid.js +49 -55
  80. package/dist/workflows/dag/node-execution.js +3 -2
  81. package/dist/workflows/dag/retry-policy.js +44 -11
  82. package/dist/workflows/dag/runner.js +6 -0
  83. package/dist/workflows/dag/scheduler.js +49 -1
  84. package/dist/workflows/dag/types.js +0 -7
  85. package/dist/workflows/dag/validate.js +16 -2
  86. package/dist/workflows/dag/workspace-checkpoint.js +8 -27
  87. package/docs/templates/agent-dag.schema.json +4 -4
  88. package/docs/templates/backend-test-dag.json +29 -32
  89. package/harness.json +1 -1
  90. package/package.json +1 -1
  91. package/skills/local-jacoco-coverage/SKILL.md +281 -0
  92. package/skills/local-jacoco-coverage/references/requirement-to-source-mapping.md +85 -0
  93. package/skills/local-jacoco-coverage/references/runtime-alignment.md +106 -0
  94. package/skills/local-jacoco-coverage/scripts/run-coverage-analysis.sh +148 -0
  95. package/skills/local-jacoco-coverage/scripts/start-jacoco-agent.sh +110 -0
  96. package/skills/loop-agent/references/command-reference.md +3 -1
  97. package/skills/loop-agent/references/source-and-plan-practice.md +13 -0
  98. package/skills/loop-agent/references/task-workflow.md +4 -0
  99. package/dist/worker/console/chat/instruction-skills.js +0 -217
  100. package/dist/worker/console/static/assets/index-CnUXAqxG.css +0 -1
  101. package/dist/worker/console/static/assets/index-CteJFFL2.js +0 -29
package/CHANGELOG.md CHANGED
@@ -2,38 +2,89 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
- ## [0.33.7-beta.0] - 2026-08-11
5
+ ## [0.34.1] - 2026-08-11
6
6
 
7
7
  ### 重点更新
8
8
 
9
- - backend-test 收口 source-fidelity P0:绑定 source coverage、有界 Markdown/pytest 同步、item-level eligibility 与单次 eligible 业务执行;my-webapp R107 16 节点 finished
9
+ - Console PRD Intake Bridge:统一粘贴、单文件与多文件 PRD 导入流程,复用确定性解析与 Semantic Intake,并回写真实 Console Draft
10
+ - 引入工程边界表单,工程路径与验证命令仅接受人工输入,杜绝使用模板内容冒充需求事实
11
+ - DAG 中断恢复增强:支持协作式 abortSignal,未启动节点明确标记为 controller-interrupted,避免粗暴终止
12
+ - Console Recovery 提供直观的 reconcile CTA,按运行状态精准展示恢复入口,破坏性操作需经浏览器 Human Gate 确认
10
13
 
11
14
  ### 新增
12
15
 
13
- - backend-test 新增绑定 source references 的来源覆盖事实、Markdown 一轮增量同步和 pytest correspondence 一轮有界修复,在业务测试体执行前保留 initial/effective/residual 证据
14
- - backend-test 新增 item-level execution eligibility:只授权 payload-safe pytest items,excluded items 与真实业务失败分开展示
16
+ - Console PRD 导入桥接:支持将粘贴、单文件与多文件导入统一接入 task advance 的确定性解析,并映射产物至 DraftStore
17
+ - Console 工程边界表单:在 PRD 导入时支持可选的 allowed/forbidden/verify 字段,并作为标志传递至 bootstrap intake
18
+ - Console Recovery CTA:基于 effective status 与 liveness 展示 dag reconcile-run(supersede/abandon)入口
19
+ - DAG runner 协作式 abortSignal:主控停止时允许已运行节点按 checkpoint 收尾,未启动节点标记 controller-interrupted
15
20
 
16
21
  ### 改进
17
22
 
18
- - backend-test Markdown/pytest map 的默认总 token 预算从 `600000` 提升到 `3000000`,降低多模块生成时过早触发 `dynamic map token budget exhausted` 的概率;仍保持预算耗尽 fail-closed
19
- - Coverage Matrix 接受 `R400001` 等错误码 Rule Key,并保留单元格内 `ACTIVE|ARCHIVED` 未转义枚举管道符,避免合法行被拆成 10 列后整表丢弃
20
- - scenario-param TP 精确匹配多行 `场景意图`,保留 `ACTIVE` 大小写字面量,识别 `"a" * N` 长度表达式与空白 `empty`,request-level `nominal` 允许 `None` 查询过滤;避免把合法 payload 误判为 MISMATCH 并错误排除 eligible items
21
- - backend-test 支持 evidence-only 元用例:Markdown 显式声明 `脚本/primary symbol=无` 且仅绑定 assertion/cross-cutting TP 时,不再强制 1:1 pytest 符号,也不再把其算作 MISSING_PYTEST/SCRIPT_MISMATCH
22
- - generated pytest 内的 `ScopeMismatch`/setup fixture 失败若完全归因于 `testcase/**` 映射脚本,则归为 REPAIRABLE 并进入唯一一轮 collection repair,不再直接 BLOCKED
23
- - scenario-param 观测跳过 `pytest.param("TP-...", value, id="TP-...")` 的前导 TP 标签,并支持长度意图用数字位置参数表达
23
+ - dag reconcile-run 增加 --json --action 参数,避免 operation runner 因期待 JSON 失败,并明确拒绝非法 action
24
+ - task advance 生命周期优化:PRD 导入后默认进入仅导入阶段,确保多文件 bootstrap 完成后再执行 Semantic Intake
25
+ - Console Recovery 状态映射细化:将 interrupted/orphaned 等状态映射为 needs-reconcile,suspected-stall 映射为 observe/doctor
26
+ - task advance writeSet gate 输出附确定性 delta,清晰展示边界扩张来源,且不破坏既有 approve 流程
24
27
 
25
28
  ### 修复
26
29
 
27
- - 修复 readiness 错误信息截断与无效 Coverage Matrix 状态语义,避免超长错误或 advisory 矩阵状态误阻断后续节点
28
- - 修复 scenario 排除项未绑定 payload 字段、request-level 不确定性被误升级为硬失败的问题
30
+ - 修复追加 PRD 后两阶段 intake 未重新投影契约、Console 可能回读旧 Semantic Intake artifact 的问题
31
+ - 修复 implement-pi provider clean timeout 且明确零写入时直接终止的问题,现对标准 bounded writer 自动重试一次
32
+ - 修复 grill-me 模板被误当作已解析需求的问题
33
+ - 修复窄屏(<1100px)下 CHANGELOG 文档中尖括号导致的 MDX JSX 解析错误
29
34
 
30
- ## [0.33.6-beta.0] - 2026-08-11
35
+ ## [0.34.0] - 2026-08-11
36
+
37
+ ### 重点更新
38
+
39
+ - Operator Chat 会话创建实现即时响应,Landing 页面与创建请求延迟从秒级降至毫秒级
40
+ - 新增桌面端紧凑响应式布局,根据工作区宽度自动适配 comfortable/compact/narrow 三档显示模式
41
+ - 新增自动会话标题功能,基于首条用户消息生成临时标题,并在首轮对话完成后异步生成正式标题
42
+ - 新增 local-jacoco-coverage 技能,将本地 Java 服务挂载、测试执行与覆盖率切片分析串成标准化链路
43
+
44
+ ### 新增
45
+
46
+ - Operator Chat 桌面紧凑响应式布局:按工作区宽度切换 comfortable(≥1450px)/ compact(1100–1449px)/ narrow(<1100px)三档;compact 档使用图标窄栏与覆盖抽屉,narrow 档使用顶栏入口;会话与仓库面板支持拖拽分栏并记忆比例
47
+ - Operator Chat 自动会话标题:首条用户消息持久化后生成清洗有界的临时标题,首轮对话成功完成后异步生成正式标题;手工重命名、归档和删除操作可拒绝迟到的自动标题覆盖
48
+ - local-jacoco-coverage 技能:将本地启动 Java 服务挂载 jacocoagent、运行 backend-test DAG、按需求文档做覆盖率切片分析三步串联为可审计、可重复的本地链路,包含强制流程手册、runtime 对齐参考、需求映射规范与编排脚本
49
+
50
+ ### 改进
51
+
52
+ - Operator Chat 会话创建改为即时响应:先落盘会话骨架与持久记录后立即返回成功,完整 AgentSession 在后台异步初始化,真实 Chrome 测试中 Landing 延迟从 1346ms 降至 22ms、创建请求从 1269ms 降至 16ms
53
+ - 会话创建期间侧栏与 Landing 页共享 busy 状态反馈,抑制双击导致的重复创建
54
+ - 删除正在初始化的会话不会被迟到的后台初始化任务复活,初始化失败统一返回 HTTP 503 错误码
55
+
56
+ ## [0.33.6] - 2026-08-11
57
+
58
+ ### 新增
59
+
60
+ - stabilize PRD intake + add Semantic Intake fallback
61
+ - Operator Chat 单事件流 + boot epoch fence(Task C)
62
+ - gate writeSet delta + run summary(操作面增强)
63
+ - supervise DAG runs autonomously
64
+ - ChatEventStore 事件持久化压缩(coalescing + maxBytes 双限制)
65
+ - reconcile(cursor_expired) 自动恢复而非全局致命错误
66
+ - 接入 Operator Chat 实时运行时控制
67
+ - open operator chat resource loading
68
+ - enrich execution report and URL resolution
69
+
70
+ ### 改进
71
+
72
+ - style(task-advance): biome autoformat 对齐(无功能变化)
73
+ - style(console): apply biome formatting from pi-lens autoformat
31
74
 
32
75
  ### 修复
33
76
 
34
- - 修复 backend-test correspondence scanner 将下一 Case 的分隔注释误归入前一个 pytest function region,造成虚假的 `MULTIPLE_PYTEST`、script mismatch 和额外 Test Point;现在按函数实际缩进边界隔离 body,并只从精确 `Case-ID:` 元数据或 primary symbol 绑定 Case
35
- - 修复 backend-test README 其他区域的 Markdown 链接误提取为业务模块、允许 `a401606` 等 opaque/hash-like stem 和异常多模块进入 map 的问题;现在只解析唯一 `## Module Index`,非法、mixed 或超过 8 个模块时 README-only 有界重试并由 manifest fail-closed
36
- - 修复 backend-test map 总 token 预算耗尽后只生成部分 Markdown/pytest 资产但 parent 仍可能显示成功的问题;budget-blocked child 现在使 backend map 明确 ERROR,并保留 expected/finished/blocked/token 事实
77
+ - unify resilient read-only command execution
78
+ - preserve user-owned Pi model routing
79
+ - show installed package metadata
80
+ - escape preflight backtick matcher
81
+ - align chat streaming reliability with pi-web
82
+ - split rerun defaults and repair selector escaping
83
+ - ignore delimiters inside pytest strings
84
+ - resolve target URL from runtime context
85
+ - reject priority-only module stems
86
+ - stop desiredSha256 mechanical checks from blocking generated-content upgrades
87
+ - use mobius header brand icon
37
88
 
38
89
  ## [0.33.5] - 2026-08-11
39
90
 
@@ -3,6 +3,9 @@ import path from "node:path";
3
3
  import { createTask, getTaskPaths, loadTaskConfig, } from "../../task/runtime.js";
4
4
  import { importPrdDocument } from "../../task/source-references.js";
5
5
  import { prepareTaskSource } from "../../task/source-prepare/index.js";
6
+ import { isIntakeSoftGapCode, resolvePrdImportRole, } from "../../task/source-prepare/artifact-meta.js";
7
+ import { evaluateSemanticIntakeEligibility, isSemanticIntakeDisabled, readSemanticIntakeAttempted, runSemanticIntake, } from "../../task/source-prepare/semantic-intake.js";
8
+ import { validateImportedPrdReferences } from "../../task/source-prepare/reference-integrity.js";
6
9
  import { logicalRevisionForState, observeTaskContractState, recoverTaskContract, } from "../../task/contract/index.js";
7
10
  import { generateTaskDagUseCase } from "../dag/generate-task-dag.js";
8
11
  import { validateDagUseCase } from "../dag/validate-dag.js";
@@ -10,7 +13,7 @@ import { runDagUseCase } from "../dag/run-dag.js";
10
13
  import { promoteRunEvidence, closeoutTask } from "../../records/promotion.js";
11
14
  import { resolveRunningControllerIdentity } from "../../shared/package-metadata.js";
12
15
  import { assessDagRunLiveness, locateDagRun, readDagRunState, } from "../../workflows/dag/lifecycle.js";
13
- import { buildGateBinding, buildWriteSetGate, controllerIdentityAnchor, gateMatches, hashCanonicalPayload, parseGateToken, } from "./gates.js";
16
+ import { buildGateBinding, buildWriteSetGate, buildWriteSetGateDelta, controllerIdentityAnchor, gateMatches, hashCanonicalPayload, parseGateToken, } from "./gates.js";
14
17
  import { appendTransition, emptyLifecycleRecord, loadLifecycleRecord, recordRunAssociation, saveLifecycleRecord, } from "./record.js";
15
18
  import { collectNormalizedWriteSetFromSpec, hashDagSpecFileContent, observeTaskLifecycle, } from "./observe.js";
16
19
  import { planTransitions, } from "./plan-transitions.js";
@@ -39,7 +42,8 @@ function hasEngineeringBoundary(input) {
39
42
  }
40
43
  /** Inputs that must produce a new managed-contract revision when explicit. */
41
44
  function hasContractChangingInput(input) {
42
- return Boolean(hasEngineeringBoundary(input) ||
45
+ return Boolean((input.prdPaths?.length ?? 0) > 0 ||
46
+ hasEngineeringBoundary(input) ||
43
47
  input.title ||
44
48
  input.taskKind ||
45
49
  input.featureId);
@@ -98,6 +102,42 @@ function toRunAssociation(input) {
98
102
  source: input.source ?? "lifecycle-record",
99
103
  };
100
104
  }
105
+ /** Compact run completion summary for the advance result (AC-005/AC-006):
106
+ * per-node status, first VERDICT line found in any node stdout, and nodes
107
+ * flagged with a write-guard / out-of-bounds failure category. */
108
+ function buildRunSummary(state) {
109
+ const nodes = Object.entries(state.nodes ?? {}).map(([id, node]) => ({
110
+ id,
111
+ status: node.status,
112
+ }));
113
+ const ordered = (state.ranks ?? []).flat();
114
+ nodes.sort((a, b) => {
115
+ const ai = ordered.indexOf(a.id);
116
+ const bi = ordered.indexOf(b.id);
117
+ return ((ai === -1 ? ordered.length : ai) - (bi === -1 ? ordered.length : bi));
118
+ });
119
+ let verdict = null;
120
+ const outOfBounds = [];
121
+ for (const [id, node] of Object.entries(state.nodes ?? {})) {
122
+ if (!verdict && typeof node.stdout === "string") {
123
+ const match = node.stdout.match(/^\s*VERDICT:\s*(.+)$/im);
124
+ if (match)
125
+ verdict = match[1].trim();
126
+ }
127
+ if (node.failureCategory &&
128
+ /^write-guard|out-of-bounds|write-set|boundary/i.test(node.failureCategory)) {
129
+ outOfBounds.push({ nodeId: id, failureCategory: node.failureCategory });
130
+ }
131
+ }
132
+ return {
133
+ runId: state.runId,
134
+ status: state.status,
135
+ nodes,
136
+ ...(verdict ? { verdict } : { verdict: null }),
137
+ ...(outOfBounds.length > 0 ? { outOfBounds } : {}),
138
+ failureCategory: state.failureCategory ?? null,
139
+ };
140
+ }
101
141
  async function buildCurrentGate(repoRoot, taskId) {
102
142
  const paths = getTaskPaths(repoRoot, taskId);
103
143
  if (!(await pathExists(paths.dagDraftPath)))
@@ -113,6 +153,7 @@ async function buildCurrentGate(repoRoot, taskId) {
113
153
  const contractState = await observeTaskContractState(repoRoot, taskId);
114
154
  const identity = resolveRunningControllerIdentity();
115
155
  let writeSet = collectNormalizedWriteSetFromSpec(spec);
156
+ let contractAllowedPaths = [];
116
157
  if (writeSet.length === 0) {
117
158
  try {
118
159
  const config = await loadTaskConfig(repoRoot, taskId);
@@ -122,6 +163,18 @@ async function buildCurrentGate(repoRoot, taskId) {
122
163
  // leave empty
123
164
  }
124
165
  }
166
+ else {
167
+ try {
168
+ const config = await loadTaskConfig(repoRoot, taskId);
169
+ contractAllowedPaths = config.allowedPaths ?? [];
170
+ }
171
+ catch {
172
+ // leave empty
173
+ }
174
+ }
175
+ const writers = spec.tasks
176
+ .filter((task) => task.writePolicy === "exclusive")
177
+ .map((task) => ({ id: task.id, writeSet: task.writeSet ?? [] }));
125
178
  const bound = buildGateBinding({
126
179
  taskId,
127
180
  contractRevision: logicalRevisionForState(contractState),
@@ -141,7 +194,12 @@ async function buildCurrentGate(repoRoot, taskId) {
141
194
  }
142
195
  : null),
143
196
  });
144
- return buildWriteSetGate({ taskId, bound });
197
+ const delta = buildWriteSetGateDelta({
198
+ writeSet,
199
+ contractAllowedPaths,
200
+ writers,
201
+ });
202
+ return buildWriteSetGate({ taskId, bound, delta });
145
203
  }
146
204
  async function monitorRun(input) {
147
205
  const started = Date.now();
@@ -213,6 +271,7 @@ export async function advanceTaskLifecycle(input) {
213
271
  let promotion = null;
214
272
  let closeout = null;
215
273
  let run = null;
274
+ let runSummary = null;
216
275
  let consumeApproveGate = Boolean(input.approveGateToken);
217
276
  /** After a successful approve, keep approve intent for finalize replan only. */
218
277
  let approvedThisCall = false;
@@ -226,6 +285,7 @@ export async function advanceTaskLifecycle(input) {
226
285
  let preparedContractThisCall = false;
227
286
  /** A successful managed-contract mutation invalidates the prior DAG/gate. */
228
287
  let regenerateDagThisCall = false;
288
+ let semanticIntakeAttemptedThisCall = false;
229
289
  let snapshot = await observeTaskLifecycle(input.repoRoot, input.taskId);
230
290
  let record = (await loadLifecycleRecord(input.repoRoot, input.taskId)) ??
231
291
  emptyLifecycleRecord(input.taskId);
@@ -280,7 +340,7 @@ export async function advanceTaskLifecycle(input) {
280
340
  verifyCommands: applied.verifyCommands ?? input.verifyCommands,
281
341
  };
282
342
  const hasBoundary = hasEngineeringBoundary(effectiveInput);
283
- let refreshManagedContractThisCall = Boolean(hasContractChangingInput(effectiveInput) &&
343
+ let refreshManagedContractThisCall = Boolean((hasContractChangingInput(effectiveInput) || record.pendingPrdRefresh) &&
284
344
  (snapshot.contract?.effectiveStatus === "managed" ||
285
345
  Boolean(snapshot.contract?.revision && snapshot.contract.revision > 0)));
286
346
  if (applied.accepted) {
@@ -456,22 +516,70 @@ export async function advanceTaskLifecycle(input) {
456
516
  continue;
457
517
  }
458
518
  if (action.type === "import-prd") {
519
+ const importedMaterializedPaths = [];
459
520
  for (const prd of input.prdPaths ?? []) {
460
- await importPrdDocument({
521
+ let markdown;
522
+ try {
523
+ markdown = await readFile(path.resolve(prd.filePath), "utf-8");
524
+ }
525
+ catch {
526
+ markdown = undefined;
527
+ }
528
+ const role = resolvePrdImportRole({
529
+ filePath: prd.filePath,
530
+ explicitRole: prd.role,
531
+ markdown,
532
+ });
533
+ const imported = await importPrdDocument({
461
534
  repoRoot: input.repoRoot,
462
535
  taskId: input.taskId,
463
536
  filePath: prd.filePath,
464
- role: prd.role,
537
+ role,
538
+ // Prefer basename-aligned names for multi-file product suites.
539
+ name: path.basename(prd.filePath).replace(/\.(md|markdown|txt)$/iu, "") || undefined,
465
540
  });
541
+ importedMaterializedPaths.push(imported.materializedRelativePath);
466
542
  }
467
543
  // Latch even when prdPaths is empty so a mis-planned import cannot loop.
468
544
  importedPrdThisCall = true;
469
545
  if ((input.prdPaths?.length ?? 0) > 0) {
470
- record = appendTransition(record, "prd-imported");
546
+ record = {
547
+ ...appendTransition(record, "prd-imported"),
548
+ pendingPrdRefresh: true,
549
+ pendingPrdRefreshPaths: Array.from(new Set([
550
+ ...(record.pendingPrdRefreshPaths ?? []),
551
+ ...importedMaterializedPaths,
552
+ ])),
553
+ };
471
554
  completed.push("prd-imported");
472
555
  await saveLifecycleRecord(input.repoRoot, record);
473
556
  }
474
557
  snapshot = await observeTaskLifecycle(input.repoRoot, input.taskId);
558
+ // Two-phase Console bridge: when this call only had --prd and no
559
+ // engineering boundary, stop after archive. Re-plan would otherwise
560
+ // immediately prepare (hasPrdInput latched false + alreadyImported)
561
+ // and burn one-shot Semantic Intake before multi-file imports finish.
562
+ if (!hasBoundary && (input.prdPaths?.length ?? 0) > 0) {
563
+ return {
564
+ taskId: input.taskId,
565
+ lifecycleState: "intake-incomplete",
566
+ completedTransitions: uniqueTransitions(record.completedTransitions, completed),
567
+ gate: null,
568
+ blockers: [],
569
+ warnings: [
570
+ ...warnings,
571
+ {
572
+ code: "BOUNDARY_REQUIRED",
573
+ message: "PRD imported; re-run task advance (optionally with --allowed-path / --verify) to prepare contract / semantic intake",
574
+ },
575
+ ],
576
+ next: {
577
+ kind: "advance",
578
+ command: `loop-agent task advance ${input.taskId} --json`,
579
+ },
580
+ recommendations,
581
+ };
582
+ }
475
583
  continue;
476
584
  }
477
585
  if (action.type === "prepare-contract") {
@@ -494,7 +602,7 @@ export async function advanceTaskLifecycle(input) {
494
602
  break;
495
603
  }
496
604
  }
497
- const prepareResult = await prepareTaskSource({
605
+ let prepareResult = await prepareTaskSource({
498
606
  repoRoot: input.repoRoot,
499
607
  taskId: input.taskId,
500
608
  mode: "apply",
@@ -507,6 +615,8 @@ export async function advanceTaskLifecycle(input) {
507
615
  : {
508
616
  kind: "facts",
509
617
  useImportedPrd: hasPrd || (snapshot.source?.importedPrdCount ?? 0) > 0,
618
+ replaceManagedRequirement: record.pendingPrdRefresh === true,
619
+ importedPrdPaths: record.pendingPrdRefreshPaths,
510
620
  text: input.fromText,
511
621
  textLabel: input.fromText ? "from-text" : undefined,
512
622
  },
@@ -520,12 +630,162 @@ export async function advanceTaskLifecycle(input) {
520
630
  },
521
631
  });
522
632
  if (!prepareResult.ok) {
633
+ const gapCodes = (prepareResult.gaps ?? [])
634
+ .filter((gap) => gap.level === "blocking")
635
+ .map((gap) => gap.code);
636
+ const softIntake = Boolean(prepareResult.error?.details?.softIntake) ||
637
+ (gapCodes.length > 0 &&
638
+ gapCodes.every((code) => isIntakeSoftGapCode(code)));
639
+ // P2: one-shot semantic intake when structure failed but sources exist.
640
+ if (softIntake &&
641
+ (completed.includes("prd-imported") ||
642
+ (snapshot.source?.importedPrdCount ?? 0) > 0) &&
643
+ !semanticIntakeAttemptedThisCall) {
644
+ const already = await readSemanticIntakeAttempted(input.repoRoot, input.taskId);
645
+ const eligibility = evaluateSemanticIntakeEligibility({
646
+ hasImportedPrd: true,
647
+ gaps: prepareResult.gaps ?? [],
648
+ alreadyAttempted: already,
649
+ disabled: isSemanticIntakeDisabled(),
650
+ });
651
+ if (eligibility.eligible) {
652
+ semanticIntakeAttemptedThisCall = true;
653
+ warnings.push({
654
+ code: "SEMANTIC_INTAKE_STARTING",
655
+ message: eligibility.reason,
656
+ });
657
+ const integrity = await validateImportedPrdReferences({
658
+ repoRoot: input.repoRoot,
659
+ taskId: input.taskId,
660
+ requireParseableRequirement: false,
661
+ });
662
+ const documents = integrity.documents
663
+ .filter((doc) => doc.content)
664
+ .map((doc) => ({
665
+ role: doc.role,
666
+ materializedPath: doc.materializedPath,
667
+ absolutePath: doc.absolutePath,
668
+ content: doc.content,
669
+ }));
670
+ const semantic = await runSemanticIntake({
671
+ repoRoot: input.repoRoot,
672
+ taskId: input.taskId,
673
+ title: input.title ?? prepareResult.title ?? input.taskId,
674
+ taskKind: input.taskKind ?? prepareResult.taskKind,
675
+ featureId: input.featureId,
676
+ documents,
677
+ flags: {
678
+ title: input.title,
679
+ taskKind: input.taskKind,
680
+ featureId: input.featureId,
681
+ allowedPaths: effectiveInput.allowedPaths,
682
+ forbiddenPaths: input.forbiddenPaths,
683
+ verifyCommands: effectiveInput.verifyCommands,
684
+ },
685
+ });
686
+ if (semantic.ok) {
687
+ const retry = await prepareTaskSource({
688
+ repoRoot: input.repoRoot,
689
+ taskId: input.taskId,
690
+ mode: "apply",
691
+ intent: {
692
+ kind: "draft",
693
+ draft: semantic.draft,
694
+ sourcePath: semantic.artifactPath,
695
+ },
696
+ flags: {
697
+ title: input.title,
698
+ taskKind: input.taskKind,
699
+ featureId: input.featureId,
700
+ allowedPaths: effectiveInput.allowedPaths,
701
+ forbiddenPaths: input.forbiddenPaths,
702
+ verifyCommands: effectiveInput.verifyCommands,
703
+ },
704
+ });
705
+ if (retry.ok) {
706
+ warnings.push({
707
+ code: "SEMANTIC_INTAKE_APPLIED",
708
+ message: "structured requirement draft via semantic intake; projected managed source",
709
+ });
710
+ preparedContractThisCall = true;
711
+ if (refreshManagedContractThisCall || input.fromDraftPath) {
712
+ refreshManagedContractThisCall = false;
713
+ regenerateDagThisCall = true;
714
+ }
715
+ record = {
716
+ ...appendTransition(record, "contract-managed"),
717
+ pendingPrdRefresh: false,
718
+ pendingPrdRefreshPaths: [],
719
+ };
720
+ completed.push("contract-managed");
721
+ await saveLifecycleRecord(input.repoRoot, record);
722
+ snapshot = await observeTaskLifecycle(input.repoRoot, input.taskId);
723
+ continue;
724
+ }
725
+ prepareResult = retry;
726
+ // fall through with retry gaps
727
+ }
728
+ else {
729
+ warnings.push({
730
+ code: semantic.code,
731
+ message: semantic.message,
732
+ });
733
+ }
734
+ }
735
+ else if (!already) {
736
+ warnings.push({
737
+ code: "SEMANTIC_INTAKE_SKIPPED",
738
+ message: eligibility.reason,
739
+ });
740
+ }
741
+ }
742
+ // Soft intake: PRD may already be imported; stop before DAG without
743
+ // treating the whole advance as a hard parse failure.
744
+ const gapCodesAfter = (prepareResult.gaps ?? [])
745
+ .filter((gap) => gap.level === "blocking")
746
+ .map((gap) => gap.code);
747
+ const softIntakeAfter = Boolean(prepareResult.error?.details?.softIntake) ||
748
+ (gapCodesAfter.length > 0 &&
749
+ gapCodesAfter.every((code) => isIntakeSoftGapCode(code)));
750
+ // Soft-stop when PRD is on disk (this call or prior import).
751
+ // Console two-phase bootstrap: import first, then advance intake
752
+ // without re-passing --prd — still must soft-stop, not hard-fail.
753
+ const hasImportedPrdOnDisk = completed.includes("prd-imported") ||
754
+ (snapshot.source?.importedPrdCount ?? 0) > 0;
755
+ if (softIntakeAfter && hasImportedPrdOnDisk) {
756
+ warnings.push({
757
+ code: "INTAKE_INCOMPLETE",
758
+ message: prepareResult.error?.message ??
759
+ "PRD imported but intake incomplete; DAG not generated",
760
+ });
761
+ for (const gap of prepareResult.gaps ?? []) {
762
+ if (gap.level === "blocking") {
763
+ warnings.push({ code: gap.code, message: gap.message });
764
+ }
765
+ }
766
+ snapshot = await observeTaskLifecycle(input.repoRoot, input.taskId);
767
+ return {
768
+ taskId: input.taskId,
769
+ lifecycleState: "intake-incomplete",
770
+ completedTransitions: uniqueTransitions(record.completedTransitions, completed),
771
+ gate: null,
772
+ blockers: [],
773
+ warnings,
774
+ next: {
775
+ kind: "advance",
776
+ command: prepareResult.next?.[0] ??
777
+ `loop-agent task advance ${input.taskId} --prd <product-requirement.md> --allowed-path <glob> --json`,
778
+ },
779
+ recommendations,
780
+ };
781
+ }
523
782
  blockers.push({
524
783
  code: prepareResult.error?.code ?? "PREPARE_FAILED",
525
784
  message: prepareResult.error?.message ?? "task source prepare failed",
526
785
  details: {
527
786
  gaps: prepareResult.gaps,
528
787
  outcome: prepareResult.outcome,
788
+ softIntake: softIntakeAfter,
529
789
  },
530
790
  });
531
791
  break;
@@ -536,7 +796,11 @@ export async function advanceTaskLifecycle(input) {
536
796
  refreshManagedContractThisCall = false;
537
797
  regenerateDagThisCall = true;
538
798
  }
539
- record = appendTransition(record, "contract-managed");
799
+ record = {
800
+ ...appendTransition(record, "contract-managed"),
801
+ pendingPrdRefresh: false,
802
+ pendingPrdRefreshPaths: [],
803
+ };
540
804
  completed.push("contract-managed");
541
805
  await saveLifecycleRecord(input.repoRoot, record);
542
806
  snapshot = await observeTaskLifecycle(input.repoRoot, input.taskId);
@@ -814,6 +1078,22 @@ export async function advanceTaskLifecycle(input) {
814
1078
  }
815
1079
  await saveLifecycleRecord(input.repoRoot, record);
816
1080
  }
1081
+ // Build the compact run summary (AC-005/AC-006) from the settled run
1082
+ // state so the operator sees node statuses/verdict/outOfBounds without
1083
+ // reading state.json. Best-effort: skip when the run dir is unavailable.
1084
+ if (run?.runDir) {
1085
+ try {
1086
+ const settledState = await readDagRunState(run.runDir);
1087
+ const summary = buildRunSummary(settledState);
1088
+ if (summary) {
1089
+ summary.next = snapshot.next;
1090
+ runSummary = summary;
1091
+ }
1092
+ }
1093
+ catch {
1094
+ // leave runSummary null
1095
+ }
1096
+ }
817
1097
  snapshot = await observeTaskLifecycle(input.repoRoot, input.taskId);
818
1098
  // Re-plan so promote/closeout can run in the same call after success.
819
1099
  continue;
@@ -869,6 +1149,25 @@ export async function advanceTaskLifecycle(input) {
869
1149
  (await buildCurrentGate(input.repoRoot, input.taskId)))
870
1150
  : snapshot.gate;
871
1151
  await exportDagDraftIfRequested(input.repoRoot, input.taskId, input.dagOutputPath);
1152
+ // Fall back to the latest associated run state when this call did not
1153
+ // itself monitor/start a run (e.g. re-observe after a previously settled
1154
+ // run): the operator still wants the compact summary in the result.
1155
+ if (!runSummary) {
1156
+ const settledRun = run ?? snapshot.latestRun ?? snapshot.activeRun;
1157
+ if (settledRun?.runDir) {
1158
+ try {
1159
+ const settledState = await readDagRunState(settledRun.runDir);
1160
+ const summary = buildRunSummary(settledState);
1161
+ if (summary) {
1162
+ summary.next = snapshot.next;
1163
+ runSummary = summary;
1164
+ }
1165
+ }
1166
+ catch {
1167
+ // leave runSummary null
1168
+ }
1169
+ }
1170
+ }
872
1171
  return {
873
1172
  taskId: input.taskId,
874
1173
  lifecycleState: snapshot.lifecycleState,
@@ -878,6 +1177,7 @@ export async function advanceTaskLifecycle(input) {
878
1177
  warnings,
879
1178
  next: snapshot.next,
880
1179
  run,
1180
+ runSummary,
881
1181
  promotion,
882
1182
  closeout,
883
1183
  recommendations,
@@ -1,4 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { pathMatchesPattern } from "../../shared/git-progress.js";
2
3
  export function normalizeWriteSet(writeSet) {
3
4
  return [
4
5
  ...new Set(writeSet.map((entry) => entry.replace(/\\/g, "/").trim()).filter(Boolean)),
@@ -42,6 +43,54 @@ export function computeGateDigest(bound) {
42
43
  writeSet: normalizeWriteSet(bound.writeSet),
43
44
  });
44
45
  }
46
+ /**
47
+ * Deterministic writeSet vs contract allowedPaths delta (2026-08-11).
48
+ *
49
+ * Classifies every normalized writeSet entry against the task contract's
50
+ * allowedPaths:
51
+ * - exactMatch: entry equals a contract allowedPath
52
+ * - narrowerThanContract: entry is covered by a broader contract glob
53
+ * (e.g. writeSet `src/worker/console/chat/**` under
54
+ * contract `src/**`)
55
+ * - additions: entry outside the contract, attributed to the
56
+ * exclusive writer node(s) that declared it
57
+ *
58
+ * `writers` is the list of { id, writeSet } from the DAG spec's exclusive
59
+ * writer tasks. The delta is a read-only field on the gate output and is
60
+ * NEVER included in the gate digest (AC-004: digest stays stable).
61
+ */
62
+ export function buildWriteSetGateDelta(input) {
63
+ const writeSet = normalizeWriteSet(input.writeSet);
64
+ const contract = normalizeWriteSet(input.contractAllowedPaths);
65
+ const exactMatch = [];
66
+ const narrowerThanContract = [];
67
+ const additions = [];
68
+ for (const entry of writeSet) {
69
+ if (contract.includes(entry)) {
70
+ exactMatch.push(entry);
71
+ continue;
72
+ }
73
+ // Covered by a broader contract glob? Use the entry itself as a probe
74
+ // path against each contract pattern (pathMatchesPattern treats a
75
+ // non-glob entry as an exact-or-prefix match and glob entries via
76
+ // globToRegExp).
77
+ const covered = contract.some((contractEntry) => pathMatchesPattern(entry, contractEntry));
78
+ if (covered) {
79
+ narrowerThanContract.push(entry);
80
+ continue;
81
+ }
82
+ const source = (input.writers ?? [])
83
+ .filter((writer) => normalizeWriteSet(writer.writeSet).includes(entry))
84
+ .map((writer) => writer.id)
85
+ .sort((a, b) => a.localeCompare(b));
86
+ additions.push({
87
+ entry,
88
+ source,
89
+ coveredByContract: false,
90
+ });
91
+ }
92
+ return { exactMatch, narrowerThanContract, additions };
93
+ }
45
94
  export function buildWriteSetGate(input) {
46
95
  const digest = computeGateDigest(input.bound);
47
96
  const id = "write-set-review";
@@ -54,6 +103,7 @@ export function buildWriteSetGate(input) {
54
103
  approveCommand: `loop-agent task advance ${input.taskId} --approve-gate ${id}:${digest} --json`,
55
104
  rejectCommand: `loop-agent task advance ${input.taskId} --reject-gate ${id}:${digest} --reason <text> --json`,
56
105
  bound: input.bound,
106
+ delta: input.delta ?? null,
57
107
  };
58
108
  }
59
109
  export function parseGateToken(token) {