@tea-agent/loop-agent 0.33.6 → 0.33.7-beta.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 +23 -23
- package/dist/application/task-lifecycle/advance.js +4 -254
- package/dist/application/task-lifecycle/gates.js +0 -50
- package/dist/application/task-lifecycle/observe.js +2 -11
- package/dist/commands/init-upgrade.js +1 -32
- package/dist/commands/init.js +3 -94
- package/dist/executors/shell-executor.js +91 -4
- package/dist/executors/shell-write-guard.js +8 -26
- package/dist/shared/operator/capabilities.js +42 -72
- package/dist/task/source-prepare/index.js +0 -2
- package/dist/task/source-prepare/parse-intent.js +10 -58
- package/dist/task/source-prepare/prepare.js +16 -180
- package/dist/task/source-prepare/reference-integrity.js +2 -18
- package/dist/worker/console/app-data.js +0 -2
- package/dist/worker/console/chat/chat-event-store.js +25 -190
- package/dist/worker/console/chat/instruction-skills.js +217 -0
- package/dist/worker/console/chat/pi-console-config.js +32 -250
- package/dist/worker/console/chat/pi-runtime.js +71 -625
- package/dist/worker/console/chat/resource-loader.js +4 -5
- package/dist/worker/console/chat/routes.js +146 -324
- package/dist/worker/console/chat/runtime-context.js +12 -48
- package/dist/worker/console/chat/runtime-selection.js +0 -59
- package/dist/worker/console/chat/shortcuts.js +0 -1
- package/dist/worker/console/chat/tool-adapter.js +3 -9
- package/dist/worker/console/chat/tools.js +1 -5
- package/dist/worker/console/operator-actions.js +68 -559
- package/dist/worker/console/server.js +15 -8
- package/dist/worker/console/static/assets/index-CnUXAqxG.css +1 -0
- package/dist/worker/console/static/assets/index-CteJFFL2.js +29 -0
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +8 -45
- package/dist/worker/console/static-src/operator-chat/refs.js +0 -9
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +0 -16
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +184 -210
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +5 -49
- package/dist/worker/console/static-src/operator-chat/useComposer.js +0 -17
- package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +74 -225
- package/dist/worker/delivery/final-verification.js +5 -13
- package/dist/worker/delivery/package.js +19 -31
- package/dist/worker/delivery/verification-bundle.js +4 -6
- package/dist/worker/observe/static/operator-chrome.css +2 -5
- package/dist/worker/observe/static/operator-chrome.js +1 -6
- package/dist/worker/observe/static/styles.css +9 -39
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +462 -33
- package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
- package/dist/workflows/dag/backend-test-markdown-workflow.js +25 -1
- package/dist/workflows/dag/backend-test-module-stem.js +5 -0
- package/dist/workflows/dag/backend-test-pytest-collection.js +345 -24
- package/dist/workflows/dag/backend-test-scenario-param.js +269 -82
- package/dist/workflows/dag/backend-test-writer-completeness.js +47 -16
- package/dist/workflows/dag/dynamic-runtime/map.js +24 -8
- package/dist/workflows/dag/frontend-worktree-diff.js +27 -12
- package/dist/workflows/dag/init-hybrid.js +46 -34
- package/dist/workflows/dag/types.js +7 -0
- package/dist/workflows/dag/workspace-checkpoint.js +27 -8
- package/docs/templates/backend-test-dag.json +32 -29
- package/harness.json +1 -1
- package/package.json +1 -1
- package/skills/loop-agent/references/command-reference.md +1 -3
- package/skills/loop-agent/references/source-and-plan-practice.md +0 -13
- package/skills/loop-agent/references/task-workflow.md +0 -4
- package/dist/shared/resilient-git.js +0 -133
- package/dist/task/source-prepare/artifact-meta.js +0 -137
- package/dist/task/source-prepare/semantic-intake.js +0 -404
- package/dist/worker/console/dag-execution-receipt.js +0 -380
- package/dist/worker/console/static/assets/index-BUOLppPr.js +0 -28
- package/dist/worker/console/static/assets/index-C1KzazY5.css +0 -1
- package/dist/worker/console/static-src/operator-chat/runtime-snapshot-store.js +0 -257
- package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +0 -196
package/CHANGELOG.md
CHANGED
|
@@ -2,38 +2,38 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
-
## [0.33.
|
|
5
|
+
## [0.33.7-beta.0] - 2026-08-11
|
|
6
|
+
|
|
7
|
+
### 重点更新
|
|
8
|
+
|
|
9
|
+
- backend-test 收口 source-fidelity P0:绑定 source coverage、有界 Markdown/pytest 同步、item-level eligibility 与单次 eligible 业务执行;my-webapp R107 16 节点 finished
|
|
6
10
|
|
|
7
11
|
### 新增
|
|
8
12
|
|
|
9
|
-
-
|
|
10
|
-
-
|
|
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
|
|
13
|
+
- backend-test 新增绑定 source references 的来源覆盖事实、Markdown 一轮增量同步和 pytest correspondence 一轮有界修复,在业务测试体执行前保留 initial/effective/residual 证据
|
|
14
|
+
- backend-test 新增 item-level execution eligibility:只授权 payload-safe 的 pytest items,excluded items 与真实业务失败分开展示
|
|
18
15
|
|
|
19
16
|
### 改进
|
|
20
17
|
|
|
21
|
-
-
|
|
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 标签,并支持长度意图用数字位置参数表达
|
|
24
|
+
|
|
25
|
+
### 修复
|
|
26
|
+
|
|
27
|
+
- 修复 readiness 错误信息截断与无效 Coverage Matrix 状态语义,避免超长错误或 advisory 矩阵状态误阻断后续节点
|
|
28
|
+
- 修复 scenario 排除项未绑定 payload 字段、request-level 不确定性被误升级为硬失败的问题
|
|
29
|
+
|
|
30
|
+
## [0.33.6-beta.0] - 2026-08-11
|
|
23
31
|
|
|
24
32
|
### 修复
|
|
25
33
|
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
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
|
|
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 事实
|
|
37
37
|
|
|
38
38
|
## [0.33.5] - 2026-08-11
|
|
39
39
|
|
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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 = [];
|