@tea-agent/loop-agent 0.33.6 → 0.33.7-beta.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 (69) hide show
  1. package/CHANGELOG.md +39 -23
  2. package/dist/application/task-lifecycle/advance.js +4 -254
  3. package/dist/application/task-lifecycle/gates.js +0 -50
  4. package/dist/application/task-lifecycle/observe.js +2 -11
  5. package/dist/commands/init-upgrade.js +1 -32
  6. package/dist/commands/init.js +3 -94
  7. package/dist/executors/shell-executor.js +91 -4
  8. package/dist/executors/shell-write-guard.js +8 -26
  9. package/dist/shared/operator/capabilities.js +42 -72
  10. package/dist/task/source-prepare/index.js +0 -2
  11. package/dist/task/source-prepare/parse-intent.js +10 -58
  12. package/dist/task/source-prepare/prepare.js +16 -180
  13. package/dist/task/source-prepare/reference-integrity.js +2 -18
  14. package/dist/worker/console/app-data.js +0 -2
  15. package/dist/worker/console/chat/chat-event-store.js +25 -190
  16. package/dist/worker/console/chat/instruction-skills.js +217 -0
  17. package/dist/worker/console/chat/pi-console-config.js +32 -250
  18. package/dist/worker/console/chat/pi-runtime.js +71 -625
  19. package/dist/worker/console/chat/resource-loader.js +4 -5
  20. package/dist/worker/console/chat/routes.js +146 -324
  21. package/dist/worker/console/chat/runtime-context.js +12 -48
  22. package/dist/worker/console/chat/runtime-selection.js +0 -59
  23. package/dist/worker/console/chat/shortcuts.js +0 -1
  24. package/dist/worker/console/chat/tool-adapter.js +3 -9
  25. package/dist/worker/console/chat/tools.js +1 -5
  26. package/dist/worker/console/operator-actions.js +68 -559
  27. package/dist/worker/console/server.js +15 -8
  28. package/dist/worker/console/static/assets/index-CnUXAqxG.css +1 -0
  29. package/dist/worker/console/static/assets/index-CteJFFL2.js +29 -0
  30. package/dist/worker/console/static/index.html +2 -2
  31. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +8 -45
  32. package/dist/worker/console/static-src/operator-chat/refs.js +0 -9
  33. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +0 -16
  34. package/dist/worker/console/static-src/operator-chat/useChatStream.js +184 -210
  35. package/dist/worker/console/static-src/operator-chat/useChatThread.js +5 -49
  36. package/dist/worker/console/static-src/operator-chat/useComposer.js +0 -17
  37. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +74 -225
  38. package/dist/worker/delivery/final-verification.js +5 -13
  39. package/dist/worker/delivery/package.js +19 -31
  40. package/dist/worker/delivery/verification-bundle.js +4 -6
  41. package/dist/worker/observe/static/operator-chrome.css +2 -5
  42. package/dist/worker/observe/static/operator-chrome.js +1 -6
  43. package/dist/worker/observe/static/styles.css +9 -39
  44. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +462 -33
  45. package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
  46. package/dist/workflows/dag/backend-test-markdown-workflow.js +25 -1
  47. package/dist/workflows/dag/backend-test-module-stem.js +5 -0
  48. package/dist/workflows/dag/backend-test-pytest-collection.js +345 -24
  49. package/dist/workflows/dag/backend-test-scenario-param.js +552 -124
  50. package/dist/workflows/dag/backend-test-writer-completeness.js +47 -16
  51. package/dist/workflows/dag/dynamic-runtime/map.js +24 -8
  52. package/dist/workflows/dag/frontend-worktree-diff.js +27 -12
  53. package/dist/workflows/dag/init-hybrid.js +49 -37
  54. package/dist/workflows/dag/types.js +7 -0
  55. package/dist/workflows/dag/workspace-checkpoint.js +27 -8
  56. package/docs/templates/backend-test-dag.json +33 -30
  57. package/harness.json +1 -1
  58. package/package.json +1 -1
  59. package/skills/loop-agent/references/command-reference.md +1 -3
  60. package/skills/loop-agent/references/source-and-plan-practice.md +0 -13
  61. package/skills/loop-agent/references/task-workflow.md +0 -4
  62. package/dist/shared/resilient-git.js +0 -133
  63. package/dist/task/source-prepare/artifact-meta.js +0 -137
  64. package/dist/task/source-prepare/semantic-intake.js +0 -404
  65. package/dist/worker/console/dag-execution-receipt.js +0 -380
  66. package/dist/worker/console/static/assets/index-BUOLppPr.js +0 -28
  67. package/dist/worker/console/static/assets/index-C1KzazY5.css +0 -1
  68. package/dist/worker/console/static-src/operator-chat/runtime-snapshot-store.js +0 -257
  69. package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +0 -196
package/CHANGELOG.md CHANGED
@@ -2,38 +2,54 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
- ## [0.33.6] - 2026-08-11
5
+ ### 改进
6
+
7
+ - scenario-param 观测识别 `_OMIT`/omit sentinel 为合法 `missing`,跳过 `"2xx"`/`"400"` 期望码位置参数,解码 Python `\t`/`\n` 空白字面量,识别 `_TITLE_LEN1` 等长度 helper 名,支持 `custom-literal:whitespace-only|whitespace-padded` 语义标签,`enum-invalid` 接受具体非法字面量,并在 `min`/`LEN1` 意图下优先读取 TP 编码长度与 `minLength` 边界
8
+ - scenario-param 兼容 `empty` 意图下的 omit/`missing-key`,规范化 `custom-literal` 外包引号与可见空白符,并把 `max+1`/`min`/`max` 的裸数字位置参数按长度 token 比较(含 oversized 数字本身);`custom-literal:trim` 视为 padded whitespace 语义标签
9
+ - backend-test writer 合同明确:`enum-invalid` 必须使用具体非法字面量且禁止 `_OMIT`/missing-key;`custom-literal:trim|ACTIVE|ARCHIVED` 必须使用真实 padded/enum 字面量,禁止 `filter-active` 类描述性伪值
10
+ - scenario-param 识别 `_*_MAX_LENGTH` / `_*_OVER_LENGTH` 等长度 helper 名,明确 `custom-literal:trim` 不能是全空白,并把无 pytest.param 的 request-level/health nominal 记为 MATCH 以免污染 eligibility
11
+
12
+ ## [0.33.7-beta.1] - 2026-08-12
13
+
14
+ ### Fixed
15
+ - scenario-param 识别 `_*_MAX_LENGTH` / `_*_OVER_LENGTH` 等长度 helper 名,明确 `custom-literal:trim` 不能是全空白,并把无 pytest.param 的 request-level/health nominal 记为 MATCH 以免污染 eligibility
16
+ - backend-test writer 合同明确:`enum-invalid` 必须使用具体非法字面量且禁止 `_OMIT`/missing-key;`custom-literal:trim|ACTIVE|ARCHIVED` 必须使用真实 padded/enum 字面量;max/min/max+1 应用长度表达式/helper
17
+
18
+ ### Changed
19
+ - 归档 R108–R110 live campaign 证据与 residual 收紧结果
20
+
21
+ ## [0.33.7-beta.0] - 2026-08-11
22
+
23
+ ### 重点更新
24
+
25
+ - backend-test 收口 source-fidelity P0:绑定 source coverage、有界 Markdown/pytest 同步、item-level eligibility 与单次 eligible 业务执行;my-webapp R107 16 节点 finished
6
26
 
7
27
  ### 新增
8
28
 
9
- - stabilize PRD intake + add Semantic Intake fallback
10
- - Operator Chat 单事件流 + boot epoch fence(Task C)
11
- - gate writeSet delta + run summary(操作面增强)
12
- - supervise DAG runs autonomously
13
- - ChatEventStore 事件持久化压缩(coalescing + maxBytes 双限制)
14
- - reconcile(cursor_expired) 自动恢复而非全局致命错误
15
- - 接入 Operator Chat 实时运行时控制
16
- - open operator chat resource loading
17
- - enrich execution report and URL resolution
29
+ - backend-test 新增绑定 source references 的来源覆盖事实、Markdown 一轮增量同步和 pytest correspondence 一轮有界修复,在业务测试体执行前保留 initial/effective/residual 证据
30
+ - backend-test 新增 item-level execution eligibility:只授权 payload-safe pytest items,excluded items 与真实业务失败分开展示
18
31
 
19
32
  ### 改进
20
33
 
21
- - style(task-advance): biome autoformat 对齐(无功能变化)
22
- - style(console): apply biome formatting from pi-lens autoformat
34
+ - 将 backend-test Markdown/pytest map 的默认总 token 预算从 `600000` 提升到 `3000000`,降低多模块生成时过早触发 `dynamic map token budget exhausted` 的概率;仍保持预算耗尽 fail-closed
35
+ - Coverage Matrix 接受 `R400001` 等错误码 Rule Key,并保留单元格内 `ACTIVE|ARCHIVED` 未转义枚举管道符,避免合法行被拆成 10 列后整表丢弃
36
+ - scenario-param 按 TP 精确匹配多行 `场景意图`,保留 `ACTIVE` 大小写字面量,识别 `"a" * N` 长度表达式与空白 `empty`,request-level `nominal` 允许 `None` 查询过滤;避免把合法 payload 误判为 MISMATCH 并错误排除 eligible items
37
+ - backend-test 支持 evidence-only 元用例:Markdown 显式声明 `脚本/primary symbol=无` 且仅绑定 assertion/cross-cutting TP 时,不再强制 1:1 pytest 符号,也不再把其算作 MISSING_PYTEST/SCRIPT_MISMATCH
38
+ - generated pytest 内的 `ScopeMismatch`/setup fixture 失败若完全归因于 `testcase/**` 映射脚本,则归为 REPAIRABLE 并进入唯一一轮 collection repair,不再直接 BLOCKED
39
+ - scenario-param 观测跳过 `pytest.param("TP-...", value, id="TP-...")` 的前导 TP 标签,并支持长度意图用数字位置参数表达
40
+
41
+ ### 修复
42
+
43
+ - 修复 readiness 错误信息截断与无效 Coverage Matrix 状态语义,避免超长错误或 advisory 矩阵状态误阻断后续节点
44
+ - 修复 scenario 排除项未绑定 payload 字段、request-level 不确定性被误升级为硬失败的问题
45
+
46
+ ## [0.33.6-beta.0] - 2026-08-11
23
47
 
24
48
  ### 修复
25
49
 
26
- - unify resilient read-only command execution
27
- - preserve user-owned Pi model routing
28
- - show installed package metadata
29
- - escape preflight backtick matcher
30
- - align chat streaming reliability with pi-web
31
- - split rerun defaults and repair selector escaping
32
- - ignore delimiters inside pytest strings
33
- - resolve target URL from runtime context
34
- - reject priority-only module stems
35
- - stop desiredSha256 mechanical checks from blocking generated-content upgrades
36
- - use mobius header brand icon
50
+ - 修复 backend-test correspondence scanner 将下一 Case 的分隔注释误归入前一个 pytest function region,造成虚假的 `MULTIPLE_PYTEST`、script mismatch 和额外 Test Point;现在按函数实际缩进边界隔离 body,并只从精确 `Case-ID:` 元数据或 primary symbol 绑定 Case
51
+ - 修复 backend-test README 其他区域的 Markdown 链接误提取为业务模块、允许 `a401606` 等 opaque/hash-like stem 和异常多模块进入 map 的问题;现在只解析唯一 `## Module Index`,非法、mixed 或超过 8 个模块时 README-only 有界重试并由 manifest fail-closed
52
+ - 修复 backend-test map 总 token 预算耗尽后只生成部分 Markdown/pytest 资产但 parent 仍可能显示成功的问题;budget-blocked child 现在使 backend map 明确 ERROR,并保留 expected/finished/blocked/token 事实
37
53
 
38
54
  ## [0.33.5] - 2026-08-11
39
55
 
@@ -3,9 +3,6 @@ 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";
9
6
  import { logicalRevisionForState, observeTaskContractState, recoverTaskContract, } from "../../task/contract/index.js";
10
7
  import { generateTaskDagUseCase } from "../dag/generate-task-dag.js";
11
8
  import { validateDagUseCase } from "../dag/validate-dag.js";
@@ -13,7 +10,7 @@ import { runDagUseCase } from "../dag/run-dag.js";
13
10
  import { promoteRunEvidence, closeoutTask } from "../../records/promotion.js";
14
11
  import { resolveRunningControllerIdentity } from "../../shared/package-metadata.js";
15
12
  import { assessDagRunLiveness, locateDagRun, readDagRunState, } from "../../workflows/dag/lifecycle.js";
16
- import { buildGateBinding, buildWriteSetGate, buildWriteSetGateDelta, controllerIdentityAnchor, gateMatches, hashCanonicalPayload, parseGateToken, } from "./gates.js";
13
+ import { buildGateBinding, buildWriteSetGate, controllerIdentityAnchor, gateMatches, hashCanonicalPayload, parseGateToken, } from "./gates.js";
17
14
  import { appendTransition, emptyLifecycleRecord, loadLifecycleRecord, recordRunAssociation, saveLifecycleRecord, } from "./record.js";
18
15
  import { collectNormalizedWriteSetFromSpec, hashDagSpecFileContent, observeTaskLifecycle, } from "./observe.js";
19
16
  import { planTransitions, } from "./plan-transitions.js";
@@ -101,42 +98,6 @@ function toRunAssociation(input) {
101
98
  source: input.source ?? "lifecycle-record",
102
99
  };
103
100
  }
104
- /** Compact run completion summary for the advance result (AC-005/AC-006):
105
- * per-node status, first VERDICT line found in any node stdout, and nodes
106
- * flagged with a write-guard / out-of-bounds failure category. */
107
- function buildRunSummary(state) {
108
- const nodes = Object.entries(state.nodes ?? {}).map(([id, node]) => ({
109
- id,
110
- status: node.status,
111
- }));
112
- const ordered = (state.ranks ?? []).flat();
113
- nodes.sort((a, b) => {
114
- const ai = ordered.indexOf(a.id);
115
- const bi = ordered.indexOf(b.id);
116
- return ((ai === -1 ? ordered.length : ai) - (bi === -1 ? ordered.length : bi));
117
- });
118
- let verdict = null;
119
- const outOfBounds = [];
120
- for (const [id, node] of Object.entries(state.nodes ?? {})) {
121
- if (!verdict && typeof node.stdout === "string") {
122
- const match = node.stdout.match(/^\s*VERDICT:\s*(.+)$/im);
123
- if (match)
124
- verdict = match[1].trim();
125
- }
126
- if (node.failureCategory &&
127
- /^write-guard|out-of-bounds|write-set|boundary/i.test(node.failureCategory)) {
128
- outOfBounds.push({ nodeId: id, failureCategory: node.failureCategory });
129
- }
130
- }
131
- return {
132
- runId: state.runId,
133
- status: state.status,
134
- nodes,
135
- ...(verdict ? { verdict } : { verdict: null }),
136
- ...(outOfBounds.length > 0 ? { outOfBounds } : {}),
137
- failureCategory: state.failureCategory ?? null,
138
- };
139
- }
140
101
  async function buildCurrentGate(repoRoot, taskId) {
141
102
  const paths = getTaskPaths(repoRoot, taskId);
142
103
  if (!(await pathExists(paths.dagDraftPath)))
@@ -152,7 +113,6 @@ async function buildCurrentGate(repoRoot, taskId) {
152
113
  const contractState = await observeTaskContractState(repoRoot, taskId);
153
114
  const identity = resolveRunningControllerIdentity();
154
115
  let writeSet = collectNormalizedWriteSetFromSpec(spec);
155
- let contractAllowedPaths = [];
156
116
  if (writeSet.length === 0) {
157
117
  try {
158
118
  const config = await loadTaskConfig(repoRoot, taskId);
@@ -162,18 +122,6 @@ async function buildCurrentGate(repoRoot, taskId) {
162
122
  // leave empty
163
123
  }
164
124
  }
165
- else {
166
- try {
167
- const config = await loadTaskConfig(repoRoot, taskId);
168
- contractAllowedPaths = config.allowedPaths ?? [];
169
- }
170
- catch {
171
- // leave empty
172
- }
173
- }
174
- const writers = spec.tasks
175
- .filter((task) => task.writePolicy === "exclusive")
176
- .map((task) => ({ id: task.id, writeSet: task.writeSet ?? [] }));
177
125
  const bound = buildGateBinding({
178
126
  taskId,
179
127
  contractRevision: logicalRevisionForState(contractState),
@@ -193,12 +141,7 @@ async function buildCurrentGate(repoRoot, taskId) {
193
141
  }
194
142
  : null),
195
143
  });
196
- const delta = buildWriteSetGateDelta({
197
- writeSet,
198
- contractAllowedPaths,
199
- writers,
200
- });
201
- return buildWriteSetGate({ taskId, bound, delta });
144
+ return buildWriteSetGate({ taskId, bound });
202
145
  }
203
146
  async function monitorRun(input) {
204
147
  const started = Date.now();
@@ -270,7 +213,6 @@ export async function advanceTaskLifecycle(input) {
270
213
  let promotion = null;
271
214
  let closeout = null;
272
215
  let run = null;
273
- let runSummary = null;
274
216
  let consumeApproveGate = Boolean(input.approveGateToken);
275
217
  /** After a successful approve, keep approve intent for finalize replan only. */
276
218
  let approvedThisCall = false;
@@ -284,7 +226,6 @@ export async function advanceTaskLifecycle(input) {
284
226
  let preparedContractThisCall = false;
285
227
  /** A successful managed-contract mutation invalidates the prior DAG/gate. */
286
228
  let regenerateDagThisCall = false;
287
- let semanticIntakeAttemptedThisCall = false;
288
229
  let snapshot = await observeTaskLifecycle(input.repoRoot, input.taskId);
289
230
  let record = (await loadLifecycleRecord(input.repoRoot, input.taskId)) ??
290
231
  emptyLifecycleRecord(input.taskId);
@@ -516,25 +457,11 @@ export async function advanceTaskLifecycle(input) {
516
457
  }
517
458
  if (action.type === "import-prd") {
518
459
  for (const prd of input.prdPaths ?? []) {
519
- let markdown;
520
- try {
521
- markdown = await readFile(path.resolve(prd.filePath), "utf-8");
522
- }
523
- catch {
524
- markdown = undefined;
525
- }
526
- const role = resolvePrdImportRole({
527
- filePath: prd.filePath,
528
- explicitRole: prd.role,
529
- markdown,
530
- });
531
460
  await importPrdDocument({
532
461
  repoRoot: input.repoRoot,
533
462
  taskId: input.taskId,
534
463
  filePath: prd.filePath,
535
- role,
536
- // Prefer basename-aligned names for multi-file product suites.
537
- name: path.basename(prd.filePath).replace(/\.(md|markdown|txt)$/iu, "") || undefined,
464
+ role: prd.role,
538
465
  });
539
466
  }
540
467
  // Latch even when prdPaths is empty so a mis-planned import cannot loop.
@@ -567,7 +494,7 @@ export async function advanceTaskLifecycle(input) {
567
494
  break;
568
495
  }
569
496
  }
570
- let prepareResult = await prepareTaskSource({
497
+ const prepareResult = await prepareTaskSource({
571
498
  repoRoot: input.repoRoot,
572
499
  taskId: input.taskId,
573
500
  mode: "apply",
@@ -593,153 +520,12 @@ export async function advanceTaskLifecycle(input) {
593
520
  },
594
521
  });
595
522
  if (!prepareResult.ok) {
596
- const gapCodes = (prepareResult.gaps ?? [])
597
- .filter((gap) => gap.level === "blocking")
598
- .map((gap) => gap.code);
599
- const softIntake = Boolean(prepareResult.error?.details?.softIntake) ||
600
- (gapCodes.length > 0 &&
601
- gapCodes.every((code) => isIntakeSoftGapCode(code)));
602
- // P2: one-shot semantic intake when structure failed but sources exist.
603
- if (softIntake &&
604
- (completed.includes("prd-imported") ||
605
- (snapshot.source?.importedPrdCount ?? 0) > 0) &&
606
- !semanticIntakeAttemptedThisCall) {
607
- const already = await readSemanticIntakeAttempted(input.repoRoot, input.taskId);
608
- const eligibility = evaluateSemanticIntakeEligibility({
609
- hasImportedPrd: true,
610
- gaps: prepareResult.gaps ?? [],
611
- alreadyAttempted: already,
612
- disabled: isSemanticIntakeDisabled(),
613
- });
614
- if (eligibility.eligible) {
615
- semanticIntakeAttemptedThisCall = true;
616
- warnings.push({
617
- code: "SEMANTIC_INTAKE_STARTING",
618
- message: eligibility.reason,
619
- });
620
- const integrity = await validateImportedPrdReferences({
621
- repoRoot: input.repoRoot,
622
- taskId: input.taskId,
623
- requireParseableRequirement: false,
624
- });
625
- const documents = integrity.documents
626
- .filter((doc) => doc.content)
627
- .map((doc) => ({
628
- role: doc.role,
629
- materializedPath: doc.materializedPath,
630
- absolutePath: doc.absolutePath,
631
- content: doc.content,
632
- }));
633
- const semantic = await runSemanticIntake({
634
- repoRoot: input.repoRoot,
635
- taskId: input.taskId,
636
- title: input.title ?? prepareResult.title ?? input.taskId,
637
- taskKind: input.taskKind ?? prepareResult.taskKind,
638
- featureId: input.featureId,
639
- documents,
640
- flags: {
641
- title: input.title,
642
- taskKind: input.taskKind,
643
- featureId: input.featureId,
644
- allowedPaths: effectiveInput.allowedPaths,
645
- forbiddenPaths: input.forbiddenPaths,
646
- verifyCommands: effectiveInput.verifyCommands,
647
- },
648
- });
649
- if (semantic.ok) {
650
- const retry = await prepareTaskSource({
651
- repoRoot: input.repoRoot,
652
- taskId: input.taskId,
653
- mode: "apply",
654
- intent: {
655
- kind: "draft",
656
- draft: semantic.draft,
657
- sourcePath: semantic.artifactPath,
658
- },
659
- flags: {
660
- title: input.title,
661
- taskKind: input.taskKind,
662
- featureId: input.featureId,
663
- allowedPaths: effectiveInput.allowedPaths,
664
- forbiddenPaths: input.forbiddenPaths,
665
- verifyCommands: effectiveInput.verifyCommands,
666
- },
667
- });
668
- if (retry.ok) {
669
- warnings.push({
670
- code: "SEMANTIC_INTAKE_APPLIED",
671
- message: "structured requirement draft via semantic intake; projected managed source",
672
- });
673
- preparedContractThisCall = true;
674
- if (refreshManagedContractThisCall || input.fromDraftPath) {
675
- refreshManagedContractThisCall = false;
676
- regenerateDagThisCall = true;
677
- }
678
- record = appendTransition(record, "contract-managed");
679
- completed.push("contract-managed");
680
- await saveLifecycleRecord(input.repoRoot, record);
681
- snapshot = await observeTaskLifecycle(input.repoRoot, input.taskId);
682
- continue;
683
- }
684
- prepareResult = retry;
685
- // fall through with retry gaps
686
- }
687
- else {
688
- warnings.push({
689
- code: semantic.code,
690
- message: semantic.message,
691
- });
692
- }
693
- }
694
- else if (!already) {
695
- warnings.push({
696
- code: "SEMANTIC_INTAKE_SKIPPED",
697
- message: eligibility.reason,
698
- });
699
- }
700
- }
701
- // Soft intake: PRD may already be imported; stop before DAG without
702
- // treating the whole advance as a hard parse failure.
703
- const gapCodesAfter = (prepareResult.gaps ?? [])
704
- .filter((gap) => gap.level === "blocking")
705
- .map((gap) => gap.code);
706
- const softIntakeAfter = Boolean(prepareResult.error?.details?.softIntake) ||
707
- (gapCodesAfter.length > 0 &&
708
- gapCodesAfter.every((code) => isIntakeSoftGapCode(code)));
709
- if (softIntakeAfter && completed.includes("prd-imported")) {
710
- warnings.push({
711
- code: "INTAKE_INCOMPLETE",
712
- message: prepareResult.error?.message ??
713
- "PRD imported but intake incomplete; DAG not generated",
714
- });
715
- for (const gap of prepareResult.gaps ?? []) {
716
- if (gap.level === "blocking") {
717
- warnings.push({ code: gap.code, message: gap.message });
718
- }
719
- }
720
- snapshot = await observeTaskLifecycle(input.repoRoot, input.taskId);
721
- return {
722
- taskId: input.taskId,
723
- lifecycleState: "intake-incomplete",
724
- completedTransitions: uniqueTransitions(record.completedTransitions, completed),
725
- gate: null,
726
- blockers: [],
727
- warnings,
728
- next: {
729
- kind: "advance",
730
- command: prepareResult.next?.[0] ??
731
- `loop-agent task advance ${input.taskId} --prd <product-requirement.md> --allowed-path <glob> --json`,
732
- },
733
- recommendations,
734
- };
735
- }
736
523
  blockers.push({
737
524
  code: prepareResult.error?.code ?? "PREPARE_FAILED",
738
525
  message: prepareResult.error?.message ?? "task source prepare failed",
739
526
  details: {
740
527
  gaps: prepareResult.gaps,
741
528
  outcome: prepareResult.outcome,
742
- softIntake: softIntakeAfter,
743
529
  },
744
530
  });
745
531
  break;
@@ -1028,22 +814,6 @@ export async function advanceTaskLifecycle(input) {
1028
814
  }
1029
815
  await saveLifecycleRecord(input.repoRoot, record);
1030
816
  }
1031
- // Build the compact run summary (AC-005/AC-006) from the settled run
1032
- // state so the operator sees node statuses/verdict/outOfBounds without
1033
- // reading state.json. Best-effort: skip when the run dir is unavailable.
1034
- if (run?.runDir) {
1035
- try {
1036
- const settledState = await readDagRunState(run.runDir);
1037
- const summary = buildRunSummary(settledState);
1038
- if (summary) {
1039
- summary.next = snapshot.next;
1040
- runSummary = summary;
1041
- }
1042
- }
1043
- catch {
1044
- // leave runSummary null
1045
- }
1046
- }
1047
817
  snapshot = await observeTaskLifecycle(input.repoRoot, input.taskId);
1048
818
  // Re-plan so promote/closeout can run in the same call after success.
1049
819
  continue;
@@ -1099,25 +869,6 @@ export async function advanceTaskLifecycle(input) {
1099
869
  (await buildCurrentGate(input.repoRoot, input.taskId)))
1100
870
  : snapshot.gate;
1101
871
  await exportDagDraftIfRequested(input.repoRoot, input.taskId, input.dagOutputPath);
1102
- // Fall back to the latest associated run state when this call did not
1103
- // itself monitor/start a run (e.g. re-observe after a previously settled
1104
- // run): the operator still wants the compact summary in the result.
1105
- if (!runSummary) {
1106
- const settledRun = run ?? snapshot.latestRun ?? snapshot.activeRun;
1107
- if (settledRun?.runDir) {
1108
- try {
1109
- const settledState = await readDagRunState(settledRun.runDir);
1110
- const summary = buildRunSummary(settledState);
1111
- if (summary) {
1112
- summary.next = snapshot.next;
1113
- runSummary = summary;
1114
- }
1115
- }
1116
- catch {
1117
- // leave runSummary null
1118
- }
1119
- }
1120
- }
1121
872
  return {
1122
873
  taskId: input.taskId,
1123
874
  lifecycleState: snapshot.lifecycleState,
@@ -1127,7 +878,6 @@ export async function advanceTaskLifecycle(input) {
1127
878
  warnings,
1128
879
  next: snapshot.next,
1129
880
  run,
1130
- runSummary,
1131
881
  promotion,
1132
882
  closeout,
1133
883
  recommendations,
@@ -1,5 +1,4 @@
1
1
  import { createHash } from "node:crypto";
2
- import { pathMatchesPattern } from "../../shared/git-progress.js";
3
2
  export function normalizeWriteSet(writeSet) {
4
3
  return [
5
4
  ...new Set(writeSet.map((entry) => entry.replace(/\\/g, "/").trim()).filter(Boolean)),
@@ -43,54 +42,6 @@ export function computeGateDigest(bound) {
43
42
  writeSet: normalizeWriteSet(bound.writeSet),
44
43
  });
45
44
  }
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
- }
94
45
  export function buildWriteSetGate(input) {
95
46
  const digest = computeGateDigest(input.bound);
96
47
  const id = "write-set-review";
@@ -103,7 +54,6 @@ export function buildWriteSetGate(input) {
103
54
  approveCommand: `loop-agent task advance ${input.taskId} --approve-gate ${id}:${digest} --json`,
104
55
  rejectCommand: `loop-agent task advance ${input.taskId} --reject-gate ${id}:${digest} --reason <text> --json`,
105
56
  bound: input.bound,
106
- delta: input.delta ?? null,
107
57
  };
108
58
  }
109
59
  export function parseGateToken(token) {
@@ -5,7 +5,7 @@ import { logicalRevisionForState, observeTaskContractState, } from "../../task/c
5
5
  import { assessDagRunLiveness, listAllDagRunEntries, readDagRunSpec, readDagRunState, } from "../../workflows/dag/lifecycle.js";
6
6
  import { resolveRunningControllerIdentity } from "../../shared/package-metadata.js";
7
7
  import { loadLifecycleRecord } from "./record.js";
8
- import { buildGateBinding, buildWriteSetGate, buildWriteSetGateDelta, controllerIdentityAnchor, hashCanonicalPayload, normalizeWriteSet, sha256Hex, } from "./gates.js";
8
+ import { buildGateBinding, buildWriteSetGate, controllerIdentityAnchor, hashCanonicalPayload, normalizeWriteSet, sha256Hex, } from "./gates.js";
9
9
  async function exists(filePath) {
10
10
  try {
11
11
  await access(filePath);
@@ -263,15 +263,6 @@ export async function observeTaskLifecycle(repoRoot, taskId) {
263
263
  : null),
264
264
  })
265
265
  : null;
266
- const gateDelta = gateBound != null
267
- ? buildWriteSetGateDelta({
268
- writeSet: gateBound.writeSet,
269
- contractAllowedPaths: taskConfigAllowed,
270
- writers: (parsedSpec?.tasks ?? [])
271
- .filter((task) => task.writePolicy === "exclusive")
272
- .map((task) => ({ id: task.id, writeSet: task.writeSet ?? [] })),
273
- })
274
- : null;
275
266
  const lastGate = record?.lastGate;
276
267
  const currentDigest = gateBound != null
277
268
  ? buildWriteSetGate({ taskId, bound: gateBound }).digest
@@ -286,7 +277,7 @@ export async function observeTaskLifecycle(repoRoot, taskId) {
286
277
  // not already approved/rejected. Rejected same-digest gates stay closed
287
278
  // until contract/source/DAG/writeSet/controller binding changes.
288
279
  const gate = gateBound && strictValidated && !approvedCurrent && !rejectedCurrent
289
- ? buildWriteSetGate({ taskId, bound: gateBound, delta: gateDelta })
280
+ ? buildWriteSetGate({ taskId, bound: gateBound })
290
281
  : null;
291
282
  const gateOpen = Boolean(gate);
292
283
  let legacyState;
@@ -1023,7 +1023,6 @@ function assertPendingAcceptance(value, state, task) {
1023
1023
  }
1024
1024
  const pending = value;
1025
1025
  const desiredSha256 = task.evidence.desiredSha256;
1026
- const sourceAnchorSha256 = task.evidence.sourceAnchorSha256;
1027
1026
  const guard = pending.guard;
1028
1027
  const receipt = pending.receipt;
1029
1028
  const transition = pending.transition;
@@ -1040,9 +1039,6 @@ function assertPendingAcceptance(value, state, task) {
1040
1039
  !isSha256(guard.desiredSha256) ||
1041
1040
  !isSha256(desiredSha256) ||
1042
1041
  guard.desiredSha256 !== desiredSha256 ||
1043
- (sourceAnchorSha256 === undefined
1044
- ? guard.sourceAnchorSha256 !== undefined
1045
- : guard.sourceAnchorSha256 !== sourceAnchorSha256) ||
1046
1042
  !isMergeReceipt(receipt) ||
1047
1043
  receipt.taskId !== guard.taskId ||
1048
1044
  receipt.path !== guard.path ||
@@ -1080,9 +1076,6 @@ function acceptedSurface(input) {
1080
1076
  acceptedEntry.acceptedMerge = {
1081
1077
  currentSha256: input.currentSha256,
1082
1078
  desiredSha256: input.desiredSha256,
1083
- ...(typeof input.task.evidence.sourceAnchorSha256 === "string"
1084
- ? { sourceAnchorSha256: input.task.evidence.sourceAnchorSha256 }
1085
- : {}),
1086
1079
  };
1087
1080
  return { beforeEntry: cloneJson(entry), accepted };
1088
1081
  }
@@ -1217,7 +1210,6 @@ async function recordAcceptedMerge(input) {
1217
1210
  if (!desiredSha256) {
1218
1211
  throw new Error(`init upgrade cannot accept ${input.task.path}: desired hash evidence is missing`);
1219
1212
  }
1220
- const sourceAnchorSha256 = input.task.evidence.sourceAnchorSha256;
1221
1213
  const canonicalPath = normalizeAllowedPath(input.task.path);
1222
1214
  const surfacePath = path.join(input.state.repoRoot, INIT_SURFACE_STATE_PATH);
1223
1215
  let pending = input.pending;
@@ -1271,9 +1263,6 @@ async function recordAcceptedMerge(input) {
1271
1263
  allowedPaths: [canonicalPath],
1272
1264
  currentSha256: input.currentSha256,
1273
1265
  desiredSha256,
1274
- ...(sourceAnchorSha256 !== undefined
1275
- ? { sourceAnchorSha256 }
1276
- : {}),
1277
1266
  },
1278
1267
  receipt,
1279
1268
  transition: {
@@ -2423,27 +2412,7 @@ async function executeUpgrade(directory, state, input) {
2423
2412
  });
2424
2413
  state.actionReceipts.push(...safe.applied);
2425
2414
  await writeJsonAtomic(path.join(directory, "change-manifest.json"), state.actionReceipts);
2426
- let post = await checkInitUpdate({ repoRoot: state.repoRoot });
2427
- // Converge remaining safe deterministic init actions before the final check.
2428
- // Project/governance-derived drift (e.g. a personalized project rename after
2429
- // an accepted merge) settles here instead of failing finalCheck. Model merge
2430
- // tasks and human decisions are never auto-applied.
2431
- while (post.deterministicActions.length > 0) {
2432
- const converged = await applyInitUpdate({
2433
- repoRoot: state.repoRoot,
2434
- applySafe: true,
2435
- clientRecovery: "project",
2436
- });
2437
- state.actionReceipts.push(...converged.applied);
2438
- await writeJsonAtomic(path.join(directory, "change-manifest.json"), state.actionReceipts);
2439
- if (converged.applied.length === 0)
2440
- break;
2441
- post = await checkInitUpdate({ repoRoot: state.repoRoot });
2442
- if (post.humanDecisions.length > 0 ||
2443
- post.modelMergeTasks.length > 0) {
2444
- break;
2445
- }
2446
- }
2415
+ const post = await checkInitUpdate({ repoRoot: state.repoRoot });
2447
2416
  state.humanDecisions = post.humanDecisions;
2448
2417
  if (post.humanDecisions.length > 0) {
2449
2418
  state.mergeTasks = [];