@tea-agent/loop-agent 0.17.1 → 0.17.2
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 +29 -0
- package/dist/application/dag/args.js +9 -2
- package/dist/executors/dag-pi-executor.js +11 -0
- package/dist/worker/console/draft-store.js +81 -3
- package/dist/worker/console/interview/grill-me.js +253 -0
- package/dist/worker/console/operation-runner.js +2 -1
- package/dist/worker/console/operator-actions.js +609 -20
- package/dist/worker/console/pi-readiness.js +2 -0
- package/dist/worker/console/prd-identity.js +102 -0
- package/dist/worker/console/resolve-dag-run-for-task.js +76 -0
- package/dist/worker/console/server.js +11 -2
- package/dist/worker/console/static/assets/index-KUSib7aM.js +16 -0
- package/dist/worker/console/static/assets/index-ucIzpaGJ.css +1 -0
- package/dist/worker/console/static/index.html +3 -3
- package/dist/worker/console/workflow-kinds.js +46 -0
- package/dist/worker/observe/routes.js +8 -1
- package/dist/worker/observe/static/index.html +8 -8
- package/dist/worker/observe/static/styles.css +296 -181
- package/package.json +1 -1
- package/dist/worker/console/static/assets/index-BEIdBogJ.js +0 -9
- package/dist/worker/console/static/assets/index-Rt0TqimP.css +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# 更新日志
|
|
2
2
|
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
## [0.17.2] - 2026-07-22
|
|
6
|
+
|
|
7
|
+
### 改进
|
|
8
|
+
|
|
9
|
+
- Operator Console Happy Path 补齐:主导航(任务/恢复/模型与认证/设置)、newTask / importPrd、grill-me Interview(一次一问 + assessment)、SSE 运行跟随;Pi readiness 可刷新。
|
|
10
|
+
- Operator Console 页面交互文案改为中文(导航、向导步骤、按钮与状态说明);产品名与 CLI 命令仍保留英文术语。
|
|
11
|
+
- Operator Console 界面与交互美化:暖色画布、胶囊导航、步骤进度条、统一按钮层级与状态条,运行面板元信息更清晰。
|
|
12
|
+
- Operator Console Happy Path 增加「流程类型」选择(后端研发 / 前端研发 / 后端测试 / 前端测试),写入 `taskKind`;访谈枚举同步支持 `frontend-implementation` 与 `frontend-test`。
|
|
13
|
+
- Console Happy Path 去掉单独的「创建任务」步骤:导入需求时根据 PRD 标题自动生成任务名称与 canonical taskId,并一键创建任务 + import-prd。
|
|
14
|
+
- Operator Console 需求访谈可跳过:一键按 grill-me 推荐补齐草稿并签发评估,可直接进入契约校验。
|
|
15
|
+
- Operator Console 将「校验契约 / 应用契约 / Spine 审计」合并为「契约落地」:自动确定性校验与差异,人工确认一次应用,成功后自动 Spine。
|
|
16
|
+
- Observe 运行看板视觉对齐 Console:暖色渐变画布、胶囊导航、柔和面板阴影与状态点,减少高饱和色块;进一步统一 badge/表格/风险块/时间线/DAG 图与 inspector、对象关系条与推荐命令等视图组件。
|
|
17
|
+
|
|
18
|
+
### 修复
|
|
19
|
+
|
|
20
|
+
- Console Draft 在 validate/diff/apply 前会投影为 CLI 严格 schema:补齐 `constraints.invariants`,并去掉仅 Console 使用的 `updatedAt`,避免误报 draft schema validation failed。
|
|
21
|
+
- 修复访谈完成后绑定 `interviewAssessmentRef` 会立刻让 assessment 过期的问题:`draftSha256` 不再纳入该绑定字段;finalize 时先清空 `openQuestions` 再签发 assessment。
|
|
22
|
+
- 修复 Console `contractApply` 默认传入 `expectedObservedHash=unknown` 导致 OBSERVED_HASH_CONFLICT / 页面只见 `exit 1`:改为先 `task contract show` 再传真实 revision/hash;异步操作失败时展示 CLI 错误信息。
|
|
23
|
+
- 修复应用契约后界面卡住:轮询操作结果时误读 `{ ok, operation }` 响应,一直等到超时且无法进入 Spine。
|
|
24
|
+
- 修复 DAG 确认准备失败:不再提交空 stub DAG;`prepareDagConfirmation` 会经 `dag run-task` 生成真实 DAG;`dag validate` 接受 `--json` 兼容 Console。
|
|
25
|
+
- 修复 Console `runDag` 因 `run-dag --json` 未识别而失败:CLI 现接受该旗标(输出本就为 JSON)。
|
|
26
|
+
- 运行 DAG 不再用短超时阻塞整页:`runDag` 改为后台接受 + SSE 跟随;Observe 按钮与任务 busy 解耦,运行中可打开。
|
|
27
|
+
- Console 打开 Observe 时,若只给 taskId,会解析该任务最新 DAG run 并跳转 `#/dag/<runId>`,不再误开 `#/task/<taskId>`。
|
|
28
|
+
|
|
3
29
|
## [0.17.1] - 2026-07-22
|
|
4
30
|
|
|
5
31
|
### 修复
|
|
@@ -42,6 +68,7 @@
|
|
|
42
68
|
- Console 与 Observe 仍为独立进程;不 mount、不 proxy、不复制第二套 read model。
|
|
43
69
|
- Phase 1 Console skeleton:`agent-worker console serve|doctor`(默认 loopback `127.0.0.1:8790`、非 loopback fail-closed)、Vite SPA 静态产物 + Node health/static server、boot capability cookie + mutation Host/Origin 门禁、封闭 ResourceLoader 骨架、sibling `loop-agent` identity doctor、`repoFingerprintV1` / Observe 深链 builder、以及 `UPSTREAM.md`(pi-web 源 commit)与预算 report。
|
|
44
70
|
- Phase 0.5 operator surface repair:`buildOperatorCapabilitiesDocument()` 在权威 `src/shared/operator` 上补齐 Console 协商块 `dag`(`generatedSpecVersion: 4`、`requiresTaskContractBinding: true`,与 `dagSpecVersions` 同源常量),并删除冲突的 `src/task/operator` 双实现;CLI `--json` 继续写 `process.stdout`,测试侧改为捕获 stdout。
|
|
71
|
+
- Nightly 自动发布改为双基线:`lastTag`(最新稳定 `vX.Y.Z`)只负责未发布增量、breaking 检测与 release notes;当前 minor 的 `vX.Y.0`(缺失则回退 `lastTag`)只累计严格 Conventional Commit `feat:` 以决定 minor/patch。手动 patch 不再抹掉同 minor 周期的 feature 信号;历史 minor 窗口中的 breaking 不再永久阻断后续自动发布。`Publish npm` 手动入口未改。
|
|
45
72
|
|
|
46
73
|
- `frontend-implementation` 绿色路径由固定全链改为按需分支:删除重复的首轮 design shell gate,初审通过直接复用原计划,只有 `request-revision` 才执行计划修订与终审;只有可修复失败才执行 repair/reverify/retrace。常规全绿路径因此少执行 3 个 Pi 与 4 个 shell 节点,同时保留 pass-only 写入门禁、验证/评审证据和只读 `frontend-closeout-pi`。原计划/修订计划的结构化合同与需求覆盖使用同一有效来源,且只有主分支产物缺失时才回退;已存在但 malformed/schema-invalid 的产物继续 fail-closed。
|
|
47
74
|
- Observe 节点检查器“规范证据”页签新增安全文件预览:已绑定任务源和已读取规范文件条目现以可聚焦按钮呈现,点击后在检查器内下钻查看内容(不新增弹窗),提供“返回规范证据”按钮并恢复触发行焦点。新增只读接口 `GET /api/dag-runs/:runId/nodes/:nodeId/spec-evidence/file?source=binding|read&path=...`,服务端重新校验证据成员身份(binding 必须在 `sourceBinding.sources`,read 必须有成功配对 read),拒 `..`/绝对/目录/二进制/仓库外符号链接;正文上限 64KB 并返回截断标志,统一走 Observe 脱敏规则;binding 返回 `sha256`/`currentSha256`/`hashMatch`,read 返回 session event 中实际读取正文与 `readAt`(不以当前工作区文件替代)。
|
|
@@ -65,6 +92,8 @@
|
|
|
65
92
|
### 修复
|
|
66
93
|
|
|
67
94
|
- Windows DAG Shell/verification 的 clean environment 现在保留 `APPDATA`,使 Python 3.12/pip 可按系统规则发现用户级 `site-packages` 与其中的 pytest;仍不会继承完整宿主环境或透传未授权 token/credential,backend-test 无需硬编码用户路径。
|
|
95
|
+
- Observe 节点检查器的“规范证据”现在会按任务目录解析 `sourceBinding` 中的相对路径,`source/需求.md` 等已绑定文件可正常打开;路径成员校验、目录穿越、绝对路径、二进制文件和符号链接逃逸防护保持不变。
|
|
96
|
+
- Decision Gate 的 Pi 执行指令不再与输出合同冲突:启用 `decisionGate` 的只读节点会明确输出唯一的 `DECISION_ENVELOPE_JSON` fenced block,普通只读节点仍保持原有无代码围栏约束,严格解析规则不放宽。
|
|
68
97
|
|
|
69
98
|
- Backend-test Analysis v2 intake 现在可确定性归一化模型常见的外部依赖 `type` / `target` 与风险 `type` 字段:dependency `target` 映射为 canonical `name`,扩展字段不落盘;dependency/risk 仍使用类别化白名单,未知字段和 `risks[].target` 继续 fail-closed,避免节点 02 因合法近似形态过早中断。
|
|
70
99
|
|
|
@@ -155,7 +155,7 @@ function parseDagRunTaskProfile(value) {
|
|
|
155
155
|
}
|
|
156
156
|
export function parseDagValidateArgs(args) {
|
|
157
157
|
if (args.length === 0) {
|
|
158
|
-
throw new Error("usage: dag validate --dag <path> [--strict-models] [--strict-governance]");
|
|
158
|
+
throw new Error("usage: dag validate --dag <path> [--strict-models] [--strict-governance] [--json]");
|
|
159
159
|
}
|
|
160
160
|
let dagPath;
|
|
161
161
|
let strictModelMatrix = false;
|
|
@@ -191,6 +191,10 @@ export function parseDagValidateArgs(args) {
|
|
|
191
191
|
strictSkills = true;
|
|
192
192
|
continue;
|
|
193
193
|
}
|
|
194
|
+
if (arg === "--json") {
|
|
195
|
+
// Accepted for operator/console callers; validate always emits JSON stdout.
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
194
198
|
if (arg === "--forbid-executor") {
|
|
195
199
|
const executor = args[++i];
|
|
196
200
|
if (!executor) {
|
|
@@ -243,7 +247,7 @@ export function parseDagValidateArgs(args) {
|
|
|
243
247
|
}
|
|
244
248
|
export function parseRunDagArgs(args, defaultCwd) {
|
|
245
249
|
if (args.length === 0) {
|
|
246
|
-
throw new Error("usage: run-dag --dag <path> [--cwd <dir>] [--init-only] [--dry-run] [--max-concurrent N] [--run-id id] [--canvas-path <abs-path> | --canvas <name> [--canvases-dir <dir>]] [--events-jsonl <path>]");
|
|
250
|
+
throw new Error("usage: run-dag --dag <path> [--cwd <dir>] [--init-only] [--dry-run] [--max-concurrent N] [--run-id id] [--canvas-path <abs-path> | --canvas <name> [--canvases-dir <dir>]] [--events-jsonl <path>] [--json]");
|
|
247
251
|
}
|
|
248
252
|
let dagPath;
|
|
249
253
|
let cwd;
|
|
@@ -305,6 +309,9 @@ export function parseRunDagArgs(args, defaultCwd) {
|
|
|
305
309
|
else if (arg.startsWith("--events-jsonl=")) {
|
|
306
310
|
eventsJsonlPath = arg.slice("--events-jsonl=".length);
|
|
307
311
|
}
|
|
312
|
+
else if (arg === "--json") {
|
|
313
|
+
// Accepted for operator/console callers; run-dag always emits JSON stdout.
|
|
314
|
+
}
|
|
308
315
|
else if (!arg.startsWith("-")) {
|
|
309
316
|
if (!dagPath)
|
|
310
317
|
dagPath = arg;
|
|
@@ -140,6 +140,17 @@ export function buildDagPiUserMessage(task, persona, step) {
|
|
|
140
140
|
"Do not wrap the output in code fences and do not add conversational preamble.",
|
|
141
141
|
].join(" ");
|
|
142
142
|
}
|
|
143
|
+
// Decision Gate nodes must emit a mandatory DECISION_ENVELOPE_JSON fenced block;
|
|
144
|
+
// do not inject the global no-fence instruction that would conflict with outputContract.
|
|
145
|
+
if (task.decisionGate?.enabled === true) {
|
|
146
|
+
return [
|
|
147
|
+
`You are executing hybrid DAG node "${task.id}" (role=${role}, piPersona=${persona}, piStep=${step}, writePolicy=${writePolicy}).`,
|
|
148
|
+
"The system prompt contains the full DAG envelope: objective, constraints, upstream context, and task.",
|
|
149
|
+
"Use read-only tools only. Do not edit, write, or commit repository files.",
|
|
150
|
+
"Return your conclusion as Markdown that includes exactly one ```DECISION_ENVELOPE_JSON fenced block (info string DECISION_ENVELOPE_JSON, not json) matching the Decision Envelope schema, plus a short evidence/risk summary.",
|
|
151
|
+
"Follow the node outputContract for the mandatory fenced block. Do not add conversational preamble outside the required Markdown structure.",
|
|
152
|
+
].join(" ");
|
|
153
|
+
}
|
|
143
154
|
return [
|
|
144
155
|
`You are executing hybrid DAG node "${task.id}" (role=${role}, piPersona=${persona}, piStep=${step}, writePolicy=${writePolicy}).`,
|
|
145
156
|
"The system prompt contains the full DAG envelope: objective, constraints, upstream context, and task.",
|
|
@@ -5,11 +5,89 @@ function draftPath(appData, taskId) {
|
|
|
5
5
|
return path.join(appData.drafts, `${safe}.json`);
|
|
6
6
|
}
|
|
7
7
|
export function computeDraftSha256(draft) {
|
|
8
|
-
// Hash a stable projection excluding
|
|
9
|
-
//
|
|
10
|
-
|
|
8
|
+
// Hash a stable projection excluding bookkeeping fields so binding a
|
|
9
|
+
// fresh assessment (interviewAssessmentRef) or timestamp churn does not
|
|
10
|
+
// invalidate the assessment that just certified this Draft content.
|
|
11
|
+
const { updatedAt: _u, interviewAssessmentRef: _ref, ...rest } = draft;
|
|
11
12
|
return sha256Json(rest);
|
|
12
13
|
}
|
|
14
|
+
function asStringList(value) {
|
|
15
|
+
if (!Array.isArray(value))
|
|
16
|
+
return [];
|
|
17
|
+
return value
|
|
18
|
+
.map((item) => {
|
|
19
|
+
if (typeof item === "string")
|
|
20
|
+
return item.trim();
|
|
21
|
+
if (item && typeof item === "object") {
|
|
22
|
+
try {
|
|
23
|
+
return JSON.stringify(item);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return String(item);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return String(item ?? "").trim();
|
|
30
|
+
})
|
|
31
|
+
.filter(Boolean);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Project Console application-data Draft into the strict CLI
|
|
35
|
+
* `taskContractDraftSchema` shape (no updatedAt; invariants required).
|
|
36
|
+
*/
|
|
37
|
+
export function toCanonicalDraftForCli(draft) {
|
|
38
|
+
const requirement = draft.requirement ?? {};
|
|
39
|
+
const constraints = draft.constraints ?? {};
|
|
40
|
+
const verification = draft.verification ?? {};
|
|
41
|
+
const canonical = {
|
|
42
|
+
schemaVersion: 1,
|
|
43
|
+
taskId: draft.taskId,
|
|
44
|
+
title: draft.title?.trim() || draft.taskId,
|
|
45
|
+
taskKind: draft.taskKind?.trim() || "standard",
|
|
46
|
+
requirement: {
|
|
47
|
+
objective: (requirement.objective ?? "").trim() || draft.title || draft.taskId,
|
|
48
|
+
scope: Array.isArray(requirement.scope) ? requirement.scope : [],
|
|
49
|
+
nonGoals: Array.isArray(requirement.nonGoals) ? requirement.nonGoals : [],
|
|
50
|
+
acceptanceCriteria: Array.isArray(requirement.acceptanceCriteria)
|
|
51
|
+
? requirement.acceptanceCriteria
|
|
52
|
+
: [],
|
|
53
|
+
},
|
|
54
|
+
constraints: {
|
|
55
|
+
invariants: Array.isArray(constraints.invariants)
|
|
56
|
+
? constraints.invariants
|
|
57
|
+
: [],
|
|
58
|
+
allowedPaths: Array.isArray(constraints.allowedPaths)
|
|
59
|
+
? constraints.allowedPaths
|
|
60
|
+
: [],
|
|
61
|
+
forbiddenPaths: Array.isArray(constraints.forbiddenPaths)
|
|
62
|
+
? constraints.forbiddenPaths
|
|
63
|
+
: [],
|
|
64
|
+
},
|
|
65
|
+
verification: {
|
|
66
|
+
commands: Array.isArray(verification.commands)
|
|
67
|
+
? verification.commands
|
|
68
|
+
: [],
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
if (draft.featureId?.trim())
|
|
72
|
+
canonical.featureId = draft.featureId.trim();
|
|
73
|
+
if (Array.isArray(draft.references) && draft.references.length > 0) {
|
|
74
|
+
canonical.references = draft.references;
|
|
75
|
+
}
|
|
76
|
+
if (Array.isArray(draft.sourceResolutions) &&
|
|
77
|
+
draft.sourceResolutions.length > 0) {
|
|
78
|
+
canonical.sourceResolutions = draft.sourceResolutions;
|
|
79
|
+
}
|
|
80
|
+
const openQuestions = asStringList(draft.openQuestions);
|
|
81
|
+
if (openQuestions.length > 0)
|
|
82
|
+
canonical.openQuestions = openQuestions;
|
|
83
|
+
const assumptions = asStringList(draft.assumptions);
|
|
84
|
+
if (assumptions.length > 0)
|
|
85
|
+
canonical.assumptions = assumptions;
|
|
86
|
+
if (draft.interviewAssessmentRef) {
|
|
87
|
+
canonical.interviewAssessmentRef = draft.interviewAssessmentRef;
|
|
88
|
+
}
|
|
89
|
+
return canonical;
|
|
90
|
+
}
|
|
13
91
|
export class DraftStore {
|
|
14
92
|
appData;
|
|
15
93
|
constructor(appData) {
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic grill-me Requirement Interview engine (design §7.8).
|
|
3
|
+
* One open question at a time; Draft updates from answers; integrity
|
|
4
|
+
* assessment when required fields are present.
|
|
5
|
+
*/
|
|
6
|
+
import { consoleWorkflowKindPromptList, normalizeConsoleWorkflowKind, } from "../workflow-kinds.js";
|
|
7
|
+
const GAP_ORDER = [
|
|
8
|
+
"objective",
|
|
9
|
+
"scope",
|
|
10
|
+
"nonGoals",
|
|
11
|
+
"acceptanceCriteria",
|
|
12
|
+
"taskKind",
|
|
13
|
+
"allowedPaths",
|
|
14
|
+
"forbiddenPaths",
|
|
15
|
+
"verifyCommands",
|
|
16
|
+
];
|
|
17
|
+
function hasText(value) {
|
|
18
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
19
|
+
}
|
|
20
|
+
function hasItems(value) {
|
|
21
|
+
return Array.isArray(value) && value.length > 0;
|
|
22
|
+
}
|
|
23
|
+
export function listDraftGaps(draft) {
|
|
24
|
+
const gaps = [];
|
|
25
|
+
const req = draft.requirement ?? {};
|
|
26
|
+
if (!hasText(req.objective))
|
|
27
|
+
gaps.push("objective");
|
|
28
|
+
if (!hasItems(req.scope))
|
|
29
|
+
gaps.push("scope");
|
|
30
|
+
if (!hasItems(req.nonGoals))
|
|
31
|
+
gaps.push("nonGoals");
|
|
32
|
+
if (!hasItems(req.acceptanceCriteria))
|
|
33
|
+
gaps.push("acceptanceCriteria");
|
|
34
|
+
if (!hasText(draft.taskKind))
|
|
35
|
+
gaps.push("taskKind");
|
|
36
|
+
if (!hasItems(draft.constraints?.allowedPaths))
|
|
37
|
+
gaps.push("allowedPaths");
|
|
38
|
+
if (!hasItems(draft.constraints?.forbiddenPaths))
|
|
39
|
+
gaps.push("forbiddenPaths");
|
|
40
|
+
if (!hasItems(draft.verification?.commands))
|
|
41
|
+
gaps.push("verifyCommands");
|
|
42
|
+
return gaps;
|
|
43
|
+
}
|
|
44
|
+
export function isDraftStructurallyComplete(draft) {
|
|
45
|
+
return listDraftGaps(draft).length === 0;
|
|
46
|
+
}
|
|
47
|
+
export function buildQuestionForGap(gapId, draft) {
|
|
48
|
+
const taskId = draft.taskId;
|
|
49
|
+
const questions = {
|
|
50
|
+
objective: {
|
|
51
|
+
id: `q-${gapId}`,
|
|
52
|
+
gapId,
|
|
53
|
+
text: `任务 ${taskId} 的核心目标(objective)是什么?请用一句话说明要交付什么。`,
|
|
54
|
+
recommendation: `实现并验证 ${taskId} 的明确、可验收交付物`,
|
|
55
|
+
affectsFields: ["requirement.objective"],
|
|
56
|
+
risk: "high",
|
|
57
|
+
},
|
|
58
|
+
scope: {
|
|
59
|
+
id: `q-${gapId}`,
|
|
60
|
+
gapId,
|
|
61
|
+
text: "本轮允许改动的范围(scope)是哪些路径/模块?用逗号或换行列出。",
|
|
62
|
+
recommendation: "src/worker/console/**",
|
|
63
|
+
affectsFields: ["requirement.scope"],
|
|
64
|
+
risk: "high",
|
|
65
|
+
},
|
|
66
|
+
nonGoals: {
|
|
67
|
+
id: `q-${gapId}`,
|
|
68
|
+
gapId,
|
|
69
|
+
text: "明确非目标(non-goals)是什么?本轮刻意不做哪些事?",
|
|
70
|
+
recommendation: "Phase 4 General Operator Chat;合并 Console/Observe",
|
|
71
|
+
affectsFields: ["requirement.nonGoals"],
|
|
72
|
+
risk: "medium",
|
|
73
|
+
},
|
|
74
|
+
acceptanceCriteria: {
|
|
75
|
+
id: `q-${gapId}`,
|
|
76
|
+
gapId,
|
|
77
|
+
text: "验收标准有哪些?可用分号或换行列出(将编号为 AC-1…)。",
|
|
78
|
+
recommendation: "Happy Path 可走通 Interview→validate→apply;无 Cancel;无直接改代码 CTA",
|
|
79
|
+
affectsFields: ["requirement.acceptanceCriteria"],
|
|
80
|
+
risk: "high",
|
|
81
|
+
},
|
|
82
|
+
taskKind: {
|
|
83
|
+
id: `q-${gapId}`,
|
|
84
|
+
gapId,
|
|
85
|
+
text: `流程类型(taskKind)应设为?(${consoleWorkflowKindPromptList()})`,
|
|
86
|
+
recommendation: "standard",
|
|
87
|
+
affectsFields: ["taskKind"],
|
|
88
|
+
risk: "medium",
|
|
89
|
+
},
|
|
90
|
+
allowedPaths: {
|
|
91
|
+
id: `q-${gapId}`,
|
|
92
|
+
gapId,
|
|
93
|
+
text: "结构化 allowedPaths(写入白名单)?逗号/换行分隔 glob。",
|
|
94
|
+
recommendation: "src/worker/console/**, test/worker/console/**",
|
|
95
|
+
affectsFields: ["constraints.allowedPaths"],
|
|
96
|
+
risk: "high",
|
|
97
|
+
},
|
|
98
|
+
forbiddenPaths: {
|
|
99
|
+
id: `q-${gapId}`,
|
|
100
|
+
gapId,
|
|
101
|
+
text: "forbiddenPaths(禁止写入)?逗号/换行分隔。",
|
|
102
|
+
recommendation: "src/cli/**, bin/**, package.json",
|
|
103
|
+
affectsFields: ["constraints.forbiddenPaths"],
|
|
104
|
+
risk: "high",
|
|
105
|
+
},
|
|
106
|
+
verifyCommands: {
|
|
107
|
+
id: `q-${gapId}`,
|
|
108
|
+
gapId,
|
|
109
|
+
text: "验证命令(verifyCommands)?可给一条或多条 shell(换行分隔)。",
|
|
110
|
+
recommendation: "npx vitest run test/worker/console",
|
|
111
|
+
affectsFields: ["verification.commands"],
|
|
112
|
+
risk: "medium",
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
return questions[gapId];
|
|
116
|
+
}
|
|
117
|
+
export function nextGrillMeQuestion(draft) {
|
|
118
|
+
const gaps = listDraftGaps(draft);
|
|
119
|
+
for (const gapId of GAP_ORDER) {
|
|
120
|
+
if (gaps.includes(gapId))
|
|
121
|
+
return buildQuestionForGap(gapId, draft);
|
|
122
|
+
}
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
function splitList(text) {
|
|
126
|
+
return text
|
|
127
|
+
.split(/[\n,;,;]+/)
|
|
128
|
+
.map((s) => s.trim())
|
|
129
|
+
.filter(Boolean);
|
|
130
|
+
}
|
|
131
|
+
function resolveAnswerText(response, recommendation, text) {
|
|
132
|
+
if (response === "accept-recommendation")
|
|
133
|
+
return recommendation;
|
|
134
|
+
if (response === "defer")
|
|
135
|
+
return text?.trim() || recommendation;
|
|
136
|
+
return (text ?? "").trim() || recommendation;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Apply one grill-me answer onto Draft. Returns updated draft (immutable merge).
|
|
140
|
+
*/
|
|
141
|
+
export function applyGrillMeAnswer(draft, question, answer) {
|
|
142
|
+
const value = resolveAnswerText(answer.response, question.recommendation, answer.text);
|
|
143
|
+
const next = {
|
|
144
|
+
...draft,
|
|
145
|
+
requirement: { ...draft.requirement },
|
|
146
|
+
constraints: {
|
|
147
|
+
invariants: draft.constraints?.invariants ?? [],
|
|
148
|
+
allowedPaths: draft.constraints?.allowedPaths ?? [],
|
|
149
|
+
forbiddenPaths: draft.constraints?.forbiddenPaths ?? [],
|
|
150
|
+
...draft.constraints,
|
|
151
|
+
},
|
|
152
|
+
verification: {
|
|
153
|
+
commands: draft.verification?.commands ?? [],
|
|
154
|
+
...draft.verification,
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
switch (question.gapId) {
|
|
158
|
+
case "objective":
|
|
159
|
+
next.requirement = { ...next.requirement, objective: value };
|
|
160
|
+
break;
|
|
161
|
+
case "scope":
|
|
162
|
+
next.requirement = { ...next.requirement, scope: splitList(value) };
|
|
163
|
+
break;
|
|
164
|
+
case "nonGoals":
|
|
165
|
+
next.requirement = { ...next.requirement, nonGoals: splitList(value) };
|
|
166
|
+
break;
|
|
167
|
+
case "acceptanceCriteria":
|
|
168
|
+
next.requirement = {
|
|
169
|
+
...next.requirement,
|
|
170
|
+
acceptanceCriteria: splitList(value).map((text, i) => ({
|
|
171
|
+
id: `AC-${i + 1}`,
|
|
172
|
+
text,
|
|
173
|
+
})),
|
|
174
|
+
};
|
|
175
|
+
break;
|
|
176
|
+
case "taskKind":
|
|
177
|
+
next.taskKind = normalizeConsoleWorkflowKind(value, "standard");
|
|
178
|
+
break;
|
|
179
|
+
case "allowedPaths":
|
|
180
|
+
next.constraints = {
|
|
181
|
+
...next.constraints,
|
|
182
|
+
allowedPaths: splitList(value),
|
|
183
|
+
};
|
|
184
|
+
break;
|
|
185
|
+
case "forbiddenPaths":
|
|
186
|
+
next.constraints = {
|
|
187
|
+
...next.constraints,
|
|
188
|
+
forbiddenPaths: splitList(value),
|
|
189
|
+
};
|
|
190
|
+
break;
|
|
191
|
+
case "verifyCommands":
|
|
192
|
+
next.verification = {
|
|
193
|
+
...next.verification,
|
|
194
|
+
commands: splitList(value).map((command, i) => ({
|
|
195
|
+
label: `verify-${i + 1}`,
|
|
196
|
+
command,
|
|
197
|
+
timeoutMs: 120_000,
|
|
198
|
+
})),
|
|
199
|
+
};
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
if (answer.response === "defer" && question.risk !== "high") {
|
|
203
|
+
const assumptions = Array.isArray(draft.assumptions)
|
|
204
|
+
? [...draft.assumptions]
|
|
205
|
+
: [];
|
|
206
|
+
assumptions.push({
|
|
207
|
+
questionId: question.id,
|
|
208
|
+
gapId: question.gapId,
|
|
209
|
+
assumed: value,
|
|
210
|
+
at: new Date().toISOString(),
|
|
211
|
+
});
|
|
212
|
+
next.assumptions = assumptions;
|
|
213
|
+
}
|
|
214
|
+
return next;
|
|
215
|
+
}
|
|
216
|
+
export function emptyDraft(taskId, title, options) {
|
|
217
|
+
const taskKind = options?.taskKind?.trim()
|
|
218
|
+
? normalizeConsoleWorkflowKind(options.taskKind, "standard")
|
|
219
|
+
: undefined;
|
|
220
|
+
return {
|
|
221
|
+
schemaVersion: 1,
|
|
222
|
+
taskId,
|
|
223
|
+
title: title?.trim() || taskId,
|
|
224
|
+
taskKind,
|
|
225
|
+
requirement: {},
|
|
226
|
+
constraints: {
|
|
227
|
+
invariants: [],
|
|
228
|
+
allowedPaths: [],
|
|
229
|
+
forbiddenPaths: [],
|
|
230
|
+
},
|
|
231
|
+
verification: {
|
|
232
|
+
commands: [],
|
|
233
|
+
},
|
|
234
|
+
openQuestions: [],
|
|
235
|
+
assumptions: [],
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
export function draftSummary(draft) {
|
|
239
|
+
return {
|
|
240
|
+
taskId: draft.taskId,
|
|
241
|
+
title: draft.title,
|
|
242
|
+
taskKind: draft.taskKind ?? null,
|
|
243
|
+
objective: draft.requirement?.objective ?? null,
|
|
244
|
+
scope: draft.requirement?.scope ?? [],
|
|
245
|
+
nonGoals: draft.requirement?.nonGoals ?? [],
|
|
246
|
+
acceptanceCriteria: draft.requirement?.acceptanceCriteria ?? [],
|
|
247
|
+
allowedPaths: draft.constraints?.allowedPaths ?? [],
|
|
248
|
+
forbiddenPaths: draft.constraints?.forbiddenPaths ?? [],
|
|
249
|
+
verifyCommands: draft.verification?.commands ?? [],
|
|
250
|
+
gaps: listDraftGaps(draft),
|
|
251
|
+
structurallyComplete: isDraftStructurallyComplete(draft),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
@@ -128,7 +128,8 @@ async function finalizeFromWorkerResult(operationId, result, deps) {
|
|
|
128
128
|
errorMessage: ok
|
|
129
129
|
? undefined
|
|
130
130
|
: (envelope?.error?.message ??
|
|
131
|
-
|
|
131
|
+
(previewStderr.trim() ||
|
|
132
|
+
`command failed with exit ${result.exitCode}`)),
|
|
132
133
|
});
|
|
133
134
|
stateEvent(deps.events, updated, state);
|
|
134
135
|
deps.events.append(operationId, {
|