@tea-agent/loop-agent 0.12.0 → 0.13.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -2
- package/README.md +30 -2
- package/dist/application/dag/generate-task-dag.js +30 -0
- package/dist/cli/command-definitions.js +10 -3
- package/dist/commands/knowledge.js +129 -31
- package/dist/governance/manifest-types.js +3 -0
- package/dist/task/config-types.js +5 -1
- package/dist/worker/cli.js +96 -1
- package/dist/worker/delivery/package.js +3 -3
- package/dist/worker/feature/decision-loader.js +37 -6
- package/dist/worker/feature/next-action.js +10 -2
- package/dist/worker/feature/ready-plan-projection.js +81 -0
- package/dist/worker/feature/reducer.js +2 -1
- package/dist/worker/feature/review.js +19 -2
- package/dist/worker/feature/run.js +27 -2
- package/dist/worker/follow-up/approve.js +5 -2
- package/dist/worker/follow-up/factory.js +1 -1
- package/dist/worker/observability/read-model.js +246 -41
- package/dist/worker/observe/routes.js +158 -15
- package/dist/worker/observe/spec-evidence.js +281 -0
- package/dist/worker/observe/static/api.js +19 -0
- package/dist/worker/observe/static/app.js +2 -2
- package/dist/worker/observe/static/relations.js +17 -12
- package/dist/worker/observe/static/router.js +8 -0
- package/dist/worker/observe/static/styles.css +12 -0
- package/dist/worker/observe/static/views/batch.js +3 -2
- package/dist/worker/observe/static/views/dag-inspector.js +123 -4
- package/dist/worker/observe/static/views/dashboard.js +8 -5
- package/dist/worker/observe/static/views/feature.js +43 -4
- package/dist/worker/observe/static/views/pool.js +5 -2
- package/dist/worker/observe/static/views/run.js +1 -1
- package/dist/worker/observe/static/views/task.js +69 -15
- package/dist/worker/pool/doctor.js +165 -0
- package/dist/worker/pool/migrate-state.js +303 -0
- package/dist/worker/pool/run-store.js +205 -17
- package/dist/worker/pool/types.js +17 -1
- package/dist/worker/pool/validation.js +100 -15
- package/dist/worker/report/morning-report.js +12 -2
- package/dist/worker/runner/run-ready.js +41 -26
- package/dist/worker/task-graph/ready-planner.js +136 -0
- package/dist/workflows/dag/convergence/controller.js +16 -8
- package/dist/workflows/dag/failure-routing.js +12 -1
- package/dist/workflows/dag/init-hybrid.js +837 -8
- package/dist/workflows/dag/types.js +1 -0
- package/docs/README.md +1 -1
- package/docs/agent-dag-recovery-playbook.md +9 -0
- package/docs/architecture/evolution.md +4 -3
- package/docs/architecture/facts-and-state.md +14 -1
- package/docs/architecture/worker-and-feature.md +6 -2
- package/docs/decisions/README.md +3 -0
- package/docs/design/README.md +8 -0
- package/docs/exec-plans/active/README.md +2 -0
- package/docs/exec-plans/completed/README.md +3 -2
- package/docs/feature-workflow.md +80 -2
- package/docs/loop-agent-harness.md +15 -4
- package/docs/progress/README.md +4 -0
- package/docs/reports/README.md +6 -0
- package/docs/templates/backend-test-dag.json +12 -0
- package/docs/templates/knowledge-graph-bootstrap-dag.json +118 -0
- package/docs/templates/knowledge-sync-dag.json +177 -0
- package/docs/templates/knowledge-sync-draft.schema.json +71 -0
- package/docs/verification-matrix.md +2 -1
- package/package.json +8 -2
- package/scripts/kb-bootstrap-init-skeleton.sh +239 -0
- package/scripts/kb-graph-incremental-prepare.mjs +372 -0
- package/scripts/kb-graph-incremental-prepare.sh +5 -0
- package/scripts/kb-graph-materialize.mjs +105 -0
- package/scripts/kb-graph-materialize.sh +4 -0
- package/scripts/kb-graph-promote.mjs +153 -0
- package/scripts/kb-graph-promote.sh +4 -0
- package/scripts/kb-query.mjs +554 -0
- package/scripts/kb-query.sh +5 -0
- package/skills/agent-worker/SKILL.md +3 -1
- package/skills/agent-worker/references/agent-worker-operator.md +18 -1
- package/skills/frontend-design-review/SKILL.md +26 -24
- package/skills/frontend-implementation/SKILL.md +29 -26
- package/skills/frontend-implementation/references/node-contracts.md +50 -19
- package/skills/frontend-review/SKILL.md +1 -1
- package/skills/loop-agent/references/command-reference.md +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,14 +2,62 @@
|
|
|
2
2
|
|
|
3
3
|
这里记录每个版本对使用者有影响的变化。命令名和配置字段保持原样,其余内容尽量用日常语言说明。
|
|
4
4
|
|
|
5
|
-
## [Unreleased]
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
## [0.13.0] - Unreleased
|
|
8
|
+
|
|
9
|
+
### 新增
|
|
6
10
|
|
|
7
|
-
|
|
11
|
+
- 前端 DAG 新增设计修订闭环:首轮 design review 输出 `request-revision` 时通过只读 `frontend-plan-revision-pi` 修订计划后经 `frontend-final-design-review-pi` 和 `frontend-final-design-gate-shell` 最终门禁,不再直接终止。只有最终 `VERDICT: pass` 才授权写入。
|
|
12
|
+
- Observe 节点检查器新增「规范证据」页签,展示 skill 注入状态、已读取规范文件(基于 session event 中成功完成的 read 工具调用)、检索操作(grep/find/ls)以及知识库查询;证据不足时提供明确提示。
|
|
13
|
+
- 规范证据接口强制要求通过 `toolCallId` 配对 `tool_execution_start` 与 `tool_execution_end` 事件;仅有 start 而无匹配成功 end、end 报错(`isError: true`)或缺失 complete 事件的 read 一律不计入已读取。
|
|
14
|
+
|
|
15
|
+
### 变更
|
|
16
|
+
|
|
17
|
+
- 前端 design gate shell 失败路由改为 `ContractMismatch` / `frontend-plan-revision-and-rerun`,不再路由为 `ProductBug` / `dev-fix`。
|
|
18
|
+
- 前端实现、design review 和 review skill 要求显式执行并记录规范检索与读取,以便后续新 run 在 Observe 中生成可观察证据。
|
|
19
|
+
|
|
20
|
+
### 破坏性变更
|
|
21
|
+
|
|
22
|
+
- Task Pool 任务身份升级为 feature-scoped 复合键 `{ featureId, taskId }`(`TaskPoolTaskRef`,ADR 0004)。新 state 使用 `schemaVersion: 2`,canonical 路径为 `.harness/task-pool/states/<featureId>/<taskId>.json`;裸 `taskId` 不再是仓库全局唯一键。
|
|
23
|
+
- `agent-worker task retry` 的 canonical 调用必须携带 `--feature-id`。跨 Feature 同名 Task 时省略 `--feature-id` 会 fail-closed,不会静默猜 Feature。
|
|
24
|
+
- 扁平 legacy `states/<taskId>.json` 在写入 / Ready 路径 fail-closed,不会被静默解释为某个 Feature 的状态。
|
|
25
|
+
|
|
26
|
+
### 迁移提示
|
|
27
|
+
|
|
28
|
+
1. 运行 `agent-worker pool doctor --repo <repo> --json` 盘点 v2 / legacy inventory。
|
|
29
|
+
2. 需要迁移时先 `agent-worker pool migrate-state --repo <repo>`(dry-run),确认映射后再 `--apply --owner <owner> --reason <reason>`。
|
|
30
|
+
3. 更新脚本与文档中的 retry 示例为 `task retry <task-id> --feature-id <feature-id> ...`。
|
|
31
|
+
4. Observe / 书签改用 `#/feature/:featureId/task/:taskId`;legacy `#/task/:taskId` 仅在 taskId 全库唯一时兼容。
|
|
32
|
+
5. `runs.jsonl` / `events.jsonl` 不会被 migrate 重写;历史 `workerRunId` 仍可追溯。
|
|
33
|
+
|
|
34
|
+
### 新增
|
|
35
|
+
|
|
36
|
+
- `agent-worker pool doctor`:只读 Task Pool inventory 与迁移证据(exit 0 也可带 findings)。
|
|
37
|
+
- `agent-worker pool migrate-state`:legacy → v2 显式迁移;默认 dry-run,apply 需 owner/reason,失败全回滚并保留审计 artifact。
|
|
38
|
+
- `agent-worker batch plan-ready`:零写入预览 TaskSpec priority 驱动的 selected、deferred 与 blocked 计划。
|
|
39
|
+
|
|
40
|
+
### 变更
|
|
41
|
+
|
|
42
|
+
- Runner Ready Queue、Feature review/run、Follow-up、Delivery / Closeout、morning report / metrics 与 Observe read-model 均按 Feature 作用域消费 state,同名 Task 跨 Feature 不串线。
|
|
43
|
+
- Observe canonical API:`GET /api/features/:featureId/tasks/:taskId[/runs]`;legacy bare task route 在歧义时返回 409 + `candidates`。
|
|
44
|
+
- Feature dry-run / review / next-action、morning report、GlobalSnapshot、Observe 消费同一 Ready Planner projection;前端与投影层不得独立重选或重排 ready tasks。
|
|
45
|
+
|
|
46
|
+
### 修复
|
|
47
|
+
|
|
48
|
+
- Windows 下发布/测试更稳:路径断言对齐 `realpathSync`(避免短路径与长路径比较失败)、Git Bash 解析与生产 `shell-executor` 一致(支持 `LOOP_AGENT_BASH` / `GIT_BASH` 与多候选路径),并放宽 self-host canary、Ring 1 materialize 与 Git-backed Delivery 集成测试在重负载下的超时上限。
|
|
49
|
+
- 前端实现 skill 会完整注入增长后的必需节点契约,不再因单引用预算仍停留在旧上限而截断设计修订闭环规则。
|
|
50
|
+
|
|
51
|
+
## [0.12.0] - 2026-07-16
|
|
8
52
|
|
|
9
53
|
### 新增
|
|
10
54
|
|
|
11
55
|
- 只读 Pi DAG 节点支持 opt-in 安全重试:仅 planner/scout/reviewer/verifier/closeout 角色且无写入能力的 Pi 节点可声明 `retryPolicy`,在同一 run 内重试模型连接中断(`network`)、provider 限流(`rate-limit`)、临时不可用(`unavailable`)或请求 timeout。生成模板默认总尝试 3 次,手工配置最多 5 次,并使用有上限的指数退避。每次 attempt 保留独立证据,节点耗时、Token 与事件数按尝试聚合;后一次成功不会覆盖前一次失败证据。`quota`、`auth`、`invalid-output`、`write-guard` 与未知失败不重试;supervisor、implementer、writer、dynamic、shell、static、docs-only、decision-gate 节点声明策略会在校验阶段失败。未声明策略的历史 DAG 不新增 attempt artifact,行为保持不变。
|
|
12
56
|
- 新增 `taskKind: "backend-test"` 专用 DAG:从需求分析开始,生成并评审后端功能用例,再生成和执行 pytest 自动化,最后产出测试复盘报告;新 DAG 沿用当前 Pi-only runtime、版本化 runtime contract 与既有执行恢复机制。
|
|
57
|
+
- 测试知识回写专用 DAG:`taskKind: "knowledge-sync"`。生成 collect→draft→validate→apply→pointer;**必须绑定 `featureId`**(`task.json.featureId` 或 hardConstraints / 需求中的 `F-…`),writeSet 收窄到 `features/<featureId>/…`。
|
|
58
|
+
- 业务知识图谱开荒 DAG:`taskKind: "knowledge-graph-bootstrap"`(preflight→inventory→propose→validate→review→gate→promote→materialize);AI 只写 `knowledge/bootstrap/staging/**`。
|
|
59
|
+
- 图谱辅助脚本与 CLI:`loop-agent knowledge query|graph-init|graph-materialize|graph-promote|graph-incremental-prepare`(薄封装 `scripts/kb-*`);`knowledge curate` 仍只负责 repair guidance 提案。
|
|
60
|
+
- B1 骨架、Phase A 查询(by_id / by_feature / search)、merge-new-only 晋升、增量 scope 准备与 materialize 索引;设计文档见 `docs/design/knowledge-*.md` 与 active plan `docs/exec-plans/active/2026-07-14-knowledge-sync-and-graph.md`。
|
|
13
61
|
|
|
14
62
|
### 修复
|
|
15
63
|
|
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ loop-agent --help
|
|
|
27
27
|
如果你拒绝版本 A,当前系统用户下不会再提醒 A;之后发布版本 B 时会继续提醒。确认更新时,CLI 会先证明当前安装来自同一 npm global root,然后安装刚确认的精确版本,例如:
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
|
-
npm install -g @tea-agent/loop-agent@0.
|
|
30
|
+
npm install -g @tea-agent/loop-agent@0.13.0
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
需要完全关闭自动检查时设置:
|
|
@@ -97,6 +97,25 @@ loop-agent run-dag --dag <temp-dir>/<task-id>-dag.json --cwd .
|
|
|
97
97
|
|
|
98
98
|
当前 `dag run-task` 也支持按 `task.json.taskKind` 选择专用模板;`frontend-implementation` 会生成前端契约、design gate、静态验证、行为验证和 review gate 节点链,`backend-test` 会生成从需求分析、功能用例、pytest 自动化到测试复盘的专用节点链。
|
|
99
99
|
|
|
100
|
+
测试结论写回与业务知识图谱(结构化 Git,非 RAG):
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
# 最终验证后:同步结论/用例/缺陷到 features/<F>/testing/**(须 featureId)
|
|
104
|
+
# task.json: { "taskKind": "knowledge-sync", "featureId": "F-2026-004" }
|
|
105
|
+
loop-agent dag run-task <task-id> --strict-models
|
|
106
|
+
|
|
107
|
+
# 图谱开荒 / 增量(AI 只写 staging,晋升默认不覆盖已有正式文件)
|
|
108
|
+
# task.json: { "taskKind": "knowledge-graph-bootstrap" }
|
|
109
|
+
loop-agent knowledge graph-init --product-name my-app
|
|
110
|
+
loop-agent knowledge graph-incremental-prepare --feature F-2026-004 --service order
|
|
111
|
+
loop-agent dag run-task <task-id> --strict-models
|
|
112
|
+
loop-agent knowledge graph-promote
|
|
113
|
+
loop-agent knowledge graph-materialize
|
|
114
|
+
loop-agent knowledge query --mode by_feature --feature F-2026-004 --json
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`knowledge curate` 仍只用于 repair/learned guidance 提案,与图谱查询不是同一能力。细节见 `docs/feature-workflow.md` 与 `docs/design/knowledge-graph-ai-bootstrap.md`。
|
|
118
|
+
|
|
100
119
|
`<temp-dir>` 表示平台原生临时目录;也可以省略 `--output`,再使用命令 JSON 输出里的 `outputPath`。
|
|
101
120
|
|
|
102
121
|
非微小工作需要 exec-plan 时,使用确定性生命周期命令维护计划与索引;`new-task` 不会自动创建计划:
|
|
@@ -158,7 +177,16 @@ Morning report 和 Observe snapshot/UI 复用同一 Feature reducer,优先展
|
|
|
158
177
|
|
|
159
178
|
nightly wrapper 按 feature 互斥,保留批次/超时退出码,并输出 morning report、Observe snapshot 和 controller 版本 artifacts。
|
|
160
179
|
|
|
161
|
-
自 0.8.0 起,`.harness/task-pool/` 是唯一受支持的 Task Pool runtime root,其中包含根级 `runs.jsonl`、`events.jsonl`、`states/`、`artifacts/`、`failure-handoffs/`、`observability/` 和 `reports/`。这是 hard cutover
|
|
180
|
+
自 0.8.0 起,`.harness/task-pool/` 是唯一受支持的 Task Pool runtime root,其中包含根级 `runs.jsonl`、`events.jsonl`、`states/`、`artifacts/`、`failure-handoffs/`、`observability/` 和 `reports/`。这是 hard cutover:此前的顶层 Task Pool 位置不读取、不迁移、不合并、不重映射。
|
|
181
|
+
|
|
182
|
+
Task Pool **state identity** 进一步固定为 feature-scoped 复合键 `{ featureId, taskId }`(ADR 0004):
|
|
183
|
+
|
|
184
|
+
- canonical state 路径:`.harness/task-pool/states/<featureId>/<taskId>.json`(schema v2,文件内容必须携带 `featureId` / `taskId`)
|
|
185
|
+
- 同仓库多 Feature 可安全使用相同 Task ID(例如 `F-A/QA-EXEC-001` 与 `F-B/QA-EXEC-001` 互不覆盖)
|
|
186
|
+
- retry 必须显式指定 Feature:`agent-worker task retry <task-id> --feature-id <feature-id> --repo <repo> --reason <reason>`
|
|
187
|
+
- 诊断与迁移:`agent-worker pool doctor --repo <repo> --json`(只读);`agent-worker pool migrate-state --repo <repo>` 默认 dry-run,apply 需 `--owner` + `--reason`
|
|
188
|
+
- 扁平 legacy `states/<taskId>.json` 不得静默解释;存在时新写入 fail-closed,必须经 doctor / migrate 处理
|
|
189
|
+
- Observe 只读:canonical `#/feature/:featureId/task/:taskId` 与 `GET /api/features/:featureId/tasks/:taskId`;legacy bare task route 仅在唯一命中时兼容,歧义返回 409 + candidates
|
|
162
190
|
|
|
163
191
|
## 核心概念
|
|
164
192
|
|
|
@@ -205,6 +205,8 @@ export async function generateTaskDagUseCase(input) {
|
|
|
205
205
|
const taskConfig = await loadTaskConfig(repoRoot, parsed.taskId);
|
|
206
206
|
const isFrontendImplementationTask = taskConfig.taskKind === "frontend-implementation";
|
|
207
207
|
const isBackendTestTask = taskConfig.taskKind === "backend-test";
|
|
208
|
+
const isKnowledgeSyncTask = taskConfig.taskKind === "knowledge-sync";
|
|
209
|
+
const isKnowledgeGraphBootstrapTask = taskConfig.taskKind === "knowledge-graph-bootstrap";
|
|
208
210
|
const candidateResult = await initHybridDagFromTask(repoRoot, parsed.taskId, {
|
|
209
211
|
outputPath: parsed.outputPath,
|
|
210
212
|
template: "standard-dag",
|
|
@@ -245,6 +247,34 @@ export async function generateTaskDagUseCase(input) {
|
|
|
245
247
|
profileRouting.selectedByProfile = parsed.profile;
|
|
246
248
|
}
|
|
247
249
|
}
|
|
250
|
+
if (isKnowledgeSyncTask) {
|
|
251
|
+
profileRouting.selectedTemplate = "knowledge-sync-dag";
|
|
252
|
+
profileRouting.source = "taskKind";
|
|
253
|
+
profileRouting.routingReasons = [
|
|
254
|
+
'taskKind "knowledge-sync" selects the dedicated knowledge-sync DAG template',
|
|
255
|
+
];
|
|
256
|
+
if (parsed.profile === "auto") {
|
|
257
|
+
profileRouting.selectedByProfile =
|
|
258
|
+
resolveAutoRoutingProfile(profileRouting.candidateProfile);
|
|
259
|
+
}
|
|
260
|
+
else if (parsed.profileExplicit) {
|
|
261
|
+
profileRouting.selectedByProfile = parsed.profile;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (isKnowledgeGraphBootstrapTask) {
|
|
265
|
+
profileRouting.selectedTemplate = "knowledge-graph-bootstrap-dag";
|
|
266
|
+
profileRouting.source = "taskKind";
|
|
267
|
+
profileRouting.routingReasons = [
|
|
268
|
+
'taskKind "knowledge-graph-bootstrap" selects the dedicated knowledge-graph bootstrap DAG template',
|
|
269
|
+
];
|
|
270
|
+
if (parsed.profile === "auto") {
|
|
271
|
+
profileRouting.selectedByProfile =
|
|
272
|
+
resolveAutoRoutingProfile(profileRouting.candidateProfile);
|
|
273
|
+
}
|
|
274
|
+
else if (parsed.profileExplicit) {
|
|
275
|
+
profileRouting.selectedByProfile = parsed.profile;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
248
278
|
const initResult = profileRouting.selectedTemplate === "standard-dag"
|
|
249
279
|
? candidateResult
|
|
250
280
|
: await initHybridDagFromTask(repoRoot, parsed.taskId, {
|
|
@@ -84,7 +84,14 @@ const HANDOFF_USAGE = "handoff <check|coverage> [taskId]";
|
|
|
84
84
|
const REFERENCE_SUBCOMMANDS = ["index"];
|
|
85
85
|
const STUDY_SUBCOMMANDS = ["init"];
|
|
86
86
|
const WORKTREE_SUBCOMMANDS = ["create", "list", "remove"];
|
|
87
|
-
const KNOWLEDGE_SUBCOMMANDS = [
|
|
87
|
+
const KNOWLEDGE_SUBCOMMANDS = [
|
|
88
|
+
"curate",
|
|
89
|
+
"query",
|
|
90
|
+
"graph-init",
|
|
91
|
+
"graph-materialize",
|
|
92
|
+
"graph-promote",
|
|
93
|
+
"graph-incremental-prepare",
|
|
94
|
+
];
|
|
88
95
|
const DAG_SUBCOMMANDS = [
|
|
89
96
|
"init-hybrid",
|
|
90
97
|
"run-task",
|
|
@@ -435,8 +442,8 @@ export const COMMAND_DEFINITIONS = [
|
|
|
435
442
|
name: "knowledge",
|
|
436
443
|
adapter: "required",
|
|
437
444
|
tier: "operator",
|
|
438
|
-
intent: "Curate
|
|
439
|
-
usage: "knowledge curate
|
|
445
|
+
intent: "Curate repair guidance proposals, query the structured knowledge graph, and run graph bootstrap helpers (init/materialize/promote/incremental).",
|
|
446
|
+
usage: "knowledge <curate|query|graph-init|graph-materialize|graph-promote|graph-incremental-prepare> ...",
|
|
440
447
|
subcommands: [...KNOWLEDGE_SUBCOMMANDS],
|
|
441
448
|
handler: async ({ repoRoot, subcommand, rest }) => {
|
|
442
449
|
await runKnowledge(repoRoot, [subcommand, ...rest].filter((arg) => Boolean(arg)));
|
|
@@ -1,41 +1,104 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
2
5
|
import { curateKnowledgePatterns } from "../workflows/dag/knowledge-curator.js";
|
|
6
|
+
export const KNOWLEDGE_CLI_SUBCOMMANDS = [
|
|
7
|
+
"curate",
|
|
8
|
+
"query",
|
|
9
|
+
"graph-init",
|
|
10
|
+
"graph-materialize",
|
|
11
|
+
"graph-promote",
|
|
12
|
+
"graph-incremental-prepare",
|
|
13
|
+
];
|
|
14
|
+
const USAGE = "usage: knowledge <curate|query|graph-init|graph-materialize|graph-promote|graph-incremental-prepare> ...";
|
|
15
|
+
function packageRoot() {
|
|
16
|
+
// dist/commands/knowledge.js or src/commands/knowledge.ts → package/repo root
|
|
17
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
18
|
+
}
|
|
19
|
+
function resolveScript(repoRoot, fileName) {
|
|
20
|
+
const candidates = [
|
|
21
|
+
path.join(repoRoot, "scripts", fileName),
|
|
22
|
+
path.join(packageRoot(), "scripts", fileName),
|
|
23
|
+
];
|
|
24
|
+
for (const c of candidates) {
|
|
25
|
+
if (existsSync(c))
|
|
26
|
+
return c;
|
|
27
|
+
}
|
|
28
|
+
throw new Error(`knowledge script not found: ${fileName} (looked under repo scripts/ and package scripts/)`);
|
|
29
|
+
}
|
|
30
|
+
function runProcess(command, args, cwd) {
|
|
31
|
+
const result = spawnSync(command, args, {
|
|
32
|
+
cwd,
|
|
33
|
+
encoding: "utf8",
|
|
34
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
35
|
+
});
|
|
36
|
+
if (result.stdout)
|
|
37
|
+
process.stdout.write(result.stdout);
|
|
38
|
+
if (result.stderr)
|
|
39
|
+
process.stderr.write(result.stderr);
|
|
40
|
+
if (result.error)
|
|
41
|
+
throw result.error;
|
|
42
|
+
if (result.status !== 0 && result.status !== null) {
|
|
43
|
+
process.exitCode = result.status;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function runNodeScript(repoRoot, scriptFile, forwardArgs) {
|
|
47
|
+
const scriptPath = resolveScript(repoRoot, scriptFile);
|
|
48
|
+
runProcess(process.execPath, [scriptPath, ...forwardArgs], repoRoot);
|
|
49
|
+
}
|
|
50
|
+
function runBashScript(repoRoot, scriptFile, forwardArgs) {
|
|
51
|
+
const scriptPath = resolveScript(repoRoot, scriptFile);
|
|
52
|
+
runProcess("bash", [scriptPath, ...forwardArgs], repoRoot);
|
|
53
|
+
}
|
|
54
|
+
function ensureRootFlag(repoRoot, rest) {
|
|
55
|
+
if (rest.includes("--root"))
|
|
56
|
+
return rest;
|
|
57
|
+
return ["--root", repoRoot, ...rest];
|
|
58
|
+
}
|
|
3
59
|
export function parseKnowledgeArgs(args) {
|
|
4
60
|
const [command, ...rest] = args;
|
|
5
|
-
if (command
|
|
6
|
-
|
|
61
|
+
if (!command ||
|
|
62
|
+
!KNOWLEDGE_CLI_SUBCOMMANDS.includes(command)) {
|
|
63
|
+
throw new Error(USAGE);
|
|
7
64
|
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
json
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
markdown
|
|
19
|
-
|
|
65
|
+
if (command === "curate") {
|
|
66
|
+
let outputPath;
|
|
67
|
+
let json = false;
|
|
68
|
+
let markdown = false;
|
|
69
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
70
|
+
const arg = rest[i];
|
|
71
|
+
if (arg === "--json") {
|
|
72
|
+
json = true;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (arg === "--markdown") {
|
|
76
|
+
markdown = true;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (arg === "--output") {
|
|
80
|
+
outputPath = rest[++i];
|
|
81
|
+
if (!outputPath) {
|
|
82
|
+
throw new Error("knowledge curate --output requires a path");
|
|
83
|
+
}
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (arg.startsWith("--output=")) {
|
|
87
|
+
outputPath = arg.slice("--output=".length);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
throw new Error(`unknown knowledge curate argument: ${arg}`);
|
|
20
91
|
}
|
|
21
|
-
if (
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
throw new Error("knowledge curate --output requires a path");
|
|
25
|
-
continue;
|
|
26
|
-
}
|
|
27
|
-
if (arg.startsWith("--output=")) {
|
|
28
|
-
outputPath = arg.slice("--output=".length);
|
|
29
|
-
continue;
|
|
30
|
-
}
|
|
31
|
-
throw new Error(`unknown knowledge curate argument: ${arg}`);
|
|
92
|
+
if (!json && !markdown)
|
|
93
|
+
json = true;
|
|
94
|
+
return { command: "curate", outputPath, json, markdown };
|
|
32
95
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
96
|
+
return {
|
|
97
|
+
command: command,
|
|
98
|
+
forward: rest,
|
|
99
|
+
};
|
|
36
100
|
}
|
|
37
|
-
|
|
38
|
-
const parsed = parseKnowledgeArgs(args);
|
|
101
|
+
async function runCurate(repoRoot, parsed) {
|
|
39
102
|
const result = await curateKnowledgePatterns({
|
|
40
103
|
repoRoot,
|
|
41
104
|
outputPath: parsed.outputPath,
|
|
@@ -50,7 +113,9 @@ export async function runKnowledge(repoRoot, args) {
|
|
|
50
113
|
console.log(JSON.stringify({
|
|
51
114
|
ok: result.ok,
|
|
52
115
|
patternsPath: path.relative(repoRoot, result.patternsPath),
|
|
53
|
-
outputPath: result.outputPath
|
|
116
|
+
outputPath: result.outputPath
|
|
117
|
+
? path.relative(repoRoot, result.outputPath)
|
|
118
|
+
: undefined,
|
|
54
119
|
patternCount: result.patternCount,
|
|
55
120
|
safetyFindings: result.safetyFindings,
|
|
56
121
|
message: result.message,
|
|
@@ -62,3 +127,36 @@ export async function runKnowledge(repoRoot, args) {
|
|
|
62
127
|
process.stdout.write(result.proposalMarkdown);
|
|
63
128
|
}
|
|
64
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* knowledge CLI:
|
|
132
|
+
* - curate: repair/learned guidance proposals (existing)
|
|
133
|
+
* - query / graph-*: thin wrappers over scripts/kb-*.{mjs,sh} for graph KB ops
|
|
134
|
+
*/
|
|
135
|
+
export async function runKnowledge(repoRoot, args) {
|
|
136
|
+
const parsed = parseKnowledgeArgs(args);
|
|
137
|
+
if (parsed.command === "curate") {
|
|
138
|
+
await runCurate(repoRoot, parsed);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const forward = ensureRootFlag(repoRoot, parsed.forward);
|
|
142
|
+
switch (parsed.command) {
|
|
143
|
+
case "query":
|
|
144
|
+
runNodeScript(repoRoot, "kb-query.mjs", forward);
|
|
145
|
+
return;
|
|
146
|
+
case "graph-init":
|
|
147
|
+
// B1 skeleton is a bash script (no LLM)
|
|
148
|
+
runBashScript(repoRoot, "kb-bootstrap-init-skeleton.sh", forward);
|
|
149
|
+
return;
|
|
150
|
+
case "graph-materialize":
|
|
151
|
+
runNodeScript(repoRoot, "kb-graph-materialize.mjs", forward);
|
|
152
|
+
return;
|
|
153
|
+
case "graph-promote":
|
|
154
|
+
runNodeScript(repoRoot, "kb-graph-promote.mjs", forward);
|
|
155
|
+
return;
|
|
156
|
+
case "graph-incremental-prepare":
|
|
157
|
+
runNodeScript(repoRoot, "kb-graph-incremental-prepare.mjs", forward);
|
|
158
|
+
return;
|
|
159
|
+
default:
|
|
160
|
+
throw new Error(USAGE);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -43,6 +43,9 @@ export const workflowPolicyDagTemplateSchema = z.enum([
|
|
|
43
43
|
"review-gated-dag",
|
|
44
44
|
"supervised-implementation",
|
|
45
45
|
"frontend-implementation",
|
|
46
|
+
"backend-test-dag",
|
|
47
|
+
"knowledge-sync-dag",
|
|
48
|
+
"knowledge-graph-bootstrap-dag",
|
|
46
49
|
]);
|
|
47
50
|
export const workflowPolicyOutputLanguageSchema = z.enum(["zh-CN", "en"]);
|
|
48
51
|
export const workflowPolicySchema = z
|
|
@@ -12,6 +12,8 @@ export const taskKindSchema = z.enum([
|
|
|
12
12
|
"feature-study",
|
|
13
13
|
"frontend-implementation",
|
|
14
14
|
"backend-test",
|
|
15
|
+
"knowledge-sync",
|
|
16
|
+
"knowledge-graph-bootstrap",
|
|
15
17
|
]);
|
|
16
18
|
export const referenceRepoConfigSchema = z.object({
|
|
17
19
|
name: z.string().min(1),
|
|
@@ -62,8 +64,10 @@ const taskConfigObjectSchema = z.object({
|
|
|
62
64
|
taskId: z.string(),
|
|
63
65
|
title: z.string(),
|
|
64
66
|
sourceFiles: z.array(z.string()),
|
|
65
|
-
/** standard: 本仓库需求实现;feature-study: 参考外部代码特性并在目标仓库落地;frontend-implementation:
|
|
67
|
+
/** standard: 本仓库需求实现;feature-study: 参考外部代码特性并在目标仓库落地;frontend-implementation: 前端实现 DAG;backend-test: 后端测试 DAG;knowledge-sync: 最终验证后回写测试知识库 DAG;knowledge-graph-bootstrap: AI 辅助业务知识图谱初始化 */
|
|
66
68
|
taskKind: taskKindSchema.optional().default("standard"),
|
|
69
|
+
/** Feature 目录 id(如 F-2026-004)。knowledge-sync 必填(也可从 hardConstraints/需求正文/taskId 解析);用于收窄 writeSet */
|
|
70
|
+
featureId: z.string().min(1).optional(),
|
|
67
71
|
referenceRepos: z.array(referenceRepoConfigSchema).optional().default([]),
|
|
68
72
|
referenceDocs: z.array(referenceDocConfigSchema).optional().default([]),
|
|
69
73
|
referenceMaxFilesPerRepo: z.number().int().positive().optional(),
|
package/dist/worker/cli.js
CHANGED
|
@@ -14,10 +14,15 @@ import { createCompositeProgressReporter } from "./observability/progress-compos
|
|
|
14
14
|
import { createRoutedWorkerEventStore } from "./observability/event-store.js";
|
|
15
15
|
import { buildGlobalSnapshot } from "./observability/read-model.js";
|
|
16
16
|
import { createObserveServer } from "./observe/server.js";
|
|
17
|
-
import {
|
|
17
|
+
import { diagnoseTaskPoolStates, formatDoctorHuman, loadOperatorMapping } from "./pool/doctor.js";
|
|
18
|
+
import { migrateTaskPoolStates } from "./pool/migrate-state.js";
|
|
19
|
+
import { getTaskPoolRoot, prepareTaskPoolRetry, readFeatureTaskPoolStates } from "./pool/run-store.js";
|
|
20
|
+
import { TaskPoolStateError } from "./pool/types.js";
|
|
18
21
|
import { taskSpecSchema } from "./task-spec/schema.js";
|
|
19
22
|
import { validateTaskSpec } from "./task-spec/validate.js";
|
|
20
23
|
import { validateFeatureTaskGraph } from "./task-graph/validate.js";
|
|
24
|
+
import { taskGraphSpecSchema } from "./task-graph/task-graph-schema.js";
|
|
25
|
+
import { planReadyTasks } from "./task-graph/ready-planner.js";
|
|
21
26
|
import { renderFeatureReview, reviewFeature } from "./feature/review.js";
|
|
22
27
|
import { renderFeatureRun, runFeature } from "./feature/run.js";
|
|
23
28
|
import { draftFollowUpDecision } from "./follow-up/factory.js";
|
|
@@ -40,6 +45,58 @@ export function buildAgentWorkerProgram() {
|
|
|
40
45
|
const report = program.command("report").description("Task Pool reporting utilities");
|
|
41
46
|
const observe = program.command("observe").description("Observe UI server and snapshot utilities");
|
|
42
47
|
const feature = program.command("feature").description("Feature-level review and delivery workflow");
|
|
48
|
+
const pool = program.command("pool").description("Task Pool state doctor and migration utilities");
|
|
49
|
+
pool
|
|
50
|
+
.command("doctor")
|
|
51
|
+
.requiredOption("--repo <repo-root>", "Target repo root")
|
|
52
|
+
.option("--json", "Emit stable JSON instead of the human summary")
|
|
53
|
+
.option("--mapping <file>", "Optional operator mapping JSON (taskId -> featureId)")
|
|
54
|
+
.description("Inventory legacy and v2 Task Pool states (read-only)")
|
|
55
|
+
.action(async (options) => {
|
|
56
|
+
const repoRoot = path.resolve(options.repo);
|
|
57
|
+
const mapping = await loadOperatorMapping(options.mapping);
|
|
58
|
+
const report = await diagnoseTaskPoolStates(repoRoot, { mapping });
|
|
59
|
+
process.stdout.write(options.json ? `${JSON.stringify(report, null, 2)}\n` : formatDoctorHuman(report));
|
|
60
|
+
});
|
|
61
|
+
pool
|
|
62
|
+
.command("migrate-state")
|
|
63
|
+
.requiredOption("--repo <repo-root>", "Target repo root")
|
|
64
|
+
.option("--dry-run", "Plan migration without writes (default when --apply is omitted)")
|
|
65
|
+
.option("--apply", "Apply migration (requires --owner and --reason)")
|
|
66
|
+
.option("--owner <owner>", "Migration owner for apply")
|
|
67
|
+
.option("--reason <reason>", "Why migration is being applied")
|
|
68
|
+
.option("--mapping <file>", "Optional operator mapping JSON (taskId -> featureId)")
|
|
69
|
+
.option("--json", "Emit stable JSON")
|
|
70
|
+
.description("Migrate legacy flat Task Pool states to feature-scoped v2 (default dry-run)")
|
|
71
|
+
.action(async (options) => {
|
|
72
|
+
try {
|
|
73
|
+
const result = await migrateTaskPoolStates({
|
|
74
|
+
repoRoot: path.resolve(options.repo),
|
|
75
|
+
apply: options.apply === true,
|
|
76
|
+
...(options.owner ? { owner: options.owner } : {}),
|
|
77
|
+
...(options.reason ? { reason: options.reason } : {}),
|
|
78
|
+
...(options.mapping ? { mappingPath: options.mapping } : {}),
|
|
79
|
+
});
|
|
80
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
81
|
+
if (result.status === "failed")
|
|
82
|
+
process.exitCode = 1;
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
86
|
+
const code = error instanceof TaskPoolStateError ? error.code : "task-pool-migrate-failed";
|
|
87
|
+
if (options.json) {
|
|
88
|
+
process.stdout.write(`${JSON.stringify({
|
|
89
|
+
schemaVersion: 1,
|
|
90
|
+
status: "failed",
|
|
91
|
+
error: { code, message: detail },
|
|
92
|
+
}, null, 2)}\n`);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
process.stderr.write(`${detail}\n`);
|
|
96
|
+
}
|
|
97
|
+
process.exitCode = 1;
|
|
98
|
+
}
|
|
99
|
+
});
|
|
43
100
|
feature
|
|
44
101
|
.command("review")
|
|
45
102
|
.requiredOption("--feature-dir <dir>", "Feature directory containing acceptance.yaml and tasks/")
|
|
@@ -206,12 +263,14 @@ export function buildAgentWorkerProgram() {
|
|
|
206
263
|
.command("retry")
|
|
207
264
|
.argument("<task-id>", "Failed Task Pool task to requeue")
|
|
208
265
|
.requiredOption("--repo <repo-root>", "Target repo root")
|
|
266
|
+
.option("--feature-id <feature-id>", "Canonical Feature id for the Task Pool state (required when identity is ambiguous)")
|
|
209
267
|
.option("--reason <reason>", "Why the failure is safe to retry")
|
|
210
268
|
.description("Explicitly requeue a failed task; the next run-ready uses a new workerRunId")
|
|
211
269
|
.action(async (taskId, options) => {
|
|
212
270
|
const result = await prepareTaskPoolRetry({
|
|
213
271
|
repoRoot: path.resolve(options.repo),
|
|
214
272
|
taskId,
|
|
273
|
+
...(options.featureId ? { featureId: options.featureId } : {}),
|
|
215
274
|
...(options.reason ? { reason: options.reason } : {}),
|
|
216
275
|
});
|
|
217
276
|
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
@@ -268,6 +327,42 @@ export function buildAgentWorkerProgram() {
|
|
|
268
327
|
handleFollowUpCliError(error, options.json ?? false, "approve-followup");
|
|
269
328
|
}
|
|
270
329
|
});
|
|
330
|
+
batch
|
|
331
|
+
.command("plan-ready")
|
|
332
|
+
.requiredOption("--feature-dir <dir>", "Feature directory containing tasks/task-graph.yaml")
|
|
333
|
+
.requiredOption("--repo <repo-root>", "Target repo root")
|
|
334
|
+
.option("--limit <count>", "Maximum ready tasks to select")
|
|
335
|
+
.option("--json", "Emit stable JSON (default human summary)")
|
|
336
|
+
.description("Preview the read-only priority-aware ready plan without writing state or artifacts")
|
|
337
|
+
.action(async (options) => {
|
|
338
|
+
// Strict zero-write preview: load graph + specs + feature-scoped states only.
|
|
339
|
+
const featureDir = path.resolve(options.featureDir);
|
|
340
|
+
const repoRoot = path.resolve(options.repo);
|
|
341
|
+
const graph = taskGraphSpecSchema.parse(YAML.parse(await readFile(path.join(featureDir, "tasks", "task-graph.yaml"), "utf-8")));
|
|
342
|
+
const taskSpecs = new Map();
|
|
343
|
+
for (const node of graph.nodes) {
|
|
344
|
+
const taskFile = node.task ?? `${node.id}.yaml`;
|
|
345
|
+
taskSpecs.set(node.id, taskSpecSchema.parse(YAML.parse(await readFile(path.join(featureDir, "tasks", taskFile), "utf-8"))));
|
|
346
|
+
}
|
|
347
|
+
const selectionLimit = options.limit
|
|
348
|
+
? Number.parseInt(options.limit, 10)
|
|
349
|
+
: graph.nodes.length;
|
|
350
|
+
if (!Number.isInteger(selectionLimit) || selectionLimit < 1) {
|
|
351
|
+
process.stderr.write("selectionLimit must be a positive integer\n");
|
|
352
|
+
process.exitCode = 1;
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
const plan = planReadyTasks({
|
|
356
|
+
featureId: graph.feature_id,
|
|
357
|
+
graph,
|
|
358
|
+
taskSpecs,
|
|
359
|
+
states: await readFeatureTaskPoolStates(repoRoot, graph.feature_id),
|
|
360
|
+
selectionLimit,
|
|
361
|
+
});
|
|
362
|
+
process.stdout.write(options.json
|
|
363
|
+
? `${JSON.stringify(plan, null, 2)}\n`
|
|
364
|
+
: `Selected: ${plan.selected.map((task) => task.taskId).join(", ") || "none"}\nDeferred: ${plan.deferred.map((task) => task.taskId).join(", ") || "none"}\nBlocked: ${plan.blocked.length}\n`);
|
|
365
|
+
});
|
|
271
366
|
batch
|
|
272
367
|
.command("run-ready")
|
|
273
368
|
.requiredOption("--feature-dir <dir>", "Feature directory containing tasks/task-graph.yaml")
|
|
@@ -6,7 +6,7 @@ import { promisify } from "node:util";
|
|
|
6
6
|
import YAML from "yaml";
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
import { controllerIdentitiesMatch, LOOP_AGENT_PACKAGE_NAME, } from "../loop-agent/loop-agent-client.js";
|
|
9
|
-
import { getRunsJsonlPath, getTaskPoolRoot,
|
|
9
|
+
import { getRunsJsonlPath, getTaskPoolRoot, readFeatureTaskPoolStates, readJsonlFile } from "../pool/run-store.js";
|
|
10
10
|
import { acceptanceSpecSchema } from "../task-graph/acceptance-schema.js";
|
|
11
11
|
import { taskGraphSpecSchema } from "../task-graph/task-graph-schema.js";
|
|
12
12
|
import { validateFeatureTaskGraph } from "../task-graph/validate.js";
|
|
@@ -104,7 +104,7 @@ export async function prepareFeatureDelivery(input) {
|
|
|
104
104
|
}
|
|
105
105
|
const acceptance = acceptanceSpecSchema.parse(YAML.parse(await readFile(path.join(featureDir, "acceptance.yaml"), "utf-8")));
|
|
106
106
|
const graph = taskGraphSpecSchema.parse(YAML.parse(await readFile(path.join(featureDir, "tasks", "task-graph.yaml"), "utf-8")));
|
|
107
|
-
const states = await
|
|
107
|
+
const states = await readFeatureTaskPoolStates(repoRoot, featureId);
|
|
108
108
|
const runs = await readJsonlFile(getRunsJsonlPath(repoRoot));
|
|
109
109
|
const taskSpecs = new Map();
|
|
110
110
|
for (const node of graph.nodes)
|
|
@@ -208,7 +208,7 @@ export async function validateDeliveryPackage(input) {
|
|
|
208
208
|
blockers.push(`Git worktree is not clean for Closeout: ${closeoutStatus}`);
|
|
209
209
|
const graph = taskGraphSpecSchema.parse(YAML.parse(await readFile(path.join(featureDir, "tasks", "task-graph.yaml"), "utf-8")));
|
|
210
210
|
const acceptance = acceptanceSpecSchema.parse(YAML.parse(await readFile(path.join(featureDir, "acceptance.yaml"), "utf-8")));
|
|
211
|
-
const states = await
|
|
211
|
+
const states = await readFeatureTaskPoolStates(repoRoot, featureId);
|
|
212
212
|
const specs = new Map();
|
|
213
213
|
for (const node of graph.nodes)
|
|
214
214
|
specs.set(node.id, taskSpecSchema.parse(YAML.parse(await readFile(path.join(featureDir, "tasks", node.task), "utf-8"))));
|
|
@@ -46,16 +46,47 @@ async function auditTaskPoolFacts(repoRoot) {
|
|
|
46
46
|
}
|
|
47
47
|
const stateDir = path.join(getTaskPoolRoot(repoRoot), "states");
|
|
48
48
|
try {
|
|
49
|
-
for (const entry of await readdir(stateDir))
|
|
50
|
-
if (entry.endsWith(".json"))
|
|
49
|
+
for (const entry of await readdir(stateDir, { withFileTypes: true })) {
|
|
50
|
+
if (entry.isFile() && entry.name.endsWith(".json")) {
|
|
51
51
|
try {
|
|
52
|
-
const parsed = taskPoolStateFactSchema.safeParse(JSON.parse(await readFile(path.join(stateDir, entry), "utf-8")));
|
|
53
|
-
if (!parsed.success || parsed.data.taskId !== entry.slice(0, -5))
|
|
54
|
-
warnings.push(`Task Pool state is semantically invalid: ${entry}`);
|
|
52
|
+
const parsed = taskPoolStateFactSchema.safeParse(JSON.parse(await readFile(path.join(stateDir, entry.name), "utf-8")));
|
|
53
|
+
if (!parsed.success || parsed.data.taskId !== entry.name.slice(0, -5)) {
|
|
54
|
+
warnings.push(`Task Pool state is semantically invalid: ${entry.name}`);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
warnings.push(`legacy Task Pool state requires migration: ${entry.name}`);
|
|
58
|
+
}
|
|
55
59
|
}
|
|
56
60
|
catch {
|
|
57
|
-
warnings.push(`Task Pool state is corrupt: ${entry}`);
|
|
61
|
+
warnings.push(`Task Pool state is corrupt: ${entry.name}`);
|
|
58
62
|
}
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (!entry.isDirectory())
|
|
66
|
+
continue;
|
|
67
|
+
const featureId = entry.name;
|
|
68
|
+
try {
|
|
69
|
+
for (const stateName of await readdir(path.join(stateDir, featureId))) {
|
|
70
|
+
if (!stateName.endsWith(".json"))
|
|
71
|
+
continue;
|
|
72
|
+
const relative = `${featureId}/${stateName}`;
|
|
73
|
+
try {
|
|
74
|
+
const parsed = taskPoolStateFactSchema.safeParse(JSON.parse(await readFile(path.join(stateDir, featureId, stateName), "utf-8")));
|
|
75
|
+
const expectedTaskId = stateName.slice(0, -5);
|
|
76
|
+
if (!parsed.success || parsed.data.taskId !== expectedTaskId || parsed.data.featureId !== featureId) {
|
|
77
|
+
warnings.push(`Task Pool state is semantically invalid: ${relative}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
warnings.push(`Task Pool state is corrupt: ${relative}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (!isNotFound(error))
|
|
87
|
+
warnings.push(`Task Pool states for ${featureId} are unreadable: ${message(error)}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
59
90
|
}
|
|
60
91
|
catch (error) {
|
|
61
92
|
if (!isNotFound(error))
|
|
@@ -35,7 +35,7 @@ export function projectNextAction(input, status) {
|
|
|
35
35
|
return {
|
|
36
36
|
kind: "retry_task",
|
|
37
37
|
label: `重试环境失败任务 ${failed.taskId}`,
|
|
38
|
-
command: `agent-worker task retry ${quote(failed.taskId)} --repo ${quote(input.repoRoot)}`,
|
|
38
|
+
command: `agent-worker task retry ${quote(failed.taskId)} --feature-id ${quote(input.featureId)} --repo ${quote(input.repoRoot)}`,
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
41
|
return {
|
|
@@ -51,9 +51,17 @@ export function projectNextAction(input, status) {
|
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
53
|
if (status === "ready") {
|
|
54
|
+
const selected = input.planning?.selected[0];
|
|
55
|
+
const deferredCount = input.planning?.deferred.length ?? 0;
|
|
56
|
+
const blockedCount = input.planning?.blocked.length ?? 0;
|
|
57
|
+
const reasonSuffix = selected && (deferredCount > 0 || blockedCount > 0)
|
|
58
|
+
? `;deferred ${deferredCount} / blocked ${blockedCount}`
|
|
59
|
+
: "";
|
|
54
60
|
return {
|
|
55
61
|
kind: "run_feature",
|
|
56
|
-
label:
|
|
62
|
+
label: selected
|
|
63
|
+
? `执行最高优先级 Ready task ${selected.taskId} (${selected.priority})${reasonSuffix}`
|
|
64
|
+
: "执行当前 Ready tasks",
|
|
57
65
|
command: `agent-worker batch run-ready ${featureArgs}`,
|
|
58
66
|
};
|
|
59
67
|
}
|