@tea-agent/loop-agent 0.8.0 → 0.10.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/AGENTS.md +2 -0
- package/CHANGELOG.md +51 -1
- package/README.md +20 -0
- package/dist/application/dag/args.js +9 -2
- package/dist/cli/command-definitions.js +7 -0
- package/dist/cli/program.js +6 -1
- package/dist/commands/dag-reconcile-run.js +118 -0
- package/dist/commands/init.js +12 -3
- package/dist/executors/shell-executor.js +74 -8
- package/dist/governance/manifest-types.js +4 -0
- package/dist/shared/reference-context.js +48 -22
- package/dist/task/config-types.js +1 -1
- package/dist/task/runtime.js +1 -1
- package/dist/worker/cli.js +216 -0
- package/dist/worker/closeout/apply.js +73 -0
- package/dist/worker/closeout/preview.js +30 -0
- package/dist/worker/delivery/final-verification.js +158 -0
- package/dist/worker/delivery/git-transaction.js +354 -0
- package/dist/worker/delivery/package.js +449 -0
- package/dist/worker/feature/decision-loader.js +68 -0
- package/dist/worker/feature/discover.js +14 -0
- package/dist/worker/feature/next-action.js +74 -0
- package/dist/worker/feature/reducer.js +133 -0
- package/dist/worker/feature/review.js +502 -0
- package/dist/worker/feature/run.js +313 -0
- package/dist/worker/feature/types.js +1 -0
- package/dist/worker/follow-up/approve.js +270 -0
- package/dist/worker/follow-up/factory.js +234 -0
- package/dist/worker/follow-up/paths.js +25 -0
- package/dist/worker/follow-up/policy.js +26 -0
- package/dist/worker/follow-up/schema.js +93 -0
- package/dist/worker/follow-up/store.js +96 -0
- package/dist/worker/loop-agent/loop-agent-client.js +51 -10
- package/dist/worker/metrics/projector.js +139 -0
- package/dist/worker/observability/read-model.js +256 -15
- package/dist/worker/observe/paths.js +17 -5
- package/dist/worker/observe/routes.js +78 -20
- package/dist/worker/observe/server.js +8 -6
- package/dist/worker/observe/static/app.js +1045 -177
- package/dist/worker/observe/static/index.html +70 -43
- package/dist/worker/observe/static/styles.css +553 -610
- package/dist/worker/pool/run-store.js +14 -2
- package/dist/worker/pool/validation.js +59 -0
- package/dist/worker/report/morning-report.js +41 -6
- package/dist/worker/run-task/run-task.js +1 -1
- package/dist/worker/runner/run-ready.js +19 -5
- package/dist/workflows/dag/init-hybrid.js +3 -1
- package/dist/workflows/dag/lifecycle.js +146 -0
- package/dist/workflows/dag/node-execution.js +3 -0
- package/dist/workflows/dag/prompt.js +16 -0
- package/dist/workflows/dag/report.js +2 -0
- package/dist/workflows/dag/runner.js +133 -104
- package/dist/workflows/dag/types.js +3 -0
- package/docs/README.md +21 -0
- package/docs/agent-dag-recovery-playbook.md +1 -1
- package/docs/architecture/runtime-boundaries.md +3 -2
- package/docs/design/README.md +13 -7
- package/docs/exec-plans/active/README.md +2 -2
- package/docs/exec-plans/completed/README.md +15 -0
- package/docs/loop-agent-harness.md +45 -2
- package/docs/progress/README.md +2 -0
- package/docs/reports/README.md +13 -0
- package/docs/templates/agent-dag-report.schema.json +5 -3
- package/docs/templates/harness.schema.json +7 -2
- package/docs/templates/init-evolution-review.md +4 -2
- package/docs/verification-matrix.md +7 -0
- package/harness.json +4 -3
- package/package.json +4 -2
- package/scripts/check-product-line-docs.sh +7 -3
- package/scripts/check-task-pool-root.sh +1 -1
- package/skills/init-capability-evolution/SKILL.md +1 -0
- package/skills/loop-agent/references/command-reference.md +21 -0
- package/skills/loop-agent/references/hybrid-dag.md +4 -3
- package/skills/loop-agent/references/verification-and-failure-handling.md +8 -3
package/AGENTS.md
CHANGED
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
- Shell 搜索优先 `rg`,按名找文件优先 `fd`;脚本确为 Bash 脚本时使用 Git Bash 或已配置的兼容 Bash,不要求 Windows 环境适配 POSIX 路径。
|
|
25
25
|
- 用 loop-agent 迭代本仓库时,控制器必须来自已发布的 npm 安装包;首次安装或有意升级可用 `@tea-agent/loop-agent@latest`,但一次自举任务启动后不要中途升级控制器,并记录 `npm list -g @tea-agent/loop-agent --depth=0` 显示的实际版本。不要用当前工作区的 `npm link` 或 `npm run dev` 控制可能改动 CLI、DAG runtime、executor、package metadata 或 build output 的任务。
|
|
26
26
|
- 反复出现的约束要固化为文档、脚本、检查项、测试或模板。
|
|
27
|
+
- 除非用户明确要求,不要自行引入外部 SDD、spec-first、brainstorming 等方法论的强制设计文档、审批门或专用目录;本仓库的工作流程以本文件和 `docs/` 中的治理规则为准。
|
|
28
|
+
- 对非微小的实现或修复,`exec-plan` 只负责记录 Contract、进度和验证证据,不能替代 Agent DAG。除非用户明确要求 one-shot,或在计划中记录了适用的 escape hatch 与理由,否则在改动实现文件前必须完成 `new-task`、结构化 `allowedPaths` / `forbiddenPaths`、`dag run-task` 和 `dag validate`;随后按已校验的 DAG 执行并审查 writer `writeSet`。
|
|
27
29
|
- 禁止占位实现;除非 contract 明确约定为脚手架且标出后续闭环。
|
|
28
30
|
|
|
29
31
|
## 开始顺序
|
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,57 @@
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
### 新增
|
|
8
|
+
|
|
9
|
+
- 严格初始化演化检查现在接受 `Head` 为当前 `HEAD` 可解析祖先提交的报告:只要 `Base` 精确匹配 `--base`、`Head` 是 `HEAD` 或其祖先,且 `reportHead..HEAD` 区间没有新的 `model-review` 高影响路径即可通过。仅 `advisory` 或 `surface-check` 的后续变化不会让已完成的高影响审查失效;`working tree` 等不可解析 Head 与无关历史报告仍被拒绝。
|
|
10
|
+
|
|
11
|
+
- 新增只读的 `agent-worker feature review`。用户无需翻查原始日志,即可查看 Feature 当前状态、主要阻塞、required AC 覆盖、证据位置和下一条建议命令;`--json` 可供脚本稳定消费。
|
|
12
|
+
- 新增 `agent-worker feature run`。它会先校验 Feature 和目标仓库,再复用现有 Worker pipeline 推进一个 Ready Task,随后刷新晨报、Observe snapshot 和 Feature review;`--dry-run` 可在零写入下预览步骤与产物。
|
|
13
|
+
- 新增 ProductBug Follow-up 人工接续流程。`agent-worker task draft-followup` 生成可审查、可去重的修复草稿;`agent-worker feature approve-followup --dry-run` 可先检查变更计划,批准后再新增 FIX Task、重连下游依赖并进入 Ready,同时保留原失败证据。
|
|
14
|
+
- Follow-up 现已覆盖全部失败分类:可执行类生成严格 TaskSpec,Spec/Risk/Human 类生成行动卡,EnvFailure 只有连续两次失败后才建议环境检查任务;审批事务增加多阶段回滚验证,避免 Packet、Ready state、approval 或 index 半写。
|
|
15
|
+
- `agent-worker feature run --git-mode checkpoint` 新增显式授权的本地 Feature 分支事务。成功任务形成带 Feature/Task/AC/run 信息的 checkpoint commit;失败任务保存 patch 与 untracked 证据后恢复 clean。默认仍不 commit,也不会 push、stash、merge 或创建远程 PR。
|
|
16
|
+
- 新增 `agent-worker feature verify-final`、`feature delivery` 与 Feature Closeout。用户可在 clean Delivery HEAD 上生成 canonical QA/最终验证证据,再从 checkpoint、成功 run 和 required AC 形成带 hash 的 Delivery Package、Acceptance Coverage 与 PR 草稿;Closeout 默认预览,只有显式 `--apply --owner` 才原子写回并保留幂等、stale facts 与回滚保护。
|
|
17
|
+
- Morning report 与 Observe 首页新增共享的 Feature Decision Summary,优先显示状态、下一步、原因、证据和 required AC。新增 `agent-worker report metrics --month YYYY-MM`,同时输出带分母、样本量、时间窗口和缺失数据说明的 JSON/Markdown。
|
|
18
|
+
- Observe 总览新增仓库横幅,显示仓库名与绝对路径;KPI 按主要执行链路排列为“Worker 执行”在前、“DAG 运行”在后,DAG 组明确展示活跃运行、执行中/等待节点、暂停和需处理运行,“活跃 Task”更名为“活跃 Worker Task”,每张卡片新增可键盘聚焦的问号帮助,解释数据来源与统计口径。
|
|
19
|
+
- Observe 总览的 Feature 决策仅在存在“运行中”或“需处理”的 Feature 时显示;只有可执行、待验收、已交付或已关闭记录时不再占用总览空间。
|
|
20
|
+
|
|
21
|
+
### 修改
|
|
22
|
+
|
|
23
|
+
- 根目录 `design/` 已并入 `docs/design/`,视觉参考与产品线设计笔记现在统一从治理文档目录查找,相关索引和引用已同步更新。
|
|
24
|
+
- Agent DAG 的 Pi/Cursor 节点现在默认使用简体中文输出说明、分析、报告和文档;可通过 `workflowPolicy.dag.outputLanguage: "en"` 切换英文,代码、命令、路径、JSON 字段和固定协议 token 不会被翻译。
|
|
25
|
+
- Dashboard 将“运行活性”调整为更直观的“执行器状态”,并明确区分执行器正常、心跳中断、已经退出、其他主机运行以及旧记录没有心跳等情况。
|
|
26
|
+
- 修复 Dashboard 综合 DAG 状态使用中文文案后丢失徽章颜色的问题;完成、失败、暂停、中断和任务已另行完成现在恢复对应的语义颜色。
|
|
27
|
+
- Dashboard 节点耗时达到 60 秒后改为按“分钟 + 秒”显示,例如 `2min 5s`,长任务更容易快速阅读。
|
|
28
|
+
- Dashboard 的 DAG 节点模型列现在会显示 Pi/Cursor 节点实际路由到的模型;历史运行从 `run.json` 模型矩阵推导,新运行会直接把解析后的模型写入节点状态,Shell/Static 节点仍显示 `-`。
|
|
29
|
+
- Dashboard 现在优先展示由生命周期、原始状态和运行活性共同判定的 DAG 当前状态,并单独保留原始记录;新增 `dag reconcile-run`,可在确认执行器已停止后将历史失联 DAG 显式标记为“任务已另行完成”或“已放弃”,同时保存收口前证据。
|
|
30
|
+
- Observe DAG 详情页主信息区精简为单一“状态”,已结束 DAG 不再展示执行器心跳等无意义内部维度,状态一致或不允许恢复/收口时也不再显示“一致”或“否”;不一致、可恢复或可收口的警告与操作能力仍在主视图可见,原始状态、生命周期、活性、状态一致性与恢复资格折叠在“诊断信息”中保留。
|
|
31
|
+
- DAG 运行现在记录本机执行器 PID、主机和持续心跳;`dag status` / `dag doctor` 可区分活跃、节点长时间无活动、心跳停滞、执行器已退出和远端状态未知,Observe 也不会再把已知孤儿或 paused 运行计为活跃 DAG。
|
|
32
|
+
- 节点检查器支持拖动左边缘调整宽度,长 Markdown 表格和输出更容易查看;点击外部空白区域可平滑收起面板。
|
|
33
|
+
- 执行过程改为更清晰的事件轨道:工具调用、AI 回复和 Agent 生命周期更容易区分;没有时间记录时会按采集顺序展示,连续协议事件会自动合并,减少无关噪声。
|
|
34
|
+
- Observe 风险区拆为“需处理 DAG”与“Worker 风险”两个子块;DAG 按运行去重,不会因同一运行同时存在多个异常信号而重复计数,也不会让历史失败记录长期占用当前风险;Snapshot 投影失败不再静默显示为全零健康状态,而是返回安全错误摘要并在总览提示“指标暂不可用”,不含堆栈或敏感信息。
|
|
35
|
+
- 修复旧版(未写 runner 心跳元数据)的运行中 DAG 被 Observe 误判为不活跃的问题:`effectiveStatus=unknown` 只表示活性证据不足,不再覆盖 `lifecycle=active` 且原始 `status` 为 `running`/`pending` 的运行事实,也不再掩盖存在 RUNNING/PENDING 节点的运行。服务端健康计数(活跃运行、执行中节点、等待节点)与前端进行中 DAG 列表现在使用一致的判定语义;`paused`、`terminal`、`stale`、`orphaned` 等明确非活跃事实仍优先于原始 `running`。
|
|
36
|
+
- `npm test` 会先运行可并行的纯测试,再串行运行会启动 CLI 或子进程的集成测试。纯测试会根据机器可用并行度自动选择 1 至 4 个 Worker,低配和高配环境都能在稳定性与速度之间取得更合适的平衡。
|
|
37
|
+
|
|
38
|
+
## [0.9.0] - 2026-07-12
|
|
39
|
+
|
|
40
|
+
### 修复
|
|
41
|
+
|
|
42
|
+
- 修复 Observe 总览误把已结束、但仍保留未执行节点的 DAG 显示为“活跃 DAG”的问题。
|
|
43
|
+
- DAG 详情页中的节点输出、错误输出、依赖图和执行过程会在轮询更新时保留当前查看位置,不再把页面拉回默认位置。
|
|
44
|
+
- DAG 和 Worker 运行详情完成首次加载后,会在运行结束时停止自动刷新;页面切到后台时会降低刷新频率,减少不必要的本机资源占用。
|
|
45
|
+
- 新建 Task 默认改为串行执行验证,避免治理检查、类型检查和测试同时抢占本机资源;需要并行时仍可显式配置。
|
|
46
|
+
- `run-dag` 和 `dag run-task --execute` 会拒绝无效的 `--max-concurrent`,避免异常参数导致同一批节点同时启动。
|
|
47
|
+
- Worker batch 的 SSE 事件流在没有新增内容时不再重复读取历史事件,并且会等上一轮完成后再继续轮询。
|
|
48
|
+
- Observe 看板在一个刷新周期内会复用同一份运行快照;同时打开总览、批次、Task 或 DAG 详情时,不会为每个请求重复扫描运行记录。
|
|
49
|
+
- DAG shell 命令和 Worker 调用遇到大量输出时,只在内存中保留最近一段内容并标明截断;完整 stdout/stderr 仍会写入运行产物,避免长日志拖慢本机。
|
|
50
|
+
- 建立 reference index 时会跳过依赖、构建产物、虚拟环境和版本控制目录,并在遍历规模过大时提前停止,减少误扫大型仓库。
|
|
51
|
+
|
|
52
|
+
### 修改
|
|
53
|
+
|
|
54
|
+
- Observe 看板改为暖白运行控制台:总览、批次、Task、DAG 和异常页面使用统一的信息层级;DAG 详情中的依赖图和节点表各自完整展示,选择节点后可从右侧检查器查看输出和执行过程。
|
|
55
|
+
- 统一整理运行看板的总览、批次详情、运行详情、DAG 详情和异常 Task 页面:先展示当前状态和风险,再呈现运行记录、输出与产物,桌面端查看信息更集中。
|
|
56
|
+
- 最近 DAG 运行改为按时间排列的状态时间线,标题过长时不会再挤压状态;DAG 详情仍可从每条记录直接进入。
|
|
57
|
+
- 优化节点表格和宽屏布局:节点 ID、模型、状态、耗时和备注更容易扫读;节点输出支持 Markdown 阅读,完成节点在依赖图中以绿色状态提示。
|
|
8
58
|
|
|
9
59
|
## [0.8.0] - 2026-07-11
|
|
10
60
|
|
package/README.md
CHANGED
|
@@ -92,9 +92,29 @@ loop-agent pi-prompt --cwd . --tools read,bash,edit,write,grep,find,ls "<包含
|
|
|
92
92
|
|
|
93
93
|
```bash
|
|
94
94
|
agent-worker task validate-feature <feature-dir>
|
|
95
|
+
agent-worker feature review --feature-dir <feature-dir> --repo <target-repo>
|
|
96
|
+
agent-worker feature run --feature-dir <feature-dir> --repo <target-repo> --dry-run [--git-mode checkpoint]
|
|
97
|
+
agent-worker feature verify-final --feature-dir <feature-dir> --repo <target-repo> --task-id <qa-execute-id>
|
|
98
|
+
agent-worker feature delivery --feature-dir <feature-dir> --repo <target-repo> --qa-evidence <path> --final-verification <path> [--dry-run]
|
|
99
|
+
agent-worker feature closeout --feature-dir <feature-dir> --repo <target-repo> [--apply --owner <owner>]
|
|
100
|
+
agent-worker report metrics --repo <target-repo> --month <YYYY-MM>
|
|
101
|
+
agent-worker task draft-followup <task-id> --worker-run-id <id> --feature-dir <feature-dir> --repo <target-repo>
|
|
102
|
+
agent-worker feature approve-followup --feature-dir <feature-dir> --followup-id <id> --repo <target-repo> --owner <owner> --dry-run
|
|
95
103
|
bash scripts/worker-nightly.sh <feature-dir> <target-repo> <batch-run-id>
|
|
96
104
|
```
|
|
97
105
|
|
|
106
|
+
`feature review` 是只读的 Feature 级入口。它从 Feature Packet 和现有 Task Pool 事实派生状态、required AC 覆盖、阻塞、证据和唯一主行动;默认输出简洁人类摘要,`--json` 输出稳定的 schemaVersion 1 读模型。它不会写入 Feature Packet 或 Task Pool。
|
|
107
|
+
|
|
108
|
+
`feature verify-final` 在 clean Delivery HEAD 上复用已完成的 `qa-execute` TaskSpec,执行独立、不会 promote/closeout 或移动 HEAD 的最终验证,并原子投影 canonical QA aggregate 与 HEAD-bound final-verification evidence。`feature delivery` 复用 checkpoint transaction,校验 branch/HEAD/clean、commit trailers、changed files、成功 run、QA、最终验证和 required AC 后,在 `.harness/task-pool/` 原子生成 Delivery manifest、Acceptance Coverage 与 `PR.md`;`--dry-run` 零写入。`feature closeout` 默认只预览 gates;显式 `--apply --owner <owner>` 才会在前后校验与整体回滚保护下写入 Feature Closeout,重复相同 facts 会幂等复用。
|
|
109
|
+
|
|
110
|
+
Morning report 和 Observe snapshot/UI 复用同一 Feature reducer,优先展示 Status、Next Action、Why、Evidence。`report metrics` 同时生成 monthly JSON/Markdown,所有比例都包含分子、分母、样本量、UTC 窗口和缺失数据说明。
|
|
111
|
+
|
|
112
|
+
`feature run` 把 validation、目标仓库 preflight、Ready Queue、现有 `run-ready`、晨报、Observe snapshot 和最终 review 串成一次 Feature 意图。默认不写 Git 且每次最多推进一个 Ready 写任务。显式 `--git-mode checkpoint` 才允许创建本地 `agent/<feature-id>` 分支:每个成功任务通过 write-boundary/sensitive-file audit 后提交 checkpoint;失败任务先保存 binary patch、changed/untracked inventory 与内容,再恢复到最近 checkpoint 并验证 clean。它从不授权 push、远程 PR、merge 或 stash。`--keep-failed-diff` 会保留失败 diff、返回 NeedsAction 并立即停止后续任务。
|
|
113
|
+
|
|
114
|
+
如果 Task 产生业务失败,`feature run` 仍会完成晨报、snapshot 和最终 review,但结果为 `needs-action`、进程退出非零,并保留 batch 与失败证据;无 Ready 时正常退出,并通过 `noReadyReason` 区分已关闭、可交付、待 QA、需人工处理、依赖阻塞或空 Feature。
|
|
115
|
+
|
|
116
|
+
失败可通过显式 Follow-up 决策闭环接续:ProductBug、TestBug、FlakyTest、DependencyFailure 会生成可批准 Task 草稿;EnvFailure 最近两次连续失败后才生成 `ENV-CHECK-*`,否则优先给出 retry;SpecUnclear、ContractMismatch、RiskyChange、NeedsHuman、Unknown 只生成人工行动卡,绝不进入 Ready。人工用 `feature approve-followup --dry-run` 复核 baseline、证据 hash、ID 冲突和事务计划,再带 `--owner` 批准。批准后新增 TaskSpec、重连下游依赖并进入 Ready;原失败 run/state/handoff 保持不变,重复 run 的新草稿会 supersede 旧 unresolved 草稿但保留历史。
|
|
117
|
+
|
|
98
118
|
nightly wrapper 按 feature 互斥,保留批次/超时退出码,并输出 morning report、Observe snapshot 和 controller 版本 artifacts。
|
|
99
119
|
|
|
100
120
|
自 0.8.0 起,`.harness/task-pool/` 是唯一受支持的 Task Pool runtime root,其中包含根级 `runs.jsonl`、`events.jsonl`、`states/`、`artifacts/`、`failure-handoffs/`、`observability/` 和 `reports/`。这是 hard cutover:旧路径不读取、不迁移、不合并、不重映射;升级意味着本地 Worker Task Pool 历史重置。
|
|
@@ -7,6 +7,13 @@ const LIFECYCLE_FILTERS = new Set([
|
|
|
7
7
|
"completed",
|
|
8
8
|
"all",
|
|
9
9
|
]);
|
|
10
|
+
function parseMaxConcurrent(value) {
|
|
11
|
+
const parsed = Number(value);
|
|
12
|
+
if (!Number.isSafeInteger(parsed) || parsed <= 0) {
|
|
13
|
+
throw new Error("max-concurrent must be a positive integer");
|
|
14
|
+
}
|
|
15
|
+
return parsed;
|
|
16
|
+
}
|
|
10
17
|
export function parseDagRunTaskArgs(args, defaultCwd) {
|
|
11
18
|
if (args.length === 0) {
|
|
12
19
|
throw new Error("usage: dag run-task <task-id> [--output <path>] [--profile auto|minimal|standard|reviewed|supervised] [--strict-models] [--no-cursor] [--execute] [--init-only] [--dry-run] [--cwd <dir>] [--max-concurrent N] [--run-id id] [--canvas-path <abs-path> | --canvas <name> [--canvases-dir <dir>]]");
|
|
@@ -68,7 +75,7 @@ export function parseDagRunTaskArgs(args, defaultCwd) {
|
|
|
68
75
|
continue;
|
|
69
76
|
}
|
|
70
77
|
if (arg === "--max-concurrent") {
|
|
71
|
-
maxConcurrent =
|
|
78
|
+
maxConcurrent = parseMaxConcurrent(args[++i]);
|
|
72
79
|
continue;
|
|
73
80
|
}
|
|
74
81
|
if (arg === "--run-id") {
|
|
@@ -276,7 +283,7 @@ export function parseRunDagArgs(args, defaultCwd) {
|
|
|
276
283
|
dryRun = true;
|
|
277
284
|
}
|
|
278
285
|
else if (arg === "--max-concurrent") {
|
|
279
|
-
maxConcurrent =
|
|
286
|
+
maxConcurrent = parseMaxConcurrent(args[++i]);
|
|
280
287
|
}
|
|
281
288
|
else if (arg === "--run-id") {
|
|
282
289
|
runId = args[++i];
|
|
@@ -29,6 +29,7 @@ import { runDagWorkflowPlan } from "../commands/dag-workflow-plan.js";
|
|
|
29
29
|
import { runDagApprove } from "../commands/dag-approve.js";
|
|
30
30
|
import { runDagReject } from "../commands/dag-reject.js";
|
|
31
31
|
import { runDagReconcileTasks } from "../commands/dag-reconcile-tasks.js";
|
|
32
|
+
import { runDagReconcileRun } from "../commands/dag-reconcile-run.js";
|
|
32
33
|
import { runDagResume } from "../commands/dag-resume.js";
|
|
33
34
|
import { runDagDecisionInspect, runDagDecisionValidate, } from "../workflows/dag/decision-envelope.js";
|
|
34
35
|
import { runDagDoctor, runDagStatus } from "../workflows/dag/lifecycle.js";
|
|
@@ -81,6 +82,7 @@ const DAG_SUBCOMMANDS = [
|
|
|
81
82
|
"doctor",
|
|
82
83
|
"report",
|
|
83
84
|
"closeout-draft",
|
|
85
|
+
"reconcile-run",
|
|
84
86
|
"reconcile-tasks",
|
|
85
87
|
"final-verification",
|
|
86
88
|
"decision",
|
|
@@ -485,6 +487,11 @@ export const COMMAND_DEFINITIONS = [
|
|
|
485
487
|
await runDagCloseoutDraft(repoRoot, closeoutArgs);
|
|
486
488
|
return;
|
|
487
489
|
}
|
|
490
|
+
if (subcommand === "reconcile-run") {
|
|
491
|
+
const reconcileRunArgs = rest.filter((arg) => Boolean(arg));
|
|
492
|
+
await runDagReconcileRun(repoRoot, reconcileRunArgs);
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
488
495
|
if (subcommand === "reconcile-tasks") {
|
|
489
496
|
const reconcileArgs = rest.filter((arg) => Boolean(arg));
|
|
490
497
|
await runDagReconcileTasks(repoRoot, reconcileArgs);
|
package/dist/cli/program.js
CHANGED
|
@@ -10,6 +10,7 @@ import { runDagApprove } from "../commands/dag-approve.js";
|
|
|
10
10
|
import { runDagFinalVerification } from "../commands/dag-final-verification.js";
|
|
11
11
|
import { runDagInitHybrid } from "../commands/dag-init-hybrid.js";
|
|
12
12
|
import { runDagReconcileTasks } from "../commands/dag-reconcile-tasks.js";
|
|
13
|
+
import { runDagReconcileRun } from "../commands/dag-reconcile-run.js";
|
|
13
14
|
import { runDagReject } from "../commands/dag-reject.js";
|
|
14
15
|
import { runDagCloseoutDraft, runDagReport } from "../commands/dag-report.js";
|
|
15
16
|
import { runDagResume } from "../commands/dag-resume.js";
|
|
@@ -300,6 +301,9 @@ async function runDagAction(repoRoot, subcommand, rest) {
|
|
|
300
301
|
case "closeout-draft":
|
|
301
302
|
await runDagCloseoutDraft(repoRoot, args);
|
|
302
303
|
return;
|
|
304
|
+
case "reconcile-run":
|
|
305
|
+
await runDagReconcileRun(repoRoot, args);
|
|
306
|
+
return;
|
|
303
307
|
case "reconcile-tasks":
|
|
304
308
|
await runDagReconcileTasks(repoRoot, args);
|
|
305
309
|
return;
|
|
@@ -319,7 +323,7 @@ async function runDagAction(repoRoot, subcommand, rest) {
|
|
|
319
323
|
throw new Error("usage: dag decision <inspect|validate> --run-id <id> [--node-id <node-id>]");
|
|
320
324
|
}
|
|
321
325
|
default:
|
|
322
|
-
throw new Error("usage: dag <init-hybrid|run-task|validate|workflow-plan|workflow-validate|workflow-compile|approve|reject|resume|status|doctor|report|closeout-draft|reconcile-tasks|final-verification|decision> ...");
|
|
326
|
+
throw new Error("usage: dag <init-hybrid|run-task|validate|workflow-plan|workflow-validate|workflow-compile|approve|reject|resume|status|doctor|report|closeout-draft|reconcile-run|reconcile-tasks|final-verification|decision> ...");
|
|
323
327
|
}
|
|
324
328
|
}
|
|
325
329
|
function compactArgs(args) {
|
|
@@ -450,6 +454,7 @@ function addDagCommand(program, defaultRepoRoot) {
|
|
|
450
454
|
"doctor",
|
|
451
455
|
"report",
|
|
452
456
|
"closeout-draft",
|
|
457
|
+
"reconcile-run",
|
|
453
458
|
"reconcile-tasks",
|
|
454
459
|
"final-verification",
|
|
455
460
|
], defaultRepoRoot);
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { writeJsonAtomic } from "../infrastructure/harness/atomic-write.js";
|
|
3
|
+
import { assessDagRunLiveness, assessDagRunRecoveryEligibility, assertDagRunTransferTargetAvailable, getDagRunDir, locateDagRun, readDagRunState, transferDagRunDir, writeDagRunState, } from "../workflows/dag/lifecycle.js";
|
|
4
|
+
export function parseDagReconcileRunArgs(args) {
|
|
5
|
+
let runId;
|
|
6
|
+
let action;
|
|
7
|
+
let reason;
|
|
8
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
9
|
+
const arg = args[index];
|
|
10
|
+
if (arg === "--run-id")
|
|
11
|
+
runId = args[++index];
|
|
12
|
+
else if (arg.startsWith("--run-id="))
|
|
13
|
+
runId = arg.slice("--run-id=".length);
|
|
14
|
+
else if (arg === "--action")
|
|
15
|
+
action = parseAction(args[++index]);
|
|
16
|
+
else if (arg.startsWith("--action="))
|
|
17
|
+
action = parseAction(arg.slice("--action=".length));
|
|
18
|
+
else if (arg === "--reason")
|
|
19
|
+
reason = args[++index];
|
|
20
|
+
else if (arg.startsWith("--reason="))
|
|
21
|
+
reason = arg.slice("--reason=".length);
|
|
22
|
+
else if (arg.startsWith("-"))
|
|
23
|
+
throw new Error(`unknown dag reconcile-run flag: ${arg}`);
|
|
24
|
+
else
|
|
25
|
+
throw new Error(`unexpected positional argument: ${arg}`);
|
|
26
|
+
}
|
|
27
|
+
if (!runId)
|
|
28
|
+
throw new Error("dag reconcile-run requires --run-id <id>");
|
|
29
|
+
if (action && !reason?.trim()) {
|
|
30
|
+
throw new Error("dag reconcile-run mutation requires --reason <text>");
|
|
31
|
+
}
|
|
32
|
+
return { runId, action, ...(reason?.trim() ? { reason: reason.trim() } : {}) };
|
|
33
|
+
}
|
|
34
|
+
function parseAction(value) {
|
|
35
|
+
if (value === "supersede" || value === "abandon")
|
|
36
|
+
return value;
|
|
37
|
+
throw new Error("dag reconcile-run --action must be supersede or abandon");
|
|
38
|
+
}
|
|
39
|
+
export async function runDagReconcileRun(repoRoot, rawArgs) {
|
|
40
|
+
const parsed = parseDagReconcileRunArgs(rawArgs);
|
|
41
|
+
const located = await locateDagRun(repoRoot, parsed.runId);
|
|
42
|
+
if (!located)
|
|
43
|
+
throw new Error(`dag run not found: ${parsed.runId}`);
|
|
44
|
+
const state = await readDagRunState(located.runDir);
|
|
45
|
+
const liveness = assessDagRunLiveness({ state });
|
|
46
|
+
const eligibility = assessDagRunRecoveryEligibility({
|
|
47
|
+
lifecycle: located.lifecycle,
|
|
48
|
+
state,
|
|
49
|
+
liveness: liveness.status,
|
|
50
|
+
});
|
|
51
|
+
if (!parsed.action) {
|
|
52
|
+
console.log(JSON.stringify({
|
|
53
|
+
action: "inspect",
|
|
54
|
+
runId: state.runId,
|
|
55
|
+
lifecycle: located.lifecycle,
|
|
56
|
+
status: state.status,
|
|
57
|
+
liveness: liveness.status,
|
|
58
|
+
eligibility,
|
|
59
|
+
nextRecommendedAction: eligibility.canReconcile
|
|
60
|
+
? "Re-run with --action supersede|abandon and --reason <text>."
|
|
61
|
+
: "Do not reconcile this run; inspect runner and lifecycle evidence first.",
|
|
62
|
+
}, null, 2));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (!eligibility.canReconcile || !eligibility.allowedActions.includes(parsed.action)) {
|
|
66
|
+
throw new Error(`dag run ${state.runId} is not eligible for reconciliation: ${eligibility.reasons.join(", ") || "unknown reason"}`);
|
|
67
|
+
}
|
|
68
|
+
const reconciledAt = new Date().toISOString();
|
|
69
|
+
const completedRunDir = getDagRunDir(repoRoot, "completed", state.runId);
|
|
70
|
+
if (located.lifecycle !== "completed") {
|
|
71
|
+
await assertDagRunTransferTargetAvailable(completedRunDir);
|
|
72
|
+
}
|
|
73
|
+
const artifactRelativePath = path.join(".harness", "dag-runs", "completed", state.runId, "reconciliation.json").replace(/\\/g, "/");
|
|
74
|
+
await writeJsonAtomic(path.join(located.runDir, "reconciliation.json"), {
|
|
75
|
+
schemaVersion: 1,
|
|
76
|
+
runId: state.runId,
|
|
77
|
+
action: parsed.action,
|
|
78
|
+
reason: parsed.reason,
|
|
79
|
+
reconciledAt,
|
|
80
|
+
previousLifecycle: located.lifecycle,
|
|
81
|
+
previousStatus: state.status,
|
|
82
|
+
liveness: liveness.status,
|
|
83
|
+
originalState: state,
|
|
84
|
+
});
|
|
85
|
+
const previousStatus = state.status;
|
|
86
|
+
for (const node of Object.values(state.nodes)) {
|
|
87
|
+
if (node.status !== "RUNNING")
|
|
88
|
+
continue;
|
|
89
|
+
node.status = "ERROR";
|
|
90
|
+
node.finishedAt = reconciledAt;
|
|
91
|
+
node.failureCategory = `operator-${parsed.action}`;
|
|
92
|
+
}
|
|
93
|
+
state.status = parsed.action === "supersede" ? "superseded" : "abandoned";
|
|
94
|
+
state.finishedAt = reconciledAt;
|
|
95
|
+
state.failureCategory = `operator-${parsed.action}`;
|
|
96
|
+
state.reconciliation = {
|
|
97
|
+
action: parsed.action,
|
|
98
|
+
reason: parsed.reason,
|
|
99
|
+
reconciledAt,
|
|
100
|
+
previousStatus,
|
|
101
|
+
previousLifecycle: located.lifecycle,
|
|
102
|
+
liveness: liveness.status,
|
|
103
|
+
artifactPath: artifactRelativePath,
|
|
104
|
+
};
|
|
105
|
+
await writeDagRunState(located.runDir, state);
|
|
106
|
+
const runDir = located.lifecycle === "completed"
|
|
107
|
+
? located.runDir
|
|
108
|
+
: await transferDagRunDir(located.runDir, completedRunDir);
|
|
109
|
+
console.log(JSON.stringify({
|
|
110
|
+
action: parsed.action,
|
|
111
|
+
runId: state.runId,
|
|
112
|
+
status: state.status,
|
|
113
|
+
lifecycle: "completed",
|
|
114
|
+
reason: parsed.reason,
|
|
115
|
+
reconciliationArtifactPath: artifactRelativePath,
|
|
116
|
+
runDir,
|
|
117
|
+
}, null, 2));
|
|
118
|
+
}
|
package/dist/commands/init.js
CHANGED
|
@@ -443,11 +443,20 @@ fi
|
|
|
443
443
|
feature_dirs=()
|
|
444
444
|
for root in features product/features dogfood/features; do
|
|
445
445
|
[[ -d "\${root}" ]] || continue
|
|
446
|
-
while IFS= read -r
|
|
447
|
-
|
|
448
|
-
|
|
446
|
+
while IFS= read -r feature_dir; do
|
|
447
|
+
if [[ ! -f "\${feature_dir}/acceptance.yaml" ]]; then
|
|
448
|
+
echo "product-line docs check failed: incomplete feature packet missing acceptance.yaml: \${feature_dir}" >&2
|
|
449
|
+
exit 1
|
|
450
|
+
fi
|
|
451
|
+
feature_dirs+=("\${feature_dir}")
|
|
452
|
+
done < <(find "\${root}" -mindepth 1 -maxdepth 1 -type d | sort)
|
|
449
453
|
done
|
|
450
454
|
|
|
455
|
+
if [[ \${#feature_dirs[@]} -eq 0 ]]; then
|
|
456
|
+
echo "product-line docs check skipped: no feature packets found"
|
|
457
|
+
exit 0
|
|
458
|
+
fi
|
|
459
|
+
|
|
451
460
|
for feature_dir in "\${feature_dirs[@]}"; do
|
|
452
461
|
echo "==> validate product-line feature: \${feature_dir}"
|
|
453
462
|
agent-worker task validate-feature "\${feature_dir}"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
2
|
+
import { appendFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { writeDagNodeTextArtifact } from "../infrastructure/harness/artifact-store.js";
|
|
5
5
|
import { truncateOutput } from "./cursor-executor.js";
|
|
@@ -11,6 +11,7 @@ const SUMMARY_STDOUT_MAX = 4_000;
|
|
|
11
11
|
const SUMMARY_STDERR_MAX = 2_000;
|
|
12
12
|
const NODE_STDOUT_MAX = 12_000;
|
|
13
13
|
const NODE_STDERR_MAX = 8_000;
|
|
14
|
+
const MAX_BUFFERED_OUTPUT_BYTES = 64 * 1024;
|
|
14
15
|
function resolveBashExecutable() {
|
|
15
16
|
const envCandidate = process.env.LOOP_AGENT_BASH ?? process.env.GIT_BASH;
|
|
16
17
|
if (envCandidate && existsSync(envCandidate))
|
|
@@ -85,8 +86,8 @@ export async function executeShellCommand(input) {
|
|
|
85
86
|
env: buildShellProcessEnv(input.envAllowlist, injectedEnv),
|
|
86
87
|
stdio: ["ignore", "pipe", "pipe"],
|
|
87
88
|
});
|
|
88
|
-
let stdout =
|
|
89
|
-
let stderr =
|
|
89
|
+
let stdout = createBoundedOutput();
|
|
90
|
+
let stderr = createBoundedOutput();
|
|
90
91
|
let timedOut = false;
|
|
91
92
|
let settled = false;
|
|
92
93
|
let timeoutHandle;
|
|
@@ -103,22 +104,36 @@ export async function executeShellCommand(input) {
|
|
|
103
104
|
command: input.command,
|
|
104
105
|
cwd: input.cwd,
|
|
105
106
|
durationMs: Date.now() - startedAt,
|
|
106
|
-
stdout,
|
|
107
|
-
|
|
107
|
+
stdout: formatBoundedOutput(stdout),
|
|
108
|
+
stdoutBytes: stdout.bytes,
|
|
109
|
+
stdoutTruncated: stdout.truncated,
|
|
110
|
+
stderr: formatBoundedOutput(stderr),
|
|
111
|
+
stderrBytes: stderr.bytes,
|
|
112
|
+
stderrTruncated: stderr.truncated,
|
|
113
|
+
...(input.outputArtifacts
|
|
114
|
+
? {
|
|
115
|
+
stdoutArtifactPath: input.outputArtifacts.stdoutPath,
|
|
116
|
+
stderrArtifactPath: input.outputArtifacts.stderrPath,
|
|
117
|
+
}
|
|
118
|
+
: {}),
|
|
108
119
|
timedOut,
|
|
109
120
|
...result,
|
|
110
121
|
});
|
|
111
122
|
}
|
|
112
123
|
child.stdout.setEncoding("utf-8");
|
|
113
124
|
child.stderr.setEncoding("utf-8");
|
|
125
|
+
initializeOutputArtifacts(input.outputArtifacts);
|
|
114
126
|
child.stdout.on("data", (chunk) => {
|
|
115
|
-
stdout
|
|
127
|
+
stdout = appendBoundedOutput(stdout, chunk);
|
|
128
|
+
appendOutputArtifact(input.outputArtifacts?.stdoutPath, chunk);
|
|
116
129
|
});
|
|
117
130
|
child.stderr.on("data", (chunk) => {
|
|
118
|
-
stderr
|
|
131
|
+
stderr = appendBoundedOutput(stderr, chunk);
|
|
132
|
+
appendOutputArtifact(input.outputArtifacts?.stderrPath, chunk);
|
|
119
133
|
});
|
|
120
134
|
child.on("error", (error) => {
|
|
121
|
-
stderr
|
|
135
|
+
stderr = appendBoundedOutput(stderr, `${stderr.text ? "\n" : ""}${error.message}`);
|
|
136
|
+
appendOutputArtifact(input.outputArtifacts?.stderrPath, error.message);
|
|
122
137
|
finish({
|
|
123
138
|
exitCode: null,
|
|
124
139
|
failureCategory: "spawn-error",
|
|
@@ -149,6 +164,46 @@ export async function executeShellCommand(input) {
|
|
|
149
164
|
}
|
|
150
165
|
});
|
|
151
166
|
}
|
|
167
|
+
function createBoundedOutput() {
|
|
168
|
+
return { bytes: 0, text: "", truncated: false };
|
|
169
|
+
}
|
|
170
|
+
function appendBoundedOutput(current, chunk) {
|
|
171
|
+
const bytes = current.bytes + Buffer.byteLength(chunk);
|
|
172
|
+
const combined = current.text + chunk;
|
|
173
|
+
if (Buffer.byteLength(combined) <= MAX_BUFFERED_OUTPUT_BYTES) {
|
|
174
|
+
return { bytes, text: combined, truncated: current.truncated };
|
|
175
|
+
}
|
|
176
|
+
const tail = Buffer.from(combined)
|
|
177
|
+
.subarray(-MAX_BUFFERED_OUTPUT_BYTES)
|
|
178
|
+
.toString("utf-8")
|
|
179
|
+
.replace(/^\uFFFD/, "");
|
|
180
|
+
return { bytes, text: tail, truncated: true };
|
|
181
|
+
}
|
|
182
|
+
function formatBoundedOutput(output) {
|
|
183
|
+
return output.truncated ? `${output.text}\n...[truncated]` : output.text;
|
|
184
|
+
}
|
|
185
|
+
function initializeOutputArtifacts(paths) {
|
|
186
|
+
if (!paths)
|
|
187
|
+
return;
|
|
188
|
+
try {
|
|
189
|
+
mkdirSync(path.dirname(paths.stdoutPath), { recursive: true });
|
|
190
|
+
writeFileSync(paths.stdoutPath, "", "utf-8");
|
|
191
|
+
writeFileSync(paths.stderrPath, "", "utf-8");
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
// Artifact persistence is best-effort; the bounded result remains available.
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function appendOutputArtifact(filePath, chunk) {
|
|
198
|
+
if (!filePath)
|
|
199
|
+
return;
|
|
200
|
+
try {
|
|
201
|
+
appendFileSync(filePath, chunk, "utf-8");
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
// Keep command execution independent from a transient artifact write failure.
|
|
205
|
+
}
|
|
206
|
+
}
|
|
152
207
|
function summarizeCommandResults(results) {
|
|
153
208
|
const stdout = results
|
|
154
209
|
.map((result) => [`$ ${result.command}`, result.stdout.trim()].filter(Boolean).join("\n"))
|
|
@@ -195,11 +250,17 @@ export function buildShellResultSummaryMarkdown(input) {
|
|
|
195
250
|
lines.push("", `## Command ${index + 1}: ${result.command}`, "", "### stdout", "", "```text", stdout.text, "```");
|
|
196
251
|
if (stdout.truncated)
|
|
197
252
|
lines.push("", "_(stdout truncated)_");
|
|
253
|
+
if (result.stdoutArtifactPath) {
|
|
254
|
+
lines.push("", `Full stdout artifact: ${result.stdoutArtifactPath}`);
|
|
255
|
+
}
|
|
198
256
|
if (stderr.text.trim()) {
|
|
199
257
|
lines.push("", "### stderr", "", "```text", stderr.text, "```");
|
|
200
258
|
if (stderr.truncated)
|
|
201
259
|
lines.push("", "_(stderr truncated)_");
|
|
202
260
|
}
|
|
261
|
+
if (result.stderrArtifactPath) {
|
|
262
|
+
lines.push("", `Full stderr artifact: ${result.stderrArtifactPath}`);
|
|
263
|
+
}
|
|
203
264
|
}
|
|
204
265
|
return `${lines.join("\n")}\n`;
|
|
205
266
|
}
|
|
@@ -235,12 +296,17 @@ export async function executeDagShellNode(input, meta) {
|
|
|
235
296
|
beforeStatus = undefined;
|
|
236
297
|
}
|
|
237
298
|
for (const command of commands) {
|
|
299
|
+
const commandNumber = results.length + 1;
|
|
238
300
|
const result = await executeShellCommand({
|
|
239
301
|
command,
|
|
240
302
|
cwd,
|
|
241
303
|
timeoutMs,
|
|
242
304
|
envAllowlist: shell.envAllowlist,
|
|
243
305
|
dagRunMeta: { runDir: meta.runDir, runId: meta.runId },
|
|
306
|
+
outputArtifacts: {
|
|
307
|
+
stdoutPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stdout.txt`),
|
|
308
|
+
stderrPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stderr.txt`),
|
|
309
|
+
},
|
|
244
310
|
});
|
|
245
311
|
results.push(result);
|
|
246
312
|
if (!result.ok)
|
|
@@ -42,6 +42,7 @@ export const workflowPolicyDagTemplateSchema = z.enum([
|
|
|
42
42
|
"review-gated-dag",
|
|
43
43
|
"supervised-implementation",
|
|
44
44
|
]);
|
|
45
|
+
export const workflowPolicyOutputLanguageSchema = z.enum(["zh-CN", "en"]);
|
|
45
46
|
export const workflowPolicySchema = z
|
|
46
47
|
.object({
|
|
47
48
|
/** Preferred implementation workflow for recoverable loop-agent work. Declarative policy; callers may still require explicit CLI flags. */
|
|
@@ -51,6 +52,9 @@ export const workflowPolicySchema = z
|
|
|
51
52
|
.default("agent-dag"),
|
|
52
53
|
dag: z
|
|
53
54
|
.object({
|
|
55
|
+
outputLanguage: workflowPolicyOutputLanguageSchema
|
|
56
|
+
.optional()
|
|
57
|
+
.default("zh-CN"),
|
|
54
58
|
defaultEntry: z
|
|
55
59
|
.enum(["dag run-task", "run-dag"])
|
|
56
60
|
.optional()
|
|
@@ -7,6 +7,17 @@ import path from 'node:path';
|
|
|
7
7
|
const DEFAULT_MAX_FILES_PER_REPO = 24;
|
|
8
8
|
const DEFAULT_MAX_TOTAL_FILES = 48;
|
|
9
9
|
const DEFAULT_MAX_FILE_BYTES = 512_000;
|
|
10
|
+
const MAX_REFERENCE_ENTRIES_PER_REPO = 1_024;
|
|
11
|
+
const SKIPPED_REFERENCE_DIRECTORIES = new Set([
|
|
12
|
+
'.git',
|
|
13
|
+
'.hg',
|
|
14
|
+
'.svn',
|
|
15
|
+
'.venv',
|
|
16
|
+
'build',
|
|
17
|
+
'dist',
|
|
18
|
+
'node_modules',
|
|
19
|
+
'target',
|
|
20
|
+
]);
|
|
10
21
|
export function isFeatureStudyTask(taskConfig) {
|
|
11
22
|
return taskConfig.taskKind === 'feature-study';
|
|
12
23
|
}
|
|
@@ -102,8 +113,8 @@ function uniqueKeepOrder(items) {
|
|
|
102
113
|
return result;
|
|
103
114
|
}
|
|
104
115
|
async function collectRepoFiles(root, focusGlobs, maxFiles) {
|
|
105
|
-
const candidates = await listFilesRecursive(root);
|
|
106
|
-
const matched = candidates
|
|
116
|
+
const candidates = await listFilesRecursive(root, MAX_REFERENCE_ENTRIES_PER_REPO);
|
|
117
|
+
const matched = candidates.files
|
|
107
118
|
.map((absolutePath) => ({
|
|
108
119
|
absolutePath,
|
|
109
120
|
relativePath: path.relative(root, absolutePath).replace(/\\/g, '/'),
|
|
@@ -111,7 +122,7 @@ async function collectRepoFiles(root, focusGlobs, maxFiles) {
|
|
|
111
122
|
.filter((entry) => focusGlobs.some((pattern) => matchGlob(pattern, entry.relativePath)))
|
|
112
123
|
.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
|
113
124
|
const files = [];
|
|
114
|
-
let truncated =
|
|
125
|
+
let truncated = candidates.truncated;
|
|
115
126
|
for (const entry of matched) {
|
|
116
127
|
if (files.length >= maxFiles) {
|
|
117
128
|
truncated = true;
|
|
@@ -122,10 +133,6 @@ async function collectRepoFiles(root, focusGlobs, maxFiles) {
|
|
|
122
133
|
continue;
|
|
123
134
|
if (fileStat.size > DEFAULT_MAX_FILE_BYTES)
|
|
124
135
|
continue;
|
|
125
|
-
const lower = entry.relativePath.toLowerCase();
|
|
126
|
-
if (lower.includes('/target/') || lower.includes('/node_modules/') || lower.includes('/.git/')) {
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
136
|
files.push({
|
|
130
137
|
relativePath: entry.relativePath,
|
|
131
138
|
absolutePath: entry.absolutePath,
|
|
@@ -134,21 +141,40 @@ async function collectRepoFiles(root, focusGlobs, maxFiles) {
|
|
|
134
141
|
}
|
|
135
142
|
return { files, truncated };
|
|
136
143
|
}
|
|
137
|
-
async function listFilesRecursive(dir) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
144
|
+
async function listFilesRecursive(dir, maxEntries) {
|
|
145
|
+
const files = [];
|
|
146
|
+
let visited = 0;
|
|
147
|
+
let truncated = false;
|
|
148
|
+
const walk = async (current) => {
|
|
149
|
+
let entries;
|
|
150
|
+
try {
|
|
151
|
+
entries = await readdir(current, { withFileTypes: true });
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
for (const entry of entries) {
|
|
157
|
+
if (visited >= maxEntries) {
|
|
158
|
+
truncated = true;
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
visited += 1;
|
|
162
|
+
if (entry.isDirectory() && SKIPPED_REFERENCE_DIRECTORIES.has(entry.name.toLowerCase())) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const target = path.join(current, entry.name);
|
|
166
|
+
if (entry.isDirectory()) {
|
|
167
|
+
await walk(target);
|
|
168
|
+
if (truncated)
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
files.push(target);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
await walk(dir);
|
|
177
|
+
return { files, truncated };
|
|
152
178
|
}
|
|
153
179
|
export function matchGlob(pattern, relativePath) {
|
|
154
180
|
const normalizedPattern = pattern.replace(/\\/g, '/');
|
|
@@ -76,7 +76,7 @@ export const taskConfigSchema = z.object({
|
|
|
76
76
|
complexity: taskComplexitySchema.optional().default("medium"),
|
|
77
77
|
/** Explicit delivery capabilities that affect writer contracts/model tier without changing risk. */
|
|
78
78
|
capabilities: z.array(taskCapabilitySchema).optional(),
|
|
79
|
-
verifyMode: verifyModeSchema.optional().default("
|
|
79
|
+
verifyMode: verifyModeSchema.optional().default("serial"),
|
|
80
80
|
verifyPreset: verifyPresetSchema.optional().default("auto"),
|
|
81
81
|
/** Task-contract commands that must be included in final DAG shell verification. */
|
|
82
82
|
verifyCommands: z.array(taskVerifyCommandSchema).optional().default([]),
|
package/dist/task/runtime.js
CHANGED
|
@@ -102,7 +102,7 @@ export async function createTask(repoRoot, taskId, title) {
|
|
|
102
102
|
complexity: 'medium',
|
|
103
103
|
dagFallbackReason: '',
|
|
104
104
|
contextProfile: 'full',
|
|
105
|
-
verifyMode: '
|
|
105
|
+
verifyMode: 'serial',
|
|
106
106
|
verifyPreset: 'auto',
|
|
107
107
|
verifyCommands: [],
|
|
108
108
|
verifyQuota: 'full',
|