@entelligentsia/forgecli 1.0.2 → 1.0.10

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 (423) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/README.md +2 -1
  3. package/dist/CHANGELOG-forge-plugin.md +124 -0
  4. package/dist/CHANGELOG-pi.md +94 -0
  5. package/dist/extensions/forgecli/audience-gate.js +1 -1
  6. package/dist/extensions/forgecli/audience-gate.js.map +1 -1
  7. package/dist/extensions/forgecli/fix-bug.d.ts +1 -2
  8. package/dist/extensions/forgecli/fix-bug.js +678 -609
  9. package/dist/extensions/forgecli/fix-bug.js.map +1 -1
  10. package/dist/extensions/forgecli/forge-artifact-tool.js +42 -7
  11. package/dist/extensions/forgecli/forge-artifact-tool.js.map +1 -1
  12. package/dist/extensions/forgecli/forge-subagent.d.ts +17 -0
  13. package/dist/extensions/forgecli/forge-subagent.js +31 -12
  14. package/dist/extensions/forgecli/forge-subagent.js.map +1 -1
  15. package/dist/extensions/forgecli/forge-tools.d.ts +6 -0
  16. package/dist/extensions/forgecli/forge-tools.js +71 -8
  17. package/dist/extensions/forgecli/forge-tools.js.map +1 -1
  18. package/dist/extensions/forgecli/run-task.js +461 -391
  19. package/dist/extensions/forgecli/run-task.js.map +1 -1
  20. package/dist/extensions/forgecli/session-registry.d.ts +12 -0
  21. package/dist/extensions/forgecli/session-registry.js +23 -0
  22. package/dist/extensions/forgecli/session-registry.js.map +1 -1
  23. package/dist/extensions/forgecli/subagent/caller-context.d.ts +35 -11
  24. package/dist/extensions/forgecli/subagent/caller-context.js +49 -21
  25. package/dist/extensions/forgecli/subagent/caller-context.js.map +1 -1
  26. package/dist/extensions/forgecli/subagent/orchestrator-transcript.d.ts +66 -0
  27. package/dist/extensions/forgecli/subagent/orchestrator-transcript.js +66 -0
  28. package/dist/extensions/forgecli/subagent/orchestrator-transcript.js.map +1 -0
  29. package/dist/extensions/forgecli/subagent/phase-guard.d.ts +34 -0
  30. package/dist/extensions/forgecli/subagent/phase-guard.js +149 -0
  31. package/dist/extensions/forgecli/subagent/phase-guard.js.map +1 -0
  32. package/dist/extensions/forgecli/subagent/phase-summary-map.d.ts +2 -0
  33. package/dist/extensions/forgecli/subagent/phase-summary-map.js +39 -0
  34. package/dist/extensions/forgecli/subagent/phase-summary-map.js.map +1 -0
  35. package/dist/extensions/forgecli/thread-switcher.js +2 -2
  36. package/dist/extensions/forgecli/thread-switcher.js.map +1 -1
  37. package/dist/extensions/forgecli/viewport-events.d.ts +4 -0
  38. package/dist/extensions/forgecli/viewport-events.js +18 -1
  39. package/dist/extensions/forgecli/viewport-events.js.map +1 -1
  40. package/dist/extensions/forgecli/viewport-renderer.d.ts +12 -2
  41. package/dist/extensions/forgecli/viewport-renderer.js +8 -6
  42. package/dist/extensions/forgecli/viewport-renderer.js.map +1 -1
  43. package/dist/forge-payload/.base-pack/workflows/_fragments/store-cli-verbs.md +18 -3
  44. package/dist/forge-payload/.base-pack/workflows/architect_approve.md +4 -5
  45. package/dist/forge-payload/.base-pack/workflows/collator_agent.md +1 -1
  46. package/dist/forge-payload/.base-pack/workflows/commit_task.md +2 -3
  47. package/dist/forge-payload/.base-pack/workflows/fix_bug.md +10 -28
  48. package/dist/forge-payload/.base-pack/workflows/implement_plan.md +3 -2
  49. package/dist/forge-payload/.base-pack/workflows/orchestrate_task.md +41 -47
  50. package/dist/forge-payload/.base-pack/workflows/triage.md +190 -0
  51. package/dist/forge-payload/.base-pack/workflows/validate_task.md +2 -3
  52. package/dist/forge-payload/.claude-plugin/plugin.json +1 -1
  53. package/dist/forge-payload/.schemas/_defs/locator.schema.json +13 -0
  54. package/dist/forge-payload/.schemas/bug.schema.json +1 -0
  55. package/dist/forge-payload/.schemas/enum-catalog.json +2 -2
  56. package/dist/forge-payload/.schemas/migrations.json +72 -0
  57. package/dist/forge-payload/.schemas/sprint.schema.json +1 -0
  58. package/dist/forge-payload/.schemas/task.schema.json +1 -0
  59. package/dist/forge-payload/integrity.json +3 -3
  60. package/dist/forge-payload/meta/fragments/tool-discipline.md +21 -2
  61. package/dist/forge-payload/meta/workflows/_fragments/store-cli-verbs.md +18 -3
  62. package/dist/forge-payload/meta/workflows/meta-approve.md +4 -5
  63. package/dist/forge-payload/meta/workflows/meta-bug-triage.md +210 -0
  64. package/dist/forge-payload/meta/workflows/meta-collate.md +1 -1
  65. package/dist/forge-payload/meta/workflows/meta-commit.md +2 -3
  66. package/dist/forge-payload/meta/workflows/meta-fix-bug.md +10 -28
  67. package/dist/forge-payload/meta/workflows/meta-implement.md +3 -2
  68. package/dist/forge-payload/meta/workflows/meta-orchestrate.md +41 -47
  69. package/dist/forge-payload/meta/workflows/meta-validate.md +2 -3
  70. package/dist/forge-payload/schemas/_defs/locator.schema.json +13 -0
  71. package/dist/forge-payload/schemas/bug.schema.json +1 -0
  72. package/dist/forge-payload/schemas/enum-catalog.json +2 -2
  73. package/dist/forge-payload/schemas/sprint.schema.json +1 -0
  74. package/dist/forge-payload/schemas/structure-manifest.json +22 -2
  75. package/dist/forge-payload/schemas/task.schema.json +1 -0
  76. package/dist/forge-payload/tools/artifact-store.cjs +242 -0
  77. package/dist/forge-payload/tools/artifact.cjs +69 -100
  78. package/dist/forge-payload/tools/lib/artifact-kinds.cjs +95 -0
  79. package/dist/forge-payload/tools/lib/store-nlp.cjs +6 -0
  80. package/dist/forge-payload/tools/lib/store-query-exec.cjs +39 -5
  81. package/dist/forge-payload/tools/lib/suggest.cjs +2 -1
  82. package/dist/forge-payload/tools/preflight-gate.cjs +55 -5
  83. package/dist/forge-payload/tools/store-cli.cjs +50 -15
  84. package/node_modules/@earendil-works/pi-agent-core/dist/harness/agent-harness.d.ts +5 -2
  85. package/node_modules/@earendil-works/pi-agent-core/dist/harness/agent-harness.d.ts.map +1 -1
  86. package/node_modules/@earendil-works/pi-agent-core/dist/harness/agent-harness.js +81 -18
  87. package/node_modules/@earendil-works/pi-agent-core/dist/harness/agent-harness.js.map +1 -1
  88. package/node_modules/@earendil-works/pi-agent-core/dist/harness/compaction/branch-summarization.d.ts.map +1 -1
  89. package/node_modules/@earendil-works/pi-agent-core/dist/harness/compaction/branch-summarization.js +1 -0
  90. package/node_modules/@earendil-works/pi-agent-core/dist/harness/compaction/branch-summarization.js.map +1 -1
  91. package/node_modules/@earendil-works/pi-agent-core/dist/harness/compaction/compaction.d.ts.map +1 -1
  92. package/node_modules/@earendil-works/pi-agent-core/dist/harness/compaction/compaction.js +19 -24
  93. package/node_modules/@earendil-works/pi-agent-core/dist/harness/compaction/compaction.js.map +1 -1
  94. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/session.d.ts +1 -0
  95. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/session.d.ts.map +1 -1
  96. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/session.js +14 -1
  97. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/session.js.map +1 -1
  98. package/node_modules/@earendil-works/pi-agent-core/dist/harness/types.d.ts +22 -8
  99. package/node_modules/@earendil-works/pi-agent-core/dist/harness/types.d.ts.map +1 -1
  100. package/node_modules/@earendil-works/pi-agent-core/dist/harness/types.js.map +1 -1
  101. package/node_modules/@earendil-works/pi-agent-core/package.json +3 -3
  102. package/node_modules/@earendil-works/pi-ai/README.md +1 -1
  103. package/node_modules/@earendil-works/pi-ai/dist/models.generated.d.ts +374 -122
  104. package/node_modules/@earendil-works/pi-ai/dist/models.generated.d.ts.map +1 -1
  105. package/node_modules/@earendil-works/pi-ai/dist/models.generated.js +424 -232
  106. package/node_modules/@earendil-works/pi-ai/dist/models.generated.js.map +1 -1
  107. package/node_modules/@earendil-works/pi-ai/dist/providers/amazon-bedrock.d.ts +1 -1
  108. package/node_modules/@earendil-works/pi-ai/dist/providers/amazon-bedrock.d.ts.map +1 -1
  109. package/node_modules/@earendil-works/pi-ai/dist/providers/amazon-bedrock.js +38 -2
  110. package/node_modules/@earendil-works/pi-ai/dist/providers/amazon-bedrock.js.map +1 -1
  111. package/node_modules/@earendil-works/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  112. package/node_modules/@earendil-works/pi-ai/dist/providers/anthropic.js +21 -12
  113. package/node_modules/@earendil-works/pi-ai/dist/providers/anthropic.js.map +1 -1
  114. package/node_modules/@earendil-works/pi-ai/dist/providers/azure-openai-responses.d.ts.map +1 -1
  115. package/node_modules/@earendil-works/pi-ai/dist/providers/azure-openai-responses.js +6 -10
  116. package/node_modules/@earendil-works/pi-ai/dist/providers/azure-openai-responses.js.map +1 -1
  117. package/node_modules/@earendil-works/pi-ai/dist/providers/google-vertex.d.ts.map +1 -1
  118. package/node_modules/@earendil-works/pi-ai/dist/providers/google-vertex.js +1 -1
  119. package/node_modules/@earendil-works/pi-ai/dist/providers/google-vertex.js.map +1 -1
  120. package/node_modules/@earendil-works/pi-ai/dist/providers/google.d.ts.map +1 -1
  121. package/node_modules/@earendil-works/pi-ai/dist/providers/google.js +5 -3
  122. package/node_modules/@earendil-works/pi-ai/dist/providers/google.js.map +1 -1
  123. package/node_modules/@earendil-works/pi-ai/dist/providers/images/openrouter.d.ts.map +1 -1
  124. package/node_modules/@earendil-works/pi-ai/dist/providers/images/openrouter.js +3 -4
  125. package/node_modules/@earendil-works/pi-ai/dist/providers/images/openrouter.js.map +1 -1
  126. package/node_modules/@earendil-works/pi-ai/dist/providers/mistral.d.ts.map +1 -1
  127. package/node_modules/@earendil-works/pi-ai/dist/providers/mistral.js +2 -3
  128. package/node_modules/@earendil-works/pi-ai/dist/providers/mistral.js.map +1 -1
  129. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-codex-responses.d.ts.map +1 -1
  130. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-codex-responses.js +159 -78
  131. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-codex-responses.js.map +1 -1
  132. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
  133. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-completions.js +16 -11
  134. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-completions.js.map +1 -1
  135. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-responses-shared.d.ts.map +1 -1
  136. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-responses-shared.js +4 -1
  137. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-responses-shared.js.map +1 -1
  138. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-responses.d.ts.map +1 -1
  139. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-responses.js +6 -10
  140. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-responses.js.map +1 -1
  141. package/node_modules/@earendil-works/pi-ai/dist/providers/simple-options.d.ts.map +1 -1
  142. package/node_modules/@earendil-works/pi-ai/dist/providers/simple-options.js +1 -0
  143. package/node_modules/@earendil-works/pi-ai/dist/providers/simple-options.js.map +1 -1
  144. package/node_modules/@earendil-works/pi-ai/dist/stream.d.ts.map +1 -1
  145. package/node_modules/@earendil-works/pi-ai/dist/stream.js +14 -2
  146. package/node_modules/@earendil-works/pi-ai/dist/stream.js.map +1 -1
  147. package/node_modules/@earendil-works/pi-ai/dist/types.d.ts +14 -4
  148. package/node_modules/@earendil-works/pi-ai/dist/types.d.ts.map +1 -1
  149. package/node_modules/@earendil-works/pi-ai/dist/types.js.map +1 -1
  150. package/node_modules/@earendil-works/pi-ai/dist/utils/abort-signals.d.ts +6 -0
  151. package/node_modules/@earendil-works/pi-ai/dist/utils/abort-signals.d.ts.map +1 -0
  152. package/node_modules/@earendil-works/pi-ai/dist/utils/abort-signals.js +34 -0
  153. package/node_modules/@earendil-works/pi-ai/dist/utils/abort-signals.js.map +1 -0
  154. package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/device-code.d.ts +9 -7
  155. package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/device-code.d.ts.map +1 -1
  156. package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/device-code.js +8 -7
  157. package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/device-code.js.map +1 -1
  158. package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/github-copilot.d.ts.map +1 -1
  159. package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/github-copilot.js +1 -1
  160. package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/github-copilot.js.map +1 -1
  161. package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/index.d.ts +1 -1
  162. package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/index.d.ts.map +1 -1
  163. package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/index.js +1 -1
  164. package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/index.js.map +1 -1
  165. package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/openai-codex.d.ts +10 -1
  166. package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/openai-codex.d.ts.map +1 -1
  167. package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/openai-codex.js +179 -79
  168. package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/openai-codex.js.map +1 -1
  169. package/node_modules/@earendil-works/pi-ai/package.json +2 -2
  170. package/node_modules/@earendil-works/pi-coding-agent/CHANGELOG.md +94 -0
  171. package/node_modules/@earendil-works/pi-coding-agent/README.md +9 -0
  172. package/node_modules/@earendil-works/pi-coding-agent/dist/cli/args.d.ts +3 -0
  173. package/node_modules/@earendil-works/pi-coding-agent/dist/cli/args.d.ts.map +1 -1
  174. package/node_modules/@earendil-works/pi-coding-agent/dist/cli/args.js +27 -0
  175. package/node_modules/@earendil-works/pi-coding-agent/dist/cli/args.js.map +1 -1
  176. package/node_modules/@earendil-works/pi-coding-agent/dist/config.d.ts.map +1 -1
  177. package/node_modules/@earendil-works/pi-coding-agent/dist/config.js +15 -2
  178. package/node_modules/@earendil-works/pi-coding-agent/dist/config.js.map +1 -1
  179. package/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session-services.d.ts +1 -0
  180. package/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session-services.d.ts.map +1 -1
  181. package/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session-services.js +1 -0
  182. package/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session-services.js.map +1 -1
  183. package/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session.d.ts +5 -1
  184. package/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  185. package/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session.js +28 -4
  186. package/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  187. package/node_modules/@earendil-works/pi-coding-agent/dist/core/compaction/compaction.d.ts.map +1 -1
  188. package/node_modules/@earendil-works/pi-coding-agent/dist/core/compaction/compaction.js +18 -24
  189. package/node_modules/@earendil-works/pi-coding-agent/dist/core/compaction/compaction.js.map +1 -1
  190. package/node_modules/@earendil-works/pi-coding-agent/dist/core/extensions/runner.d.ts +1 -1
  191. package/node_modules/@earendil-works/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  192. package/node_modules/@earendil-works/pi-coding-agent/dist/core/extensions/runner.js +8 -2
  193. package/node_modules/@earendil-works/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  194. package/node_modules/@earendil-works/pi-coding-agent/dist/core/extensions/types.d.ts +7 -5
  195. package/node_modules/@earendil-works/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  196. package/node_modules/@earendil-works/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  197. package/node_modules/@earendil-works/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  198. package/node_modules/@earendil-works/pi-coding-agent/dist/core/model-registry.js +65 -13
  199. package/node_modules/@earendil-works/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  200. package/node_modules/@earendil-works/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
  201. package/node_modules/@earendil-works/pi-coding-agent/dist/core/model-resolver.js +1 -1
  202. package/node_modules/@earendil-works/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
  203. package/node_modules/@earendil-works/pi-coding-agent/dist/core/resolve-config-value.d.ts +9 -1
  204. package/node_modules/@earendil-works/pi-coding-agent/dist/core/resolve-config-value.d.ts.map +1 -1
  205. package/node_modules/@earendil-works/pi-coding-agent/dist/core/resolve-config-value.js +134 -11
  206. package/node_modules/@earendil-works/pi-coding-agent/dist/core/resolve-config-value.js.map +1 -1
  207. package/node_modules/@earendil-works/pi-coding-agent/dist/core/sdk.d.ts +2 -0
  208. package/node_modules/@earendil-works/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
  209. package/node_modules/@earendil-works/pi-coding-agent/dist/core/sdk.js +10 -6
  210. package/node_modules/@earendil-works/pi-coding-agent/dist/core/sdk.js.map +1 -1
  211. package/node_modules/@earendil-works/pi-coding-agent/dist/core/session-manager.d.ts +6 -7
  212. package/node_modules/@earendil-works/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
  213. package/node_modules/@earendil-works/pi-coding-agent/dist/core/session-manager.js +75 -28
  214. package/node_modules/@earendil-works/pi-coding-agent/dist/core/session-manager.js.map +1 -1
  215. package/node_modules/@earendil-works/pi-coding-agent/dist/core/settings-manager.d.ts +2 -0
  216. package/node_modules/@earendil-works/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  217. package/node_modules/@earendil-works/pi-coding-agent/dist/core/settings-manager.js +14 -9
  218. package/node_modules/@earendil-works/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  219. package/node_modules/@earendil-works/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
  220. package/node_modules/@earendil-works/pi-coding-agent/dist/core/system-prompt.js +0 -3
  221. package/node_modules/@earendil-works/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
  222. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/edit.d.ts.map +1 -1
  223. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/edit.js +7 -10
  224. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/edit.js.map +1 -1
  225. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/find.d.ts.map +1 -1
  226. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/find.js.map +1 -1
  227. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/grep.d.ts.map +1 -1
  228. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/grep.js.map +1 -1
  229. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/ls.d.ts.map +1 -1
  230. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/ls.js +5 -7
  231. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/ls.js.map +1 -1
  232. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/read.d.ts.map +1 -1
  233. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/read.js +6 -7
  234. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/read.js.map +1 -1
  235. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/render-utils.d.ts +5 -2
  236. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/render-utils.d.ts.map +1 -1
  237. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/render-utils.js +17 -1
  238. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/render-utils.js.map +1 -1
  239. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/write.d.ts.map +1 -1
  240. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/write.js +5 -6
  241. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/write.js.map +1 -1
  242. package/node_modules/@earendil-works/pi-coding-agent/dist/index.d.ts +2 -0
  243. package/node_modules/@earendil-works/pi-coding-agent/dist/index.d.ts.map +1 -1
  244. package/node_modules/@earendil-works/pi-coding-agent/dist/index.js +2 -0
  245. package/node_modules/@earendil-works/pi-coding-agent/dist/index.js.map +1 -1
  246. package/node_modules/@earendil-works/pi-coding-agent/dist/main.d.ts.map +1 -1
  247. package/node_modules/@earendil-works/pi-coding-agent/dist/main.js +69 -16
  248. package/node_modules/@earendil-works/pi-coding-agent/dist/main.js.map +1 -1
  249. package/node_modules/@earendil-works/pi-coding-agent/dist/migrations.d.ts.map +1 -1
  250. package/node_modules/@earendil-works/pi-coding-agent/dist/migrations.js +118 -1
  251. package/node_modules/@earendil-works/pi-coding-agent/dist/migrations.js.map +1 -1
  252. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/login-dialog.d.ts +1 -3
  253. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  254. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/login-dialog.js +2 -4
  255. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/login-dialog.js.map +1 -1
  256. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  257. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/user-message.js +1 -1
  258. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/user-message.js.map +1 -1
  259. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +3 -0
  260. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  261. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.js +59 -6
  262. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  263. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  264. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/theme/theme.js +10 -0
  265. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/theme/theme.js.map +1 -1
  266. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  267. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/rpc/rpc-mode.js +3 -1
  268. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/rpc/rpc-mode.js.map +1 -1
  269. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts +1 -0
  270. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  271. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/rpc/rpc-types.js.map +1 -1
  272. package/node_modules/@earendil-works/pi-coding-agent/dist/utils/deprecation.d.ts +4 -0
  273. package/node_modules/@earendil-works/pi-coding-agent/dist/utils/deprecation.d.ts.map +1 -0
  274. package/node_modules/@earendil-works/pi-coding-agent/dist/utils/deprecation.js +13 -0
  275. package/node_modules/@earendil-works/pi-coding-agent/dist/utils/deprecation.js.map +1 -0
  276. package/node_modules/@earendil-works/pi-coding-agent/dist/utils/json.d.ts +3 -0
  277. package/node_modules/@earendil-works/pi-coding-agent/dist/utils/json.d.ts.map +1 -0
  278. package/node_modules/@earendil-works/pi-coding-agent/dist/utils/json.js +7 -0
  279. package/node_modules/@earendil-works/pi-coding-agent/dist/utils/json.js.map +1 -0
  280. package/node_modules/@earendil-works/pi-coding-agent/docs/custom-provider.md +13 -10
  281. package/node_modules/@earendil-works/pi-coding-agent/docs/development.md +1 -1
  282. package/node_modules/@earendil-works/pi-coding-agent/docs/extensions.md +12 -6
  283. package/node_modules/@earendil-works/pi-coding-agent/docs/models.md +25 -12
  284. package/node_modules/@earendil-works/pi-coding-agent/docs/providers.md +13 -5
  285. package/node_modules/@earendil-works/pi-coding-agent/docs/quickstart.md +1 -0
  286. package/node_modules/@earendil-works/pi-coding-agent/docs/rpc.md +2 -1
  287. package/node_modules/@earendil-works/pi-coding-agent/docs/sdk.md +6 -0
  288. package/node_modules/@earendil-works/pi-coding-agent/docs/session-format.md +1 -1
  289. package/node_modules/@earendil-works/pi-coding-agent/docs/sessions.md +8 -0
  290. package/node_modules/@earendil-works/pi-coding-agent/docs/settings.md +7 -3
  291. package/node_modules/@earendil-works/pi-coding-agent/docs/terminal-setup.md +2 -0
  292. package/node_modules/@earendil-works/pi-coding-agent/docs/tui.md +2 -2
  293. package/node_modules/@earendil-works/pi-coding-agent/docs/usage.md +9 -0
  294. package/node_modules/@earendil-works/pi-coding-agent/examples/extensions/README.md +1 -0
  295. package/node_modules/@earendil-works/pi-coding-agent/examples/extensions/custom-provider-anthropic/index.ts +1 -1
  296. package/node_modules/@earendil-works/pi-coding-agent/examples/extensions/custom-provider-anthropic/package.json +1 -1
  297. package/node_modules/@earendil-works/pi-coding-agent/examples/extensions/custom-provider-gitlab-duo/index.ts +54 -3
  298. package/node_modules/@earendil-works/pi-coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  299. package/node_modules/@earendil-works/pi-coding-agent/examples/extensions/git-merge-and-resolve.ts +115 -0
  300. package/node_modules/@earendil-works/pi-coding-agent/examples/extensions/input-transform-streaming.ts +39 -0
  301. package/node_modules/@earendil-works/pi-coding-agent/examples/extensions/sandbox/package.json +1 -1
  302. package/node_modules/@earendil-works/pi-coding-agent/examples/extensions/with-deps/package.json +1 -1
  303. package/node_modules/@earendil-works/pi-coding-agent/npm-shrinkwrap.json +443 -61
  304. package/node_modules/@earendil-works/pi-coding-agent/package.json +6 -6
  305. package/node_modules/@earendil-works/pi-tui/README.md +2 -2
  306. package/node_modules/@earendil-works/pi-tui/dist/components/editor.d.ts.map +1 -1
  307. package/node_modules/@earendil-works/pi-tui/dist/components/editor.js +24 -83
  308. package/node_modules/@earendil-works/pi-tui/dist/components/editor.js.map +1 -1
  309. package/node_modules/@earendil-works/pi-tui/dist/components/input.d.ts.map +1 -1
  310. package/node_modules/@earendil-works/pi-tui/dist/components/input.js +7 -55
  311. package/node_modules/@earendil-works/pi-tui/dist/components/input.js.map +1 -1
  312. package/node_modules/@earendil-works/pi-tui/dist/components/markdown.d.ts +7 -1
  313. package/node_modules/@earendil-works/pi-tui/dist/components/markdown.d.ts.map +1 -1
  314. package/node_modules/@earendil-works/pi-tui/dist/components/markdown.js +12 -2
  315. package/node_modules/@earendil-works/pi-tui/dist/components/markdown.js.map +1 -1
  316. package/node_modules/@earendil-works/pi-tui/dist/index.d.ts +1 -1
  317. package/node_modules/@earendil-works/pi-tui/dist/index.d.ts.map +1 -1
  318. package/node_modules/@earendil-works/pi-tui/dist/index.js.map +1 -1
  319. package/node_modules/@earendil-works/pi-tui/dist/terminal-image.d.ts +1 -1
  320. package/node_modules/@earendil-works/pi-tui/dist/terminal-image.d.ts.map +1 -1
  321. package/node_modules/@earendil-works/pi-tui/dist/terminal-image.js +34 -7
  322. package/node_modules/@earendil-works/pi-tui/dist/terminal-image.js.map +1 -1
  323. package/node_modules/@earendil-works/pi-tui/dist/terminal.d.ts +33 -10
  324. package/node_modules/@earendil-works/pi-tui/dist/terminal.d.ts.map +1 -1
  325. package/node_modules/@earendil-works/pi-tui/dist/terminal.js +172 -37
  326. package/node_modules/@earendil-works/pi-tui/dist/terminal.js.map +1 -1
  327. package/node_modules/@earendil-works/pi-tui/dist/utils.d.ts +6 -1
  328. package/node_modules/@earendil-works/pi-tui/dist/utils.d.ts.map +1 -1
  329. package/node_modules/@earendil-works/pi-tui/dist/utils.js +27 -15
  330. package/node_modules/@earendil-works/pi-tui/dist/utils.js.map +1 -1
  331. package/node_modules/@earendil-works/pi-tui/dist/word-navigation.d.ts +25 -0
  332. package/node_modules/@earendil-works/pi-tui/dist/word-navigation.d.ts.map +1 -0
  333. package/node_modules/@earendil-works/pi-tui/dist/word-navigation.js +96 -0
  334. package/node_modules/@earendil-works/pi-tui/dist/word-navigation.js.map +1 -0
  335. package/node_modules/@earendil-works/pi-tui/package.json +2 -2
  336. package/node_modules/@entelligentsia/forge-compress/LICENSE +21 -0
  337. package/node_modules/@entelligentsia/forge-compress/README.md +85 -0
  338. package/node_modules/@entelligentsia/forge-compress/dist/compressor.d.ts +6 -0
  339. package/node_modules/@entelligentsia/forge-compress/dist/compressor.js +137 -0
  340. package/node_modules/@entelligentsia/forge-compress/dist/entropy.d.ts +3 -0
  341. package/node_modules/@entelligentsia/forge-compress/dist/entropy.js +99 -0
  342. package/node_modules/@entelligentsia/forge-compress/dist/forge/entity.d.ts +8 -0
  343. package/node_modules/@entelligentsia/forge-compress/dist/forge/entity.js +149 -0
  344. package/node_modules/@entelligentsia/forge-compress/dist/forge/index.d.ts +7 -0
  345. package/node_modules/@entelligentsia/forge-compress/dist/forge/index.js +4 -0
  346. package/node_modules/@entelligentsia/forge-compress/dist/forge/markdown.d.ts +5 -0
  347. package/node_modules/@entelligentsia/forge-compress/dist/forge/markdown.js +92 -0
  348. package/node_modules/@entelligentsia/forge-compress/dist/forge/query.d.ts +7 -0
  349. package/node_modules/@entelligentsia/forge-compress/dist/forge/query.js +60 -0
  350. package/node_modules/@entelligentsia/forge-compress/dist/forge/validate.d.ts +1 -0
  351. package/node_modules/@entelligentsia/forge-compress/dist/forge/validate.js +82 -0
  352. package/node_modules/@entelligentsia/forge-compress/dist/index.d.ts +6 -0
  353. package/node_modules/@entelligentsia/forge-compress/dist/index.js +5 -0
  354. package/node_modules/@entelligentsia/forge-compress/dist/progressive.d.ts +1 -0
  355. package/node_modules/@entelligentsia/forge-compress/dist/progressive.js +108 -0
  356. package/node_modules/@entelligentsia/forge-compress/dist/strip.d.ts +4 -0
  357. package/node_modules/@entelligentsia/forge-compress/dist/strip.js +55 -0
  358. package/node_modules/@entelligentsia/forge-compress/dist/tokens.d.ts +2 -0
  359. package/node_modules/@entelligentsia/forge-compress/dist/tokens.js +17 -0
  360. package/node_modules/@entelligentsia/forge-compress/package.json +45 -0
  361. package/node_modules/@entelligentsia/forge-compress/src/__tests__/compress.test.ts +409 -0
  362. package/node_modules/@entelligentsia/forge-compress/src/compressor.ts +147 -0
  363. package/node_modules/@entelligentsia/forge-compress/src/entropy.ts +105 -0
  364. package/node_modules/@entelligentsia/forge-compress/src/forge/entity.ts +184 -0
  365. package/node_modules/@entelligentsia/forge-compress/src/forge/index.ts +10 -0
  366. package/node_modules/@entelligentsia/forge-compress/src/forge/markdown.ts +122 -0
  367. package/node_modules/@entelligentsia/forge-compress/src/forge/query.ts +105 -0
  368. package/node_modules/@entelligentsia/forge-compress/src/forge/validate.ts +86 -0
  369. package/node_modules/@entelligentsia/forge-compress/src/index.ts +22 -0
  370. package/node_modules/@entelligentsia/forge-compress/src/progressive.ts +123 -0
  371. package/node_modules/@entelligentsia/forge-compress/src/strip.ts +58 -0
  372. package/node_modules/@entelligentsia/forge-compress/src/tokens.ts +19 -0
  373. package/node_modules/@mariozechner/clipboard/Cargo.toml +3 -3
  374. package/node_modules/@mariozechner/clipboard/index.d.ts +34 -20
  375. package/node_modules/@mariozechner/clipboard/index.js +546 -257
  376. package/node_modules/@mariozechner/clipboard/package.json +5 -6
  377. package/node_modules/@mariozechner/clipboard/package.json.prepack-backup +14 -14
  378. package/node_modules/@mariozechner/clipboard/src/lib.rs +4 -9
  379. package/node_modules/@mariozechner/clipboard-linux-x64-gnu/clipboard.linux-x64-gnu.node +0 -0
  380. package/node_modules/@mariozechner/clipboard-linux-x64-gnu/package.json +2 -2
  381. package/package.json +11 -16
  382. package/dist/forge-payload/.base-pack/commands/quiz-agent.md +0 -6
  383. package/dist/forge-payload/.base-pack/commands/retrospective.md +0 -6
  384. package/dist/forge-payload/.base-pack/commands/sprint-intake.md +0 -6
  385. package/dist/forge-payload/.base-pack/commands/sprint-plan.md +0 -6
  386. package/dist/forge-payload/commands/calibrate.md +0 -10
  387. package/dist/forge-payload/commands/materialize.md +0 -119
  388. package/dist/forge-payload/commands/migrate.md +0 -12
  389. package/dist/forge-payload/commands/quiz-agent.md +0 -6
  390. package/dist/forge-payload/commands/regenerate.md +0 -6
  391. package/dist/forge-payload/commands/store-query.md +0 -6
  392. package/dist/forge-payload/commands/store-repair.md +0 -6
  393. package/dist/forge-payload/commands/update-tools.md +0 -10
  394. package/dist/forge-payload/meta/templates/meta-retrospective.md +0 -28
  395. package/dist/forge-payload/tools/prompts/sprint-plan-prompt.md +0 -70
  396. package/dist/forge-payload/tools/schemas/task-list.schema.json +0 -53
  397. package/node_modules/@earendil-works/pi-agent-core/dist/harness/execution-env.d.ts +0 -4
  398. package/node_modules/@earendil-works/pi-agent-core/dist/harness/execution-env.d.ts.map +0 -1
  399. package/node_modules/@earendil-works/pi-agent-core/dist/harness/execution-env.js +0 -3
  400. package/node_modules/@earendil-works/pi-agent-core/dist/harness/execution-env.js.map +0 -1
  401. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/repo/jsonl.d.ts +0 -20
  402. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/repo/jsonl.d.ts.map +0 -1
  403. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/repo/jsonl.js +0 -92
  404. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/repo/jsonl.js.map +0 -1
  405. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/repo/memory.d.ts +0 -18
  406. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/repo/memory.d.ts.map +0 -1
  407. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/repo/memory.js +0 -42
  408. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/repo/memory.js.map +0 -1
  409. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/repo/shared.d.ts +0 -10
  410. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/repo/shared.d.ts.map +0 -1
  411. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/repo/shared.js +0 -31
  412. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/repo/shared.js.map +0 -1
  413. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/storage/jsonl.d.ts +0 -30
  414. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/storage/jsonl.d.ts.map +0 -1
  415. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/storage/jsonl.js +0 -170
  416. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/storage/jsonl.js.map +0 -1
  417. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/storage/memory.d.ts +0 -26
  418. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/storage/memory.d.ts.map +0 -1
  419. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/storage/memory.js +0 -90
  420. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/storage/memory.js.map +0 -1
  421. package/node_modules/@mariozechner/clipboard-linux-x64-musl/README.md +0 -3
  422. package/node_modules/@mariozechner/clipboard-linux-x64-musl/clipboard.linux-x64-musl.node +0 -0
  423. package/node_modules/@mariozechner/clipboard-linux-x64-musl/package.json +0 -25
@@ -29,6 +29,7 @@ import { isStateStale as isJsonStateStale, readJsonState, taskStateFilePath, wri
29
29
  import { resolveModelForPhase } from "./model-resolver.js";
30
30
  import { loadWorkflow } from "./parsers/workflow-loader.js";
31
31
  import { getSessionRegistry } from "./session-registry.js";
32
+ import { OrchestratorTranscriptWriter } from "./subagent/orchestrator-transcript.js";
32
33
  import { resolveToCanonicalId, resolveToolDir } from "./store-resolver.js";
33
34
  import { attachViewportObserver } from "./viewport-events.js";
34
35
  import { fmtPhaseSummary } from "./viewport-renderer.js";
@@ -420,463 +421,532 @@ export async function runTaskPipeline(opts) {
420
421
  // on OpenAI. Falls back to a task-scoped key if the task is unattached.
421
422
  const taskRecordAtStart = readTaskRecord(taskId, storeCli, cwd);
422
423
  const cacheSessionId = taskRecordAtStart?.sprintId ? `forge:${taskRecordAtStart.sprintId}` : `forge:task:${taskId}`;
423
- while (currentPhaseIndex < PHASES.length) {
424
- // ── Between-phase cancellation gate ────────────────────────────
425
- if (opts.signal?.aborted) {
426
- ctx.ui.notify(`⊘ forge:run-task — ${taskId} cancelled by user.`, "info");
427
- registry.completePhase(taskId, PHASES[currentPhaseIndex]?.role ?? "unknown", "cancelled");
428
- registry.confirmCancelled(taskId);
429
- // ADR-S21-01: preserve state file so cancelled runs are resumable
430
- // from the beginning of the cancelled phase (not deleted).
431
- writeState(cwd, {
432
- taskId,
433
- phaseIndex: currentPhaseIndex,
434
- iterationCounts,
435
- halted: false,
436
- status: "cancelled",
437
- lastError: undefined,
438
- savedAt: new Date().toISOString(),
439
- });
440
- return { status: "cancelled", lastPhaseIndex: currentPhaseIndex, iterationCounts };
441
- }
442
- const phase = PHASES[currentPhaseIndex];
443
- if (!phase) {
444
- ctx.ui.notify(`× forge:run-task — invalid phase index ${currentPhaseIndex}`, "error");
445
- return {
446
- status: "failed",
447
- lastPhaseIndex: currentPhaseIndex,
448
- iterationCounts,
449
- lastError: `invalid phase index ${currentPhaseIndex}`,
450
- };
451
- }
452
- ctx.ui.setStatus?.(STATUS_KEY, `run-task ${taskId}: phase ${currentPhaseIndex + 1}/${PHASES.length} (${phase.role})`);
453
- ctx.ui.notify(`→ ${taskId}: ${phase.role} (phase ${currentPhaseIndex + 1}/${PHASES.length})`, "info");
454
- const subWorkflowPath = path.join(cwd, ".forge", "workflows", `${phase.workflowFile}.md`);
455
- // ── Read sub-workflow ─────────────────────────────────────────
456
- let subWorkflowMd;
457
- let subWorkflowAudience = "any";
458
- try {
459
- const loaded = loadWorkflow(subWorkflowPath);
460
- subWorkflowMd = loaded.rawMarkdown;
461
- subWorkflowAudience = loaded.audience;
462
- }
463
- catch (err) {
464
- const e = err;
465
- ctx.ui.notify(`× forge:run-task — failed to read sub-workflow for ${phase.role}: ${e.message ?? "unknown"}`, "error");
466
- writeState(cwd, {
467
- taskId,
424
+ // ── Orchestrator transcript ──────────────────────────────────────────
425
+ // FORGE-BUG-040 follow-up: one JSONL file per pipeline run, ISO-prefixed
426
+ // in its filename so review-loop iterations preserve their own logs
427
+ // instead of overwriting. Captures every ctx.ui.notify line plus
428
+ // structured phase-boundary events.
429
+ const orchTranscript = new OrchestratorTranscriptWriter({
430
+ cwd,
431
+ entityKind: "task",
432
+ entityId: taskId,
433
+ });
434
+ const __origNotify = ctx.ui.notify.bind(ctx.ui);
435
+ ctx.ui.notify = ((msg, level) => {
436
+ __origNotify(msg, level);
437
+ orchTranscript.record({
438
+ kind: "notify",
439
+ ts: new Date().toISOString(),
440
+ level: (level ?? "info"),
441
+ message: typeof msg === "string" ? msg : String(msg),
442
+ });
443
+ });
444
+ const pipelineStartMs = Date.now();
445
+ try {
446
+ while (currentPhaseIndex < PHASES.length) {
447
+ // ── Between-phase cancellation gate ────────────────────────────
448
+ if (opts.signal?.aborted) {
449
+ ctx.ui.notify(`⊘ forge:run-task — ${taskId} cancelled by user.`, "info");
450
+ registry.completePhase(taskId, PHASES[currentPhaseIndex]?.role ?? "unknown", "cancelled");
451
+ registry.confirmCancelled(taskId);
452
+ // ADR-S21-01: preserve state file so cancelled runs are resumable
453
+ // from the beginning of the cancelled phase (not deleted).
454
+ writeState(cwd, {
455
+ taskId,
456
+ phaseIndex: currentPhaseIndex,
457
+ iterationCounts,
458
+ halted: false,
459
+ status: "cancelled",
460
+ lastError: undefined,
461
+ savedAt: new Date().toISOString(),
462
+ });
463
+ return { status: "cancelled", lastPhaseIndex: currentPhaseIndex, iterationCounts };
464
+ }
465
+ const phase = PHASES[currentPhaseIndex];
466
+ if (!phase) {
467
+ ctx.ui.notify( forge:run-task — invalid phase index ${currentPhaseIndex}`, "error");
468
+ return {
469
+ status: "failed",
470
+ lastPhaseIndex: currentPhaseIndex,
471
+ iterationCounts,
472
+ lastError: `invalid phase index ${currentPhaseIndex}`,
473
+ };
474
+ }
475
+ ctx.ui.setStatus?.(STATUS_KEY, `run-task ${taskId}: phase ${currentPhaseIndex + 1}/${PHASES.length} (${phase.role})`);
476
+ ctx.ui.notify(`→ ${taskId}: ${phase.role} (phase ${currentPhaseIndex + 1}/${PHASES.length})`, "info");
477
+ orchTranscript.record({
478
+ kind: "phase-start",
479
+ ts: new Date().toISOString(),
480
+ phase: phase.role,
468
481
  phaseIndex: currentPhaseIndex,
469
- iterationCounts,
470
- halted: true,
471
- lastError: `sub-workflow read failed: ${e.message ?? "unknown"}`,
472
- savedAt: new Date().toISOString(),
482
+ phaseCount: PHASES.length,
483
+ attempt: (iterationCounts[phase.role] ?? 0) + 1,
484
+ workflowFile: phase.workflowFile,
485
+ persona: phase.personaNoun,
473
486
  });
474
- return {
475
- status: "failed",
476
- lastPhaseIndex: currentPhaseIndex,
477
- iterationCounts,
478
- lastError: `sub-workflow read failed: ${e.message ?? "unknown"}`,
479
- };
480
- }
481
- // ── 6a. Preflight gate ────────────────────────────────────────
482
- if (fs.existsSync(preflightGate)) {
483
- const preflightResult = runPreflightGate(preflightGate, phase.role, taskId, cwd);
484
- if (preflightResult === "halt") {
485
- ctx.ui.notify(`× forge:run-task — preflight gate failed for phase ${phase.role} (exit 1); halting.`, "error");
487
+ const subWorkflowPath = path.join(cwd, ".forge", "workflows", `${phase.workflowFile}.md`);
488
+ // ── Read sub-workflow ─────────────────────────────────────────
489
+ let subWorkflowMd;
490
+ let subWorkflowAudience = "any";
491
+ try {
492
+ const loaded = loadWorkflow(subWorkflowPath);
493
+ subWorkflowMd = loaded.rawMarkdown;
494
+ subWorkflowAudience = loaded.audience;
495
+ }
496
+ catch (err) {
497
+ const e = err;
498
+ ctx.ui.notify(`× forge:run-task — failed to read sub-workflow for ${phase.role}: ${e.message ?? "unknown"}`, "error");
486
499
  writeState(cwd, {
487
500
  taskId,
488
501
  phaseIndex: currentPhaseIndex,
489
502
  iterationCounts,
490
503
  halted: true,
491
- lastError: `preflight gate exit 1 for ${phase.role}`,
504
+ lastError: `sub-workflow read failed: ${e.message ?? "unknown"}`,
492
505
  savedAt: new Date().toISOString(),
493
506
  });
494
507
  return {
495
- status: "halted",
508
+ status: "failed",
509
+ lastPhaseIndex: currentPhaseIndex,
510
+ iterationCounts,
511
+ lastError: `sub-workflow read failed: ${e.message ?? "unknown"}`,
512
+ };
513
+ }
514
+ // ── 6a. Preflight gate ────────────────────────────────────────
515
+ if (fs.existsSync(preflightGate)) {
516
+ const preflightResult = runPreflightGate(preflightGate, phase.role, taskId, cwd);
517
+ if (preflightResult === "halt") {
518
+ ctx.ui.notify(`× forge:run-task — preflight gate failed for phase ${phase.role} (exit 1); halting.`, "error");
519
+ writeState(cwd, {
520
+ taskId,
521
+ phaseIndex: currentPhaseIndex,
522
+ iterationCounts,
523
+ halted: true,
524
+ lastError: `preflight gate exit 1 for ${phase.role}`,
525
+ savedAt: new Date().toISOString(),
526
+ });
527
+ return {
528
+ status: "halted",
529
+ lastPhaseIndex: currentPhaseIndex,
530
+ iterationCounts,
531
+ lastError: `preflight gate exit 1 for ${phase.role}`,
532
+ };
533
+ }
534
+ if (preflightResult === "escalate") {
535
+ ctx.ui.notify(`× forge:run-task — preflight gate escalated for phase ${phase.role} (exit 2); manual intervention required.`, "error");
536
+ writeState(cwd, {
537
+ taskId,
538
+ phaseIndex: currentPhaseIndex,
539
+ iterationCounts,
540
+ halted: true,
541
+ lastError: `preflight gate exit 2 (escalate) for ${phase.role}`,
542
+ savedAt: new Date().toISOString(),
543
+ });
544
+ return {
545
+ status: "escalated",
546
+ lastPhaseIndex: currentPhaseIndex,
547
+ iterationCounts,
548
+ lastError: `preflight gate exit 2 (escalate) for ${phase.role}`,
549
+ };
550
+ }
551
+ }
552
+ // ── 6. Materialization-marker check ───────────────────────────
553
+ const markerCheck = checkMaterialization(subWorkflowPath, subWorkflowMd);
554
+ if (!markerCheck.ok) {
555
+ for (const marker of markerCheck.missing) {
556
+ ctx.ui.notify(`× workflow regression: ${marker} not found in ${subWorkflowPath}`, "error");
557
+ }
558
+ return {
559
+ status: "failed",
496
560
  lastPhaseIndex: currentPhaseIndex,
497
561
  iterationCounts,
498
- lastError: `preflight gate exit 1 for ${phase.role}`,
562
+ lastError: `materialization markers missing: ${markerCheck.missing.join(", ")}`,
499
563
  };
500
564
  }
501
- if (preflightResult === "escalate") {
502
- ctx.ui.notify(`× forge:run-task preflight gate escalated for phase ${phase.role} (exit 2); manual intervention required.`, "error");
565
+ // ── 5. Audience check ─────────────────────────────────────────
566
+ // Wrap with CallerContextStore.asSubagent so assertAudience treats
567
+ // this as a subagent context (IL10: we ARE dispatching from subagent chain).
568
+ const audienceOk = CallerContextStore.asSubagent(phase.role, () => assertAudience({ workflowName: phase.workflowFile, audience: subWorkflowAudience }, ctx));
569
+ if (!audienceOk) {
503
570
  writeState(cwd, {
504
571
  taskId,
505
572
  phaseIndex: currentPhaseIndex,
506
573
  iterationCounts,
507
574
  halted: true,
508
- lastError: `preflight gate exit 2 (escalate) for ${phase.role}`,
575
+ lastError: `audience check failed for ${phase.workflowFile}`,
509
576
  savedAt: new Date().toISOString(),
510
577
  });
511
578
  return {
512
- status: "escalated",
579
+ status: "failed",
513
580
  lastPhaseIndex: currentPhaseIndex,
514
581
  iterationCounts,
515
- lastError: `preflight gate exit 2 (escalate) for ${phase.role}`,
582
+ lastError: `audience check failed for ${phase.workflowFile}`,
516
583
  };
517
584
  }
518
- }
519
- // ── 6. Materialization-marker check ───────────────────────────
520
- const markerCheck = checkMaterialization(subWorkflowPath, subWorkflowMd);
521
- if (!markerCheck.ok) {
522
- for (const marker of markerCheck.missing) {
523
- ctx.ui.notify(`× workflow regression: ${marker} not found in ${subWorkflowPath}`, "error");
524
- }
525
- return {
526
- status: "failed",
527
- lastPhaseIndex: currentPhaseIndex,
528
- iterationCounts,
529
- lastError: `materialization markers missing: ${markerCheck.missing.join(", ")}`,
530
- };
531
- }
532
- // ── 5. Audience check ─────────────────────────────────────────
533
- // Wrap with CallerContextStore.asSubagent so assertAudience treats
534
- // this as a subagent context (IL10: we ARE dispatching from subagent chain).
535
- const audienceOk = CallerContextStore.asSubagent(() => assertAudience({ workflowName: phase.workflowFile, audience: subWorkflowAudience }, ctx));
536
- if (!audienceOk) {
537
- writeState(cwd, {
538
- taskId,
539
- phaseIndex: currentPhaseIndex,
540
- iterationCounts,
541
- halted: true,
542
- lastError: `audience check failed for ${phase.workflowFile}`,
543
- savedAt: new Date().toISOString(),
544
- });
545
- return {
546
- status: "failed",
547
- lastPhaseIndex: currentPhaseIndex,
548
- iterationCounts,
549
- lastError: `audience check failed for ${phase.workflowFile}`,
550
- };
551
- }
552
- // ── Persona load ──────────────────────────────────────────────
553
- let persona;
554
- try {
555
- persona = loadForgePersona(phase.personaNoun, cwd);
556
- }
557
- catch (err) {
558
- const e = err;
559
- ctx.ui.notify(`× forge:run-task — persona '${phase.personaNoun}' not found for phase ${phase.role}: ${e.message ?? "unknown"}. ` +
560
- "Run /forge:regenerate to materialize persona files.", "error");
561
- writeState(cwd, {
562
- taskId,
563
- phaseIndex: currentPhaseIndex,
564
- iterationCounts,
565
- halted: true,
566
- lastError: `persona load failed: ${e.message ?? "unknown"}`,
567
- savedAt: new Date().toISOString(),
568
- });
569
- return {
570
- status: "failed",
571
- lastPhaseIndex: currentPhaseIndex,
572
- iterationCounts,
573
- lastError: `persona load failed: ${e.message ?? "unknown"}`,
574
- };
575
- }
576
- // ── 4. Dispatch via runForgeSubagent (IL10) ───────────────────
577
- // NEVER sendKickoff here — that would reproduce issue #30 (same-context inline = no fork).
578
- // Read fresh task record to carry forward prior phase summaries (forge-cli#19).
579
- const taskRecordForSummaries = currentPhaseIndex > 0 ? readTaskRecord(taskId, storeCli, cwd) : null;
580
- const summariesBlock = buildSummariesBlock(taskRecordForSummaries?.summaries);
581
- const taskBody = composeTaskBody(subWorkflowMd, taskId, summariesBlock || undefined);
582
- // Log whether carry-forward summaries were injected (forge-cli#19).
583
- if (summariesBlock) {
584
- const debugCarryPath = path.join(cwd, ".forge", "cache", `run-task-debug-${taskId}.jsonl`);
585
- try {
586
- fs.mkdirSync(path.dirname(debugCarryPath), { recursive: true });
587
- fs.appendFileSync(debugCarryPath, `${JSON.stringify({ ts: new Date().toISOString(), phase: phase.role, kind: "carry_forward_injected", summariesLength: summariesBlock.length, summariesBlock })}\n`, "utf8");
588
- }
589
- catch { /* best-effort debug log */ }
590
- }
591
- // Resolve per-phase model from layered config (Plan 16 Slice 2).
592
- // Pipeline name "default" matches the Forge plugin's shipped pipeline.
593
- // When config is absent or cascade bottoms out, resolves to inherit
594
- // (model: undefined) — setModel is skipped and pi's current model is used.
595
- const modelResolution = resolveModelForPhase("default", phase.role, phase.personaNoun, modelRoutingConfig);
596
- const phaseStart = Date.now();
597
- // Stabilization debug log — every subagent event appended as JSONL.
598
- const debugLogPath = path.join(cwd, ".forge", "cache", `run-task-debug-${taskId}.jsonl`);
599
- const writeDebug = (rec) => {
585
+ // ── Persona load ──────────────────────────────────────────────
586
+ let persona;
600
587
  try {
601
- fs.mkdirSync(path.dirname(debugLogPath), { recursive: true });
602
- fs.appendFileSync(debugLogPath, `${JSON.stringify({ ts: new Date().toISOString(), phase: phase.role, ...rec })}\n`, "utf8");
588
+ persona = loadForgePersona(phase.personaNoun, cwd);
603
589
  }
604
- catch {
605
- // non-fatal; debug log is best-effort
606
- }
607
- };
608
- writeDebug({ kind: "phase_start", phaseIndex: currentPhaseIndex });
609
- writeDebug({
610
- kind: "requested_model",
611
- requested: modelResolution.model ?? null,
612
- source: modelResolution.source,
613
- persona: phase.personaNoun,
614
- });
615
- registry.startPhase(taskId, phase.role, currentPhaseIndex);
616
- // Capture the first stream-observed model on turn_end (IL10 visibility).
617
- // If pi auto-substitutes or setModel silently no-ops, this line will diverge
618
- // from requested_model — exactly the diagnostic signal we want.
619
- let modelObservedLogged = false;
620
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
621
- const wrappedOnEvent = (event) => {
622
- if (!modelObservedLogged && event?.type === "turn_end" && typeof event?.message?.model === "string") {
623
- modelObservedLogged = true;
624
- writeDebug({
625
- kind: "model_observed",
626
- provider: event.message.provider ?? null,
627
- model: event.message.model,
590
+ catch (err) {
591
+ const e = err;
592
+ ctx.ui.notify(`× forge:run-task — persona '${phase.personaNoun}' not found for phase ${phase.role}: ${e.message ?? "unknown"}. ` +
593
+ "Run /forge:regenerate to materialize persona files.", "error");
594
+ writeState(cwd, {
595
+ taskId,
596
+ phaseIndex: currentPhaseIndex,
597
+ iterationCounts,
598
+ halted: true,
599
+ lastError: `persona load failed: ${e.message ?? "unknown"}`,
600
+ savedAt: new Date().toISOString(),
628
601
  });
602
+ return {
603
+ status: "failed",
604
+ lastPhaseIndex: currentPhaseIndex,
605
+ iterationCounts,
606
+ lastError: `persona load failed: ${e.message ?? "unknown"}`,
607
+ };
629
608
  }
630
- observer.onEvent(event);
631
- };
632
- const refreshStatus = () => {
633
- if (process.env.FORGE_VERBOSE !== "1")
634
- return;
635
- const elapsed = Math.floor((Date.now() - phaseStart) / 1000);
636
- const tail = observer.state.lastTool ? ` · ${observer.state.lastTool}` : "";
637
- ctx.ui.setStatus?.(STATUS_KEY, `run-task ${taskId}: ${phase.role} · t${observer.state.turn} · tools ${observer.state.toolCount}${observer.state.errCount ? ` · err ${observer.state.errCount}` : ""} · ${elapsed}s${tail}`);
638
- };
639
- const observer = attachViewportObserver({
640
- registry,
641
- sessionId: taskId,
642
- phaseRole: phase.role,
643
- beginHeader: `─── phase ${currentPhaseIndex + 1}/${PHASES.length} ${phase.role} begin · ${taskId} ───`,
644
- writeDebug,
645
- notify: (msg, level) => ctx.ui.notify(msg, level),
646
- setStatusVerbose: process.env.FORGE_VERBOSE === "1" ? (k, v) => ctx.ui.setStatus?.(k, v) : undefined,
647
- verboseKeys: { messageKey: MESSAGE_KEY },
648
- afterEach: refreshStatus,
649
- });
650
- let result;
651
- try {
652
- result = await runForgeSubagent({
653
- persona,
654
- task: taskBody,
655
- cwd,
656
- exportTag: `${taskId}__${phase.role}`,
657
- cacheSessionId,
658
- streamFn: opts.streamFnFactory?.({ kind: "task-phase", persona: persona.name, phase: phase.role, taskId }),
659
- onEvent: wrappedOnEvent,
660
- requestedModel: modelResolution.model,
661
- modelRegistry: ctx.modelRegistry,
662
- signal: opts.signal,
663
- customTools: opts.forgeToolDefs ? getSubagentTools(opts.forgeToolDefs, persona.name) : undefined,
664
- });
665
- }
666
- catch (err) {
667
- const e = err;
668
- ctx.ui.notify(`× forge:run-task — runForgeSubagent threw for phase ${phase.role}: ${e.message ?? "unknown"}`, "error");
669
- writeState(cwd, {
670
- taskId,
671
- phaseIndex: currentPhaseIndex,
672
- iterationCounts,
673
- halted: true,
674
- lastError: `runForgeSubagent threw: ${e.message ?? "unknown"}`,
675
- savedAt: new Date().toISOString(),
676
- });
677
- return {
678
- status: "failed",
679
- lastPhaseIndex: currentPhaseIndex,
680
- iterationCounts,
681
- lastError: `runForgeSubagent threw: ${e.message ?? "unknown"}`,
609
+ // ── 4. Dispatch via runForgeSubagent (IL10) ───────────────────
610
+ // NEVER sendKickoff here — that would reproduce issue #30 (same-context inline = no fork).
611
+ // Read fresh task record to carry forward prior phase summaries (forge-cli#19).
612
+ const taskRecordForSummaries = currentPhaseIndex > 0 ? readTaskRecord(taskId, storeCli, cwd) : null;
613
+ const summariesBlock = buildSummariesBlock(taskRecordForSummaries?.summaries);
614
+ const taskBody = composeTaskBody(subWorkflowMd, taskId, summariesBlock || undefined);
615
+ // Log whether carry-forward summaries were injected (forge-cli#19).
616
+ if (summariesBlock) {
617
+ const debugCarryPath = path.join(cwd, ".forge", "cache", `run-task-debug-${taskId}.jsonl`);
618
+ try {
619
+ fs.mkdirSync(path.dirname(debugCarryPath), { recursive: true });
620
+ fs.appendFileSync(debugCarryPath, `${JSON.stringify({ ts: new Date().toISOString(), phase: phase.role, kind: "carry_forward_injected", summariesLength: summariesBlock.length, summariesBlock })}\n`, "utf8");
621
+ }
622
+ catch { /* best-effort debug log */ }
623
+ }
624
+ // Resolve per-phase model from layered config (Plan 16 Slice 2).
625
+ // Pipeline name "default" matches the Forge plugin's shipped pipeline.
626
+ // When config is absent or cascade bottoms out, resolves to inherit
627
+ // (model: undefined) — setModel is skipped and pi's current model is used.
628
+ const modelResolution = resolveModelForPhase("default", phase.role, phase.personaNoun, modelRoutingConfig);
629
+ const phaseStart = Date.now();
630
+ // Stabilization debug log — every subagent event appended as JSONL.
631
+ const debugLogPath = path.join(cwd, ".forge", "cache", `run-task-debug-${taskId}.jsonl`);
632
+ const writeDebug = (rec) => {
633
+ try {
634
+ fs.mkdirSync(path.dirname(debugLogPath), { recursive: true });
635
+ fs.appendFileSync(debugLogPath, `${JSON.stringify({ ts: new Date().toISOString(), phase: phase.role, ...rec })}\n`, "utf8");
636
+ }
637
+ catch {
638
+ // non-fatal; debug log is best-effort
639
+ }
682
640
  };
683
- }
684
- // ── Post-subagent abort detection ─────────────────────────────────
685
- // If the abort signal fired during the subagent run, treat it as
686
- // cancellation regardless of the exit code (subagent may have been
687
- // mid-turn when aborted — exitCode could be 0 or 1).
688
- // This check MUST come before halt-on-failure so that
689
- // stopReason="aborted" + exitCode=1 is classified as cancellation,
690
- // not a phase failure.
691
- if (result.stopReason === "aborted" || opts.signal?.aborted) {
692
- ctx.ui.notify(`⊘ forge:run-task — ${taskId} phase ${phase.role} cancelled.`, "info");
693
- registry.completePhase(taskId, phase.role, "cancelled");
694
- registry.confirmCancelled(taskId);
695
- // ADR-S21-01: preserve state file so cancelled runs are resumable
696
- writeState(cwd, {
697
- taskId,
698
- phaseIndex: currentPhaseIndex,
699
- iterationCounts,
700
- halted: false,
701
- status: "cancelled",
702
- lastError: undefined,
703
- savedAt: new Date().toISOString(),
704
- });
705
- return { status: "cancelled", lastPhaseIndex: currentPhaseIndex, iterationCounts };
706
- }
707
- // ── Halt-on-failure ───────────────────────────────────────────
708
- if (result.exitCode !== 0) {
709
- ctx.ui.notify(`× forge:run-task — phase ${phase.role} failed (exit ${result.exitCode})` +
710
- (result.errorMessage ? `: ${result.errorMessage}` : "") +
711
- (result.stopReason ? ` [${result.stopReason}]` : ""), "error");
712
- writeState(cwd, {
713
- taskId,
714
- phaseIndex: currentPhaseIndex,
715
- iterationCounts,
716
- halted: true,
717
- lastError: result.errorMessage ?? result.stopReason ?? "subagent exit non-zero",
718
- savedAt: new Date().toISOString(),
641
+ writeDebug({ kind: "phase_start", phaseIndex: currentPhaseIndex });
642
+ writeDebug({
643
+ kind: "requested_model",
644
+ requested: modelResolution.model ?? null,
645
+ source: modelResolution.source,
646
+ persona: phase.personaNoun,
719
647
  });
720
- return {
721
- status: "failed",
722
- lastPhaseIndex: currentPhaseIndex,
723
- iterationCounts,
724
- lastError: result.errorMessage ?? result.stopReason ?? "subagent exit non-zero",
648
+ registry.startPhase(taskId, phase.role, currentPhaseIndex);
649
+ // Capture the first stream-observed model on turn_end (IL10 visibility).
650
+ // If pi auto-substitutes or setModel silently no-ops, this line will diverge
651
+ // from requested_model — exactly the diagnostic signal we want.
652
+ let modelObservedLogged = false;
653
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
654
+ const wrappedOnEvent = (event) => {
655
+ if (!modelObservedLogged && event?.type === "turn_end" && typeof event?.message?.model === "string") {
656
+ modelObservedLogged = true;
657
+ writeDebug({
658
+ kind: "model_observed",
659
+ provider: event.message.provider ?? null,
660
+ model: event.message.model,
661
+ });
662
+ }
663
+ observer.onEvent(event);
725
664
  };
726
- }
727
- // Capture model/provider from subagent result (REVIEW FIX #1).
728
- if (result.model)
729
- lastModel = result.model;
730
- if (result.provider)
731
- lastProvider = result.provider;
732
- // Phase-complete liveliness ping (counts + duration).
733
- {
734
- const elapsed = Math.floor((Date.now() - phaseStart) / 1000);
735
- const { turn, toolCount, errCount, cumUsage } = observer.state;
736
- ctx.ui.notify(`✓ ${phase.role}: ${turn} turn${turn === 1 ? "" : "s"} · ${toolCount} tool call${toolCount === 1 ? "" : "s"}${errCount ? ` · ${errCount} err` : ""} · ${elapsed}s`, "info");
737
- registry.appendTail(taskId, phase.role, fmtPhaseSummary({
738
- role: phase.role,
739
- turns: turn,
740
- tools: toolCount,
741
- errors: errCount,
742
- wallSeconds: elapsed,
743
- usage: cumUsage,
744
- model: result.model,
745
- provider: result.provider,
746
- }));
747
- }
748
- // ── Plan 11 / Slice 2: orchestrator emits phase event ─────────
749
- const phaseEndMs = Date.now();
750
- const taskRecord = readTaskRecord(taskId, storeCli, cwd);
751
- const sprintId = taskRecord?.sprintId;
752
- if (!sprintId) {
753
- ctx.ui.notify(`⚠ forge:run-task — could not resolve sprintId for ${taskId}; ` +
754
- `skipping orchestrator emit for phase ${phase.role}`, "warning");
755
- writeDebug({ kind: "emit_skipped", reason: "no-sprintId" });
756
- }
757
- else {
758
- const phaseIteration = (iterationCounts[phase.role] ?? 0) + 1;
759
- const emitCtx = {
760
- entityType: "task",
761
- taskId,
762
- sprintId,
763
- phase,
764
- iteration: phaseIteration,
765
- startMs: phaseStart,
766
- endMs: phaseEndMs,
767
- model: result.model ?? "unknown",
768
- provider: result.provider ?? "unknown",
769
- usage: {
770
- input: result.usage.input,
771
- output: result.usage.output,
772
- cacheRead: result.usage.cacheRead,
773
- cacheWrite: result.usage.cacheWrite,
774
- },
775
- judgement: judgementFromSummary(taskRecord, phase.role),
776
- storeCli,
777
- cwd,
665
+ const refreshStatus = () => {
666
+ if (process.env.FORGE_VERBOSE !== "1")
667
+ return;
668
+ const elapsed = Math.floor((Date.now() - phaseStart) / 1000);
669
+ const tail = observer.state.lastTool ? ` · ${observer.state.lastTool}` : "";
670
+ ctx.ui.setStatus?.(STATUS_KEY, `run-task ${taskId}: ${phase.role} · t${observer.state.turn} · tools ${observer.state.toolCount}${observer.state.errCount ? ` · err ${observer.state.errCount}` : ""} · ${elapsed}s${tail}`);
778
671
  };
779
- const phaseEvent = buildPhaseEvent(emitCtx);
780
- const emitResult = emitEvent(storeCli, cwd, sprintId, phaseEvent);
781
- if (!emitResult.ok) {
782
- ctx.ui.notify(`⚠ forge:run-task phase event emit failed for ${phase.role}: ${emitResult.stderr.trim()}`, "warning");
783
- writeDebug({ kind: "emit_failed", stderr: emitResult.stderr });
672
+ const observer = attachViewportObserver({
673
+ registry,
674
+ sessionId: taskId,
675
+ phaseRole: phase.role,
676
+ beginHeader: `─── phase ${currentPhaseIndex + 1}/${PHASES.length} ${phase.role} begin · ${taskId} ───`,
677
+ writeDebug,
678
+ notify: (msg, level) => ctx.ui.notify(msg, level),
679
+ setStatusVerbose: process.env.FORGE_VERBOSE === "1" ? (k, v) => ctx.ui.setStatus?.(k, v) : undefined,
680
+ verboseKeys: { messageKey: MESSAGE_KEY },
681
+ afterEach: refreshStatus,
682
+ });
683
+ let result;
684
+ try {
685
+ // FORGE-BUG-040: wrap the runForgeSubagent dispatch in the phase
686
+ // caller context (parity with fix-bug.ts) so the phase-ownership
687
+ // guard can verify tool calls from the subagent. Single setter
688
+ // of phase context for the task pipeline.
689
+ result = await CallerContextStore.asSubagent(phase.role, () => runForgeSubagent({
690
+ persona,
691
+ task: taskBody,
692
+ cwd,
693
+ exportTag: `${taskId}__${phase.role}`,
694
+ cacheSessionId,
695
+ streamFn: opts.streamFnFactory?.({ kind: "task-phase", persona: persona.name, phase: phase.role, taskId }),
696
+ onEvent: wrappedOnEvent,
697
+ requestedModel: modelResolution.model,
698
+ modelRegistry: ctx.modelRegistry,
699
+ signal: opts.signal,
700
+ customTools: opts.forgeToolDefs ? getSubagentTools(opts.forgeToolDefs, persona.name) : undefined,
701
+ }));
784
702
  }
785
- else {
786
- writeDebug({ kind: "emit_ok", eventId: phaseEvent.eventId });
703
+ catch (err) {
704
+ const e = err;
705
+ ctx.ui.notify(`× forge:run-task — runForgeSubagent threw for phase ${phase.role}: ${e.message ?? "unknown"}`, "error");
706
+ writeState(cwd, {
707
+ taskId,
708
+ phaseIndex: currentPhaseIndex,
709
+ iterationCounts,
710
+ halted: true,
711
+ lastError: `runForgeSubagent threw: ${e.message ?? "unknown"}`,
712
+ savedAt: new Date().toISOString(),
713
+ });
714
+ return {
715
+ status: "failed",
716
+ lastPhaseIndex: currentPhaseIndex,
717
+ iterationCounts,
718
+ lastError: `runForgeSubagent threw: ${e.message ?? "unknown"}`,
719
+ };
787
720
  }
788
- // Notify sprint-level observer (FORGE-S21-T03).
789
- if (onPhaseEvent)
790
- onPhaseEvent(phaseEvent);
791
- // Drain friction file for this phase, if any.
792
- const frictionPath = path.join(cwd, ".forge", "cache", `FRICTION-${phase.role}.jsonl`);
793
- const drain = drainFrictionFile(frictionPath, emitCtx);
794
- if (drain.emitted + drain.failed > 0) {
795
- writeDebug({ kind: "friction_drain", ...drain });
796
- if (drain.failed > 0) {
797
- ctx.ui.notify(`⚠ forge:run-task — friction drain for ${phase.role}: ${drain.emitted} ok, ${drain.failed} failed`, "warning");
798
- }
721
+ // ── Post-subagent abort detection ─────────────────────────────────
722
+ // If the abort signal fired during the subagent run, treat it as
723
+ // cancellation regardless of the exit code (subagent may have been
724
+ // mid-turn when aborted exitCode could be 0 or 1).
725
+ // This check MUST come before halt-on-failure so that
726
+ // stopReason="aborted" + exitCode=1 is classified as cancellation,
727
+ // not a phase failure.
728
+ if (result.stopReason === "aborted" || opts.signal?.aborted) {
729
+ ctx.ui.notify(`⊘ forge:run-task — ${taskId} phase ${phase.role} cancelled.`, "info");
730
+ registry.completePhase(taskId, phase.role, "cancelled");
731
+ registry.confirmCancelled(taskId);
732
+ // ADR-S21-01: preserve state file so cancelled runs are resumable
733
+ writeState(cwd, {
734
+ taskId,
735
+ phaseIndex: currentPhaseIndex,
736
+ iterationCounts,
737
+ halted: false,
738
+ status: "cancelled",
739
+ lastError: undefined,
740
+ savedAt: new Date().toISOString(),
741
+ });
742
+ return { status: "cancelled", lastPhaseIndex: currentPhaseIndex, iterationCounts };
799
743
  }
800
- }
801
- // ── 6b. Verdict check (review phases only) ────────────────────
802
- if (phase.isReview) {
803
- const verdict = readVerdict(taskId, phase.role, storeCli, cwd);
804
- if (verdict === "missing") {
805
- ctx.ui.notify(`× forge:run-task — verdict missing for phase ${phase.role} after subagent completed. ` +
806
- "Subagent may have crashed or failed to write summaries. Escalating.", "error");
744
+ // ── Halt-on-failure ───────────────────────────────────────────
745
+ if (result.exitCode !== 0) {
746
+ ctx.ui.notify(`× forge:run-task — phase ${phase.role} failed (exit ${result.exitCode})` +
747
+ (result.errorMessage ? `: ${result.errorMessage}` : "") +
748
+ (result.stopReason ? ` [${result.stopReason}]` : ""), "error");
807
749
  writeState(cwd, {
808
750
  taskId,
809
751
  phaseIndex: currentPhaseIndex,
810
752
  iterationCounts,
811
753
  halted: true,
812
- lastError: `verdict missing for ${phase.role}`,
754
+ lastError: result.errorMessage ?? result.stopReason ?? "subagent exit non-zero",
813
755
  savedAt: new Date().toISOString(),
814
756
  });
815
757
  return {
816
758
  status: "failed",
817
759
  lastPhaseIndex: currentPhaseIndex,
818
760
  iterationCounts,
819
- lastError: `verdict missing for ${phase.role}`,
761
+ lastError: result.errorMessage ?? result.stopReason ?? "subagent exit non-zero",
762
+ };
763
+ }
764
+ // Capture model/provider from subagent result (REVIEW FIX #1).
765
+ if (result.model)
766
+ lastModel = result.model;
767
+ if (result.provider)
768
+ lastProvider = result.provider;
769
+ // Phase-complete liveliness ping (counts + duration).
770
+ {
771
+ const elapsed = Math.floor((Date.now() - phaseStart) / 1000);
772
+ const { turn, toolCount, errCount, cumUsage } = observer.state;
773
+ ctx.ui.notify(`✓ ${phase.role}: ${turn} turn${turn === 1 ? "" : "s"} · ${toolCount} tool call${toolCount === 1 ? "" : "s"}${errCount ? ` · ${errCount} err` : ""} · ${elapsed}s`, "info");
774
+ orchTranscript.record({
775
+ kind: "phase-end",
776
+ ts: new Date().toISOString(),
777
+ phase: phase.role,
778
+ phaseIndex: currentPhaseIndex,
779
+ attempt: (iterationCounts[phase.role] ?? 0) + 1,
780
+ verdict: "n/a",
781
+ elapsedMs: Date.now() - phaseStart,
782
+ turns: turn,
783
+ toolCount,
784
+ errCount,
785
+ });
786
+ const { cumCompression } = observer.state;
787
+ registry.appendTail(taskId, phase.role, fmtPhaseSummary({
788
+ role: phase.role,
789
+ turns: turn,
790
+ tools: toolCount,
791
+ errors: errCount,
792
+ wallSeconds: elapsed,
793
+ usage: cumUsage,
794
+ model: result.model,
795
+ provider: result.provider,
796
+ compression: cumCompression.tokensSaved > 0 ? cumCompression : undefined,
797
+ }));
798
+ }
799
+ // ── Plan 11 / Slice 2: orchestrator emits phase event ─────────
800
+ const phaseEndMs = Date.now();
801
+ const taskRecord = readTaskRecord(taskId, storeCli, cwd);
802
+ const sprintId = taskRecord?.sprintId;
803
+ if (!sprintId) {
804
+ ctx.ui.notify(`⚠ forge:run-task — could not resolve sprintId for ${taskId}; ` +
805
+ `skipping orchestrator emit for phase ${phase.role}`, "warning");
806
+ writeDebug({ kind: "emit_skipped", reason: "no-sprintId" });
807
+ }
808
+ else {
809
+ const phaseIteration = (iterationCounts[phase.role] ?? 0) + 1;
810
+ const emitCtx = {
811
+ entityType: "task",
812
+ taskId,
813
+ sprintId,
814
+ phase,
815
+ iteration: phaseIteration,
816
+ startMs: phaseStart,
817
+ endMs: phaseEndMs,
818
+ model: result.model ?? "unknown",
819
+ provider: result.provider ?? "unknown",
820
+ usage: {
821
+ input: result.usage.input,
822
+ output: result.usage.output,
823
+ cacheRead: result.usage.cacheRead,
824
+ cacheWrite: result.usage.cacheWrite,
825
+ },
826
+ judgement: judgementFromSummary(taskRecord, phase.role),
827
+ storeCli,
828
+ cwd,
820
829
  };
830
+ const phaseEvent = buildPhaseEvent(emitCtx);
831
+ const emitResult = emitEvent(storeCli, cwd, sprintId, phaseEvent);
832
+ if (!emitResult.ok) {
833
+ ctx.ui.notify(`⚠ forge:run-task — phase event emit failed for ${phase.role}: ${emitResult.stderr.trim()}`, "warning");
834
+ writeDebug({ kind: "emit_failed", stderr: emitResult.stderr });
835
+ }
836
+ else {
837
+ writeDebug({ kind: "emit_ok", eventId: phaseEvent.eventId });
838
+ }
839
+ // Notify sprint-level observer (FORGE-S21-T03).
840
+ if (onPhaseEvent)
841
+ onPhaseEvent(phaseEvent);
842
+ // Drain friction file for this phase, if any.
843
+ const frictionPath = path.join(cwd, ".forge", "cache", `FRICTION-${phase.role}.jsonl`);
844
+ const drain = drainFrictionFile(frictionPath, emitCtx);
845
+ if (drain.emitted + drain.failed > 0) {
846
+ writeDebug({ kind: "friction_drain", ...drain });
847
+ if (drain.failed > 0) {
848
+ ctx.ui.notify(`⚠ forge:run-task — friction drain for ${phase.role}: ${drain.emitted} ok, ${drain.failed} failed`, "warning");
849
+ }
850
+ }
821
851
  }
822
- if (verdict === "revision") {
823
- // Increment iteration count for this review phase
824
- iterationCounts[phase.role] = (iterationCounts[phase.role] ?? 0) + 1;
825
- if (iterationCounts[phase.role] >= phase.maxIterations) {
826
- ctx.ui.notify(`× forge:run-task — revision cap reached for phase ${phase.role} ` +
827
- `(${iterationCounts[phase.role]}/${phase.maxIterations} iterations). Escalating.`, "error");
852
+ // ── 6b. Verdict check (review phases only) ────────────────────
853
+ if (phase.isReview) {
854
+ const verdict = readVerdict(taskId, phase.role, storeCli, cwd);
855
+ if (verdict === "missing") {
856
+ ctx.ui.notify(`× forge:run-task — verdict missing for phase ${phase.role} after subagent completed. ` +
857
+ "Subagent may have crashed or failed to write summaries. Escalating.", "error");
828
858
  writeState(cwd, {
829
859
  taskId,
830
860
  phaseIndex: currentPhaseIndex,
831
861
  iterationCounts,
832
862
  halted: true,
833
- lastError: `revision cap reached for ${phase.role}`,
863
+ lastError: `verdict missing for ${phase.role}`,
834
864
  savedAt: new Date().toISOString(),
835
865
  });
836
866
  return {
837
- status: "escalated",
867
+ status: "failed",
838
868
  lastPhaseIndex: currentPhaseIndex,
839
869
  iterationCounts,
840
- lastError: `revision cap reached for ${phase.role}`,
870
+ lastError: `verdict missing for ${phase.role}`,
841
871
  };
842
872
  }
843
- // Loop back to predecessor non-review phase
844
- const predIndex = findPredecessorIndex(PHASES, currentPhaseIndex);
845
- ctx.ui.notify(`⟳ forge:run-task — ${phase.role} returned revision; looping to ${PHASES[predIndex]?.role ?? predIndex} ` +
846
- `(attempt ${iterationCounts[phase.role]}/${phase.maxIterations})`, "info");
847
- // Write intermediate state (not halted still running)
848
- writeState(cwd, {
849
- taskId,
850
- phaseIndex: predIndex,
851
- iterationCounts,
852
- halted: false,
853
- savedAt: new Date().toISOString(),
854
- });
855
- currentPhaseIndex = predIndex;
856
- continue;
873
+ if (verdict === "revision") {
874
+ // Increment iteration count for this review phase
875
+ iterationCounts[phase.role] = (iterationCounts[phase.role] ?? 0) + 1;
876
+ if (iterationCounts[phase.role] >= phase.maxIterations) {
877
+ ctx.ui.notify(`× forge:run-task revision cap reached for phase ${phase.role} ` +
878
+ `(${iterationCounts[phase.role]}/${phase.maxIterations} iterations). Escalating.`, "error");
879
+ writeState(cwd, {
880
+ taskId,
881
+ phaseIndex: currentPhaseIndex,
882
+ iterationCounts,
883
+ halted: true,
884
+ lastError: `revision cap reached for ${phase.role}`,
885
+ savedAt: new Date().toISOString(),
886
+ });
887
+ return {
888
+ status: "escalated",
889
+ lastPhaseIndex: currentPhaseIndex,
890
+ iterationCounts,
891
+ lastError: `revision cap reached for ${phase.role}`,
892
+ };
893
+ }
894
+ // Loop back to predecessor non-review phase
895
+ const predIndex = findPredecessorIndex(PHASES, currentPhaseIndex);
896
+ ctx.ui.notify(`⟳ forge:run-task — ${phase.role} returned revision; looping to ${PHASES[predIndex]?.role ?? predIndex} ` +
897
+ `(attempt ${iterationCounts[phase.role]}/${phase.maxIterations})`, "info");
898
+ orchTranscript.record({
899
+ kind: "phase-loopback",
900
+ ts: new Date().toISOString(),
901
+ fromPhase: phase.role,
902
+ toPhase: PHASES[predIndex]?.role ?? String(predIndex),
903
+ fromPhaseIndex: currentPhaseIndex,
904
+ toPhaseIndex: predIndex,
905
+ reason: `${phase.role} returned revision (attempt ${iterationCounts[phase.role]}/${phase.maxIterations})`,
906
+ });
907
+ // Write intermediate state (not halted — still running)
908
+ writeState(cwd, {
909
+ taskId,
910
+ phaseIndex: predIndex,
911
+ iterationCounts,
912
+ halted: false,
913
+ savedAt: new Date().toISOString(),
914
+ });
915
+ currentPhaseIndex = predIndex;
916
+ continue;
917
+ }
918
+ // verdict === "approved": fall through to advance
857
919
  }
858
- // verdict === "approved": fall through to advance
920
+ // ── Advance to next phase ─────────────────────────────────────
921
+ registry.completePhase(taskId, phase.role, "completed");
922
+ writeState(cwd, {
923
+ taskId,
924
+ phaseIndex: currentPhaseIndex,
925
+ iterationCounts,
926
+ halted: false,
927
+ savedAt: new Date().toISOString(),
928
+ });
929
+ currentPhaseIndex++;
859
930
  }
860
- // ── Advance to next phase ─────────────────────────────────────
861
- registry.completePhase(taskId, phase.role, "completed");
862
- writeState(cwd, {
863
- taskId,
864
- phaseIndex: currentPhaseIndex,
865
- iterationCounts,
866
- halted: false,
867
- savedAt: new Date().toISOString(),
931
+ // ── All phases complete ───────────────────────────────────────────
932
+ deleteState(cwd, taskId);
933
+ orchTranscript.record({
934
+ kind: "pipeline-end",
935
+ ts: new Date().toISOString(),
936
+ outcome: "complete",
937
+ elapsedMs: Date.now() - pipelineStartMs,
868
938
  });
869
- currentPhaseIndex++;
939
+ return {
940
+ status: "completed",
941
+ lastPhaseIndex: PHASES.length - 1,
942
+ iterationCounts,
943
+ model: lastModel,
944
+ provider: lastProvider,
945
+ };
946
+ }
947
+ finally {
948
+ ctx.ui.notify = __origNotify;
870
949
  }
871
- // ── All phases complete ───────────────────────────────────────────
872
- deleteState(cwd, taskId);
873
- return {
874
- status: "completed",
875
- lastPhaseIndex: PHASES.length - 1,
876
- iterationCounts,
877
- model: lastModel,
878
- provider: lastProvider,
879
- };
880
950
  }
881
951
  export function registerRunTask(pi, options = {}) {
882
952
  pi.registerCommand("forge:run-task", {