@tea-agent/loop-agent 0.34.1 → 0.34.3
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 +30 -0
- package/dist/application/task-lifecycle/advance.js +5 -1
- package/dist/shared/timeout-policy.js +5 -0
- package/dist/task/source-prepare/index.js +1 -0
- package/dist/task/source-prepare/placeholder-paths.js +77 -0
- package/dist/task/source-prepare/semantic-intake.js +192 -27
- package/dist/worker/cli.js +16 -0
- package/dist/worker/console/prd-intake-bridge.js +51 -10
- package/dist/worker/console/server.js +28 -0
- package/dist/workflows/dag/liveness-policy.js +2 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.34.3] - 2026-08-12
|
|
6
|
+
|
|
7
|
+
### 重点更新
|
|
8
|
+
|
|
9
|
+
- 修复 PRD 导入时 grill-me 模板占位符路径污染写入集的问题,确保仅处理真实需求路径
|
|
10
|
+
|
|
11
|
+
### 改进
|
|
12
|
+
|
|
13
|
+
- 语义化导入现可作为兜底方案推荐工程边界,提升需求解析的准确性
|
|
14
|
+
- 将语义约束与验证信息合并至控制台草稿,避免表单默认值覆盖精确的验证命令
|
|
15
|
+
|
|
16
|
+
### 修复
|
|
17
|
+
|
|
18
|
+
- 修复 grill-me 模板占位符路径未被过滤即进入写入集,导致占位内容污染实际工程数据的问题
|
|
19
|
+
|
|
20
|
+
## [0.34.2] - 2026-08-12
|
|
21
|
+
|
|
22
|
+
### 重点更新
|
|
23
|
+
|
|
24
|
+
- 修复控制台 DAG 执行在 120 秒后被强制终止导致状态卡死的问题,默认超时现提升至 4 小时
|
|
25
|
+
|
|
26
|
+
### 改进
|
|
27
|
+
|
|
28
|
+
- DAG 挂钟时间常量统一收敛至 src/shared 共享模块管理,移除了 workflows 中不必要的内核导入
|
|
29
|
+
|
|
30
|
+
### 修复
|
|
31
|
+
|
|
32
|
+
- 修复控制台任务导航执行 DAG 时因 LoopAgentClient 默认 120 秒超时被强制终止的问题,现已将默认超时提升至 4 小时,并支持通过 --default-timeout-ms 参数覆盖
|
|
33
|
+
- 修正自定义模型合并报告中的文档描述错误
|
|
34
|
+
|
|
5
35
|
## [0.34.1] - 2026-08-11
|
|
6
36
|
|
|
7
37
|
### 重点更新
|
|
@@ -4,6 +4,7 @@ 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
6
|
import { isIntakeSoftGapCode, resolvePrdImportRole, } from "../../task/source-prepare/artifact-meta.js";
|
|
7
|
+
import { sanitizeDraftPlaceholders } from "../../task/source-prepare/placeholder-paths.js";
|
|
7
8
|
import { evaluateSemanticIntakeEligibility, isSemanticIntakeDisabled, readSemanticIntakeAttempted, runSemanticIntake, } from "../../task/source-prepare/semantic-intake.js";
|
|
8
9
|
import { validateImportedPrdReferences } from "../../task/source-prepare/reference-integrity.js";
|
|
9
10
|
import { logicalRevisionForState, observeTaskContractState, recoverTaskContract, } from "../../task/contract/index.js";
|
|
@@ -587,9 +588,12 @@ export async function advanceTaskLifecycle(input) {
|
|
|
587
588
|
if (input.fromDraftPath) {
|
|
588
589
|
try {
|
|
589
590
|
const draftRaw = await readFile(path.resolve(input.fromDraftPath), "utf-8");
|
|
591
|
+
const parsedDraft = JSON.parse(draftRaw);
|
|
592
|
+
// Strip Console grill-me placeholder strings so they never become
|
|
593
|
+
// writeSet globs / projected 需求.md bullets.
|
|
590
594
|
draftIntent = {
|
|
591
595
|
kind: "draft",
|
|
592
|
-
draft:
|
|
596
|
+
draft: sanitizeDraftPlaceholders(parsedDraft),
|
|
593
597
|
sourcePath: input.fromDraftPath,
|
|
594
598
|
};
|
|
595
599
|
}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { DEFAULT_TIMEOUT_MS, MAX_ALLOWED_TIMEOUT_MS } from "../executors/pi-executor.js";
|
|
2
2
|
export { MAX_ALLOWED_TIMEOUT_MS };
|
|
3
|
+
/**
|
|
4
|
+
* DAG runner 绝对最大墙钟(4h)。不可被空心跳续期;
|
|
5
|
+
* 是 DEFAULT_LIVENESS_POLICY 与 Console LoopAgentClient 默认超时的单一真源。
|
|
6
|
+
*/
|
|
7
|
+
export const DAG_ABSOLUTE_MAX_WALL_CLOCK_MS = 14_400_000;
|
|
3
8
|
/**
|
|
4
9
|
* Clamp timeout values for Pi steps. Undefined or non-positive values fall back to
|
|
5
10
|
* DEFAULT_TIMEOUT_MS so task-level invalid values do not disable Pi execution timeouts.
|
|
@@ -2,6 +2,7 @@ export * from "./types.js";
|
|
|
2
2
|
export * from "./path-policy.js";
|
|
3
3
|
export * from "./parse-intent.js";
|
|
4
4
|
export * from "./artifact-meta.js";
|
|
5
|
+
export * from "./placeholder-paths.js";
|
|
5
6
|
export * from "./semantic-intake.js";
|
|
6
7
|
export * from "./reference-integrity.js";
|
|
7
8
|
export * from "./completeness.js";
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared filters for Console / grill-me placeholder text that must never
|
|
3
|
+
* become writeSet paths or contract scope bullets.
|
|
4
|
+
*
|
|
5
|
+
* Placeholders are Chinese cue-words + fullwidth brackets from interview
|
|
6
|
+
* recommendations (e.g. "(工程边界须人类确认;示例:src/**, test/**)").
|
|
7
|
+
* Do not require glob syntax — legitimate paths like `package.json` must pass.
|
|
8
|
+
*/
|
|
9
|
+
export const PLACEHOLDER_PATH_RE = /请|示例|模板|(|)|须人类确认|勿套用|暂无|填写|确认|按.*结构/;
|
|
10
|
+
export function isNonPlaceholderPath(value) {
|
|
11
|
+
return (typeof value === "string" &&
|
|
12
|
+
value.trim().length > 0 &&
|
|
13
|
+
!PLACEHOLDER_PATH_RE.test(value));
|
|
14
|
+
}
|
|
15
|
+
export function filterPlaceholderPaths(values) {
|
|
16
|
+
if (!Array.isArray(values))
|
|
17
|
+
return [];
|
|
18
|
+
return values
|
|
19
|
+
.map((item) => (typeof item === "string" ? item.trim() : ""))
|
|
20
|
+
.filter(isNonPlaceholderPath);
|
|
21
|
+
}
|
|
22
|
+
function isPlaceholderText(value) {
|
|
23
|
+
return typeof value === "string" && PLACEHOLDER_PATH_RE.test(value);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Strip grill-me / form placeholder strings from a draft before prepare.
|
|
27
|
+
* Keeps real defaults like `npm run typecheck` and `.harness/**`.
|
|
28
|
+
*/
|
|
29
|
+
export function sanitizeDraftPlaceholders(draft) {
|
|
30
|
+
if (!draft || typeof draft !== "object")
|
|
31
|
+
return draft;
|
|
32
|
+
const root = draft;
|
|
33
|
+
const next = { ...root };
|
|
34
|
+
if (root.constraints && typeof root.constraints === "object") {
|
|
35
|
+
const constraints = root.constraints;
|
|
36
|
+
next.constraints = {
|
|
37
|
+
...constraints,
|
|
38
|
+
allowedPaths: filterPlaceholderPaths(constraints.allowedPaths),
|
|
39
|
+
forbiddenPaths: filterPlaceholderPaths(constraints.forbiddenPaths),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
if (root.requirement && typeof root.requirement === "object") {
|
|
43
|
+
const requirement = root.requirement;
|
|
44
|
+
let scope = requirement.scope;
|
|
45
|
+
if (Array.isArray(requirement.scope)) {
|
|
46
|
+
scope = requirement.scope
|
|
47
|
+
.map((item) => (typeof item === "string" ? item.trim() : ""))
|
|
48
|
+
.filter((item) => item.length > 0 && !isPlaceholderText(item));
|
|
49
|
+
}
|
|
50
|
+
let nonGoals = requirement.nonGoals;
|
|
51
|
+
if (Array.isArray(requirement.nonGoals)) {
|
|
52
|
+
nonGoals = requirement.nonGoals
|
|
53
|
+
.map((item) => (typeof item === "string" ? item.trim() : ""))
|
|
54
|
+
.filter((item) => item.length > 0 && !isPlaceholderText(item));
|
|
55
|
+
}
|
|
56
|
+
let acceptanceCriteria = requirement.acceptanceCriteria;
|
|
57
|
+
if (Array.isArray(requirement.acceptanceCriteria)) {
|
|
58
|
+
acceptanceCriteria = requirement.acceptanceCriteria.filter((item) => {
|
|
59
|
+
if (typeof item === "string") {
|
|
60
|
+
return item.trim().length > 0 && !isPlaceholderText(item);
|
|
61
|
+
}
|
|
62
|
+
if (item && typeof item === "object") {
|
|
63
|
+
const text = item.text;
|
|
64
|
+
return typeof text === "string" && !isPlaceholderText(text);
|
|
65
|
+
}
|
|
66
|
+
return true;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
next.requirement = {
|
|
70
|
+
...requirement,
|
|
71
|
+
scope,
|
|
72
|
+
nonGoals,
|
|
73
|
+
acceptanceCriteria,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return next;
|
|
77
|
+
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* P2 Semantic Intake: one-shot, read-only Pi structuring of imported PRDs
|
|
3
3
|
* into TaskContractDraftV1. Does not write source/需求.md directly — caller
|
|
4
|
-
* re-enters prepare with kind:"draft".
|
|
5
|
-
*
|
|
4
|
+
* re-enters prepare with kind:"draft".
|
|
5
|
+
*
|
|
6
|
+
* Engineering boundary priority: explicit flags > existing task config >
|
|
7
|
+
* optional model recommendation (repo-layout-aware fallback for Console
|
|
8
|
+
* web-wizard flows that never collect human paths).
|
|
6
9
|
*/
|
|
7
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
10
|
+
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
8
11
|
import path from "node:path";
|
|
9
12
|
import { z } from "zod";
|
|
10
13
|
import { executePiStep } from "../../executors/pi-executor.js";
|
|
@@ -14,6 +17,7 @@ import { loadHarnessManifest } from "../../governance/harness.js";
|
|
|
14
17
|
import { TASK_CONTRACT_DRAFT_SCHEMA_VERSION } from "../contract/constants.js";
|
|
15
18
|
import { getTaskPaths } from "../runtime.js";
|
|
16
19
|
import { detectProductArtifactMeta } from "./artifact-meta.js";
|
|
20
|
+
import { filterPlaceholderPaths } from "./placeholder-paths.js";
|
|
17
21
|
export const SEMANTIC_INTAKE_ARTIFACT_REL = "artifacts/intake/semantic-draft.json";
|
|
18
22
|
export const SEMANTIC_INTAKE_ATTEMPT_MARKER = "artifacts/intake/semantic-intake-attempted.json";
|
|
19
23
|
const productArtifactBlockers = new Set([
|
|
@@ -22,6 +26,20 @@ const productArtifactBlockers = new Set([
|
|
|
22
26
|
"PRODUCT_REQUIREMENT_PENDING",
|
|
23
27
|
"PRODUCT_ARTIFACT_NOT_EXECUTABLE",
|
|
24
28
|
]);
|
|
29
|
+
const engineeringRecommendationSchema = z
|
|
30
|
+
.object({
|
|
31
|
+
taskKind: z.string().min(1).optional(),
|
|
32
|
+
allowedPaths: z.array(z.string()).optional(),
|
|
33
|
+
forbiddenPaths: z.array(z.string()).optional(),
|
|
34
|
+
verifyCommands: z
|
|
35
|
+
.array(z.object({
|
|
36
|
+
label: z.string().min(1),
|
|
37
|
+
command: z.string().min(1),
|
|
38
|
+
timeoutMs: z.number().int().positive().optional(),
|
|
39
|
+
}))
|
|
40
|
+
.optional(),
|
|
41
|
+
})
|
|
42
|
+
.optional();
|
|
25
43
|
const requirementDraftSchema = z.object({
|
|
26
44
|
objective: z.string().min(1),
|
|
27
45
|
scope: z.array(z.string()).default([]),
|
|
@@ -39,6 +57,8 @@ const requirementDraftSchema = z.object({
|
|
|
39
57
|
openQuestions: z.array(z.string()).optional(),
|
|
40
58
|
assumptions: z.array(z.string()).optional(),
|
|
41
59
|
title: z.string().optional(),
|
|
60
|
+
/** Optional model engineering recommendation (flags/existing still win). */
|
|
61
|
+
engineering: engineeringRecommendationSchema,
|
|
42
62
|
});
|
|
43
63
|
export function isSemanticIntakeDisabled(env = process.env) {
|
|
44
64
|
const raw = (env.LOOP_AGENT_SEMANTIC_INTAKE ?? "").trim().toLowerCase();
|
|
@@ -130,26 +150,62 @@ export function assertSemanticDraftAcceptable(draft) {
|
|
|
130
150
|
}
|
|
131
151
|
}
|
|
132
152
|
export function buildDraftFromSemanticIntake(input) {
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
153
|
+
const modelEng = input.semantic.engineering ?? {};
|
|
154
|
+
const modelAllowed = filterPlaceholderPaths(modelEng.allowedPaths);
|
|
155
|
+
const modelForbidden = filterPlaceholderPaths(modelEng.forbiddenPaths);
|
|
156
|
+
const modelVerify = (modelEng.verifyCommands ?? []).filter((c) => c.label?.trim() && c.command?.trim());
|
|
157
|
+
// Priority: explicit flags > existing task config > model recommendation.
|
|
158
|
+
const flagAllowed = input.flags.allowedPaths ?? [];
|
|
159
|
+
const existingAllowed = input.existingAllowedPaths ?? [];
|
|
160
|
+
let allowedPaths;
|
|
161
|
+
if (flagAllowed.length > 0) {
|
|
162
|
+
allowedPaths = [...existingAllowed, ...flagAllowed];
|
|
163
|
+
}
|
|
164
|
+
else if (existingAllowed.length > 0) {
|
|
165
|
+
allowedPaths = existingAllowed;
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
allowedPaths = modelAllowed;
|
|
169
|
+
}
|
|
170
|
+
const flagForbidden = input.flags.forbiddenPaths ?? [];
|
|
171
|
+
const existingForbidden = input.existingForbiddenPaths ?? [];
|
|
172
|
+
let forbiddenPaths;
|
|
173
|
+
if (flagForbidden.length > 0) {
|
|
174
|
+
forbiddenPaths = [...existingForbidden, ...flagForbidden];
|
|
175
|
+
}
|
|
176
|
+
else if (existingForbidden.length > 0) {
|
|
177
|
+
forbiddenPaths = existingForbidden;
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
forbiddenPaths = modelForbidden;
|
|
181
|
+
}
|
|
182
|
+
let verifyCommands;
|
|
183
|
+
if (input.flags.verifyCommands?.length) {
|
|
184
|
+
verifyCommands = input.flags.verifyCommands;
|
|
185
|
+
}
|
|
186
|
+
else if (input.existingVerify?.length) {
|
|
187
|
+
verifyCommands = input.existingVerify;
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
verifyCommands = modelVerify;
|
|
191
|
+
}
|
|
192
|
+
// flags.taskKind is hard; bare caller "standard" is a soft default and must
|
|
193
|
+
// not block a model recommendation for Console web-wizard flows.
|
|
194
|
+
const explicitTaskKind = input.flags.taskKind?.trim();
|
|
195
|
+
const callerTaskKind = input.taskKind?.trim();
|
|
196
|
+
const modelTaskKind = modelEng.taskKind?.trim();
|
|
197
|
+
const taskKind = explicitTaskKind ||
|
|
198
|
+
(callerTaskKind && callerTaskKind !== "standard"
|
|
199
|
+
? callerTaskKind
|
|
200
|
+
: undefined) ||
|
|
201
|
+
modelTaskKind ||
|
|
202
|
+
callerTaskKind ||
|
|
203
|
+
"standard";
|
|
148
204
|
const draft = {
|
|
149
205
|
schemaVersion: TASK_CONTRACT_DRAFT_SCHEMA_VERSION,
|
|
150
206
|
taskId: input.taskId,
|
|
151
207
|
title: input.semantic.title?.trim() || input.title,
|
|
152
|
-
taskKind
|
|
208
|
+
taskKind,
|
|
153
209
|
requirement: {
|
|
154
210
|
objective: input.semantic.objective.trim(),
|
|
155
211
|
scope: input.semantic.scope.map((s) => s.trim()).filter(Boolean),
|
|
@@ -161,7 +217,9 @@ export function buildDraftFromSemanticIntake(input) {
|
|
|
161
217
|
},
|
|
162
218
|
constraints: {
|
|
163
219
|
invariants: input.flags.invariants ?? [],
|
|
164
|
-
allowedPaths: [
|
|
220
|
+
allowedPaths: [
|
|
221
|
+
...new Set(allowedPaths.map((p) => p.trim()).filter(Boolean)),
|
|
222
|
+
],
|
|
165
223
|
forbiddenPaths: [
|
|
166
224
|
...new Set(forbiddenPaths.map((p) => p.trim()).filter(Boolean)),
|
|
167
225
|
],
|
|
@@ -192,7 +250,96 @@ export function buildDraftFromSemanticIntake(input) {
|
|
|
192
250
|
}
|
|
193
251
|
return draft;
|
|
194
252
|
}
|
|
195
|
-
|
|
253
|
+
/**
|
|
254
|
+
* Cheap repo-layout hint injected into the semantic intake prompt so the model
|
|
255
|
+
* can recommend minimal write boundaries and real verify commands when humans
|
|
256
|
+
* never fill Console form engineering fields.
|
|
257
|
+
*/
|
|
258
|
+
export async function buildRepoHint(repoRoot) {
|
|
259
|
+
const lines = ["Repo layout hint (best-effort, may be incomplete):"];
|
|
260
|
+
try {
|
|
261
|
+
const top = await readdir(repoRoot, { withFileTypes: true });
|
|
262
|
+
const dirs = top
|
|
263
|
+
.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."))
|
|
264
|
+
.map((entry) => entry.name)
|
|
265
|
+
.slice(0, 24);
|
|
266
|
+
const files = top
|
|
267
|
+
.filter((entry) => entry.isFile())
|
|
268
|
+
.map((entry) => entry.name)
|
|
269
|
+
.slice(0, 24);
|
|
270
|
+
lines.push(`top-level dirs: ${dirs.join(", ") || "(none)"}`);
|
|
271
|
+
lines.push(`top-level files: ${files.join(", ") || "(none)"}`);
|
|
272
|
+
const packageCandidates = [
|
|
273
|
+
"package.json",
|
|
274
|
+
path.join("source", "package.json"),
|
|
275
|
+
path.join("packages", "package.json"),
|
|
276
|
+
];
|
|
277
|
+
for (const rel of packageCandidates) {
|
|
278
|
+
const abs = path.join(repoRoot, rel);
|
|
279
|
+
try {
|
|
280
|
+
const raw = await readFile(abs, "utf-8");
|
|
281
|
+
const pkg = JSON.parse(raw);
|
|
282
|
+
const scripts = Object.keys(pkg.scripts ?? {}).slice(0, 20);
|
|
283
|
+
lines.push(`${rel} scripts: ${scripts.join(", ") || "(none)"}`);
|
|
284
|
+
if (pkg.workspaces !== undefined) {
|
|
285
|
+
lines.push(`${rel} workspaces: ${JSON.stringify(pkg.workspaces).slice(0, 200)}`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
// optional
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
// Sample test-like files under common roots (bounded).
|
|
293
|
+
const testRoots = ["src", "source", "packages", "test", "tests"].filter((name) => dirs.includes(name));
|
|
294
|
+
const samples = [];
|
|
295
|
+
const counts = new Map();
|
|
296
|
+
const walk = async (dir, depth) => {
|
|
297
|
+
if (depth > 3 || samples.length >= 15)
|
|
298
|
+
return;
|
|
299
|
+
let entries;
|
|
300
|
+
try {
|
|
301
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
for (const entry of entries) {
|
|
307
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules")
|
|
308
|
+
continue;
|
|
309
|
+
const abs = path.join(dir, entry.name);
|
|
310
|
+
if (entry.isDirectory()) {
|
|
311
|
+
await walk(abs, depth + 1);
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
if (!entry.isFile())
|
|
315
|
+
continue;
|
|
316
|
+
if (!/\.(test|spec)\.(ts|tsx|js|jsx|mjs|cjs)$/i.test(entry.name))
|
|
317
|
+
continue;
|
|
318
|
+
const rel = path.relative(repoRoot, abs).replace(/\\/g, "/");
|
|
319
|
+
const top = rel.split("/")[0] ?? rel;
|
|
320
|
+
counts.set(top, (counts.get(top) ?? 0) + 1);
|
|
321
|
+
if (samples.length < 15)
|
|
322
|
+
samples.push(rel);
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
for (const root of testRoots) {
|
|
326
|
+
await walk(path.join(repoRoot, root), 0);
|
|
327
|
+
}
|
|
328
|
+
const total = [...counts.values()].reduce((sum, n) => sum + n, 0);
|
|
329
|
+
if (total > 0) {
|
|
330
|
+
const byTop = [...counts.entries()]
|
|
331
|
+
.map(([name, n]) => `${name}:${n}`)
|
|
332
|
+
.join(", ");
|
|
333
|
+
lines.push(`test-like files: total=${total} byTop=${byTop}`);
|
|
334
|
+
lines.push(`test samples: ${samples.join(", ")}`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
lines.push("(repo scan failed)");
|
|
339
|
+
}
|
|
340
|
+
return lines.join("\n");
|
|
341
|
+
}
|
|
342
|
+
async function buildPrompt(input) {
|
|
196
343
|
const bodies = input.documents
|
|
197
344
|
.map((doc, index) => {
|
|
198
345
|
const meta = detectProductArtifactMeta(doc.content);
|
|
@@ -213,11 +360,18 @@ function buildPrompt(input) {
|
|
|
213
360
|
].join("\n");
|
|
214
361
|
})
|
|
215
362
|
.join("\n\n");
|
|
363
|
+
const repoHint = input.repoRoot
|
|
364
|
+
? await buildRepoHint(input.repoRoot)
|
|
365
|
+
: "(no repo layout hint)";
|
|
216
366
|
return [
|
|
217
367
|
"You are a read-only requirement structurer for loop-agent task intake.",
|
|
218
368
|
"Convert imported product documents into a thin requirement draft JSON.",
|
|
219
369
|
"Do NOT invent product behavior that is not supported by the documents.",
|
|
220
|
-
"
|
|
370
|
+
"You MAY recommend an optional engineering block when the repo layout hint supports it:",
|
|
371
|
+
"- allowedPaths / forbiddenPaths as minimal precise globs (e.g. source/packages/**/*.test.ts)",
|
|
372
|
+
"- verifyCommands as real package scripts (e.g. pnpm -C source test)",
|
|
373
|
+
"- taskKind when clearly implied (backend-test / frontend-test / standard / …)",
|
|
374
|
+
"Prefer smallest correct write boundary; do not invent product paths that are not in the layout hint.",
|
|
221
375
|
"Do NOT rewrite or suggest editing the original PRD files.",
|
|
222
376
|
"Prefer explicit text; use provenance derived for rephrasing; use inferred only when clearly implied — if you must invent ACs, leave acceptanceCriteria empty so the system can stop for clarification.",
|
|
223
377
|
"Return exactly one JSON object, no markdown prose outside optional fence:",
|
|
@@ -235,9 +389,18 @@ function buildPrompt(input) {
|
|
|
235
389
|
],
|
|
236
390
|
openQuestions: ["optional"],
|
|
237
391
|
assumptions: ["optional"],
|
|
392
|
+
engineering: {
|
|
393
|
+
taskKind: "optional string",
|
|
394
|
+
allowedPaths: ["optional globs"],
|
|
395
|
+
forbiddenPaths: ["optional globs"],
|
|
396
|
+
verifyCommands: [
|
|
397
|
+
{ label: "test", command: "pnpm -C source test" },
|
|
398
|
+
],
|
|
399
|
+
},
|
|
238
400
|
}, null, 2),
|
|
239
401
|
`taskId: ${input.taskId}`,
|
|
240
402
|
`fallbackTitle: ${input.title}`,
|
|
403
|
+
repoHint,
|
|
241
404
|
"Documents:",
|
|
242
405
|
bodies,
|
|
243
406
|
].join("\n\n");
|
|
@@ -314,14 +477,16 @@ export async function runSemanticIntake(input) {
|
|
|
314
477
|
const attached = usableDocs
|
|
315
478
|
.map((d) => d.absolutePath)
|
|
316
479
|
.filter((p) => Boolean(p));
|
|
480
|
+
const prompt = await buildPrompt({
|
|
481
|
+
taskId: input.taskId,
|
|
482
|
+
title: input.title,
|
|
483
|
+
documents: usableDocs,
|
|
484
|
+
repoRoot: input.repoRoot,
|
|
485
|
+
});
|
|
317
486
|
const result = await execute({
|
|
318
487
|
attachedFiles: attached,
|
|
319
488
|
modelConfig: resolveDagPiModelConfig(model),
|
|
320
|
-
prompt
|
|
321
|
-
taskId: input.taskId,
|
|
322
|
-
title: input.title,
|
|
323
|
-
documents: usableDocs,
|
|
324
|
-
}),
|
|
489
|
+
prompt,
|
|
325
490
|
repoRoot: input.repoRoot,
|
|
326
491
|
step: "analyze",
|
|
327
492
|
toolNames: ["read", "grep", "find", "ls"],
|
package/dist/worker/cli.js
CHANGED
|
@@ -720,6 +720,7 @@ export function buildAgentWorkerProgram() {
|
|
|
720
720
|
.option("--host <host>", "Bind host (default 127.0.0.1; use 0.0.0.0 for LAN, no network auth)", "127.0.0.1")
|
|
721
721
|
.option("--debug", "Log request source/method/path/status to stderr")
|
|
722
722
|
.option("--app-data <path>", "Override Console application-data root (Draft/Operation/confirmation)")
|
|
723
|
+
.option("--default-timeout-ms <ms>", "LoopAgentClient hard timeout for Console operations (ms; default 14400000 / 4h; 0 disables)")
|
|
723
724
|
.action(async (options) => {
|
|
724
725
|
await runConsoleServe({
|
|
725
726
|
repoRoot: path.resolve(options.repo),
|
|
@@ -729,6 +730,7 @@ export function buildAgentWorkerProgram() {
|
|
|
729
730
|
appDataRoot: options.appData
|
|
730
731
|
? path.resolve(options.appData)
|
|
731
732
|
: undefined,
|
|
733
|
+
defaultTimeoutMsRaw: options.defaultTimeoutMs,
|
|
732
734
|
});
|
|
733
735
|
});
|
|
734
736
|
consoleCmd
|
|
@@ -738,6 +740,7 @@ export function buildAgentWorkerProgram() {
|
|
|
738
740
|
.option("--host <host>", "Bind host (default 127.0.0.1; use 0.0.0.0 for LAN, no network auth)", "127.0.0.1")
|
|
739
741
|
.option("--debug", "Log request source/method/path/status to stderr")
|
|
740
742
|
.option("--app-data <path>", "Override Console application-data root (Draft/Operation/confirmation)")
|
|
743
|
+
.option("--default-timeout-ms <ms>", "LoopAgentClient hard timeout for Console operations (ms; default 14400000 / 4h; 0 disables)")
|
|
741
744
|
.description("Start Loop Operator Console HTTP server (compat entry; equivalent to bare `agent-worker console`)")
|
|
742
745
|
.action(async (options) => {
|
|
743
746
|
await runConsoleServe({
|
|
@@ -748,6 +751,7 @@ export function buildAgentWorkerProgram() {
|
|
|
748
751
|
appDataRoot: options.appData
|
|
749
752
|
? path.resolve(options.appData)
|
|
750
753
|
: undefined,
|
|
754
|
+
defaultTimeoutMsRaw: options.defaultTimeoutMs,
|
|
751
755
|
});
|
|
752
756
|
});
|
|
753
757
|
consoleCmd
|
|
@@ -835,6 +839,17 @@ export async function main(argv = process.argv) {
|
|
|
835
839
|
async function readTaskYaml(taskSpecPath) {
|
|
836
840
|
return YAML.parse(await readFile(taskSpecPath, "utf-8"));
|
|
837
841
|
}
|
|
842
|
+
/** Parse optional `--default-timeout-ms`; undefined keeps Console default. */
|
|
843
|
+
function parseOptionalTimeoutMs(raw) {
|
|
844
|
+
if (raw === undefined || raw.trim() === "") {
|
|
845
|
+
return undefined;
|
|
846
|
+
}
|
|
847
|
+
const value = Number(raw);
|
|
848
|
+
if (!Number.isFinite(value) || value < 0 || !Number.isInteger(value)) {
|
|
849
|
+
throw new Error(`--default-timeout-ms must be a non-negative integer (ms); got ${JSON.stringify(raw)}`);
|
|
850
|
+
}
|
|
851
|
+
return value;
|
|
852
|
+
}
|
|
838
853
|
/** Shared console serve entry used by both bare `console` and `console serve`. */
|
|
839
854
|
async function runConsoleServe(options) {
|
|
840
855
|
const { createConsoleServer } = await import("./console/server.js");
|
|
@@ -845,6 +860,7 @@ async function runConsoleServe(options) {
|
|
|
845
860
|
port: options.port,
|
|
846
861
|
debug: options.debug === true,
|
|
847
862
|
appDataRoot: options.appDataRoot,
|
|
863
|
+
defaultTimeoutMs: parseOptionalTimeoutMs(options.defaultTimeoutMsRaw),
|
|
848
864
|
});
|
|
849
865
|
process.stdout.write(`${server.url}\n`);
|
|
850
866
|
await new Promise((resolve) => {
|
|
@@ -8,6 +8,7 @@ import path from "node:path";
|
|
|
8
8
|
import { getTaskPaths } from "../../task/runtime.js";
|
|
9
9
|
import { SEMANTIC_INTAKE_ARTIFACT_REL, } from "../../task/source-prepare/semantic-intake.js";
|
|
10
10
|
import { extractRequirementFactsFromMarkdown } from "../../task/source-prepare/parse-intent.js";
|
|
11
|
+
import { filterPlaceholderPaths, isNonPlaceholderPath, } from "../../task/source-prepare/placeholder-paths.js";
|
|
11
12
|
import { emptyDraft } from "./interview/grill-me.js";
|
|
12
13
|
import { normalizeConsoleWorkflowKind } from "./workflow-kinds.js";
|
|
13
14
|
function asStringList(value) {
|
|
@@ -105,13 +106,13 @@ export function parseEngineeringBoundaryFromParams(p) {
|
|
|
105
106
|
if (Array.isArray(value)) {
|
|
106
107
|
return value
|
|
107
108
|
.map((item) => (typeof item === "string" ? item.trim() : ""))
|
|
108
|
-
.filter(
|
|
109
|
+
.filter(isNonPlaceholderPath);
|
|
109
110
|
}
|
|
110
111
|
if (typeof value === "string") {
|
|
111
112
|
return value
|
|
112
113
|
.split(/[\n,;,;]+/)
|
|
113
114
|
.map((s) => s.trim())
|
|
114
|
-
.filter(
|
|
115
|
+
.filter(isNonPlaceholderPath);
|
|
115
116
|
}
|
|
116
117
|
return [];
|
|
117
118
|
};
|
|
@@ -176,10 +177,10 @@ export function parseEngineeringBoundaryFromParams(p) {
|
|
|
176
177
|
};
|
|
177
178
|
}
|
|
178
179
|
export function appendEngineeringCliArgs(args, boundary) {
|
|
179
|
-
for (const pathGlob of boundary.allowedPaths
|
|
180
|
+
for (const pathGlob of filterPlaceholderPaths(boundary.allowedPaths)) {
|
|
180
181
|
args.push("--allowed-path", pathGlob);
|
|
181
182
|
}
|
|
182
|
-
for (const pathGlob of boundary.forbiddenPaths
|
|
183
|
+
for (const pathGlob of filterPlaceholderPaths(boundary.forbiddenPaths)) {
|
|
183
184
|
args.push("--forbidden-path", pathGlob);
|
|
184
185
|
}
|
|
185
186
|
for (const cmd of boundary.verifyCommands ?? []) {
|
|
@@ -187,8 +188,10 @@ export function appendEngineeringCliArgs(args, boundary) {
|
|
|
187
188
|
}
|
|
188
189
|
}
|
|
189
190
|
export function applyEngineeringBoundaryToDraft(draft, boundary) {
|
|
190
|
-
|
|
191
|
-
|
|
191
|
+
const allowedPaths = filterPlaceholderPaths(boundary.allowedPaths);
|
|
192
|
+
const forbiddenPaths = filterPlaceholderPaths(boundary.forbiddenPaths);
|
|
193
|
+
if (allowedPaths.length === 0 &&
|
|
194
|
+
forbiddenPaths.length === 0 &&
|
|
192
195
|
!(boundary.verifyCommands?.length)) {
|
|
193
196
|
return draft;
|
|
194
197
|
}
|
|
@@ -196,11 +199,11 @@ export function applyEngineeringBoundaryToDraft(draft, boundary) {
|
|
|
196
199
|
...draft,
|
|
197
200
|
constraints: {
|
|
198
201
|
invariants: draft.constraints?.invariants ?? [],
|
|
199
|
-
allowedPaths:
|
|
200
|
-
?
|
|
202
|
+
allowedPaths: allowedPaths.length > 0
|
|
203
|
+
? allowedPaths
|
|
201
204
|
: (draft.constraints?.allowedPaths ?? []),
|
|
202
|
-
forbiddenPaths:
|
|
203
|
-
?
|
|
205
|
+
forbiddenPaths: forbiddenPaths.length > 0
|
|
206
|
+
? forbiddenPaths
|
|
204
207
|
: (draft.constraints?.forbiddenPaths ?? []),
|
|
205
208
|
},
|
|
206
209
|
verification: {
|
|
@@ -253,15 +256,53 @@ export async function buildDraftFromTaskIntake(input) {
|
|
|
253
256
|
const title = artifact?.draft?.title?.trim() ||
|
|
254
257
|
artifact?.semantic?.title?.trim() ||
|
|
255
258
|
input.title;
|
|
259
|
+
// Preserve semantic engineering boundary (paths/verify/taskKind) so the
|
|
260
|
+
// Console draft does not fall back to interview defaults and overwrite
|
|
261
|
+
// TX1 precise values on contractApply --from-draft.
|
|
262
|
+
const semDraftConstraints = artifact?.draft?.constraints;
|
|
263
|
+
const semDraftVerification = artifact?.draft?.verification;
|
|
264
|
+
const semTaskKind = artifact?.draft?.taskKind?.trim();
|
|
265
|
+
const mergedAllowed = filterPlaceholderPaths(semDraftConstraints?.allowedPaths);
|
|
266
|
+
const mergedForbidden = filterPlaceholderPaths(semDraftConstraints?.forbiddenPaths);
|
|
267
|
+
const mergedVerify = Array.isArray(semDraftVerification?.commands)
|
|
268
|
+
? semDraftVerification.commands
|
|
269
|
+
.filter((cmd) => typeof cmd?.command === "string" && cmd.command.trim().length > 0)
|
|
270
|
+
.map((cmd) => ({
|
|
271
|
+
label: typeof cmd.label === "string" && cmd.label.trim()
|
|
272
|
+
? cmd.label.trim()
|
|
273
|
+
: "verify",
|
|
274
|
+
command: cmd.command.trim(),
|
|
275
|
+
...(typeof cmd.timeoutMs === "number"
|
|
276
|
+
? { timeoutMs: cmd.timeoutMs }
|
|
277
|
+
: {}),
|
|
278
|
+
}))
|
|
279
|
+
: [];
|
|
256
280
|
draft = {
|
|
257
281
|
...draft,
|
|
258
282
|
title,
|
|
283
|
+
...(semTaskKind
|
|
284
|
+
? { taskKind: normalizeConsoleWorkflowKind(semTaskKind, taskKind) }
|
|
285
|
+
: {}),
|
|
259
286
|
requirement: {
|
|
260
287
|
objective: semanticReq.objective.trim(),
|
|
261
288
|
scope: asStringList(semanticReq.scope),
|
|
262
289
|
nonGoals: asStringList(semanticReq.nonGoals),
|
|
263
290
|
acceptanceCriteria: asAcceptance(semanticReq.acceptanceCriteria),
|
|
264
291
|
},
|
|
292
|
+
...(mergedAllowed.length > 0 ||
|
|
293
|
+
mergedForbidden.length > 0 ||
|
|
294
|
+
Array.isArray(semDraftConstraints?.invariants)
|
|
295
|
+
? {
|
|
296
|
+
constraints: {
|
|
297
|
+
invariants: asStringList(semDraftConstraints?.invariants),
|
|
298
|
+
allowedPaths: mergedAllowed,
|
|
299
|
+
forbiddenPaths: mergedForbidden,
|
|
300
|
+
},
|
|
301
|
+
}
|
|
302
|
+
: {}),
|
|
303
|
+
...(mergedVerify.length > 0
|
|
304
|
+
? { verification: { commands: mergedVerify } }
|
|
305
|
+
: {}),
|
|
265
306
|
openQuestions: asStringList(artifact?.draft?.openQuestions ?? artifact?.semantic?.openQuestions) || undefined,
|
|
266
307
|
assumptions: asStringList(artifact?.draft?.assumptions ?? artifact?.semantic?.assumptions) || undefined,
|
|
267
308
|
references: artifact?.draft?.references,
|
|
@@ -4,6 +4,7 @@ import { createServer } from "node:http";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { LoopAgentClient } from "../loop-agent/loop-agent-client.js";
|
|
7
|
+
import { DAG_ABSOLUTE_MAX_WALL_CLOCK_MS } from "../../shared/timeout-policy.js";
|
|
7
8
|
import { openConsoleAppData } from "./app-data.js";
|
|
8
9
|
import { DagConfirmationStore } from "./dag-confirmation.js";
|
|
9
10
|
import { DagExecutionReceiptStore } from "./dag-execution-receipt.js";
|
|
@@ -24,6 +25,30 @@ import { resolveSiblingLoopAgentBin } from "./sibling-controller.js";
|
|
|
24
25
|
import { NightAuxTicker } from "./night-aux-ticker.js";
|
|
25
26
|
import { deriveObserveRouteCapabilities } from "../observe/health.js";
|
|
26
27
|
import { createObserveRouteContext, defaultObserveStaticDir, handleMatchedObserveRoute, matchObserveGetRoute, ROUTES, serveObserveStatic, } from "../observe/routes.js";
|
|
28
|
+
/**
|
|
29
|
+
* Default hard timeout for the Console-owned LoopAgentClient.
|
|
30
|
+
*
|
|
31
|
+
* Console task-nav operations (`dag execute`, `task advance --approve-gate`,
|
|
32
|
+
* etc.) spawn a long-lived sibling runner via the operation runner. The
|
|
33
|
+
* worker client hard-kills children after `defaultTimeoutMs` (default 120s),
|
|
34
|
+
* which is far below multi-node DAG wall clocks and leaves `state.json`
|
|
35
|
+
* stuck in `running` with no error. Align with shared `DAG_ABSOLUTE_MAX_WALL_CLOCK_MS`
|
|
36
|
+
* (4h) so the client is a safety net, not a false deadline.
|
|
37
|
+
*/
|
|
38
|
+
export const DEFAULT_CONSOLE_CLIENT_TIMEOUT_MS = DAG_ABSOLUTE_MAX_WALL_CLOCK_MS;
|
|
39
|
+
/**
|
|
40
|
+
* Resolve the Console LoopAgentClient default timeout.
|
|
41
|
+
* `0` disables the client hard timeout (DAG kernel owns wall clock).
|
|
42
|
+
*/
|
|
43
|
+
export function resolveConsoleDefaultTimeoutMs(override) {
|
|
44
|
+
if (override === undefined) {
|
|
45
|
+
return DEFAULT_CONSOLE_CLIENT_TIMEOUT_MS;
|
|
46
|
+
}
|
|
47
|
+
if (!Number.isFinite(override) || override < 0) {
|
|
48
|
+
throw new Error(`console serve: invalid defaultTimeoutMs ${String(override)} (expected >= 0)`);
|
|
49
|
+
}
|
|
50
|
+
return override;
|
|
51
|
+
}
|
|
27
52
|
/** Built static assets live next to the compiled server under dist/worker/console/static. */
|
|
28
53
|
export function defaultConsoleStaticDir(fromFileUrl = import.meta.url) {
|
|
29
54
|
return path.join(path.dirname(fileURLToPath(fromFileUrl)), "static");
|
|
@@ -74,6 +99,9 @@ export async function createConsoleServer(options) {
|
|
|
74
99
|
loopAgentBin: resolveSiblingLoopAgentBin(),
|
|
75
100
|
artifactRoot,
|
|
76
101
|
resolveIdentity: true,
|
|
102
|
+
// See DEFAULT_CONSOLE_CLIENT_TIMEOUT_MS: dag execute / task advance
|
|
103
|
+
// must not inherit the 120s worker-command default.
|
|
104
|
+
defaultTimeoutMs: resolveConsoleDefaultTimeoutMs(options.defaultTimeoutMs),
|
|
77
105
|
});
|
|
78
106
|
if (!options.skipReconcile) {
|
|
79
107
|
await operations.reconcileOnBoot();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { DAG_ABSOLUTE_MAX_WALL_CLOCK_MS } from "../../shared/timeout-policy.js";
|
|
2
3
|
/**
|
|
3
4
|
* Adaptive liveness policy for DAG/Pi supervision.
|
|
4
5
|
*
|
|
@@ -15,7 +16,7 @@ export const DEFAULT_LIVENESS_POLICY = {
|
|
|
15
16
|
stallProbeMs: 900_000,
|
|
16
17
|
abortGraceMs: 30_000,
|
|
17
18
|
/** 4h absolute max wall clock — cannot be renewed by empty heartbeats. */
|
|
18
|
-
absoluteMaxWallClockMs:
|
|
19
|
+
absoluteMaxWallClockMs: DAG_ABSOLUTE_MAX_WALL_CLOCK_MS,
|
|
19
20
|
};
|
|
20
21
|
export const dagLivenessPolicySchema = z
|
|
21
22
|
.object({
|