@hassangameryt/freeflow 0.8.0 → 0.9.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 (290) hide show
  1. package/.agents/plugins/marketplace.json +2 -2
  2. package/.claude-plugin/marketplace.json +1 -1
  3. package/.claude-plugin/plugin.json +2 -2
  4. package/.codex-plugin/plugin.json +3 -3
  5. package/.cursor-plugin/plugin.json +1 -1
  6. package/README.md +96 -149
  7. package/capabilities/cognitive-routing/SKILL.md +21 -129
  8. package/capabilities/cognitive-routing/references/coordinator.md +163 -0
  9. package/capabilities/cognitive-routing/references/worker.md +38 -0
  10. package/capabilities/compaction/SKILL.md +3 -3
  11. package/capabilities/compaction/references/summary-format.md +26 -15
  12. package/capabilities/messaging/SKILL.md +49 -0
  13. package/capabilities/task-memory/SKILL.md +87 -0
  14. package/command-surface.json +6 -54
  15. package/gemini-extension.json +1 -1
  16. package/hooks/claude/hooks.json +13 -3
  17. package/hooks/codex/hooks.json +2 -1
  18. package/hooks/shared/runtime-context.mjs +69 -77
  19. package/package.json +25 -8
  20. package/pi-extension/dist/ask-user/ask.js +108 -0
  21. package/pi-extension/dist/ask-user/form.js +250 -0
  22. package/pi-extension/dist/ask-user/questions.js +66 -0
  23. package/pi-extension/dist/ask-user/tool.js +124 -0
  24. package/pi-extension/dist/cognitive-routing/assembler.js +35 -11
  25. package/pi-extension/dist/cognitive-routing/bootstrap.js +90 -0
  26. package/pi-extension/dist/cognitive-routing/config.js +3 -4
  27. package/pi-extension/dist/cognitive-routing/event-store.js +4 -1
  28. package/pi-extension/dist/cognitive-routing/gate.js +20 -5
  29. package/pi-extension/dist/cognitive-routing/handlers.js +15 -13
  30. package/pi-extension/dist/cognitive-routing/model-control.js +17 -8
  31. package/pi-extension/dist/cognitive-routing/projection.js +23 -6
  32. package/pi-extension/dist/cognitive-routing/provenance.js +3 -1
  33. package/pi-extension/dist/cognitive-routing/render.js +15 -15
  34. package/pi-extension/dist/cognitive-routing/runtime.js +4 -2
  35. package/pi-extension/dist/cognitive-routing/schemas.js +4 -4
  36. package/pi-extension/dist/cognitive-routing/session.js +3 -1
  37. package/pi-extension/dist/cognitive-routing/sources.js +29 -4
  38. package/pi-extension/dist/cognitive-routing/state.js +18 -8
  39. package/pi-extension/dist/cognitive-routing/status.js +4 -3
  40. package/pi-extension/dist/cognitive-routing/tools.js +29 -15
  41. package/pi-extension/dist/cognitive-routing/types.js +10 -1
  42. package/pi-extension/dist/compaction/carry.js +36 -22
  43. package/pi-extension/dist/compaction/config.js +4 -2
  44. package/pi-extension/dist/compaction/controller.js +52 -29
  45. package/pi-extension/dist/compaction/harness.js +19 -3
  46. package/pi-extension/dist/compaction/results.js +14 -13
  47. package/pi-extension/dist/compaction/tool.js +6 -6
  48. package/pi-extension/dist/host/catalog.js +20 -35
  49. package/pi-extension/dist/host/config-validation.js +233 -0
  50. package/pi-extension/dist/host/config.js +78 -91
  51. package/pi-extension/dist/host/inbound.js +142 -0
  52. package/pi-extension/dist/host/projection-tags.js +1 -1
  53. package/pi-extension/dist/host/prompts.js +24 -13
  54. package/pi-extension/dist/host/read-only-session.js +6 -1
  55. package/pi-extension/dist/host/request-history.js +7 -3
  56. package/pi-extension/dist/host/rows.js +25 -0
  57. package/pi-extension/dist/host/runtime-state.js +17 -2
  58. package/pi-extension/dist/host/section-keeper.js +85 -0
  59. package/pi-extension/dist/host/settings/freeflow-command.js +846 -93
  60. package/pi-extension/dist/host/settings/settings-view.js +27 -8
  61. package/pi-extension/dist/host/status.js +19 -5
  62. package/pi-extension/dist/index.js +638 -72
  63. package/pi-extension/dist/messaging/config.js +27 -0
  64. package/pi-extension/dist/messaging/messaging.js +398 -0
  65. package/pi-extension/dist/messaging/registry.js +57 -0
  66. package/pi-extension/dist/messaging/render.js +90 -0
  67. package/pi-extension/dist/messaging/tools.js +109 -0
  68. package/pi-extension/dist/messaging/transport.js +101 -0
  69. package/pi-extension/dist/provider-support/cache/keepalive.js +16 -4
  70. package/pi-extension/dist/task-memory/config.js +49 -0
  71. package/pi-extension/dist/task-memory/registry.js +119 -0
  72. package/pi-extension/dist/task-memory/render.js +107 -0
  73. package/pi-extension/dist/task-memory/runtime.js +994 -0
  74. package/pi-extension/dist/task-memory/tool.js +233 -0
  75. package/pi-extension/dist/tool-execution/apply-patch/tool.js +1 -1
  76. package/pi-extension/dist/tool-execution/background.js +220 -28
  77. package/pi-extension/dist/tool-execution/config.js +14 -3
  78. package/pi-extension/dist/tool-execution/file-tracking.js +24 -4
  79. package/pi-extension/dist/tool-execution/guidance-notices.js +124 -0
  80. package/pi-extension/dist/tool-execution/skill-reads.js +143 -0
  81. package/pi-extension/dist/tool-execution/tools.js +2 -2
  82. package/runtime/prompts/cognitive-routing.md +4 -4
  83. package/runtime/prompts/core.md +24 -14
  84. package/runtime/prompts/interaction-contract.md +7 -4
  85. package/runtime/prompts/tool-execution.md +3 -16
  86. package/runtime/prompts/working-method.md +16 -99
  87. package/skills/deliver-work/SKILL.md +76 -0
  88. package/skills/deliver-work/references/branch-closeout.md +134 -0
  89. package/skills/deliver-work/references/commit.md +83 -0
  90. package/skills/deliver-work/references/launch.md +94 -0
  91. package/skills/deliver-work/references/release.md +85 -0
  92. package/skills/deliver-work/scripts/commit-state.mjs +139 -0
  93. package/skills/deliver-work/scripts/delivery-state.mjs +107 -0
  94. package/skills/deliver-work/scripts/lib/git-state.mjs +178 -0
  95. package/skills/deliver-work/scripts/push-route.mjs +122 -0
  96. package/skills/deliver-work/scripts/release-preflight.mjs +177 -0
  97. package/skills/discuss-work/SKILL.md +148 -0
  98. package/skills/{discuss → discuss-work}/references/discussion-continuity.md +10 -27
  99. package/skills/discuss-work/references/learning-action.md +36 -0
  100. package/skills/discuss-work/scripts/choices.mjs +101 -0
  101. package/skills/discuss-work/scripts/lib/choices.mjs +159 -0
  102. package/skills/evaluate-behavior/SKILL.md +107 -0
  103. package/skills/evaluate-behavior/references/authoring.md +381 -0
  104. package/skills/evaluate-behavior/references/coding-tasks.md +145 -0
  105. package/skills/evaluate-behavior/references/conversations-pipelines-judges.md +177 -0
  106. package/skills/evaluate-behavior/references/design-and-measurement.md +273 -0
  107. package/skills/evaluate-behavior/references/running-and-evidence.md +245 -0
  108. package/skills/evaluate-behavior/scripts/claude-guard.mjs +76 -0
  109. package/skills/evaluate-behavior/scripts/guard-rules.mjs +309 -0
  110. package/skills/evaluate-behavior/scripts/lib/agreement.mjs +90 -0
  111. package/skills/evaluate-behavior/scripts/lib/arms.mjs +55 -0
  112. package/skills/evaluate-behavior/scripts/lib/budget.mjs +87 -0
  113. package/skills/evaluate-behavior/scripts/lib/build-source.mjs +287 -0
  114. package/skills/evaluate-behavior/scripts/lib/clear.mjs +74 -0
  115. package/skills/evaluate-behavior/scripts/lib/container.mjs +395 -0
  116. package/skills/evaluate-behavior/scripts/lib/decision.mjs +418 -0
  117. package/skills/evaluate-behavior/scripts/lib/definitions.mjs +1072 -0
  118. package/skills/evaluate-behavior/scripts/lib/freeflow.mjs +103 -0
  119. package/skills/evaluate-behavior/scripts/lib/grade-command.mjs +591 -0
  120. package/skills/evaluate-behavior/scripts/lib/grade-comparison.mjs +90 -0
  121. package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/grade-interaction.mjs +19 -18
  122. package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/grade-support.mjs +32 -5
  123. package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/grade-workspace.mjs +28 -12
  124. package/skills/evaluate-behavior/scripts/lib/grade.mjs +101 -0
  125. package/skills/evaluate-behavior/scripts/lib/hooks.mjs +98 -0
  126. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/confinement.mjs +196 -0
  127. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/constraints.mjs +111 -0
  128. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/index.mjs +48 -0
  129. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/isolation.mjs +74 -0
  130. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/launch.mjs +123 -0
  131. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/model-call.mjs +73 -0
  132. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/recorder.mjs +178 -0
  133. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/session.mjs +494 -0
  134. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/skills.mjs +81 -0
  135. package/skills/evaluate-behavior/scripts/lib/hosts/index.mjs +31 -0
  136. package/skills/evaluate-behavior/scripts/lib/hosts/names.mjs +3 -0
  137. package/skills/evaluate-behavior/scripts/lib/hosts/pi/confinement.mjs +173 -0
  138. package/skills/evaluate-behavior/scripts/lib/hosts/pi/host.mjs +148 -0
  139. package/skills/evaluate-behavior/scripts/lib/hosts/pi/index.mjs +70 -0
  140. package/skills/evaluate-behavior/scripts/lib/hosts/pi/isolation.mjs +81 -0
  141. package/skills/evaluate-behavior/scripts/lib/hosts/pi/model-call.mjs +79 -0
  142. package/skills/evaluate-behavior/scripts/lib/hosts/pi/process.mjs +241 -0
  143. package/skills/{evaluate-skill/scripts/lib → evaluate-behavior/scripts/lib/hosts/pi}/rpc.mjs +144 -26
  144. package/skills/evaluate-behavior/scripts/lib/identity.mjs +82 -0
  145. package/skills/evaluate-behavior/scripts/lib/judge.mjs +688 -0
  146. package/skills/evaluate-behavior/scripts/lib/metrics.mjs +332 -0
  147. package/skills/evaluate-behavior/scripts/lib/model-call.mjs +17 -0
  148. package/skills/evaluate-behavior/scripts/lib/participants.mjs +616 -0
  149. package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/process.mjs +22 -0
  150. package/skills/evaluate-behavior/scripts/lib/queue.mjs +54 -0
  151. package/skills/evaluate-behavior/scripts/lib/reference.mjs +343 -0
  152. package/skills/evaluate-behavior/scripts/lib/reuse.mjs +277 -0
  153. package/skills/evaluate-behavior/scripts/lib/review.mjs +933 -0
  154. package/skills/evaluate-behavior/scripts/lib/runner.mjs +732 -0
  155. package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/sandbox.mjs +119 -14
  156. package/skills/evaluate-behavior/scripts/lib/scan-shell.mjs +457 -0
  157. package/skills/evaluate-behavior/scripts/lib/scan.mjs +479 -0
  158. package/skills/evaluate-behavior/scripts/lib/seal.mjs +91 -0
  159. package/skills/evaluate-behavior/scripts/lib/shell.mjs +560 -0
  160. package/skills/evaluate-behavior/scripts/lib/source-override.mjs +64 -0
  161. package/skills/evaluate-behavior/scripts/lib/subject-file.mjs +88 -0
  162. package/skills/{evaluate-skill/scripts/lib/pi.mjs → evaluate-behavior/scripts/lib/subject.mjs} +235 -265
  163. package/skills/evaluate-behavior/scripts/lib/task-copy.mjs +124 -0
  164. package/skills/evaluate-behavior/scripts/lib/trials.mjs +524 -0
  165. package/skills/evaluate-behavior/scripts/lib/validity.mjs +68 -0
  166. package/skills/evaluate-behavior/scripts/lib/view-html.mjs +454 -0
  167. package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/view.mjs +159 -18
  168. package/skills/{evaluate-skill → evaluate-behavior}/scripts/pi-command-tool.mjs +21 -6
  169. package/skills/evaluate-behavior/scripts/pi-container-route.mjs +23 -0
  170. package/skills/evaluate-behavior/scripts/pi-guard.mjs +77 -0
  171. package/skills/evaluate-behavior/scripts/pi-login-refresh.mjs +31 -0
  172. package/skills/evaluate-behavior/scripts/skill-eval.mjs +586 -0
  173. package/skills/execute-work/SKILL.md +96 -104
  174. package/skills/execute-work/references/code-practices.md +27 -51
  175. package/skills/execute-work/references/design.md +271 -0
  176. package/skills/execute-work/references/diagnose.md +117 -0
  177. package/skills/execute-work/references/migration.md +108 -0
  178. package/skills/execute-work/references/simplify.md +65 -0
  179. package/skills/execute-work/scripts/self-review.mjs +422 -0
  180. package/skills/execute-work/scripts/touch-map.mjs +290 -0
  181. package/skills/review-work/SKILL.md +89 -65
  182. package/skills/review-work/references/adjudication.md +94 -0
  183. package/skills/review-work/references/artifact.md +198 -0
  184. package/skills/review-work/references/evals-and-judges.md +66 -0
  185. package/skills/review-work/references/work.md +177 -0
  186. package/skills/review-work/scripts/check-brief.mjs +128 -0
  187. package/skills/review-work/scripts/review-packet.mjs +233 -0
  188. package/skills/setup-freeflow/SKILL.md +68 -17
  189. package/skills/track-work/SKILL.md +52 -20
  190. package/skills/track-work/references/handoff.md +190 -0
  191. package/skills/track-work/references/projects.md +51 -0
  192. package/skills/track-work/references/working-record-format.md +36 -18
  193. package/skills/track-work/scripts/lib/document.mjs +17 -13
  194. package/skills/track-work/scripts/lib/format.mjs +2 -0
  195. package/skills/track-work/scripts/lib/recover.mjs +254 -0
  196. package/skills/track-work/scripts/lib/registry.mjs +656 -0
  197. package/skills/track-work/scripts/lib/search.mjs +40 -0
  198. package/skills/track-work/scripts/lib/store.mjs +46 -15
  199. package/skills/track-work/scripts/lib/views.mjs +50 -1
  200. package/skills/track-work/scripts/working-record.mjs +232 -13
  201. package/skills/verify-work/SKILL.md +45 -37
  202. package/skills/verify-work/references/feature-map.md +120 -0
  203. package/skills/verify-work/references/runtime-evidence.md +203 -0
  204. package/skills/verify-work/references/test-design.md +25 -6
  205. package/skills/verify-work/scripts/test-changes.mjs +246 -0
  206. package/skills/write-docs/SKILL.md +113 -25
  207. package/skills/write-docs/references/documentation-shapes.md +68 -3
  208. package/skills/write-docs/references/spec-and-plan-shapes.md +308 -0
  209. package/skills/write-docs/scripts/artifact-check.mjs +148 -0
  210. package/skills/write-docs/scripts/docs-check.mjs +96 -6
  211. package/skills/write-docs/scripts/plan-lint.mjs +123 -0
  212. package/skills/write-skill/SKILL.md +92 -52
  213. package/skills/write-skill/references/activation-and-wording.md +143 -0
  214. package/skills/write-skill/references/placement-and-dependencies.md +154 -0
  215. package/skills/write-skill/scripts/lib/compare.mjs +701 -0
  216. package/skills/write-skill/scripts/lib/skill-author.mjs +197 -3
  217. package/skills/write-skill/scripts/skill-author.mjs +51 -8
  218. package/capabilities/cognitive-routing/references/both-mode.md +0 -36
  219. package/capabilities/cognitive-routing/references/executor-mode.md +0 -48
  220. package/capabilities/cognitive-routing/references/helper-mode.md +0 -47
  221. package/plugin.json +0 -33
  222. package/skills/action-selection/SKILL.md +0 -92
  223. package/skills/action-selection/references/trajectory-stalls.md +0 -73
  224. package/skills/bypass/SKILL.md +0 -153
  225. package/skills/commit-work/SKILL.md +0 -93
  226. package/skills/commit-work/references/git-state-edges.md +0 -94
  227. package/skills/commit-work/references/simple-push.md +0 -94
  228. package/skills/commit-work/references/staging-decisions.md +0 -106
  229. package/skills/decision-gate/SKILL.md +0 -87
  230. package/skills/design-for-depth/SKILL.md +0 -108
  231. package/skills/design-for-depth/references/design-pressure-signals.md +0 -84
  232. package/skills/design-for-depth/references/implementation-decisions.md +0 -76
  233. package/skills/design-for-depth/references/interface-design-loop.md +0 -105
  234. package/skills/design-for-depth/references/module-and-dependency-design.md +0 -117
  235. package/skills/design-for-depth/references/state-and-failure-boundaries.md +0 -125
  236. package/skills/diagnose-failure/SKILL.md +0 -140
  237. package/skills/diagnose-failure/references/diagnostic-loop-catalog.md +0 -50
  238. package/skills/diagnose-failure/references/flaky-and-performance-diagnosis.md +0 -51
  239. package/skills/discuss/SKILL.md +0 -140
  240. package/skills/evaluate-skill/SKILL.md +0 -188
  241. package/skills/evaluate-skill/references/evaluation-definition-schema.md +0 -415
  242. package/skills/evaluate-skill/references/evaluation-design.md +0 -112
  243. package/skills/evaluate-skill/references/execution-and-evidence.md +0 -111
  244. package/skills/evaluate-skill/references/review-and-revision.md +0 -94
  245. package/skills/evaluate-skill/scripts/lib/definitions.mjs +0 -448
  246. package/skills/evaluate-skill/scripts/lib/grade-comparison.mjs +0 -60
  247. package/skills/evaluate-skill/scripts/lib/grade.mjs +0 -64
  248. package/skills/evaluate-skill/scripts/lib/review.mjs +0 -196
  249. package/skills/evaluate-skill/scripts/lib/runner.mjs +0 -405
  250. package/skills/evaluate-skill/scripts/lib/trials.mjs +0 -103
  251. package/skills/evaluate-skill/scripts/pi-guard.mjs +0 -181
  252. package/skills/evaluate-skill/scripts/skill-eval.mjs +0 -182
  253. package/skills/execute-work/references/execute-work-edges.md +0 -78
  254. package/skills/finish-branch/SKILL.md +0 -117
  255. package/skills/finish-branch/references/integration-options.md +0 -80
  256. package/skills/handoff/SKILL.md +0 -82
  257. package/skills/handoff/references/handoff-templates.md +0 -150
  258. package/skills/launch-work/SKILL.md +0 -116
  259. package/skills/launch-work/references/launch-readiness.md +0 -85
  260. package/skills/migration-work/SKILL.md +0 -115
  261. package/skills/migration-work/references/migration-lifecycle.md +0 -82
  262. package/skills/release-work/SKILL.md +0 -118
  263. package/skills/release-work/references/release-evidence.md +0 -78
  264. package/skills/review-artifact/SKILL.md +0 -143
  265. package/skills/review-artifact/references/adjudicate-artifact-review.md +0 -101
  266. package/skills/review-artifact/references/independent-artifact-reviewer-contract.md +0 -159
  267. package/skills/review-work/references/adjudicate-work-review.md +0 -97
  268. package/skills/review-work/references/independent-work-reviewer-contract.md +0 -139
  269. package/skills/review-work/references/security-risk-lens.md +0 -67
  270. package/skills/setup-freeflow/references/activation-contract.md +0 -103
  271. package/skills/setup-freeflow/references/host-setup.md +0 -67
  272. package/skills/simplify-code/SKILL.md +0 -85
  273. package/skills/simplify-code/references/simplification-patterns.md +0 -70
  274. package/skills/verify-work/references/browser-runtime-evidence.md +0 -51
  275. package/skills/verify-work/references/integration-evidence.md +0 -49
  276. package/skills/verify-work/references/performance-evidence.md +0 -59
  277. package/skills/workflow/SKILL.md +0 -171
  278. package/skills/workflow/references/domain-skill-composition.md +0 -45
  279. package/skills/write-plan/SKILL.md +0 -92
  280. package/skills/write-plan/references/plan-shapes.md +0 -156
  281. package/skills/write-skill/references/activation-boundaries.md +0 -106
  282. package/skills/write-skill/references/agent-first-instructions.md +0 -133
  283. package/skills/write-skill/references/progressive-disclosure.md +0 -129
  284. package/skills/write-skill/references/skill-dependency-graphs.md +0 -192
  285. package/skills/write-spec/SKILL.md +0 -85
  286. package/skills/write-spec/references/artifact-standards.md +0 -86
  287. package/skills/write-spec/references/decision-records.md +0 -80
  288. package/skills/write-spec/references/spec-shapes.md +0 -143
  289. /package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/evidence.mjs +0 -0
  290. /package/skills/{evaluate-skill → evaluate-behavior}/scripts/pi-observer.mjs +0 -0
@@ -0,0 +1,233 @@
1
+ import { autoApproveGroup } from "./config.js";
2
+ import { renderTaskCall, renderTaskResult } from "./render.js";
3
+ import { confirmWithUser } from "../ask-user/ask.js";
4
+ export const TASK_MEMORY_TOOL = "task_memory";
5
+ const DESCRIPTION =
6
+ "Task memory: the Working Records (Track Work) this session works on, and their projects. Read the task-memory skill before your first proposal; Track Work decides what records say. Reading needs no permission: projects lists every project; list lists this repository's records, or a project's tasks; view reads a record's resume view or one entry by task ID; search finds lines in the focused task, its tree, or its project. Attention: focus switches to an attached record; detach stops working on one. Proposals, each confirmed by the user in a dialog before anything happens: attach a record; create a task in this repository's project and attach it; child or fanout (a child with its own Git worktree) of the focused task for a parallel session; project_new or project_link for this repository; import a repository record into a project; confirm a held record; env to change a task's directories; remove a task with its children, or project_remove a whole project, which moves their records to the trash. Only the user empties the trash, with /freeflow trash empty: when asked to, tell the user to run it. A decline is the user's answer: do not retry it or work around it.";
7
+ const ACTIONS = [
8
+ "projects",
9
+ "list",
10
+ "view",
11
+ "search",
12
+ "attach",
13
+ "focus",
14
+ "detach",
15
+ "create",
16
+ "child",
17
+ "fanout",
18
+ "project_new",
19
+ "project_link",
20
+ "import",
21
+ "confirm",
22
+ "env",
23
+ "remove",
24
+ "project_remove",
25
+ ];
26
+ // Actions that change which records this session holds; the Runtime State follows them.
27
+ const CHANGES_STATE = new Set(["attach", "focus", "detach", "create", "import", "confirm", "remove", "project_remove"]);
28
+ /** Actions any profile may take: they only read. `env` reads too, unless it changes a directory. */
29
+ const READS = new Set(["projects", "list", "view", "search"]);
30
+ const reads = (params) =>
31
+ READS.has(params?.action) ||
32
+ (params?.action === "env" &&
33
+ !params?.primary?.trim?.() &&
34
+ !params?.add_dir?.trim?.() &&
35
+ !params?.remove_dir?.trim?.());
36
+ /** Registered once, declared only while task memory is effective (see applyTaskMemoryTools). */
37
+ export function registerTaskMemoryTool(pi, host) {
38
+ pi.registerTool({
39
+ name: TASK_MEMORY_TOOL,
40
+ label: "Task memory",
41
+ description: DESCRIPTION,
42
+ parameters: {
43
+ type: "object",
44
+ additionalProperties: false,
45
+ properties: {
46
+ action: { type: "string", enum: ACTIONS },
47
+ record: {
48
+ type: "string",
49
+ description: "A task ID such as my-project/task-003, or a record.md path; remove takes a task ID",
50
+ },
51
+ project: { type: "string", description: "list: a project's name; import: the project to move the record into" },
52
+ name: { type: "string", description: "project_new, project_link, project_remove: the project's name" },
53
+ worktrees: { type: "string", description: "project_new, project_link: where children's worktrees go" },
54
+ title: { type: "string", description: "create, child, fanout: the new task's title" },
55
+ goal: {
56
+ type: "string",
57
+ description:
58
+ "create, child, fanout: its goal, one line; omitted, the title is the goal (the dialog shows it)",
59
+ },
60
+ entry: { type: "string", description: "view: one entry ID such as S-004" },
61
+ query: { type: "string", description: "search: text to find" },
62
+ scope: { type: "string", enum: ["task", "tree", "project"], description: "search: where to look" },
63
+ primary: { type: "string", description: "env: the task's new primary directory" },
64
+ add_dir: { type: "string", description: "env: a directory to add" },
65
+ remove_dir: { type: "string", description: "env: a directory to remove" },
66
+ },
67
+ required: ["action"],
68
+ },
69
+ // It asks the user, so scripts must not call it (Pi's guidance for such tools); called directly, its own
70
+ // transcript views apply, which a codemode script's nested call would bypass.
71
+ exposure: "model-only",
72
+ defaultActive: false,
73
+ executionMode: "sequential",
74
+ renderCall: (args, theme) => renderTaskCall(args, theme),
75
+ renderResult: (result, options, theme, context) => renderTaskResult(result, options, theme, context),
76
+ async execute(_id, params, _signal, _update, ctx) {
77
+ const refused = reads(params) ? undefined : host.refusal?.();
78
+ const group = reads(params) ? undefined : autoApproveGroup(params?.action);
79
+ const approved = group !== undefined && host.autoApproved?.(group) === true;
80
+ const result = refused ? { ok: false, text: refused } : await run(host, params, ctx, approved);
81
+ if (approved && result.ok)
82
+ result.text = `Auto-approved by the user's taskMemory.autoApprove setting (${group}); no dialog was shown.\n\n${result.text}`;
83
+ if (result.ok && CHANGES_STATE.has(params?.action)) host.changed(ctx);
84
+ let next;
85
+ if (result.ok && result.snapshot) {
86
+ const brief = await host.memory.brief(result.snapshot);
87
+ result.headline ??= `${params.action === "focus" ? "Focus on" : "Attached"} ${brief.label} · ${brief.state}`;
88
+ next = brief.next || undefined;
89
+ }
90
+ return {
91
+ content: [{ type: "text", text: result.text }],
92
+ details: {
93
+ action: params?.action,
94
+ ...(result.snapshot ? { snapshot: result.snapshot } : {}),
95
+ ...(result.headline ? { headline: result.headline } : {}),
96
+ ...(next ? { next } : {}),
97
+ ...(result.command ? { command: result.command } : {}),
98
+ ...(result.id ? { id: result.id } : {}),
99
+ },
100
+ ...(result.ok ? {} : { isError: true }),
101
+ };
102
+ },
103
+ });
104
+ }
105
+ /** Ask the user before an action taken on their authority; without a UI the proposal cannot be confirmed. */
106
+ async function confirmProposal(ctx, title, message) {
107
+ if (!ctx.hasUI) return { ok: false, text: `${title} needs the user's confirmation, and no UI is available.` };
108
+ if (await confirmWithUser(ctx, title, message)) return undefined;
109
+ return { ok: false, headline: "Declined by the user", text: "The user declined. Do not retry or work around it." };
110
+ }
111
+ const show = (value) => (typeof value === "string" ? value.trim() : "");
112
+ async function run(host, params, ctx, approved = false) {
113
+ const { memory } = host;
114
+ // An action the user's auto-approve setting covers runs without asking.
115
+ const ask = (context, title, message) =>
116
+ approved ? Promise.resolve(undefined) : confirmProposal(context, title, message);
117
+ const cwd = ctx.cwd;
118
+ switch (params?.action) {
119
+ case "projects":
120
+ return memory.projects(cwd);
121
+ case "list":
122
+ return { ok: true, text: await memory.list(cwd, params.project) };
123
+ case "view":
124
+ return memory.view(params.record, params.entry, cwd);
125
+ case "search":
126
+ return memory.search(params.query, params.scope, cwd);
127
+ case "focus":
128
+ return memory.focus(params.record, cwd);
129
+ case "detach":
130
+ return memory.detach(params.record, cwd);
131
+ case "attach":
132
+ return (
133
+ (await ask(
134
+ ctx,
135
+ "Attach task record?",
136
+ `The agent proposes working on ${show(params.record)} in this session.`,
137
+ )) ?? memory.attach(params.record, cwd)
138
+ );
139
+ case "create": {
140
+ const declined = await ask(
141
+ ctx,
142
+ "Create task?",
143
+ `Create "${show(params.title)}" in this repository's project and attach it to this session? Goal: ${show(params.goal) || show(params.title)}`,
144
+ );
145
+ if (declined) return declined;
146
+ const created = await memory.create(params.title, params.goal, cwd);
147
+ if (!created.ok || !created.id) return created;
148
+ const attached = await memory.attach(created.id, cwd);
149
+ return attached.ok
150
+ ? {
151
+ ...attached,
152
+ headline: `Created and attached ${created.id} — ${show(params.title)}`,
153
+ text: `Created ${created.id} and attached it to this session.\n\n${attached.text}`,
154
+ }
155
+ : { ...attached, text: `Created ${created.id}, but it was not attached: ${attached.text}` };
156
+ }
157
+ case "child":
158
+ return (
159
+ (await ask(
160
+ ctx,
161
+ "Create child task?",
162
+ `Create "${show(params.title)}" as a child of the focused task, for a parallel session you start?`,
163
+ )) ?? memory.child(params.title, params.goal)
164
+ );
165
+ case "fanout":
166
+ return (
167
+ (await ask(
168
+ ctx,
169
+ "Fan out a child task?",
170
+ `Create child "${show(params.title)}" of the focused task with its own Git worktree and branch? You start its session in a new terminal.`,
171
+ )) ?? memory.fanout(params.title, params.goal)
172
+ );
173
+ case "project_new":
174
+ case "project_link": {
175
+ const operation = params.action === "project_new" ? "new" : "link";
176
+ const declined = await ask(
177
+ ctx,
178
+ operation === "new" ? "Create project?" : "Link repository?",
179
+ operation === "new"
180
+ ? `Create project "${show(params.name)}" and link this repository (${cwd}) to it?${show(params.worktrees) ? ` Child worktrees go in ${show(params.worktrees)}.` : ""}`
181
+ : `Link this repository (${cwd}) to project "${show(params.name)}"?`,
182
+ );
183
+ return declined ?? memory.project(operation, params.name, params.worktrees, cwd);
184
+ }
185
+ case "import":
186
+ return (
187
+ (await ask(
188
+ ctx,
189
+ "Import record?",
190
+ `Move ${show(params.record)} into the ${show(params.project)} project's store, with a task ID?`,
191
+ )) ?? memory.importRecord(params.record, params.project, cwd)
192
+ );
193
+ case "confirm":
194
+ return (
195
+ (await ask(
196
+ ctx,
197
+ "Make held record writable?",
198
+ `Let this session write ${show(params.record) || "its held records"}, taking ownership where another session owns one?`,
199
+ )) ?? memory.confirm(params.record, cwd)
200
+ );
201
+ case "env": {
202
+ const change = { primary: show(params.primary), add: show(params.add_dir), remove: show(params.remove_dir) };
203
+ if (change.primary || change.add || change.remove) {
204
+ const declined = await ask(
205
+ ctx,
206
+ "Change task directories?",
207
+ `For ${show(params.record)}: ${[change.primary && `primary ${change.primary}`, change.add && `add ${change.add}`, change.remove && `remove ${change.remove}`].filter(Boolean).join(", ")}?`,
208
+ );
209
+ if (declined) return declined;
210
+ }
211
+ return memory.environment(params.record, change, cwd);
212
+ }
213
+ case "remove":
214
+ case "project_remove": {
215
+ // Removal always asks: no auto-approve setting covers it (Task 006).
216
+ const target = params.action === "remove" ? { task: params.record } : { project: params.name };
217
+ const { question, error } = await memory.removalQuestion(target);
218
+ if (!question) return { ok: false, text: error ?? "Nothing to remove." };
219
+ const title = params.action === "remove" ? "Remove task?" : "Remove project?";
220
+ return (await confirmProposal(ctx, title, question)) ?? memory.remove(target);
221
+ }
222
+ default:
223
+ return { ok: false, text: `action must be one of ${ACTIONS.join(", ")}.` };
224
+ }
225
+ }
226
+ /** Declare or withdraw task_memory when task memory's effective state changes, keeping other tools in place. */
227
+ export function applyTaskMemoryTools(pi, effective) {
228
+ if (typeof pi.getActiveTools !== "function" || typeof pi.setActiveTools !== "function") return;
229
+ const current = pi.getActiveTools();
230
+ const present = current.includes(TASK_MEMORY_TOOL);
231
+ if (effective && !present) pi.setActiveTools([...current, TASK_MEMORY_TOOL]);
232
+ if (!effective && present) pi.setActiveTools(current.filter((name) => name !== TASK_MEMORY_TOOL));
233
+ }
@@ -2,7 +2,7 @@ import { APPLY_PATCH_LARK_GRAMMAR } from "./grammar.js";
2
2
  import { applyPatch } from "./plan.js";
3
3
  export const APPLY_PATCH = "apply_patch";
4
4
  const DESCRIPTION =
5
- "Use the apply_patch tool to edit files. Its input is a patch: *** Begin Patch, then *** Add File:, *** Update File: (optionally followed by *** Move to:) or *** Delete File: sections, then *** End Patch. In update sections, @@ lines anchor a hunk and every other line starts with space (context), - (remove) or + (add). Include enough context lines for each hunk to match one place.";
5
+ "Use apply_patch to change several files in one call, or to add, delete or move files together with other changes; to change one existing file, use edit. The input is a patch: *** Begin Patch, then *** Add File:, *** Update File: (optionally followed by *** Move to:) or *** Delete File: sections, then *** End Patch. In update sections, @@ lines anchor a hunk and every other line starts with space (context), - (remove) or + (add). Copy context lines from your latest view of the file, enough for each hunk to match one place.";
6
6
  const CODES = { add: "A", update: "M", move: "M", delete: "D" };
7
7
  /** Codex's summary on success, so GPT models read the result they were trained on. */
8
8
  export function resultText(result) {
@@ -3,14 +3,27 @@ import { createWriteStream } from "node:fs";
3
3
  import { mkdir } from "node:fs/promises";
4
4
  import { tmpdir } from "node:os";
5
5
  import { join } from "node:path";
6
+ import { StringDecoder } from "node:string_decoder";
6
7
  import { createLocalBashOperations } from "@earendil-works/pi-coding-agent";
7
8
  export const BASH_BACKGROUND = "bash_background";
9
+ export const MONITOR = "monitor";
8
10
  export const STOP_BACKGROUND = "stop_background";
9
11
  export const NOTICE_TYPE = "freeflow-background";
12
+ export const MONITOR_TYPE = "freeflow-monitor";
13
+ /** Background commands and monitors together. */
10
14
  export const MAX_RUNNING = 16;
15
+ export const MONITOR_DEFAULT_MS = 5 * 60_000;
16
+ export const MONITOR_MAX_MS = 30 * 60_000;
17
+ /** Event notices a monitor may send per minute before it is stopped; each one can start a model turn. */
18
+ export const MONITOR_PER_MINUTE = 20;
19
+ /** Lines one event notice shows; the rest stay in the output file. */
20
+ export const MONITOR_MAX_LINES = 40;
21
+ const MAX_LINE_CHARS = 1_000;
11
22
  /** Marks harness messages, which arrive on their own schedule, as not written by the user. */
12
- export const NOTICE_PREFIX = "[Freeflow notice, not from the user]";
13
- const newId = () => `bg${randomBytes(6).readUIntBE(0, 6).toString(36).slice(-6).padStart(6, "0")}`;
23
+ import { freeflowNotice } from "../host/inbound.js";
24
+ export { freeflowNotice };
25
+ const newId = (prefix) => `${prefix}${randomBytes(6).readUIntBE(0, 6).toString(36).slice(-6).padStart(6, "0")}`;
26
+ const shellQuote = (text) => `'${text.replace(/'/g, `'\\''`)}'`;
14
27
  function labelFor(command, description) {
15
28
  const text = description?.trim() || command.trim().split("\n")[0];
16
29
  return text.length > 80 ? `${text.slice(0, 77)}...` : text;
@@ -28,6 +41,10 @@ function close(stream, trailer) {
28
41
  export class BackgroundJobs {
29
42
  host;
30
43
  jobs = new Map();
44
+ /** Launches between their check of MAX_RUNNING and their job's registration. */
45
+ starting = 0;
46
+ /** Changes when the session ends, so a launch that was still starting then does not run. */
47
+ generation = 0;
31
48
  constructor(host) {
32
49
  this.host = host;
33
50
  }
@@ -35,39 +52,101 @@ export class BackgroundJobs {
35
52
  return [...this.jobs.values()].filter((job) => job.status === "running");
36
53
  }
37
54
  async start(command, description, cwd, sessionId) {
38
- const running = this.running();
39
- if (running.length >= MAX_RUNNING)
55
+ return this.launch("command", command, description, cwd, sessionId);
56
+ }
57
+ /**
58
+ * Claude Code's Monitor: each line the command prints to stdout is an event for the model, batched by the host;
59
+ * stderr goes to the output file only. One notice when the command exits, expires, or is stopped for sending too
60
+ * much.
61
+ */
62
+ async monitor(command, description, timeoutMs, cwd, sessionId) {
63
+ const ms = Math.min(Math.max(Math.round(timeoutMs) || MONITOR_DEFAULT_MS, 1_000), MONITOR_MAX_MS);
64
+ return this.launch("monitor", command, description, cwd, sessionId, ms);
65
+ }
66
+ async launch(kind, command, description, cwd, sessionId, timeoutMs) {
67
+ if (this.running().length + this.starting >= MAX_RUNNING)
40
68
  throw new Error(`${MAX_RUNNING} background commands are running. Stop one with stop_background, then retry.`);
41
- let id = newId();
42
- while (this.jobs.has(id)) id = newId();
69
+ const prefix = kind === "monitor" ? "mon" : "bg";
70
+ let id = newId(prefix);
71
+ while (this.jobs.has(id)) id = newId(prefix);
72
+ const generation = this.generation;
43
73
  const folder = join(tmpdir(), "freeflow", sessionId.replace(/[^A-Za-z0-9._-]/g, "_"), "background");
44
- await mkdir(folder, { recursive: true, mode: 0o700 });
45
74
  const outputPath = join(folder, `${id}.output`);
46
- const stream = createWriteStream(outputPath, { flags: "w", mode: 0o600 });
47
- await new Promise((resolve, reject) => {
48
- stream.once("open", () => resolve());
49
- stream.once("error", reject);
50
- });
75
+ // A monitor's stderr is appended to the same file by the shell, so both writers append.
76
+ let stream;
77
+ this.starting++;
78
+ try {
79
+ await mkdir(folder, { recursive: true, mode: 0o700 });
80
+ stream = createWriteStream(outputPath, { flags: kind === "monitor" ? "a" : "w", mode: 0o600 });
81
+ await new Promise((resolve, reject) => {
82
+ stream.once("open", () => resolve());
83
+ stream.once("error", reject);
84
+ });
85
+ } finally {
86
+ this.starting--;
87
+ }
88
+ if (generation !== this.generation) {
89
+ await close(stream, "[not started: the session ended]\n");
90
+ throw new Error("The session ended before the command started.");
91
+ }
51
92
  const controller = new AbortController();
52
93
  let endsWithNewline = true;
53
94
  const job = {
54
95
  id,
96
+ kind,
55
97
  label: labelFor(command, description),
56
98
  outputPath,
57
99
  controller,
58
100
  status: "running",
101
+ events: 0,
59
102
  done: Promise.resolve(),
60
103
  };
61
- const run = createLocalBashOperations({ shellPath: this.host.shellPath() }).exec(command, cwd, {
104
+ let partial = "";
105
+ // One decoder per job, so a character split across two reads is decoded whole.
106
+ const decoder = new StringDecoder("utf8");
107
+ const event = (line) => {
108
+ job.events++;
109
+ const text = line.length > MAX_LINE_CHARS ? `${line.slice(0, MAX_LINE_CHARS)}... (line cut)` : line;
110
+ this.host.event(
111
+ id,
112
+ text,
113
+ (lines) => this.eventText(job, lines),
114
+ () => this.halt(job, "limit"),
115
+ );
116
+ };
117
+ const script = kind === "monitor" ? `{\n${command}\n} 2>>${shellQuote(outputPath)}` : command;
118
+ const run = createLocalBashOperations({ shellPath: this.host.shellPath() }).exec(script, cwd, {
62
119
  onData: (data) => {
63
120
  if (data.length === 0) return;
64
121
  endsWithNewline = data[data.length - 1] === 10;
65
122
  stream.write(data);
123
+ if (kind !== "monitor" || job.stopping) return;
124
+ const lines = (partial + decoder.write(data)).split("\n");
125
+ // An unfinished line keeps only what a terminal would show (the text after its last carriage return), so a
126
+ // progress bar redrawn with \r does not grow it without bound; the output file has everything.
127
+ partial = (lines.pop() ?? "")
128
+ .split("\r")
129
+ .at(-1)
130
+ .slice(-4 * MAX_LINE_CHARS);
131
+ for (const line of lines) if (line.trim() !== "") event(line.replace(/\r$/, ""));
66
132
  },
67
133
  signal: controller.signal,
68
134
  });
135
+ const timer =
136
+ timeoutMs === undefined ? undefined : setTimeout(() => this.halt(job, "expired"), timeoutMs).unref?.();
137
+ const finish = () => {
138
+ if (timer) clearTimeout(timer);
139
+ job.ended = true;
140
+ if (kind !== "monitor") return;
141
+ partial += decoder.end();
142
+ if (partial.trim() !== "" && !job.stopping) event(partial);
143
+ partial = "";
144
+ if (job.stopping === "shutdown" && this.host.discardEvents) this.host.discardEvents(id);
145
+ else this.host.endEvents(id);
146
+ };
69
147
  job.done = run.then(
70
148
  async ({ exitCode }) => {
149
+ finish();
71
150
  const code = exitCode ?? 1;
72
151
  job.exitCode = code;
73
152
  job.status = code === 0 ? "completed" : "failed";
@@ -75,10 +154,20 @@ export class BackgroundJobs {
75
154
  this.notify(job);
76
155
  },
77
156
  async (error) => {
157
+ finish();
78
158
  const lead = endsWithNewline ? "" : "\n";
79
159
  if (job.stopping) {
80
160
  job.status = "stopped";
81
- await close(stream, `${lead}${job.stopping === "tool" ? "[stopped]" : "[stopped at session end]"}\n`);
161
+ const trailer =
162
+ job.stopping === "tool"
163
+ ? "[stopped]"
164
+ : job.stopping === "shutdown"
165
+ ? "[stopped at session end]"
166
+ : job.stopping === "expired"
167
+ ? "[stopped: timeout]"
168
+ : "[stopped: too many events]";
169
+ await close(stream, `${lead}${trailer}\n`);
170
+ if (job.stopping === "expired" || job.stopping === "limit") this.notify(job);
82
171
  return;
83
172
  }
84
173
  // The command could not run (for example, the working directory is gone).
@@ -89,7 +178,25 @@ export class BackgroundJobs {
89
178
  },
90
179
  );
91
180
  this.jobs.set(id, job);
92
- return { id, outputPath };
181
+ return { id, outputPath, ...(timeoutMs === undefined ? {} : { timeoutMs }) };
182
+ }
183
+ /** Stop a monitor on its own account: its timeout passed, or it sent more than its rate limit allows. */
184
+ halt(job, reason) {
185
+ if (job.status !== "running" || job.stopping || job.ended) return;
186
+ job.stopping = reason;
187
+ job.controller.abort();
188
+ }
189
+ eventText(job, lines) {
190
+ const shown = lines.slice(0, MONITOR_MAX_LINES);
191
+ const more = lines.length - shown.length;
192
+ return freeflowNotice(
193
+ "monitor",
194
+ [
195
+ `Monitor ${job.id} (${job.label}):`,
196
+ ...shown,
197
+ ...(more > 0 ? [`... ${more} more lines in ${job.outputPath}`] : []),
198
+ ].join("\n"),
199
+ );
93
200
  }
94
201
  async stop(id) {
95
202
  const job = this.jobs.get(id);
@@ -117,6 +224,7 @@ export class BackgroundJobs {
117
224
  }
118
225
  /** Session end: stop every running job, wait for the processes to exit, forget them. No notice is sent. */
119
226
  async stopAll() {
227
+ this.generation++;
120
228
  const running = this.running();
121
229
  for (const job of running) {
122
230
  job.stopping = "shutdown";
@@ -129,15 +237,45 @@ export class BackgroundJobs {
129
237
  restate() {
130
238
  const running = this.running();
131
239
  if (running.length === 0) return;
132
- const list = running.map((job) => `${job.id} (${job.label}), output ${job.outputPath}`).join("; ");
240
+ const list = running
241
+ .map((job) => `${job.kind === "monitor" ? "monitor " : ""}${job.id} (${job.label}), output ${job.outputPath}`)
242
+ .join("; ");
133
243
  this.host.send(
134
- `${NOTICE_PREFIX} Still running: ${list}. Do not start them again; stop one with stop_background before restarting it.`,
244
+ freeflowNotice(
245
+ "background",
246
+ `Still running: ${list}. Do not start them again; stop one with stop_background before restarting it.`,
247
+ ),
135
248
  { running: running.map((job) => ({ id: job.id, outputPath: job.outputPath })) },
136
249
  false,
137
250
  );
138
251
  }
139
252
  notify(job, runError) {
140
- if (job.stopping) return;
253
+ if (job.stopping === "tool" || job.stopping === "shutdown") return;
254
+ const details = {
255
+ id: job.id,
256
+ ...(job.kind === "monitor" ? { kind: job.kind } : {}),
257
+ status: job.status,
258
+ ...(job.exitCode === undefined ? {} : { exitCode: job.exitCode }),
259
+ ...(job.kind === "monitor" ? { events: job.events } : {}),
260
+ outputPath: job.outputPath,
261
+ };
262
+ if (job.kind === "monitor") {
263
+ const events = `${job.events} ${job.events === 1 ? "event" : "events"}`;
264
+ const outcome =
265
+ job.stopping === "expired"
266
+ ? `reached its timeout after ${events} and was stopped. Start it again to keep watching.`
267
+ : job.stopping === "limit"
268
+ ? `was stopped after ${events}: it sent more than ${MONITOR_PER_MINUTE} notices in a minute. Filter its output to the lines you would act on, then start it again.`
269
+ : runError !== undefined
270
+ ? `failed to run: ${runError}.`
271
+ : `exited with code ${job.exitCode} after ${events}.`;
272
+ this.host.send(
273
+ freeflowNotice("monitor", `Monitor ${job.id} (${job.label}) ${outcome} Output: ${job.outputPath}.`),
274
+ details,
275
+ true,
276
+ );
277
+ return;
278
+ }
141
279
  const outcome =
142
280
  runError !== undefined
143
281
  ? `failed to run: ${runError}`
@@ -145,23 +283,29 @@ export class BackgroundJobs {
145
283
  ? "completed"
146
284
  : `failed with exit code ${job.exitCode}`;
147
285
  this.host.send(
148
- `${NOTICE_PREFIX} Background command ${job.id} (${job.label}) ${outcome}. Output: ${job.outputPath}.`,
149
- {
150
- id: job.id,
151
- status: job.status,
152
- ...(job.exitCode === undefined ? {} : { exitCode: job.exitCode }),
153
- outputPath: job.outputPath,
154
- },
286
+ freeflowNotice(
287
+ "background",
288
+ `Background command ${job.id} (${job.label}) ${outcome}. Output: ${job.outputPath}.`,
289
+ ),
290
+ details,
155
291
  true,
156
292
  );
157
293
  }
158
294
  }
159
295
  const START_DESCRIPTION = [
160
- "Run a shell command in the background. Use it for servers, watchers and commands expected to run longer than two minutes. Returns an ID and an output file at once; you are notified once, when the command exits. Read the output file for output so far.",
296
+ "Run a shell command in the background; it returns at once with an ID and an output file. Use it for servers, watchers, and builds, test runs or installs expected to take over two minutes; run shorter commands with bash.",
297
+ "You are notified once, when the command exits; if you are idle then, the notice starts a new turn. Meanwhile do other work, and read the output file only for output so far; do not poll it.",
161
298
  "To be notified when something becomes ready, run a command that exits when it is: until grep -qE 'Listening|Error|Traceback' server.log; do sleep 1; done. Match the failure lines too: a check that waits only for success stays silent when the process crashes.",
299
+ "Commands stop when the session ends; their output files stay readable.",
300
+ ].join("\n\n");
301
+ const MONITOR_DESCRIPTION = [
302
+ "Use monitor to act on a running command's lines as they appear: errors in a log while you test, failures in a long test run. For one signal (a server is ready, a build finished), use bash_background with a command that exits when the condition holds.",
303
+ "Each line the command prints to stdout reaches you as a notice (lines within 200 ms join one notice); stderr goes only to the output file. You also get one notice when the command exits or when timeout_ms passes (default 5 minutes, at most 30); start it again to keep watching. A notice that arrives while you are idle starts a model turn, so make the command print only lines you would act on; one that prints too much is stopped.",
304
+ "Every stage of a pipe must flush each line (grep --line-buffered, awk with fflush()), or lines wait in a buffer; head cannot.",
305
+ "Silence is not success: match every outcome you would act on, failures as well as success, for example tail -f app.log | grep -E --line-buffered 'ready|ERROR|Traceback|FAILED'.",
162
306
  ].join("\n\n");
163
307
  const STOP_DESCRIPTION =
164
- "Stop a background command started with bash_background. Returns after the command has exited.";
308
+ "Stop a background command or monitor once neither you nor the user needs it, such as a server after the checks that used it. Returns after it has exited.";
165
309
  /** Registered once, inactive until Tool Execution is effective (see tool-execution/tools.ts). */
166
310
  export function registerBackgroundTools(pi, jobs, host) {
167
311
  const guard = () => {
@@ -208,6 +352,54 @@ export function registerBackgroundTools(pi, jobs, host) {
208
352
  };
209
353
  },
210
354
  });
355
+ pi.registerTool({
356
+ name: MONITOR,
357
+ label: MONITOR,
358
+ description: MONITOR_DESCRIPTION,
359
+ parameters: {
360
+ type: "object",
361
+ additionalProperties: false,
362
+ properties: {
363
+ command: { type: "string", description: "Shell command whose stdout lines are the events" },
364
+ description: { type: "string", description: "A few words saying what is watched; shown with every event" },
365
+ timeout_ms: {
366
+ type: "number",
367
+ minimum: 1_000,
368
+ maximum: MONITOR_MAX_MS,
369
+ description: `Stop watching after this long (default ${MONITOR_DEFAULT_MS}, at most ${MONITOR_MAX_MS})`,
370
+ },
371
+ },
372
+ required: ["command", "description"],
373
+ },
374
+ outputSchema: {
375
+ type: "object",
376
+ properties: { id: { type: "string" }, outputPath: { type: "string" }, timeoutMs: { type: "number" } },
377
+ required: ["id", "outputPath", "timeoutMs"],
378
+ },
379
+ exposure: "direct",
380
+ defaultActive: false,
381
+ async execute(_id, params, _signal, _u, ctx) {
382
+ guard();
383
+ const started = await jobs.monitor(
384
+ String(params?.command ?? ""),
385
+ String(params?.description ?? ""),
386
+ Number(params?.timeout_ms ?? MONITOR_DEFAULT_MS),
387
+ ctx.cwd,
388
+ ctx.sessionManager?.getSessionId?.() ?? "session",
389
+ );
390
+ const minutes = Math.round(((started.timeoutMs ?? MONITOR_DEFAULT_MS) / 60_000) * 10) / 10;
391
+ return {
392
+ content: [
393
+ {
394
+ type: "text",
395
+ text: `Monitor ${started.id} started. Each line it prints reaches you as a notice; it stops when the command exits, after ${minutes} min, or with stop_background. When you tell the user you are watching, say it stops after ${minutes} min unless restarted. Output: ${started.outputPath}.`,
396
+ },
397
+ ],
398
+ details: started,
399
+ structuredContent: started,
400
+ };
401
+ },
402
+ });
211
403
  pi.registerTool({
212
404
  name: STOP_BACKGROUND,
213
405
  label: STOP_BACKGROUND,
@@ -215,7 +407,7 @@ export function registerBackgroundTools(pi, jobs, host) {
215
407
  parameters: {
216
408
  type: "object",
217
409
  additionalProperties: false,
218
- properties: { id: { type: "string", description: "The ID bash_background returned" } },
410
+ properties: { id: { type: "string", description: "The ID bash_background or monitor returned" } },
219
411
  required: ["id"],
220
412
  },
221
413
  outputSchema: {
@@ -1,4 +1,9 @@
1
- export const DEFAULT_TOOL_EXECUTION_CONFIG = Object.freeze({ enabled: false });
1
+ export const DEFAULT_TOOL_EXECUTION_CONFIG = Object.freeze({
2
+ enabled: true,
3
+ guidanceNotices: true,
4
+ });
5
+ /** The keys `toolExecution` acts on; the settings test checks each has a /freeflow settings row. */
6
+ export const TOOL_EXECUTION_KEYS = ["enabled", "guidanceNotices"];
2
7
  // Settings of the retired v2 runtime (.deprecated/tool-execution-v2). Existing configuration files keep loading;
3
8
  // the values no longer do anything.
4
9
  const RETIRED_KEYS = ["capture", "programs", "workspace", "discovery", "adapters", "accounting"];
@@ -7,10 +12,12 @@ function record(value) {
7
12
  }
8
13
  export function validateToolExecutionConfig(value) {
9
14
  if (!record(value)) return "toolExecution must be an object";
10
- const unknown = Object.keys(value).find((key) => key !== "enabled" && !RETIRED_KEYS.includes(key));
15
+ const unknown = Object.keys(value).find((key) => !TOOL_EXECUTION_KEYS.includes(key) && !RETIRED_KEYS.includes(key));
11
16
  if (unknown) return `toolExecution has unsupported key: ${unknown}`;
12
17
  if (value.enabled !== undefined && typeof value.enabled !== "boolean")
13
18
  return "toolExecution.enabled must be a boolean";
19
+ if (value.guidanceNotices !== undefined && typeof value.guidanceNotices !== "boolean")
20
+ return "toolExecution.guidanceNotices must be a boolean";
14
21
  return null;
15
22
  }
16
23
  export function resolveToolExecutionConfig(repository, local, freeflowEnabled) {
@@ -18,5 +25,9 @@ export function resolveToolExecutionConfig(repository, local, freeflowEnabled) {
18
25
  const localTool = record(local.toolExecution) ? local.toolExecution : {};
19
26
  const setting = Object.hasOwn(localTool, "enabled") ? localTool.enabled : repositoryTool.enabled;
20
27
  const enabled = setting === undefined ? DEFAULT_TOOL_EXECUTION_CONFIG.enabled : setting === true;
21
- return { enabled, effective: freeflowEnabled && enabled };
28
+ const notices = Object.hasOwn(localTool, "guidanceNotices")
29
+ ? localTool.guidanceNotices
30
+ : repositoryTool.guidanceNotices;
31
+ const guidanceNotices = notices === undefined ? DEFAULT_TOOL_EXECUTION_CONFIG.guidanceNotices : notices === true;
32
+ return { enabled, guidanceNotices, effective: freeflowEnabled && enabled };
22
33
  }