@tea-agent/loop-agent 0.24.4-beta.0 → 0.24.5

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.
Files changed (29) hide show
  1. package/AGENTS.md +13 -5
  2. package/CHANGELOG.md +52 -0
  3. package/README.md +5 -2
  4. package/dist/commands/init.js +16 -2
  5. package/dist/worker/cli.js +59 -14
  6. package/dist/worker/console/operator-actions.js +135 -24
  7. package/dist/worker/console/static/assets/index-CSRIhuzh.js +29 -0
  8. package/dist/worker/console/static/assets/index-Yyn3ynVv.css +1 -0
  9. package/dist/worker/console/static/index.html +2 -2
  10. package/dist/worker/console/static-src/prd-file-import.js +218 -0
  11. package/dist/workflows/dag/backend-test-markdown-workflow.js +77 -11
  12. package/dist/workflows/dag/init-hybrid.js +15 -15
  13. package/dist/workflows/dag/l5-report-metrics.js +0 -3
  14. package/docs/architecture/evolution.md +1 -1
  15. package/docs/architecture/system-overview.md +1 -1
  16. package/docs/operations/local-development-environment.md +19 -0
  17. package/docs/templates/backend-test-dag.generate-pytest.prompt.md +3 -2
  18. package/docs/templates/backend-test-dag.json +7 -7
  19. package/docs/templates/backend-test-dag.retrospect.prompt.md +0 -3
  20. package/docs/templates/backend-test-dag.review-cases.prompt.md +1 -1
  21. package/docs/templates/evaluation/agents-map-slim-v1.md +1 -1
  22. package/docs/templates/evaluation/agents-map-verbose-v0.md +2 -2
  23. package/harness.json +2 -2
  24. package/package.json +1 -1
  25. package/skills/agent-worker/SKILL.md +1 -1
  26. package/skills/loop-agent/SKILL.md +1 -1
  27. package/skills/loop-agent/references/command-reference.md +1 -0
  28. package/dist/worker/console/static/assets/index-Cv68Ge5n.js +0 -27
  29. package/dist/worker/console/static/assets/index-Dp_2sKGk.css +0 -1
package/AGENTS.md CHANGED
@@ -37,7 +37,7 @@
37
37
  4. 涉及测试纪律/验证声明/调试时继续读:`docs/governance/harness-methodology-*.md`。
38
38
  5. 查看最近提交、相关 plan/progress/report;`git status --short --branch`;跑最小基线验证。
39
39
  6. 后端/接口/pytest → `taskKind: "backend-test"`(不是 `--profile`);知识回写 `knowledge-sync`;图谱开荒 `knowledge-graph-bootstrap`。`--profile` 仅 `auto|minimal|standard|reviewed|supervised`。
40
- 7. 看板/observe → `agent-worker console serve --repo . --port 8790`(`/inspect/` 只读);`observe serve` 仅为兼容入口。
40
+ 7. 看板/observe → `agent-worker console`(默认 repo=当前目录、port=8790;兼容入口 `agent-worker console serve --repo . --port 8790`)(`/inspect/` 只读);`observe serve` 仅为兼容入口。
41
41
  8. 分支合并 → 先读 `docs/operations/branch-merge-guideline.md`。
42
42
 
43
43
  ## 会话协议
@@ -64,13 +64,21 @@
64
64
 
65
65
  ## 验证
66
66
 
67
- 权威源:`docs/governance/verification-matrix.md`。常用:
67
+ 权威源:`docs/governance/verification-matrix.md`。
68
+
69
+ 默认节奏:编辑中只跑矩阵「最低验证」(typecheck + 定向 Vitest);`git commit` 的 `pre-commit` 只做 `check-repo`;`git push` 的 `pre-push` 跑 `bash scripts/ci.sh`(全量 typecheck + `npm test`)。每个 clone 安装一次 hooks:`bash scripts/install-git-hooks.sh`。
70
+
71
+ 常用:
68
72
 
69
73
  ```bash
70
- npm run typecheck && npm test && npm run build
71
- node bin/loop-agent.js --help
72
- bash scripts/check-repo.sh
74
+ # 编辑中
75
+ npm run typecheck
76
+ npx vitest run <相关测试路径>
77
+
78
+ # push / 交付前(或依赖 pre-push)
73
79
  bash scripts/ci.sh
80
+ npm run build
81
+ node bin/loop-agent.js --help
74
82
  ```
75
83
 
76
84
  文档站变更:`npm run docs:build`。init / architecture / skill entry / pack 定向验证见 verification-matrix。
package/CHANGELOG.md CHANGED
@@ -2,8 +2,60 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.24.5] - 2026-07-28
6
+
7
+ ### 重点更新
8
+
9
+ - 后端测试 Markdown 模块与 pytest 脚本实现一对一精准映射,提升测试执行的确定性
10
+ - 修复后端测试 HTML 报告中请求参数显示为空的问题,正确展示请求与响应日志
11
+ - 初始化项目支持将「帮我完成/实现/修复」等通用需求表达直接路由进 Agent DAG
12
+ - 新增裸 agent-worker console 入口,简化本地看板启动流程
13
+
14
+ ### 新增
15
+
16
+ - 初始化后的目标项目支持将「帮我完成/实现/修复/开发」等通用需求表达确定性地路由进 Agent DAG(new-task → dag run-task → dag validate → run-dag),无需追加额外的 cli 关键词
17
+ - 新增裸 agent-worker console 入口,直接启动 Loop Operator Console,repo 默认当前目录、port 默认 8790
18
+ - check-skill-entry.sh 及初始化投影脚本支持解析 YAML 折叠块,对通用需求触发词建立机器门禁以防漂移
19
+
20
+ ### 改进
21
+
22
+ - 后端测试收敛模块映射,生成与评审 prompt 强制模块文件一对一映射,在映射缺失时支持确定性恢复
23
+ - 后端测试 execute/report 节点文案与 pytest-html 真源对齐,移除残留的 JUnit 表述
24
+ - 后端测试 HTML 报告对齐 R04 模板样式,采用全 inline style 设计(渐变 banner、可折叠用例卡片、状态胶囊及请求/响应分块展示)
25
+ - 本地看板启动更简,端口被占用时不再自动更换,改为明确提示使用 --port 显式指定其它端口
26
+ - 旧项目经 init check-update 与 apply-safe 即可安全补回新的「通用需求实现」路由行,并保留用户自定义前言
27
+
28
+ ### 修复
29
+
30
+ - 修复后端测试 HTML 报告中请求参数显示为空的问题:渲染器现已兼容 payload= 键或 JSON request_record 格式的请求摘要
31
+ - 修复后端测试报告无法正确获取请求/响应日志的问题:兼容 HTTP_REQUEST 与 HTTP REQUEST 两种日志前缀,并在解析前正确解码 HTML 实体
32
+ - 修复 shell-executor 在 Windows 环境下因反斜杠路径导致 runDir 损坏的问题,确保路径注入稳定
33
+ - 修复前端 lint baseline 在 Windows 下因路径解析逻辑缺陷,拒绝相对路径输出导致评估误报为不可用的问题
34
+ - 修复多个测试固件在 Windows 下因嵌套 Shell 引号剥离导致命令执行输出为空或报错的问题(涉及 composite-shell 与 init-command)
35
+
36
+ ## [0.24.4] - 2026-07-28
37
+
38
+ ### 重点更新
39
+
40
+ - 控制台「导入需求」支持多文件批量导入,自动合成可编辑正文后创建任务
41
+ - 后端测试移除重复运行稳定性门槛,每次执行同时生成用户可读的 HTML 与 Markdown 报告
42
+ - 本地 pre-push 钩子升级为执行完整 CI 流程,覆盖治理检查、类型检查与全量测试
43
+
44
+ ### 新增
45
+
46
+ - 控制台「导入需求」支持选择或拖入一份或多份 Markdown/TXT 文档(严格 UTF-8,单文件 ≤8 MiB,最多 8 个),多文件分卷归档并合成可编辑正文后创建任务
47
+
5
48
  ### 改进
6
49
 
50
+ - 后端测试 L-5 结论不再将重复运行稳定性作为门槛,每次执行同时产出用户可读的 HTML 与 Markdown 报告,内部 facts 文件保留用于审计
51
+ - 本地 pre-push 钩子改为执行完整 CI 流程(治理检查 + 类型检查 + 全量测试),pre-commit 保持仅执行 check-repo
52
+ - harness 中等复杂度(MED)模型调整为 grok-4.5,并同步对齐相关 smoke/init 断言
53
+ - 归档已完成的设计文档与执行计划,刷新 0.24.x 六个月规划与能力摘要,并同步验证与 hooks 文档
54
+
55
+ ## [0.24.3] - 2026-07-28
56
+
57
+ ### 重点更新
58
+
7
59
  - 发布工作流改为发布已通过完整门禁并经 SHA-256 校验的 npm tarball,避免 `npm publish` 再次触发全量测试、类型检查与构建。
8
60
  - 默认 `standard` 实现任务在 React、Next.js 或 Vue 项目中直接选择 `frontend-implementation` DAG,不再依赖需求关键词;明确后端、文档/测试范围或前端否定仍保留对应通用流程。
9
61
 
package/README.md CHANGED
@@ -52,6 +52,8 @@ loop-agent init reconcile --repo-root .
52
52
 
53
53
  ### 运行第一个任务
54
54
 
55
+ 初始化后,在已初始化的目标项目中你可以直接对宿主 agent 说「**loop-agent 帮我完成 XXX 需求**」(或「帮我实现 / 帮我修复 / 帮我开发 XXX」「使用 loop-agent 完成 XXX」「按 loop-agent 流程处理 XXX」),初始化写入的 `AGENTS.md` 与 `.agents/skills/loop-agent/` 会把这类通用需求表达确定性地路由进 Agent DAG,**无需**追加额外的 `cli` 关键词。主会话只负责编排与验证,业务实现由受治理 DAG writer 完成。
56
+
55
57
  ```bash
56
58
  loop-agent new-task example-task "实现一个有明确验收标准的小功能"
57
59
  ```
@@ -90,12 +92,13 @@ loop-agent dag report --run-id <run-id>
90
92
  | 入口 | 命令 | 角色 |
91
93
  | --- | --- | --- |
92
94
  | 受治理 CLI | `loop-agent …` / `agent-worker …` | 权威写入与诊断 |
93
- | Loop Operator Console | `agent-worker console serve --repo .` | Official 本地控制面(默认 `127.0.0.1:8790`,含 Operate 与 Inspect) |
95
+ | Loop Operator Console | `agent-worker console` | Official 本地控制面(默认 repo=当前目录、`127.0.0.1:8790`,含 Operate 与 Inspect) |
94
96
  | Observe | `agent-worker observe serve --repo . --port 8787` | 兼容期独立只读入口;不再作为推荐路径 |
95
97
 
96
98
  ```bash
97
99
  # 终端 A:Console(任务/运行操作面;canonical mutation 只经 sibling loop-agent)
98
- agent-worker console serve --repo .
100
+ agent-worker console # 默认 repo=当前目录、port=8790
101
+ agent-worker console serve --repo . # 兼容入口,等价于裸入口
99
102
  agent-worker console doctor --repo .
100
103
 
101
104
  ```
@@ -415,6 +415,18 @@ check_skill "loop-agent" \\
415
415
  "references/command-reference.md"
416
416
  check_skill "agent-worker" "references/agent-worker-operator.md"
417
417
 
418
+ # loop-agent description is a YAML folded block (>-); grep ^description: only
419
+ # captures the indicator line. Validate the general-demand trigger terms
420
+ # against the whole skill file so host auto-discovery phrasing cannot drift.
421
+ loop_agent_skill=".agents/skills/loop-agent/SKILL.md"
422
+ loop_agent_text="$(tr '[:upper:]' '[:lower:]' < "\${loop_agent_skill}")"
423
+ for term in "loop-agent 帮我完成" "帮我实现" "帮我修复" "帮我开发" "agent dag" "new-task" "dag run-task" "dag validate" "run-dag"; do
424
+ if [[ "\${loop_agent_text}" != *"\${term}"* ]]; then
425
+ echo "skill entry 检查失败(loop-agent):description 缺少触发/路由词 \${term}" >&2
426
+ exit 1
427
+ fi
428
+ done
429
+
418
430
  description_line="$(grep -E '^description:' .agents/skills/agent-worker/SKILL.md | head -n 1 | tr '[:upper:]' '[:lower:]')"
419
431
  for term in "agent-worker" "feature packet" "taskspec" "task pool" "self-host" "candidate" "loop-agent"; do
420
432
  if [[ "\${description_line}" != *"\${term}"* ]]; then
@@ -802,6 +814,7 @@ function buildManagedAgentsBlock(input) {
802
814
  "| 初始化更新校验 / 检查初始化更新 / loop-agent 初始化更新校验 / loop agent初始化更新校验 | 更新校验 | 只读报告,不写入 |",
803
815
  "| 初始化对齐 / 升级后对齐 / reconcile 初始化 / loop-agent 初始化对齐 | 升级对齐 | 自动应用确定性安全动作;活跃 DAG/Worker 或人工决策时零写入 |",
804
816
  "| 初始化安全更新 / 应用初始化更新 / loop-agent 初始化安全更新 / loop agent初始化安全更新 | 安全更新 | 先 check-update,再只执行确定性安全动作 |",
817
+ "| loop-agent 帮我完成 / 帮我实现 / 帮我修复 / 帮我开发 <需求>;使用 loop-agent 完成 <X>;按 loop-agent 流程处理 <X> | 通用需求实现 | 先建任务源与 `task.json.allowedPaths` / `forbiddenPaths`,再 `new-task` → `dag run-task` → `dag validate` → `run-dag`;主会话不得直接修改业务实现 |",
805
818
  "",
806
819
  "**更新校验(只读)**:只读执行下面命令;**不得自动**执行 `apply-safe` 或模型合并。",
807
820
  "",
@@ -861,10 +874,11 @@ function buildManagedAgentsBlock(input) {
861
874
  "### 运行看板(只读)",
862
875
  "",
863
876
  "```bash",
864
- "agent-worker console serve --repo . --port 8790",
877
+ "agent-worker console # 默认 repo=当前目录,port=8790",
878
+ "agent-worker console serve --repo . --port 8790 # 兼容入口,等价于上面裸入口",
865
879
  "```",
866
880
  "",
867
- "浏览器打开 `http://127.0.0.1:8790/`;检视面为 `http://127.0.0.1:8790/inspect/`。默认绑定本机 `127.0.0.1`;不要直接暴露到公开网络。`agent-worker observe serve` 仅为兼容入口。",
881
+ "浏览器打开 `http://127.0.0.1:8790/`;检视面为 `http://127.0.0.1:8790/inspect/`。默认绑定本机 `127.0.0.1`;不要直接暴露到公开网络。端口被占用时不会自动更换,请用 `--port <port>` 显式指定。`agent-worker observe serve` 仅为兼容入口。",
868
882
  "",
869
883
  "### DAG 诊断与收口",
870
884
  "",
@@ -41,7 +41,8 @@ export function buildAgentWorkerProgram() {
41
41
  .description("Local product-line worker utilities for TaskSpec validation")
42
42
  .version(readPackageVersion(path.dirname(path.dirname(path.dirname(fileURLToPath(import.meta.url))))) ?? "0.0.0", "-V, --version", "display version")
43
43
  .showHelpAfterError()
44
- .showSuggestionAfterError();
44
+ .showSuggestionAfterError()
45
+ .enablePositionalOptions();
45
46
  const task = program.command("task").description("TaskSpec utilities");
46
47
  const batch = program
47
48
  .command("batch")
@@ -593,18 +594,17 @@ export function buildAgentWorkerProgram() {
593
594
  process.stdout.write(`${JSON.stringify(snapshot)}\n`);
594
595
  });
595
596
  consoleCmd
596
- .command("serve")
597
- .requiredOption("--repo <repo-root>", "Target repo root")
597
+ .description("Loop Operator Console (local); bare `agent-worker console` starts it directly (repo defaults to current working directory, port defaults to 8790)")
598
+ .enablePositionalOptions()
599
+ .passThroughOptions()
600
+ .option("--repo <repo-root>", "Target repo root (default: current working directory)", process.cwd())
598
601
  .option("--port <port>", "HTTP port", "8790")
599
602
  .option("--host <host>", "Bind host (default 127.0.0.1; use 0.0.0.0 for LAN, no network auth)", "127.0.0.1")
600
603
  .option("--debug", "Log request source/method/path/status to stderr")
601
604
  .option("--app-data <path>", "Override Console application-data root (Draft/Operation/confirmation)")
602
- .description("Start Loop Operator Console HTTP server (Operate + Inspect; static SPA + Operator API)")
603
605
  .action(async (options) => {
604
- const { createConsoleServer } = await import("./console/server.js");
605
- const repoRoot = path.resolve(options.repo);
606
- const server = await createConsoleServer({
607
- repoRoot,
606
+ await runConsoleServe({
607
+ repoRoot: path.resolve(options.repo),
608
608
  host: options.host,
609
609
  port: Number.parseInt(options.port, 10),
610
610
  debug: options.debug === true,
@@ -612,13 +612,25 @@ export function buildAgentWorkerProgram() {
612
612
  ? path.resolve(options.appData)
613
613
  : undefined,
614
614
  });
615
- process.stdout.write(`${server.url}\n`);
616
- await new Promise((resolve) => {
617
- const shutdown = () => resolve();
618
- process.once("SIGINT", shutdown);
619
- process.once("SIGTERM", shutdown);
615
+ });
616
+ consoleCmd
617
+ .command("serve")
618
+ .option("--repo <repo-root>", "Target repo root (default: current working directory)", process.cwd())
619
+ .option("--port <port>", "HTTP port", "8790")
620
+ .option("--host <host>", "Bind host (default 127.0.0.1; use 0.0.0.0 for LAN, no network auth)", "127.0.0.1")
621
+ .option("--debug", "Log request source/method/path/status to stderr")
622
+ .option("--app-data <path>", "Override Console application-data root (Draft/Operation/confirmation)")
623
+ .description("Start Loop Operator Console HTTP server (compat entry; equivalent to bare `agent-worker console`)")
624
+ .action(async (options) => {
625
+ await runConsoleServe({
626
+ repoRoot: path.resolve(options.repo),
627
+ host: options.host,
628
+ port: Number.parseInt(options.port, 10),
629
+ debug: options.debug === true,
630
+ appDataRoot: options.appData
631
+ ? path.resolve(options.appData)
632
+ : undefined,
620
633
  });
621
- await server.close();
622
634
  });
623
635
  consoleCmd
624
636
  .command("doctor")
@@ -693,6 +705,39 @@ export async function main(argv = process.argv) {
693
705
  async function readTaskYaml(taskSpecPath) {
694
706
  return YAML.parse(await readFile(taskSpecPath, "utf-8"));
695
707
  }
708
+ /** Shared console serve entry used by both bare `console` and `console serve`. */
709
+ async function runConsoleServe(options) {
710
+ const { createConsoleServer } = await import("./console/server.js");
711
+ try {
712
+ const server = await createConsoleServer({
713
+ repoRoot: options.repoRoot,
714
+ host: options.host,
715
+ port: options.port,
716
+ debug: options.debug === true,
717
+ appDataRoot: options.appDataRoot,
718
+ });
719
+ process.stdout.write(`${server.url}\n`);
720
+ await new Promise((resolve) => {
721
+ const shutdown = () => resolve();
722
+ process.once("SIGINT", shutdown);
723
+ process.once("SIGTERM", shutdown);
724
+ });
725
+ await server.close();
726
+ }
727
+ catch (error) {
728
+ const detail = error instanceof Error ? error.message : String(error);
729
+ const code = error?.code;
730
+ const isPortInUse = code === "EADDRINUSE" ||
731
+ /listen EADDRINUSE/i.test(detail) ||
732
+ /address already in use/i.test(detail);
733
+ if (isPortInUse) {
734
+ process.stderr.write(`端口 ${options.port} 已被占用,请使用 --port <port> 指定其它端口后重试。\n`);
735
+ process.exit(1);
736
+ }
737
+ process.stderr.write(`${detail}\n`);
738
+ process.exit(1);
739
+ }
740
+ }
696
741
  function handlePoolRecoveryCliError(error) {
697
742
  const detail = error instanceof Error ? error.message : String(error);
698
743
  process.stderr.write(`${detail}\n`);
@@ -1490,9 +1490,14 @@ async function allocateUniqueTaskId(repoRoot, preferredId) {
1490
1490
  }
1491
1491
  async function dispatchBootstrapFromPrd(ctx, p) {
1492
1492
  const content = str(p.content) ?? str(p.fileText);
1493
- if (!content) {
1493
+ if (!content?.trim()) {
1494
1494
  return invalid("bootstrapFromPrd", "content is required");
1495
1495
  }
1496
+ const parsedDocs = parseBootstrapDocuments(p.documents);
1497
+ if (!parsedDocs.ok) {
1498
+ return invalid("bootstrapFromPrd", parsedDocs.message);
1499
+ }
1500
+ const documents = parsedDocs.documents;
1496
1501
  const taskKind = normalizeConsoleWorkflowKind(str(p.taskKind), "standard");
1497
1502
  const identity = deriveTaskIdentityFromPrd(content);
1498
1503
  let taskId;
@@ -1539,28 +1544,86 @@ async function dispatchBootstrapFromPrd(ctx, p) {
1539
1544
  }),
1540
1545
  };
1541
1546
  }
1542
- const staged = await stageUtf8Text(ctx.appData, content, {
1543
- extension: ".md",
1544
- });
1545
- const imported = await run(["import-prd", taskId, "--file", staged.path, "--json"], {
1546
- cwd: ctx.repoRoot,
1547
- artifactName: "bootstrap-import-prd",
1548
- expectJson: true,
1549
- timeoutMs: 60_000,
1550
- });
1551
- if (!imported.ok || imported.exitCode !== 0) {
1552
- return {
1553
- kind: "error",
1554
- status: 400,
1555
- body: operatorFailed({
1556
- command: "bootstrapFromPrd",
1557
- outcome: "rejected",
1558
- code: "INTERNAL_ERROR",
1559
- message: imported.stderr?.trim() ||
1560
- `import-prd failed with exit ${imported.exitCode}`,
1561
- details: { exitCode: imported.exitCode, taskId },
1562
- }),
1563
- };
1547
+ const succeededImports = [];
1548
+ const importResults = [];
1549
+ const runImportPrd = async (label, text, name) => {
1550
+ const staged = await stageUtf8Text(ctx.appData, text, {
1551
+ extension: ".md",
1552
+ });
1553
+ const imported = await run([
1554
+ "import-prd",
1555
+ taskId,
1556
+ "--file",
1557
+ staged.path,
1558
+ "--name",
1559
+ name,
1560
+ "--role",
1561
+ "requirement",
1562
+ "--json",
1563
+ ], {
1564
+ cwd: ctx.repoRoot,
1565
+ artifactName: `bootstrap-import-prd-${label}`,
1566
+ expectJson: true,
1567
+ timeoutMs: 60_000,
1568
+ });
1569
+ if (!imported.ok || imported.exitCode !== 0) {
1570
+ return {
1571
+ kind: "error",
1572
+ status: 400,
1573
+ body: operatorFailed({
1574
+ command: "bootstrapFromPrd",
1575
+ outcome: "rejected",
1576
+ code: "INTERNAL_ERROR",
1577
+ message: imported.stderr?.trim() ||
1578
+ `import-prd failed for "${name}" with exit ${imported.exitCode}. 任务已创建但不完整,请用 CLI 补 import-prd 或换 taskId 重试。`,
1579
+ details: {
1580
+ exitCode: imported.exitCode,
1581
+ taskId,
1582
+ failedName: name,
1583
+ succeededImports: [...succeededImports],
1584
+ },
1585
+ }),
1586
+ };
1587
+ }
1588
+ succeededImports.push(name);
1589
+ importResults.push(imported.json ?? null);
1590
+ return null;
1591
+ };
1592
+ if (documents && documents.length > 0) {
1593
+ for (const doc of documents) {
1594
+ const failure = await runImportPrd(doc.name, doc.content, doc.name);
1595
+ if (failure)
1596
+ return failure;
1597
+ }
1598
+ const composedFailure = await runImportPrd("composed", content, "composed");
1599
+ if (composedFailure)
1600
+ return composedFailure;
1601
+ }
1602
+ else {
1603
+ const staged = await stageUtf8Text(ctx.appData, content, {
1604
+ extension: ".md",
1605
+ });
1606
+ const imported = await run(["import-prd", taskId, "--file", staged.path, "--json"], {
1607
+ cwd: ctx.repoRoot,
1608
+ artifactName: "bootstrap-import-prd",
1609
+ expectJson: true,
1610
+ timeoutMs: 60_000,
1611
+ });
1612
+ if (!imported.ok || imported.exitCode !== 0) {
1613
+ return {
1614
+ kind: "error",
1615
+ status: 400,
1616
+ body: operatorFailed({
1617
+ command: "bootstrapFromPrd",
1618
+ outcome: "rejected",
1619
+ code: "INTERNAL_ERROR",
1620
+ message: imported.stderr?.trim() ||
1621
+ `import-prd failed with exit ${imported.exitCode}`,
1622
+ details: { exitCode: imported.exitCode, taskId },
1623
+ }),
1624
+ };
1625
+ }
1626
+ importResults.push(imported.json ?? null);
1564
1627
  }
1565
1628
  const draftSaved = await ctx.drafts.save({
1566
1629
  ...emptyDraft(taskId, title, { taskKind }),
@@ -1578,10 +1641,58 @@ async function dispatchBootstrapFromPrd(ctx, p) {
1578
1641
  identitySource: identity.source,
1579
1642
  preferredTaskId: identity.taskId,
1580
1643
  draftSha256: draftSaved.draftSha256,
1581
- import: imported.json ?? null,
1644
+ import: importResults.length === 1 ? importResults[0] : null,
1645
+ imports: importResults,
1646
+ documentNames: documents && documents.length > 0
1647
+ ? [...documents.map((d) => d.name), "composed"]
1648
+ : undefined,
1582
1649
  }),
1583
1650
  };
1584
1651
  }
1652
+ const MAX_BOOTSTRAP_DOCUMENTS = 8;
1653
+ function parseBootstrapDocuments(raw) {
1654
+ if (raw === undefined || raw === null) {
1655
+ return { ok: true, documents: null };
1656
+ }
1657
+ if (!Array.isArray(raw)) {
1658
+ return { ok: false, message: "documents must be an array" };
1659
+ }
1660
+ if (raw.length === 0) {
1661
+ return { ok: true, documents: null };
1662
+ }
1663
+ if (raw.length > MAX_BOOTSTRAP_DOCUMENTS) {
1664
+ return {
1665
+ ok: false,
1666
+ message: `documents supports at most ${MAX_BOOTSTRAP_DOCUMENTS} entries`,
1667
+ };
1668
+ }
1669
+ const documents = [];
1670
+ for (let i = 0; i < raw.length; i += 1) {
1671
+ const entry = raw[i];
1672
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
1673
+ return { ok: false, message: `documents[${i}] must be an object` };
1674
+ }
1675
+ const rec = entry;
1676
+ const name = str(rec.name)?.trim();
1677
+ const content = str(rec.content);
1678
+ const fileName = str(rec.fileName)?.trim();
1679
+ if (!name) {
1680
+ return { ok: false, message: `documents[${i}].name is required` };
1681
+ }
1682
+ if (!content?.trim()) {
1683
+ return {
1684
+ ok: false,
1685
+ message: `documents[${i}].content is required`,
1686
+ };
1687
+ }
1688
+ documents.push({
1689
+ name,
1690
+ content,
1691
+ ...(fileName ? { fileName } : {}),
1692
+ });
1693
+ }
1694
+ return { ok: true, documents };
1695
+ }
1585
1696
  async function ensureDraft(ctx, taskId, title, options) {
1586
1697
  const existing = await ctx.drafts.get(taskId);
1587
1698
  if (existing) {