@mickorz/opencode-agentic-workflow 0.4.0 → 0.5.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.
Files changed (127) hide show
  1. package/README.md +306 -5
  2. package/dist/cli/config.d.ts +121 -0
  3. package/dist/cli/config.js +331 -0
  4. package/dist/cli/config.js.map +1 -0
  5. package/dist/cli/doctor.d.ts +5 -0
  6. package/dist/cli/doctor.js +141 -0
  7. package/dist/cli/doctor.js.map +1 -0
  8. package/dist/cli/index.d.ts +18 -0
  9. package/dist/cli/index.js +120 -0
  10. package/dist/cli/index.js.map +1 -0
  11. package/dist/cli/install.d.ts +19 -0
  12. package/dist/cli/install.js +135 -0
  13. package/dist/cli/install.js.map +1 -0
  14. package/dist/cli/uninstall.d.ts +16 -0
  15. package/dist/cli/uninstall.js +110 -0
  16. package/dist/cli/uninstall.js.map +1 -0
  17. package/dist/cli/update.d.ts +15 -0
  18. package/dist/cli/update.js +86 -0
  19. package/dist/cli/update.js.map +1 -0
  20. package/dist/core/index.d.ts +39 -0
  21. package/dist/core/index.js +38 -0
  22. package/dist/core/index.js.map +1 -0
  23. package/dist/observability/events.d.ts +43 -0
  24. package/dist/observability/events.js.map +1 -1
  25. package/dist/plugin/checkpoint-override.d.ts +7 -9
  26. package/dist/plugin/checkpoint-override.js +8 -14
  27. package/dist/plugin/checkpoint-override.js.map +1 -1
  28. package/dist/plugin/define-workflow.d.ts +33 -0
  29. package/dist/plugin/define-workflow.js +103 -0
  30. package/dist/plugin/define-workflow.js.map +1 -0
  31. package/dist/plugin/index.js +406 -26
  32. package/dist/plugin/index.js.map +1 -1
  33. package/dist/plugin/opencode-v2-executor.js +58 -46
  34. package/dist/plugin/opencode-v2-executor.js.map +1 -1
  35. package/dist/plugin/progress-board.d.ts +64 -0
  36. package/dist/plugin/progress-board.js +150 -0
  37. package/dist/plugin/progress-board.js.map +1 -0
  38. package/dist/plugin/progress-rpc.d.ts +390 -0
  39. package/dist/plugin/progress-rpc.js +318 -0
  40. package/dist/plugin/progress-rpc.js.map +1 -0
  41. package/dist/plugin/progress-view.d.ts +82 -0
  42. package/dist/plugin/progress-view.js +244 -0
  43. package/dist/plugin/progress-view.js.map +1 -0
  44. package/dist/plugin/run-sessions.d.ts +28 -0
  45. package/dist/plugin/run-sessions.js +46 -0
  46. package/dist/plugin/run-sessions.js.map +1 -0
  47. package/dist/plugin/tool-args.d.ts +15 -0
  48. package/dist/plugin/tool-args.js +28 -0
  49. package/dist/plugin/tool-args.js.map +1 -0
  50. package/dist/plugin/tui.d.ts +11 -4
  51. package/dist/plugin/tui.js +149 -5
  52. package/dist/plugin/tui.js.map +1 -1
  53. package/dist/plugin/workflow-control.d.ts +16 -0
  54. package/dist/plugin/workflow-control.js +85 -0
  55. package/dist/plugin/workflow-control.js.map +1 -0
  56. package/dist/plugin/workflow-schedule.d.ts +24 -0
  57. package/dist/plugin/workflow-schedule.js +97 -0
  58. package/dist/plugin/workflow-schedule.js.map +1 -0
  59. package/dist/quality/checkpoint.d.ts +5 -2
  60. package/dist/quality/checkpoint.js +11 -6
  61. package/dist/quality/checkpoint.js.map +1 -1
  62. package/dist/quality/verify.d.ts +21 -2
  63. package/dist/quality/verify.js +36 -14
  64. package/dist/quality/verify.js.map +1 -1
  65. package/dist/registry/definition.d.ts +12 -0
  66. package/dist/registry/definition.js.map +1 -1
  67. package/dist/registry/registry.d.ts +4 -0
  68. package/dist/registry/registry.js +20 -1
  69. package/dist/registry/registry.js.map +1 -1
  70. package/dist/registry/run-control.d.ts +27 -0
  71. package/dist/registry/run-control.js +51 -0
  72. package/dist/registry/run-control.js.map +1 -0
  73. package/dist/registry/runner.d.ts +29 -6
  74. package/dist/registry/runner.js +153 -35
  75. package/dist/registry/runner.js.map +1 -1
  76. package/dist/runtime/errors.d.ts +26 -0
  77. package/dist/runtime/errors.js +30 -0
  78. package/dist/runtime/errors.js.map +1 -1
  79. package/dist/runtime/executor.d.ts +22 -0
  80. package/dist/runtime/executor.js.map +1 -1
  81. package/dist/runtime/run-context.d.ts +44 -0
  82. package/dist/runtime/run-context.js +32 -0
  83. package/dist/runtime/run-context.js.map +1 -0
  84. package/dist/scheduler/cron.d.ts +29 -0
  85. package/dist/scheduler/cron.js +171 -0
  86. package/dist/scheduler/cron.js.map +1 -0
  87. package/dist/scheduler/service.d.ts +78 -0
  88. package/dist/scheduler/service.js +210 -0
  89. package/dist/scheduler/service.js.map +1 -0
  90. package/dist/scheduler/store.d.ts +26 -0
  91. package/dist/scheduler/store.js +94 -0
  92. package/dist/scheduler/store.js.map +1 -0
  93. package/dist/scheduler/types.d.ts +53 -0
  94. package/dist/scheduler/types.js +12 -0
  95. package/dist/scheduler/types.js.map +1 -0
  96. package/dist/state/journal.d.ts +23 -0
  97. package/dist/state/journal.js +2 -0
  98. package/dist/state/journal.js.map +1 -1
  99. package/dist/state/recorder.d.ts +67 -0
  100. package/dist/state/recorder.js +148 -2
  101. package/dist/state/recorder.js.map +1 -1
  102. package/dist/workflow/agent.d.ts +42 -1
  103. package/dist/workflow/agent.js +116 -6
  104. package/dist/workflow/agent.js.map +1 -1
  105. package/dist/workflow/judge-panel.d.ts +37 -0
  106. package/dist/workflow/judge-panel.js +66 -0
  107. package/dist/workflow/judge-panel.js.map +1 -0
  108. package/dist/workflow/pipeline.d.ts +21 -0
  109. package/dist/workflow/pipeline.js +71 -0
  110. package/dist/workflow/pipeline.js.map +1 -0
  111. package/dist/workflow/race.d.ts +16 -0
  112. package/dist/workflow/race.js +46 -0
  113. package/dist/workflow/race.js.map +1 -0
  114. package/dist/workflow/sequence.js +6 -0
  115. package/dist/workflow/sequence.js.map +1 -1
  116. package/dist/workflows/loader.d.ts +124 -0
  117. package/dist/workflows/loader.js +500 -0
  118. package/dist/workflows/loader.js.map +1 -0
  119. package/dist/workspace/ambient.d.ts +6 -4
  120. package/dist/workspace/ambient.js +10 -4
  121. package/dist/workspace/ambient.js.map +1 -1
  122. package/package.json +16 -6
  123. package/skills/workflow-authoring/SKILL.md +111 -0
  124. package/skills/workflow-optimize/SKILL.md +70 -0
  125. package/dist/workflows/index.d.ts +0 -19
  126. package/dist/workflows/index.js +0 -40
  127. package/dist/workflows/index.js.map +0 -1
package/README.md CHANGED
@@ -78,6 +78,31 @@ journal 记录每一步,崩溃后 resume 按精确版本解析定义、跳过
78
78
 
79
79
  **前置**:OpenCode V2(`opencode` CLI 可用且有能正常对话的模型)、Node.js 20+、git。
80
80
 
81
+ ### 方式一:安装器(推荐)
82
+
83
+ ```bash
84
+ # 交互式(安装方式 / 模型 / journal / skills 逐项确认,配置合并保留注释、自动 .bak)
85
+ npx @mickorz/opencode-agentic-workflow install
86
+
87
+ # 无头(flags 指齐 + --yes,零交互——适合脚本 / CI)
88
+ npx @mickorz/opencode-agentic-workflow install --project --model glm/glm-5.3-flash --yes
89
+ ```
90
+
91
+ 三种方式:`--global`(`~/.config/opencode/opencode.json`,全项目生效)、
92
+ `--project`(当前项目 `opencode.json`,推荐)、`--locked`(npm 装进
93
+ `node_modules` + 配置指本地路径,适合团队协作;skills 零拷贝)。
94
+ 生成的 options 最小可用集:`model` + `agent: "build"` +
95
+ `journalDir: ".agentic-workflow/journal"`(`--no-journal` / `--no-skills`
96
+ 可关)。skills 拷贝进原生扫描目录(`.opencode/skills/` 或
97
+ `~/.config/opencode/skills/`)。
98
+
99
+ 配套命令:`update`(locked 走 `npm update`;其余清宿主插件缓存,重启后
100
+ 拉最新)、`uninstall`(与安装对称:条目清空连键删、空壳配置整文件删、
101
+ locked 无条件 `npm uninstall`;skill 目录与 journal 属用户数据,交互确认
102
+ 或打印手动清理命令)、`doctor`(只读排查 `[OK]/[WARN]/[FAIL]` 清单)。
103
+
104
+ ### 方式二:手改配置
105
+
81
106
  ```jsonc
82
107
  // 1. 在你的项目里配置插件(<你的项目>/opencode.json)
83
108
  // package 用 npm 包名;model 换成你的 providerID/modelId
@@ -307,6 +332,9 @@ workflow 结束时同步写 `metrics.json` 快照(失败也写)。
307
332
  | `isolation.dir` | `string` | 仓库同级 | worktree 父目录 |
308
333
  | `isolation.baseRef` | `string` | HEAD | worktree 基准 ref |
309
334
  | `isolation.cleanup` | `always` / `on-success` / `never` | `on-success` | 清理策略 |
335
+ | `workflows` | `string[]` | — | 声明式流程路径(.json 文件或目录) |
336
+ | `schedulesDir` | `string` | 关闭 | 定时任务目录(配置 + 游标 + 触发记录);需同时配置 `journalDir`,启用 `workflow_schedule` 工具与调度器 |
337
+ | `maxConcurrentRuns` | `number` | `3` | 并发顶层 run 上限(超出返回可读失败并提示用 `workflow_control` 查看或停止在飞 run) |
310
338
 
311
339
  相对路径一律以**项目目录**(不是 service 进程 cwd)为基准解析。
312
340
 
@@ -316,12 +344,282 @@ workflow 结束时同步写 `metrics.json` 快照(失败也写)。
316
344
 
317
345
  | 参数 | 说明 |
318
346
  |------|------|
319
- | `flow` | workflow id(枚举由注册表驱动;缺省 `smoke`) |
320
- | `topic` | 主题参数 |
321
- | `resumeRunId` | 恢复指定 run(优先于 flow/topic;用失败输出里的 runId) |
347
+ | `flow` | workflow id(见工具描述内清单,含 `workflow_define` 定义的自定义流程;缺省 `smoke`;未知 id 报错并列出可用清单) |
348
+ | `topic` | 主题参数(恒传顶层) |
349
+ | `args` | flow 声明的其余参数(对象;描述内 `[args: …]` 有提示;required/类型不符即报具体问题) |
350
+ | `resumeRunId` | 恢复指定 run(优先于 flow/topic;用失败输出里的 runId;aborted 的 run 也可恢复) |
351
+ | `checkpointMode` | 调用级审批覆盖:`"auto-approve" | "auto-reject"`(headless 必传前者,除非明确要拒) |
352
+ | `background` | `true` = 后台启动并立即返回 runId(需 journalDir;用 `workflow_control` 轮询/停止)。**并发顶层 run 已支持**:最多 `maxConcurrentRuns`(默认 3)个 run 同时在飞;来自 run 内部(其 agent 会话或派生子会话)的调用仍被拒绝(递归防护,防信号量自饿死) |
353
+
354
+ **`workflow_define`** —— 对话中定义自定义流程(声明式 JSON:校验 → 立即注册 → 落盘,
355
+ 之后每次启动自动装载;幂等重定义 / 改内容必须升 version)。
356
+
357
+ **`workflow_control`** —— run 检查与控制(需 journalDir):
358
+ `action=status`(全量列表或单 run 详情,含最终 output);
359
+ `action=stop`(协作式停止——下一步骤边界生效,journal 收口为 `aborted`;
360
+ 进程重启遗留的悬置 running run 会被直接收口)。
322
361
 
323
362
  **`workflow_metrics`** —— 只读查询本服务累计指标(`format: "text" | "json"`)。
324
363
 
364
+ **`workflow_schedule`** —— 定时任务管理(需 `schedulesDir` + `journalDir`):
365
+
366
+ | 参数 | 说明 |
367
+ |------|------|
368
+ | `action` | `create` / `list` / `get` / `delete` / `runNow` / `enable` / `disable` |
369
+ | `id` | schedule id(kebab-case;create 必填) |
370
+ | `flow` / `cron` | create 必填:workflow id + 四模式 cron(`* * * * *`、`*/n`、`m * * * *`、`m h * * *`、`m h * * W`,本地时区) |
371
+ | `args` | 触发时透传的 workflow 参数(含 `topic`) |
372
+ | `name` / `enabled` | 展示名 / 初始启用态(缺省 true) |
373
+
374
+ 语义边界(如实):调度器随宿主进程存活(进程退出即停);停机错过的 slot
375
+ 重启后**合并为最近一个**补跑;创建时刻为游标基线(更早的 slot 不补跑,
376
+ 要立即跑用 `runNow`);触发时若有任一 run 在跑(调度器单飞,与手动并发
377
+ 上限独立——保持节奏可预期、避免并行成本意外)该 slot 记录 `skipped`
378
+ (跳过不是延迟);scheduled run 无人值守——checkpoint 强制 auto-approve、
379
+ 不启用 worktree 隔离;触发时用 registry 当前最新版本。
380
+
381
+ ## Skills
382
+
383
+ npm 包内附两个 agent skill(`skills/` 目录,随包分发),教主 agent 用上面的工具
384
+ 完成「定义流程」与「迭代优化」两类高频任务:
385
+
386
+ | Skill | 触发场景 |
387
+ |-------|----------|
388
+ | `workflow-authoring` | 「帮我定义/创建一个 workflow」「给流程加步骤/参数」「workflow_define 报错了」——从需求澄清到声明式 JSON 构造、落盘注册、试跑验证的完整链路 |
389
+ | `workflow-optimize` | 「这个流程跑得慢/贵/老失败」「优化迭代一下」「对比改动前后」——metrics/journal 诊断 → 单主题改动 → 升版重定义 → 同参重跑 → 对比报告 |
390
+
391
+ 启用方式(二选一):
392
+
393
+ ```jsonc
394
+ // 方式一(推荐,零拷贝):opencode.json 里把包内 skills 目录挂进 skills 数组
395
+ {
396
+ "plugins": [{ "package": "opencode-agentic-workflow", "options": { "...": "..." } }],
397
+ "skills": ["node_modules/opencode-agentic-workflow/skills"]
398
+ }
399
+
400
+ // 方式二:把 skills/ 下的子目录拷进 .opencode/skills/(项目级)
401
+ // 或 ~/.config/opencode/skills/(全局)
402
+ ```
403
+
404
+ 挂载后 agent 会在相关请求时自动加载;也可在 prompt 里显式 `@workflow-authoring`
405
+ 指定。
406
+
407
+ ## TUI 进度面板
408
+
409
+ 在交互式 TUI 里输入 `/workflow`(或命令面板搜 "Workflow progress")打开进度面板:
410
+ 近期 run 一览(含 journal 里的历史 run),最新 run 展开步骤树,状态实时刷新——
411
+ journal 每次状态变更都会派发 `run.progress` 全量快照事件。
412
+
413
+ ```
414
+ Agentic Workflow
415
+ ✗ feature-development@1.0.0 5.0s
416
+ ✓ gather 2.0s
417
+ ▶ implement 2.5s
418
+ · verify
419
+ ↳ verify rejected the patch
420
+ ✓ paced@1.0.0 24.0s
421
+ ── detail
422
+ ✓ artifact@1.0.0 · completed · 42.0s
423
+ run_9f3c… · args {"topic":"节点详情验收"}
424
+ ✓ write 18.2s
425
+ → 已写入 artifact.md(约 1200 字)……
426
+ ✓ check 1.1s
427
+ → artifact.md exists (4.2 KB)
428
+ ```
429
+
430
+ 面板下半区是**节点详情**(P2-8b):最新 run 的 journal 单读投影——runId /
431
+ args 预览 / 每步骤的输出或错误预览(折行 + 截断)/ 步骤与总时长,以及
432
+ **步骤级 token/模型元数据**(agent 步显示 `· 1.5k tok · glm/glm-5.3-flash`
433
+ 后缀;同一 run 的 agent 调用按 runId 聚合到所在步骤,subflow 互不串账)。
434
+ 详情经 `agentic-workflow-progress` RPC 的 `detail` 方法拉取(同一状态只拉
435
+ 一次,终态转换再拉一次收尾);未配置 `journalDir` 时详情区静默缺省,
436
+ 面板其余不受影响。
437
+
438
+ ### Open Session 回放
439
+
440
+ 每个 agent 调用实际发生在宿主子会话里(挂在主会话下,TUI 会话列表可导航)。
441
+ v1 的「打开会话」在 v2 的对位实现:executor 把子会话 ID 透传到
442
+ `agent.completed` 事件,journal 按步骤聚合成 `sessionIDs`(pipeline/race 步
443
+ 是多会话,按发生顺序累积;reopen 重跑会清掉旧清单)。面板据此自动回放
444
+ **最新 run 最后一个带会话的步骤**的完整对话:
445
+
446
+ ```
447
+ ── session
448
+ fanout · ses_8b1f…
449
+ ❯ 针对条目「回放甲」写一句结论…
450
+ · 回放甲:并发回放链路通畅。
451
+ ❯ 针对条目「回放乙」写一句结论…
452
+ · 回放乙:会话 ID 已贯通落盘。
453
+ ```
454
+
455
+ 回放经同一 RPC 的 `session` 方法拉取(`{runId, step, index?}`——index 选
456
+ pipeline 步的第几个会话,缺省最后一个;消息取 user/assistant 文本,单条
457
+ 预览 800 字、至多 50 条)。任意步骤的会话都可经该 RPC 取回,不限于面板
458
+ 自动选择的那一个。
459
+
460
+ 数据链路:RunJournal 状态转换 → `run.progress` 事件总线 → ProgressBoard(容量 20,
461
+ journalDir 配置时用历史 run 做种子)→ `agentic-workflow-progress` RPC。headless
462
+ (`opencode run`)下没有 TUI 监听,转发零成本;同一份事件流也会写进 traceDir
463
+ (`events.jsonl`),可作为无头观测替代。
464
+
465
+ 注意:面板渲染属交互式 TUI 行为,需在真实 TUI 里人工确认(本仓库自动化覆盖到
466
+ server 侧链路:事件发射、board 维护、RPC 契约(snapshot/detail/session)均有
467
+ 测试与 E2E 证据;journal 落盘 sessionIDs 有真机 E2E 断言)。
468
+
469
+ ## 嵌套工作流(subflow)
470
+
471
+ P2-9 起支持把另一个已注册 workflow 作为步骤运行(v1 的 `workflow()` 原语对位)。
472
+ 两条路:
473
+
474
+ **声明式**——步骤键 `subflow`(七类互斥键之一:agent / checkpoint / verify /
475
+ fileExists / subflow / pipeline / race):
476
+
477
+ ```jsonc
478
+ {
479
+ "id": "release-orchestrator",
480
+ "version": "1.0.0",
481
+ "steps": [
482
+ { "name": "notes", "subflow": "release-notes", "args": { "topic": "{{topic}}" } },
483
+ { "name": "gate", "checkpoint": "发布说明已生成({{steps.notes}}),批准?" }
484
+ ],
485
+ "output": "orchestrated: {{steps.notes}}"
486
+ }
487
+ ```
488
+
489
+ **代码式**——definition 内 `ctx.subflow(id, args)`:
490
+
491
+ ```ts
492
+ defineWorkflow({
493
+ id: "my-orchestrator",
494
+ version: "1.0.0",
495
+ stepNames: ["child"],
496
+ async run(args, ctx) {
497
+ const child = await ctx.subflow!("release-notes", { topic: args.topic })
498
+ return { output: child.output }
499
+ },
500
+ })
501
+ ```
502
+
503
+ 语义:
504
+
505
+ - **子 run 独立 journal**:`parentRunId`/`depth` 记录 lineage;进度面板把子 run
506
+ 缩进挂在父 run 下;子 run 失败按普通步骤失败处理(父 run fail-fast)
507
+ - **gate/workspace 继承**:子 run 走父 run 的审批门与工作区(隔离是顶层
508
+ run 的属性;子 agent 的 cwd 落在父 run 的 worktree)
509
+ - **深度上限 3**:超出明确报错(防失控递归;v1 自饿死事故的教训)
510
+ - **依赖 journalDir**:subflow run 必须有 journal(lineage 落盘);未配置时
511
+ 声明式步骤给出清晰报错、代码式 ctx 不提供该方法
512
+ - agent 步骤里的递归守卫不变:workflow 内的 agent 不能再调 workflow_start
513
+ (那会绕过编排);要组合流程就用 subflow
514
+
515
+ 同一机制也顺带解决了 0.4.0 的已知限制「全局 gate 单例竞态」:checkpoint
516
+ gate 与 workspace 现在经 run 级上下文(AsyncLocalStorage)解析,
517
+ scheduler 的无人值守门与 `checkpointMode` 调用级覆盖都不再换装全局单例。
518
+
519
+ ## 声明式并发步骤(pipeline / race)
520
+
521
+ 代码组合子 `pipeline()` / `race()` 的 JSON 形态(v1 parallel/race 原语对位)——
522
+ 不想写 TS 定义也能声明并发结构:
523
+
524
+ ```jsonc
525
+ {
526
+ "id": "multi-analysis",
527
+ "steps": [
528
+ // 条目并发 fan-out:items 每项是模板,解析后作为条目值;{{item}} 引用条目
529
+ { "name": "fanout", "pipeline": "分析 {{item}}(主题 {{topic}})",
530
+ "items": ["{{topic}}-甲", "{{topic}}-乙", "{{topic}}-丙"],
531
+ "outputAs": "reports", // 结果数组并入 state 的键(缺省 = 步骤名)
532
+ "onFailure": "continue", // 可选:fail-fast(缺省)| continue
533
+ "model": "glm/glm-5.3-flash", "timeoutMs": 300000, "retries": 1 },
534
+ // 竞速首胜:≥2 提示并发起跑,首个成功者胜出,全败抛 WorkflowRaceError
535
+ { "name": "fastest", "race": ["方案A:{{topic}}", "方案B:{{topic}}"] }
536
+ ],
537
+ "output": "{{steps.fanout}}" // pipeline 结果 = 与 items 对齐的数组(JSON 串)
538
+ }
539
+ ```
540
+
541
+ - **pipeline**:条目间并发(共享插件 `concurrency` 信号量)、单阶段;结果数组与
542
+ `items` 顺序对齐;`{{steps.<name>}}` 拿到 JSON 串。调用级选项
543
+ (model/timeoutMs/retries)每条目同规则透传。
544
+ - **race**:败者不拖整体(与 run 控制同一诚实语义);可选 `outputAs`。
545
+ - **resume 粒度(如实)**:两者各占一个 journal 步骤单元——completed 即整体
546
+ 跳过,中断后重跑整步(条目级断点不落盘,与 sequence 前缀语义一致)。
547
+ 需要条目级恢复就拆 subflow 步或用代码式定义。
548
+
549
+ ## 代码流程(自定义 JS 逻辑)
550
+
551
+ 声明式 JSON 表达不了的东西——**定义变量、写辅助方法、确定性计算、
552
+ 条件分支**——用代码流程模块(P2-14)。flows 目录放 `.js` / `.mjs` /
553
+ `.cjs`,与 JSON 同目录混装、同规则装载(坏文件跳过并告警、保留 id 拦截、
554
+ 同 id@version 去重):
555
+
556
+ ```js
557
+ // flows/custom-logic.mjs —— 需要确定性逻辑时的形态
558
+ import { defineWorkflow, agent } from "@mickorz/opencode-agentic-workflow/core"
559
+
560
+ const slugify = (s) => String(s).trim().replaceAll(/\s+/g, " ").replaceAll(" ", "-").toLowerCase()
561
+
562
+ export default defineWorkflow({
563
+ id: "custom-logic",
564
+ version: "1.0.0",
565
+ stepNames: ["research", "wrap"], // 静态声明:journal/resume 依赖步骤序号稳定
566
+ async run(args, ctx) {
567
+ const slug = slugify(args.topic) // ← 自定义变量 + 方法
568
+ const state = await ctx.runSteps([ // runSteps = journal 记录 + resume 前缀跳过的统一入口
569
+ async () => ({ research: (await agent(`针对「${args.topic}」写一句结论…`)).output }),
570
+ async (prev) => ({ wrap: (await agent(`原样返回:${prev?.research ?? ""}`)).output }),
571
+ ])
572
+ return { output: `${slug}: ${state.wrap}` }
573
+ },
574
+ })
575
+ ```
576
+
577
+ > ⚠️ `agent()` 返回 `AgentResult` **对象**(含 usage/model/sessionID),进模板/拼接
578
+ > 要取 `.output`——直接插值会得到 `[object Object]`(真机 E2E 实测踩到)。
579
+
580
+ 可用 API(`<pkg>/core` 导出,与内置流程同源):
581
+
582
+ | 类别 | 导出 |
583
+ |---|---|
584
+ | 定义 | `defineWorkflow`(+ `WorkflowDefinition` / `WorkflowContext` 等类型) |
585
+ | 编排 | `sequence` / `resumeSequence` / `parallel` / `pipeline` / `race` / `fallback` / `retry` / `phase` |
586
+ | 子代理 | `agent`(+ `AgentCallOptions` / `AgentResult` / `TokenUsage`) |
587
+ | 质量门 | `assert` / `verify` / `assertVerify` / `checkpoint`(+ `ReviewVerdict`) |
588
+ | 谓词/工具 | `fileExists` / `isFile` / `isDirectory` / `commandSuccess` / `getCommandRunner` |
589
+ | 并发 | `withConcurrencyLimit` |
590
+
591
+ 要点:
592
+
593
+ - **与声明式完全同权**:registry 眼里都是 `id@version` 资产——journal / resume /
594
+ 版本解析 / metrics / 面板详情与 Open Session 回放全部一致(代码流程的
595
+ sessionIDs 同样落盘)
596
+ - **journal 契约**:编排步骤统一走 `ctx.runSteps(steps, { stepNames })`——
597
+ 手动 `await` 编排不会进 journal,resume 无从谈起
598
+ - **形态约定**:模块 `export default`(或具名 `definition`)一个
599
+ `defineWorkflow({...})` 结果;`id` 唯一且不撞内置(smoke/reliable/
600
+ artifact/feature-development),结构变更必须升 `version`
601
+ - **发布前联调**:包还没装进项目时,import 可临时写编译产物绝对路径
602
+ (`…/opencode-agentic-workflow/dist/core/index.js`)
603
+ - TS 用户:先 `tsc` 出 `.js`(装载器只认 `.js/.mjs/.cjs`,不内嵌 TS 编译)
604
+
605
+ ## 并发 run(顶层多飞)
606
+
607
+ gate/workspace 的 run 级化让**并发顶层 run** 成为安全默认:同一进程可同时
608
+ 跑最多 `maxConcurrentRuns`(默认 3)个 workflow——各自独立 journal /
609
+ workspace(worktree)/ checkpoint 门 / 进度快照,agent 步共享同一并发
610
+ 信号量(公平排队,不会互相饿死)。
611
+
612
+ 递归防护(防信号量自饿死)从「任一 run 在飞即拒绝一切」改为**按调用来源
613
+ 判别**:executor 为每个 agent 任务创建的子会话在执行期内登记(`run-sessions`),
614
+ workflow 工具凭调用会话的自身/祖先链(`session.get` 的 `parentID`,封顶
615
+ 8 跳)识别「来自 run 内部的调用」并拒绝(提示改用 subflow 组合);顶层
616
+ 用户会话不在任何 run 树内,正常放行。会话身份缺失的宿主环境保守退回旧
617
+ 单飞语义。
618
+
619
+ 调度器行为不变:scheduled slot 触发时若有任一 run 在跑(含手动并发 run),
620
+ 该 slot 记 `skipped`——保持调度节奏可预期、避免并行成本意外,这是与手动
621
+ 并发上限相互独立的设计取舍。
622
+
325
623
  ## Journal 数据模型
326
624
 
327
625
  `<journalDir>/<runId>.json`(每次状态变更原子落盘):
@@ -332,6 +630,8 @@ workflow 结束时同步写 `metrics.json` 快照(失败也写)。
332
630
  "workflow": { "id": "artifact", "version": "1.0.0" }, // resume 精确版本解析依据
333
631
  "args": { "topic": "火星基地能源方案" },
334
632
  "workspace": { "provider": "git-worktree", "path": "...", "branch": "agw/run_..." },
633
+ "parentRunId": "run_1790994700000_xxxx", // P2-9 lineage:subflow 子 run 指回父 run(顶层无)
634
+ "depth": 1, // 嵌套深度(顶层 0)
335
635
  "status": "failed", // running / completed / failed / aborted
336
636
  "steps": [
337
637
  { "name": "write", "status": "completed", "startedAt": 1760000000000 },
@@ -354,7 +654,7 @@ workflow 结束时同步写 `metrics.json` 快照(失败也写)。
354
654
  | 平台 | OpenCode **V1** 插件 API | OpenCode **V2** 插件 API |
355
655
  | workflow 形态 | 主 agent **运行时生成 JS 编排脚本**(VM 沙箱) | **声明式定义** + 语义版本注册(代码内) |
356
656
  | 调用方式 | 自然语言 → 生成脚本 → 执行 | `flow=<id>` + args(或 `resumeRunId`) |
357
- | 原语 | agent / parallel / sequence / fallback / race | agent / parallel / sequence / phase + check / verify / checkpoint |
657
+ | 原语 | agent / parallel / sequence / fallback / race | agent / parallel / sequence / phase + check / verify / checkpoint + **subflow** + **pipeline/race(声明式或代码式)** |
358
658
  | 崩溃恢复 | 无 | journal + resume(completed 跳过、精确版本、幂等重放) |
359
659
  | 隔离 | 子会话 | git worktree per run(resume reattach) |
360
660
  | 观测 | TUI 进度树 | events.jsonl trace + metrics/成本聚合 |
@@ -367,7 +667,8 @@ workflow 结束时同步写 `metrics.json` 快照(失败也写)。
367
667
  中固化为 `WorkflowDefinition`(`src/workflows/` 有三个完整样例)。换来的是
368
668
  可恢复、可版本化、可审计。
369
669
  2. **一次性的动态编排仍有价值**:不需要 durable 的临时任务,直接让主 agent
370
- 自己并行开子会话完成即可(v2 明确拒绝嵌套 workflow 调用防递归死锁)。
670
+ 自己并行开子会话完成即可(workflow 内的 agent 仍不可再调 workflow 工具;
671
+ 流程级组合用 subflow 步骤 / ctx.subflow,见「嵌套工作流」一节)。
371
672
  3. OpenCode 平台自身 V1→V2 的配置/插件迁移见
372
673
  `dev-docs/guides/Migrate from V1.md`(官方指南剪藏)。
373
674
 
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Installer CLI 配置与检测工具(install / update / uninstall / doctor 共用)
3
+ *
4
+ * P2-10(详见 dev-docs/planning/P2-10-installer-cli.md)。
5
+ * v2 形态与 v1 安装器的差异:
6
+ * - plugins 数组元素是对象 { package, options }(v1 是字符串 plugin 数组)
7
+ * - 无 tui.json 二次注册(v2 TUI 面板走同一插件)
8
+ * - skills 两形态:零拷贝 config `skills: ["node_modules/<pkg>/skills"]`(locked)
9
+ * 或拷贝进原生扫描目录(global: ~/.config/opencode/skills;project: .opencode/skills)
10
+ *
11
+ * 纪律:JSONC 增量合并保留注释;写前 .bak;modify 传 undefined = 删键;
12
+ * 数组整体替换(jsonc-parser 逐下标删除语义不可靠)。
13
+ */
14
+ /** npm 包名(plugins 条目的包名形式) */
15
+ export declare const PKG_NAME = "@mickorz/opencode-agentic-workflow";
16
+ /** 包在 node_modules 下的相对路径(正斜杠形式;匹配时统一替换反斜杠) */
17
+ export declare const PKG_IN_NODE_MODULES = "node_modules/@mickorz/opencode-agentic-workflow";
18
+ /** 锁定模式的 plugins.package(相对项目根,指向编译产物目录) */
19
+ export declare const PLUGIN_LOCAL_SPEC = "./node_modules/@mickorz/opencode-agentic-workflow/dist/plugin";
20
+ /** 零拷贝 skills 数组条目(相对项目根) */
21
+ export declare const SKILLS_LOCAL_SPEC = "node_modules/@mickorz/opencode-agentic-workflow/skills";
22
+ /** 包内 skills/ 目录下的 skill 名 */
23
+ export declare const SKILL_NAMES: readonly ["workflow-authoring", "workflow-optimize"];
24
+ /** 安装器写入的 journal 目录(相对项目根;v2 相对路径以项目目录解析) */
25
+ export declare const DEFAULT_JOURNAL_DIR = ".agentic-workflow/journal";
26
+ /** 全局配置目录:优先 ~/.config/opencode,Windows 回退 %APPDATA%/opencode */
27
+ export declare function globalConfigDir(): string;
28
+ export declare function globalOpenCodeJsonPath(): string;
29
+ export declare function projectOpenCodeJsonPath(cwd: string): string;
30
+ /** v2 项目级 skill 原生扫描目录(.opencode/skills/<name>/SKILL.md) */
31
+ export declare function projectSkillsTargetDir(cwd: string): string;
32
+ /** 全局 skill 原生扫描目录 */
33
+ export declare function globalSkillsTargetDir(): string;
34
+ /** 项目 node_modules 中包本体的位置 */
35
+ export declare function projectPackageDir(cwd: string): string;
36
+ /**
37
+ * OpenCode 插件缓存中本包的候选目录(update 清缓存 best-effort 用)。
38
+ * v1 1.18.x 实测为 bun wrapper 结构;v2 未官方化——目录不存在不算错误。
39
+ */
40
+ export declare function pluginCacheDir(): string;
41
+ /** CLI 自身包根(npx 运行时位于 npx 缓存;skill 拷贝源取此处) */
42
+ export declare function cliPackageRoot(): string;
43
+ /** 模型引用("providerID/modelId")解析;不合法返回 null */
44
+ export declare function parseModelRef(ref: string): {
45
+ providerID: string;
46
+ id: string;
47
+ } | null;
48
+ export interface PluginEntryOptions {
49
+ model: {
50
+ providerID: string;
51
+ id: string;
52
+ };
53
+ agent: string;
54
+ journalDir?: string;
55
+ }
56
+ /** 构造 plugins 数组条目对象(package 字段按安装方式取包名或本地路径) */
57
+ export declare function makePluginEntry(options: PluginEntryOptions, locked: boolean): Record<string, unknown>;
58
+ /** plugins 数组条目(对象)是否为本插件的:看 package 字段(包名 / 本地路径结尾匹配) */
59
+ export declare function isPluginEntry(entry: unknown): boolean;
60
+ /** skills 数组条目(字符串)是否为本插件零拷贝路径 */
61
+ export declare function isSkillPathEntry(entry: unknown): boolean;
62
+ /** 读取已配置条目的 options(doctor 检查 model/agent 用);未安装返回 null */
63
+ export declare function installedEntryOptions(path: string): PluginEntryOptions | null;
64
+ interface LoadedConfig {
65
+ path: string;
66
+ text: string;
67
+ data: Record<string, unknown>;
68
+ }
69
+ /** 读 JSONC 文件;不存在或顶层不是对象返回 null */
70
+ export declare function readJsonc(path: string): LoadedConfig | null;
71
+ /**
72
+ * 向 plugins 数组增量写入条目对象(已有本插件条目则跳过,幂等)。
73
+ * 数组不存在时创建;写回前备份 .bak;返回是否发生了修改。
74
+ */
75
+ export declare function mergePluginEntry(path: string, entry: Record<string, unknown>): boolean;
76
+ /**
77
+ * 向 skills 数组增量写入零拷贝路径(locked 模式;已存在则跳过)。
78
+ * 返回是否发生了修改。
79
+ */
80
+ export declare function mergeSkillsEntry(path: string, skillsPath: string): boolean;
81
+ /**
82
+ * 从配置移除本插件相关条目:plugins 中匹配 isPluginEntry 的对象、
83
+ * skills 中匹配 isSkillPathEntry 的字符串。数组清空后连键删除(不留空壳)。
84
+ * 返回是否发生了修改。
85
+ */
86
+ export declare function removePluginEntries(path: string): boolean;
87
+ /** 配置移除条目后是否只剩空壳($schema 与空数组/空对象)——卸载时整文件删除 */
88
+ export declare function isShellConfig(path: string): boolean;
89
+ /** 整体删除配置文件与其 .bak(不存在则静默跳过) */
90
+ export declare function removeConfigWithBackup(path: string): void;
91
+ /**
92
+ * 收窄 @clack 交互返回值:用户取消(ctrl+c)直接退出进程。
93
+ * (isCancel 的 type guard 指向 unique symbol,泛型联合无法负向收窄,
94
+ * 因此签名收 unknown、调用方以显式类型参声明期望类型。)
95
+ */
96
+ export declare function unwrap<T>(value: unknown): T;
97
+ export type InstallKind = "global" | "project" | "locked";
98
+ export interface DetectResult {
99
+ kind: InstallKind;
100
+ /** 命中证据描述(哪个文件 / 哪个目录) */
101
+ evidence: string;
102
+ }
103
+ /** 检测三种安装方式的存在性(globalDir 供测试注入) */
104
+ export declare function detectInstalled(cwd: string, globalDir?: string): DetectResult[];
105
+ /** 读取已安装包版本:优先项目 node_modules,其次全局插件缓存;都没有返回 null */
106
+ export declare function readInstalledVersion(cwd: string): string | null;
107
+ export interface SkillCopyTarget {
108
+ name: string;
109
+ destDir: string;
110
+ }
111
+ /** skill 拷贝目标列表:global 到 ~/.config/opencode/skills,其余到 .opencode/skills */
112
+ export declare function skillTargets(cwd: string, mode: "global" | "project" | "locked"): SkillCopyTarget[];
113
+ /** 递归拷贝单个 skill 目录(目标已存在时整体替换) */
114
+ export declare function copySkill(sourceBaseDir: string, target: SkillCopyTarget): void;
115
+ /** 删除已拷贝的 skill 目录(不存在则静默跳过) */
116
+ export declare function removeSkillTarget(target: SkillCopyTarget): void;
117
+ /** 供 doctor / uninstall 判断 skill 目录是否已拷贝 */
118
+ export declare function skillTargetExists(target: SkillCopyTarget): boolean;
119
+ /** 解析用户输入的 ~ 前缀路径(提示文案用) */
120
+ export declare function expandHome(p: string): string;
121
+ export {};