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