@tea-agent/loop-agent 0.34.2 → 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 +15 -0
- package/dist/application/task-lifecycle/advance.js +5 -1
- 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/console/prd-intake-bridge.js +51 -10
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
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
|
+
|
|
5
20
|
## [0.34.2] - 2026-08-12
|
|
6
21
|
|
|
7
22
|
### 重点更新
|
|
@@ -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
|
}
|
|
@@ -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"],
|
|
@@ -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,
|