@tea-agent/loop-agent 0.33.7-beta.0 → 0.34.0
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.
- package/CHANGELOG.md +38 -17
- package/dist/application/task-lifecycle/advance.js +254 -4
- package/dist/application/task-lifecycle/gates.js +50 -0
- package/dist/application/task-lifecycle/observe.js +11 -2
- package/dist/commands/init-upgrade.js +32 -1
- package/dist/commands/init.js +94 -3
- package/dist/executors/shell-executor.js +4 -91
- package/dist/executors/shell-write-guard.js +26 -8
- package/dist/shared/operator/capabilities.js +72 -42
- package/dist/shared/resilient-git.js +133 -0
- package/dist/task/source-prepare/artifact-meta.js +137 -0
- package/dist/task/source-prepare/index.js +2 -0
- package/dist/task/source-prepare/parse-intent.js +58 -10
- package/dist/task/source-prepare/prepare.js +180 -16
- package/dist/task/source-prepare/reference-integrity.js +18 -2
- package/dist/task/source-prepare/semantic-intake.js +404 -0
- package/dist/worker/console/app-data.js +2 -0
- package/dist/worker/console/chat/chat-event-store.js +190 -25
- package/dist/worker/console/chat/model-resolver.js +17 -0
- package/dist/worker/console/chat/pi-console-config.js +250 -32
- package/dist/worker/console/chat/pi-runtime.js +1007 -188
- package/dist/worker/console/chat/resource-loader.js +5 -4
- package/dist/worker/console/chat/routes.js +495 -157
- package/dist/worker/console/chat/runtime-context.js +48 -12
- package/dist/worker/console/chat/runtime-selection.js +59 -0
- package/dist/worker/console/chat/session-store.js +39 -0
- package/dist/worker/console/chat/shortcuts.js +1 -0
- package/dist/worker/console/chat/tool-adapter.js +9 -3
- package/dist/worker/console/chat/tools.js +5 -1
- package/dist/worker/console/dag-execution-receipt.js +380 -0
- package/dist/worker/console/operator-actions.js +559 -68
- package/dist/worker/console/server.js +8 -15
- package/dist/worker/console/static/assets/index-BQkhJpV8.css +1 -0
- package/dist/worker/console/static/assets/index-CMHovlqG.js +32 -0
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +45 -8
- package/dist/worker/console/static-src/operator-chat/landing-density.js +23 -0
- package/dist/worker/console/static-src/operator-chat/refs.js +9 -0
- package/dist/worker/console/static-src/operator-chat/runtime-snapshot-store.js +257 -0
- package/dist/worker/console/static-src/operator-chat/session-title-watcher.js +128 -0
- package/dist/worker/console/static-src/operator-chat/sidebar-split.js +90 -0
- package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +37 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +125 -22
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +215 -184
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +49 -5
- package/dist/worker/console/static-src/operator-chat/useComposer.js +17 -0
- package/dist/worker/console/static-src/operator-chat/useOverlayFocus.js +84 -0
- package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +225 -74
- package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +196 -0
- package/dist/worker/console/static-src/operator-chat/useWorkspaceLayout.js +58 -0
- package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +31 -0
- package/dist/worker/delivery/final-verification.js +13 -5
- package/dist/worker/delivery/package.js +31 -19
- package/dist/worker/delivery/verification-bundle.js +6 -4
- package/dist/worker/observe/static/operator-chrome.css +5 -2
- package/dist/worker/observe/static/operator-chrome.js +6 -1
- package/dist/worker/observe/static/styles.css +39 -9
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +33 -462
- package/dist/workflows/dag/backend-test-case-manifest.js +0 -4
- package/dist/workflows/dag/backend-test-markdown-workflow.js +1 -25
- package/dist/workflows/dag/backend-test-module-stem.js +0 -5
- package/dist/workflows/dag/backend-test-pytest-collection.js +24 -345
- package/dist/workflows/dag/backend-test-scenario-param.js +82 -269
- package/dist/workflows/dag/backend-test-writer-completeness.js +16 -47
- package/dist/workflows/dag/dynamic-runtime/map.js +8 -24
- package/dist/workflows/dag/frontend-worktree-diff.js +12 -27
- package/dist/workflows/dag/init-hybrid.js +34 -46
- package/dist/workflows/dag/types.js +0 -7
- package/dist/workflows/dag/workspace-checkpoint.js +8 -27
- package/docs/templates/backend-test-dag.json +29 -32
- package/harness.json +1 -1
- package/package.json +1 -1
- package/skills/local-jacoco-coverage/SKILL.md +281 -0
- package/skills/local-jacoco-coverage/references/requirement-to-source-mapping.md +85 -0
- package/skills/local-jacoco-coverage/references/runtime-alignment.md +106 -0
- package/skills/local-jacoco-coverage/scripts/run-coverage-analysis.sh +148 -0
- package/skills/local-jacoco-coverage/scripts/start-jacoco-agent.sh +110 -0
- package/skills/loop-agent/references/command-reference.md +3 -1
- package/skills/loop-agent/references/source-and-plan-practice.md +13 -0
- package/skills/loop-agent/references/task-workflow.md +4 -0
- package/dist/worker/console/chat/instruction-skills.js +0 -217
- package/dist/worker/console/static/assets/index-CnUXAqxG.css +0 -1
- package/dist/worker/console/static/assets/index-CteJFFL2.js +0 -29
package/CHANGELOG.md
CHANGED
|
@@ -2,38 +2,59 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
-
## [0.
|
|
5
|
+
## [0.34.0] - 2026-08-11
|
|
6
6
|
|
|
7
7
|
### 重点更新
|
|
8
8
|
|
|
9
|
-
-
|
|
9
|
+
- Operator Chat 会话创建实现即时响应,Landing 页面与创建请求延迟从秒级降至毫秒级
|
|
10
|
+
- 新增桌面端紧凑响应式布局,根据工作区宽度自动适配 comfortable/compact/narrow 三档显示模式
|
|
11
|
+
- 新增自动会话标题功能,基于首条用户消息生成临时标题,并在首轮对话完成后异步生成正式标题
|
|
12
|
+
- 新增 local-jacoco-coverage 技能,将本地 Java 服务挂载、测试执行与覆盖率切片分析串成标准化链路
|
|
10
13
|
|
|
11
14
|
### 新增
|
|
12
15
|
|
|
13
|
-
-
|
|
14
|
-
-
|
|
16
|
+
- Operator Chat 桌面紧凑响应式布局:按工作区宽度切换 comfortable(≥1450px)/ compact(1100–1449px)/ narrow(<1100px)三档;compact 档使用图标窄栏与覆盖抽屉,narrow 档使用顶栏入口;会话与仓库面板支持拖拽分栏并记忆比例
|
|
17
|
+
- Operator Chat 自动会话标题:首条用户消息持久化后生成清洗有界的临时标题,首轮对话成功完成后异步生成正式标题;手工重命名、归档和删除操作可拒绝迟到的自动标题覆盖
|
|
18
|
+
- local-jacoco-coverage 技能:将本地启动 Java 服务挂载 jacocoagent、运行 backend-test DAG、按需求文档做覆盖率切片分析三步串联为可审计、可重复的本地链路,包含强制流程手册、runtime 对齐参考、需求映射规范与编排脚本
|
|
15
19
|
|
|
16
20
|
### 改进
|
|
17
21
|
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
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 标签,并支持长度意图用数字位置参数表达
|
|
22
|
+
- Operator Chat 会话创建改为即时响应:先落盘会话骨架与持久记录后立即返回成功,完整 AgentSession 在后台异步初始化,真实 Chrome 测试中 Landing 延迟从 1346ms 降至 22ms、创建请求从 1269ms 降至 16ms
|
|
23
|
+
- 会话创建期间侧栏与 Landing 页共享 busy 状态反馈,抑制双击导致的重复创建
|
|
24
|
+
- 删除正在初始化的会话不会被迟到的后台初始化任务复活,初始化失败统一返回 HTTP 503 错误码
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
## [0.33.6] - 2026-08-11
|
|
27
|
+
|
|
28
|
+
### 新增
|
|
26
29
|
|
|
27
|
-
-
|
|
28
|
-
-
|
|
30
|
+
- stabilize PRD intake + add Semantic Intake fallback
|
|
31
|
+
- Operator Chat 单事件流 + boot epoch fence(Task C)
|
|
32
|
+
- gate writeSet delta + run summary(操作面增强)
|
|
33
|
+
- supervise DAG runs autonomously
|
|
34
|
+
- ChatEventStore 事件持久化压缩(coalescing + maxBytes 双限制)
|
|
35
|
+
- reconcile(cursor_expired) 自动恢复而非全局致命错误
|
|
36
|
+
- 接入 Operator Chat 实时运行时控制
|
|
37
|
+
- open operator chat resource loading
|
|
38
|
+
- enrich execution report and URL resolution
|
|
39
|
+
|
|
40
|
+
### 改进
|
|
29
41
|
|
|
30
|
-
|
|
42
|
+
- style(task-advance): biome autoformat 对齐(无功能变化)
|
|
43
|
+
- style(console): apply biome formatting from pi-lens autoformat
|
|
31
44
|
|
|
32
45
|
### 修复
|
|
33
46
|
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
-
|
|
47
|
+
- unify resilient read-only command execution
|
|
48
|
+
- preserve user-owned Pi model routing
|
|
49
|
+
- show installed package metadata
|
|
50
|
+
- escape preflight backtick matcher
|
|
51
|
+
- align chat streaming reliability with pi-web
|
|
52
|
+
- split rerun defaults and repair selector escaping
|
|
53
|
+
- ignore delimiters inside pytest strings
|
|
54
|
+
- resolve target URL from runtime context
|
|
55
|
+
- reject priority-only module stems
|
|
56
|
+
- stop desiredSha256 mechanical checks from blocking generated-content upgrades
|
|
57
|
+
- use mobius header brand icon
|
|
37
58
|
|
|
38
59
|
## [0.33.5] - 2026-08-11
|
|
39
60
|
|
|
@@ -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";
|
|
@@ -98,6 +101,42 @@ function toRunAssociation(input) {
|
|
|
98
101
|
source: input.source ?? "lifecycle-record",
|
|
99
102
|
};
|
|
100
103
|
}
|
|
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
|
+
}
|
|
101
140
|
async function buildCurrentGate(repoRoot, taskId) {
|
|
102
141
|
const paths = getTaskPaths(repoRoot, taskId);
|
|
103
142
|
if (!(await pathExists(paths.dagDraftPath)))
|
|
@@ -113,6 +152,7 @@ async function buildCurrentGate(repoRoot, taskId) {
|
|
|
113
152
|
const contractState = await observeTaskContractState(repoRoot, taskId);
|
|
114
153
|
const identity = resolveRunningControllerIdentity();
|
|
115
154
|
let writeSet = collectNormalizedWriteSetFromSpec(spec);
|
|
155
|
+
let contractAllowedPaths = [];
|
|
116
156
|
if (writeSet.length === 0) {
|
|
117
157
|
try {
|
|
118
158
|
const config = await loadTaskConfig(repoRoot, taskId);
|
|
@@ -122,6 +162,18 @@ async function buildCurrentGate(repoRoot, taskId) {
|
|
|
122
162
|
// leave empty
|
|
123
163
|
}
|
|
124
164
|
}
|
|
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 ?? [] }));
|
|
125
177
|
const bound = buildGateBinding({
|
|
126
178
|
taskId,
|
|
127
179
|
contractRevision: logicalRevisionForState(contractState),
|
|
@@ -141,7 +193,12 @@ async function buildCurrentGate(repoRoot, taskId) {
|
|
|
141
193
|
}
|
|
142
194
|
: null),
|
|
143
195
|
});
|
|
144
|
-
|
|
196
|
+
const delta = buildWriteSetGateDelta({
|
|
197
|
+
writeSet,
|
|
198
|
+
contractAllowedPaths,
|
|
199
|
+
writers,
|
|
200
|
+
});
|
|
201
|
+
return buildWriteSetGate({ taskId, bound, delta });
|
|
145
202
|
}
|
|
146
203
|
async function monitorRun(input) {
|
|
147
204
|
const started = Date.now();
|
|
@@ -213,6 +270,7 @@ export async function advanceTaskLifecycle(input) {
|
|
|
213
270
|
let promotion = null;
|
|
214
271
|
let closeout = null;
|
|
215
272
|
let run = null;
|
|
273
|
+
let runSummary = null;
|
|
216
274
|
let consumeApproveGate = Boolean(input.approveGateToken);
|
|
217
275
|
/** After a successful approve, keep approve intent for finalize replan only. */
|
|
218
276
|
let approvedThisCall = false;
|
|
@@ -226,6 +284,7 @@ export async function advanceTaskLifecycle(input) {
|
|
|
226
284
|
let preparedContractThisCall = false;
|
|
227
285
|
/** A successful managed-contract mutation invalidates the prior DAG/gate. */
|
|
228
286
|
let regenerateDagThisCall = false;
|
|
287
|
+
let semanticIntakeAttemptedThisCall = false;
|
|
229
288
|
let snapshot = await observeTaskLifecycle(input.repoRoot, input.taskId);
|
|
230
289
|
let record = (await loadLifecycleRecord(input.repoRoot, input.taskId)) ??
|
|
231
290
|
emptyLifecycleRecord(input.taskId);
|
|
@@ -457,11 +516,25 @@ export async function advanceTaskLifecycle(input) {
|
|
|
457
516
|
}
|
|
458
517
|
if (action.type === "import-prd") {
|
|
459
518
|
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
|
+
});
|
|
460
531
|
await importPrdDocument({
|
|
461
532
|
repoRoot: input.repoRoot,
|
|
462
533
|
taskId: input.taskId,
|
|
463
534
|
filePath: prd.filePath,
|
|
464
|
-
role
|
|
535
|
+
role,
|
|
536
|
+
// Prefer basename-aligned names for multi-file product suites.
|
|
537
|
+
name: path.basename(prd.filePath).replace(/\.(md|markdown|txt)$/iu, "") || undefined,
|
|
465
538
|
});
|
|
466
539
|
}
|
|
467
540
|
// Latch even when prdPaths is empty so a mis-planned import cannot loop.
|
|
@@ -494,7 +567,7 @@ export async function advanceTaskLifecycle(input) {
|
|
|
494
567
|
break;
|
|
495
568
|
}
|
|
496
569
|
}
|
|
497
|
-
|
|
570
|
+
let prepareResult = await prepareTaskSource({
|
|
498
571
|
repoRoot: input.repoRoot,
|
|
499
572
|
taskId: input.taskId,
|
|
500
573
|
mode: "apply",
|
|
@@ -520,12 +593,153 @@ export async function advanceTaskLifecycle(input) {
|
|
|
520
593
|
},
|
|
521
594
|
});
|
|
522
595
|
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
|
+
}
|
|
523
736
|
blockers.push({
|
|
524
737
|
code: prepareResult.error?.code ?? "PREPARE_FAILED",
|
|
525
738
|
message: prepareResult.error?.message ?? "task source prepare failed",
|
|
526
739
|
details: {
|
|
527
740
|
gaps: prepareResult.gaps,
|
|
528
741
|
outcome: prepareResult.outcome,
|
|
742
|
+
softIntake: softIntakeAfter,
|
|
529
743
|
},
|
|
530
744
|
});
|
|
531
745
|
break;
|
|
@@ -814,6 +1028,22 @@ export async function advanceTaskLifecycle(input) {
|
|
|
814
1028
|
}
|
|
815
1029
|
await saveLifecycleRecord(input.repoRoot, record);
|
|
816
1030
|
}
|
|
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
|
+
}
|
|
817
1047
|
snapshot = await observeTaskLifecycle(input.repoRoot, input.taskId);
|
|
818
1048
|
// Re-plan so promote/closeout can run in the same call after success.
|
|
819
1049
|
continue;
|
|
@@ -869,6 +1099,25 @@ export async function advanceTaskLifecycle(input) {
|
|
|
869
1099
|
(await buildCurrentGate(input.repoRoot, input.taskId)))
|
|
870
1100
|
: snapshot.gate;
|
|
871
1101
|
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
|
+
}
|
|
872
1121
|
return {
|
|
873
1122
|
taskId: input.taskId,
|
|
874
1123
|
lifecycleState: snapshot.lifecycleState,
|
|
@@ -878,6 +1127,7 @@ export async function advanceTaskLifecycle(input) {
|
|
|
878
1127
|
warnings,
|
|
879
1128
|
next: snapshot.next,
|
|
880
1129
|
run,
|
|
1130
|
+
runSummary,
|
|
881
1131
|
promotion,
|
|
882
1132
|
closeout,
|
|
883
1133
|
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) {
|
|
@@ -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, controllerIdentityAnchor, hashCanonicalPayload, normalizeWriteSet, sha256Hex, } from "./gates.js";
|
|
8
|
+
import { buildGateBinding, buildWriteSetGate, buildWriteSetGateDelta, controllerIdentityAnchor, hashCanonicalPayload, normalizeWriteSet, sha256Hex, } from "./gates.js";
|
|
9
9
|
async function exists(filePath) {
|
|
10
10
|
try {
|
|
11
11
|
await access(filePath);
|
|
@@ -263,6 +263,15 @@ 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;
|
|
266
275
|
const lastGate = record?.lastGate;
|
|
267
276
|
const currentDigest = gateBound != null
|
|
268
277
|
? buildWriteSetGate({ taskId, bound: gateBound }).digest
|
|
@@ -277,7 +286,7 @@ export async function observeTaskLifecycle(repoRoot, taskId) {
|
|
|
277
286
|
// not already approved/rejected. Rejected same-digest gates stay closed
|
|
278
287
|
// until contract/source/DAG/writeSet/controller binding changes.
|
|
279
288
|
const gate = gateBound && strictValidated && !approvedCurrent && !rejectedCurrent
|
|
280
|
-
? buildWriteSetGate({ taskId, bound: gateBound })
|
|
289
|
+
? buildWriteSetGate({ taskId, bound: gateBound, delta: gateDelta })
|
|
281
290
|
: null;
|
|
282
291
|
const gateOpen = Boolean(gate);
|
|
283
292
|
let legacyState;
|
|
@@ -1023,6 +1023,7 @@ 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;
|
|
1026
1027
|
const guard = pending.guard;
|
|
1027
1028
|
const receipt = pending.receipt;
|
|
1028
1029
|
const transition = pending.transition;
|
|
@@ -1039,6 +1040,9 @@ function assertPendingAcceptance(value, state, task) {
|
|
|
1039
1040
|
!isSha256(guard.desiredSha256) ||
|
|
1040
1041
|
!isSha256(desiredSha256) ||
|
|
1041
1042
|
guard.desiredSha256 !== desiredSha256 ||
|
|
1043
|
+
(sourceAnchorSha256 === undefined
|
|
1044
|
+
? guard.sourceAnchorSha256 !== undefined
|
|
1045
|
+
: guard.sourceAnchorSha256 !== sourceAnchorSha256) ||
|
|
1042
1046
|
!isMergeReceipt(receipt) ||
|
|
1043
1047
|
receipt.taskId !== guard.taskId ||
|
|
1044
1048
|
receipt.path !== guard.path ||
|
|
@@ -1076,6 +1080,9 @@ function acceptedSurface(input) {
|
|
|
1076
1080
|
acceptedEntry.acceptedMerge = {
|
|
1077
1081
|
currentSha256: input.currentSha256,
|
|
1078
1082
|
desiredSha256: input.desiredSha256,
|
|
1083
|
+
...(typeof input.task.evidence.sourceAnchorSha256 === "string"
|
|
1084
|
+
? { sourceAnchorSha256: input.task.evidence.sourceAnchorSha256 }
|
|
1085
|
+
: {}),
|
|
1079
1086
|
};
|
|
1080
1087
|
return { beforeEntry: cloneJson(entry), accepted };
|
|
1081
1088
|
}
|
|
@@ -1210,6 +1217,7 @@ async function recordAcceptedMerge(input) {
|
|
|
1210
1217
|
if (!desiredSha256) {
|
|
1211
1218
|
throw new Error(`init upgrade cannot accept ${input.task.path}: desired hash evidence is missing`);
|
|
1212
1219
|
}
|
|
1220
|
+
const sourceAnchorSha256 = input.task.evidence.sourceAnchorSha256;
|
|
1213
1221
|
const canonicalPath = normalizeAllowedPath(input.task.path);
|
|
1214
1222
|
const surfacePath = path.join(input.state.repoRoot, INIT_SURFACE_STATE_PATH);
|
|
1215
1223
|
let pending = input.pending;
|
|
@@ -1263,6 +1271,9 @@ async function recordAcceptedMerge(input) {
|
|
|
1263
1271
|
allowedPaths: [canonicalPath],
|
|
1264
1272
|
currentSha256: input.currentSha256,
|
|
1265
1273
|
desiredSha256,
|
|
1274
|
+
...(sourceAnchorSha256 !== undefined
|
|
1275
|
+
? { sourceAnchorSha256 }
|
|
1276
|
+
: {}),
|
|
1266
1277
|
},
|
|
1267
1278
|
receipt,
|
|
1268
1279
|
transition: {
|
|
@@ -2412,7 +2423,27 @@ async function executeUpgrade(directory, state, input) {
|
|
|
2412
2423
|
});
|
|
2413
2424
|
state.actionReceipts.push(...safe.applied);
|
|
2414
2425
|
await writeJsonAtomic(path.join(directory, "change-manifest.json"), state.actionReceipts);
|
|
2415
|
-
|
|
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
|
+
}
|
|
2416
2447
|
state.humanDecisions = post.humanDecisions;
|
|
2417
2448
|
if (post.humanDecisions.length > 0) {
|
|
2418
2449
|
state.mergeTasks = [];
|