@plainconceptsplatform/loop-task 2.6.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 (202) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +534 -0
  3. package/dist/app/App.js +132 -0
  4. package/dist/app/BoardLayout.js +5 -0
  5. package/dist/app/index.js +48 -0
  6. package/dist/app/providers/index.js +1 -0
  7. package/dist/app/router/index.js +16 -0
  8. package/dist/app/types.js +1 -0
  9. package/dist/cli/import-validator.js +152 -0
  10. package/dist/cli/import-writer.js +81 -0
  11. package/dist/cli.js +573 -0
  12. package/dist/client/cli-format.js +7 -0
  13. package/dist/client/commands.js +162 -0
  14. package/dist/client/ipc.js +96 -0
  15. package/dist/client/project-commands.js +164 -0
  16. package/dist/core/command/command-runner.js +300 -0
  17. package/dist/core/command/process-tree.js +73 -0
  18. package/dist/core/command/resolve-cwd.js +8 -0
  19. package/dist/core/command/stdout-capture-transform.js +73 -0
  20. package/dist/core/context/context-parser.js +65 -0
  21. package/dist/core/context/log-context.js +3 -0
  22. package/dist/core/context/template.js +20 -0
  23. package/dist/core/context/validate-context.js +19 -0
  24. package/dist/core/foreground/index.js +81 -0
  25. package/dist/core/logging/bounded-log-reader.js +368 -0
  26. package/dist/core/logging/log-follower.js +26 -0
  27. package/dist/core/logging/log-parser.js +29 -0
  28. package/dist/core/logging/log-rotator.js +34 -0
  29. package/dist/core/logging/rotating-log-stream.js +137 -0
  30. package/dist/core/loop/chain-executor.js +171 -0
  31. package/dist/core/loop/delay-utils.js +65 -0
  32. package/dist/core/loop/loop-controller.js +240 -0
  33. package/dist/core/loop/loop-runner.js +194 -0
  34. package/dist/core/loop/run-executor.js +144 -0
  35. package/dist/core/loop/types.js +1 -0
  36. package/dist/core/scheduling/index.js +16 -0
  37. package/dist/daemon/daemon-log.js +11 -0
  38. package/dist/daemon/diagnostics.js +85 -0
  39. package/dist/daemon/http/helpers.js +81 -0
  40. package/dist/daemon/http/openapi.js +119 -0
  41. package/dist/daemon/http/route-loops.js +311 -0
  42. package/dist/daemon/http/route-misc.js +60 -0
  43. package/dist/daemon/http/route-projects.js +55 -0
  44. package/dist/daemon/http/route-tasks.js +141 -0
  45. package/dist/daemon/http/routes.js +19 -0
  46. package/dist/daemon/http/server.js +82 -0
  47. package/dist/daemon/http/sse.js +36 -0
  48. package/dist/daemon/index.js +189 -0
  49. package/dist/daemon/ipc/send.js +5 -0
  50. package/dist/daemon/managers/loop-entry.js +27 -0
  51. package/dist/daemon/managers/loop-manager.js +346 -0
  52. package/dist/daemon/managers/loop-options.js +17 -0
  53. package/dist/daemon/managers/loop-serialization.js +53 -0
  54. package/dist/daemon/managers/project-manager.js +170 -0
  55. package/dist/daemon/managers/task-manager.js +94 -0
  56. package/dist/daemon/mcp/index.js +2 -0
  57. package/dist/daemon/mcp/openapi-sync.js +50 -0
  58. package/dist/daemon/mcp/server.js +167 -0
  59. package/dist/daemon/mcp/tools.js +382 -0
  60. package/dist/daemon/recipe/deferred-reload.js +32 -0
  61. package/dist/daemon/recipe/diagram-reader.js +26 -0
  62. package/dist/daemon/recipe/file-writer.js +52 -0
  63. package/dist/daemon/recipe/id-remapper.js +46 -0
  64. package/dist/daemon/recipe/runtime-state.js +23 -0
  65. package/dist/daemon/recipe/scanner.js +165 -0
  66. package/dist/daemon/recipe/task-store.js +33 -0
  67. package/dist/daemon/recipe/validator.js +35 -0
  68. package/dist/daemon/server/handlers/diagnostics-handlers.js +7 -0
  69. package/dist/daemon/server/handlers/index.js +101 -0
  70. package/dist/daemon/server/handlers/log-handlers.js +129 -0
  71. package/dist/daemon/server/handlers/loop-handlers.js +64 -0
  72. package/dist/daemon/server/handlers/project-handlers.js +42 -0
  73. package/dist/daemon/server/handlers/settings-handlers.js +8 -0
  74. package/dist/daemon/server/handlers/task-handlers.js +20 -0
  75. package/dist/daemon/server/handlers/telemetry-handlers.js +5 -0
  76. package/dist/daemon/server/index.js +117 -0
  77. package/dist/daemon/settings-manager.js +73 -0
  78. package/dist/daemon/spawner/index.js +204 -0
  79. package/dist/daemon/state/index.js +209 -0
  80. package/dist/daemon/telemetry/agent-integrations/agent-integration.js +1 -0
  81. package/dist/daemon/telemetry/agent-integrations/claude-code-integration.js +55 -0
  82. package/dist/daemon/telemetry/agent-integrations/detect-agent-integration.js +20 -0
  83. package/dist/daemon/telemetry/agent-integrations/index.js +3 -0
  84. package/dist/daemon/telemetry/agent-integrations/opencode-integration.js +72 -0
  85. package/dist/daemon/telemetry/index.js +6 -0
  86. package/dist/daemon/telemetry/noop-telemetry-adapter.js +53 -0
  87. package/dist/daemon/telemetry/open-telemetry-adapter.js +464 -0
  88. package/dist/daemon/telemetry/telemetry-manager.js +147 -0
  89. package/dist/daemon/telemetry/telemetry-redaction.js +67 -0
  90. package/dist/daemon/telemetry/telemetry-types.js +45 -0
  91. package/dist/daemon/telemetry/telemetry.js +1 -0
  92. package/dist/daemon/watcher/index.js +250 -0
  93. package/dist/duration.js +24 -0
  94. package/dist/entities/loops/filters.js +109 -0
  95. package/dist/entities/loops/index.js +1 -0
  96. package/dist/entities/projects/filters.js +69 -0
  97. package/dist/entities/projects/index.js +1 -0
  98. package/dist/entities/tasks/filters.js +41 -0
  99. package/dist/entities/tasks/index.js +1 -0
  100. package/dist/entry.js +16 -0
  101. package/dist/esm-loader.js +39 -0
  102. package/dist/features/chain-editor/ChainEditor.js +116 -0
  103. package/dist/features/chain-editor/mermaidToAscii.js +375 -0
  104. package/dist/features/chain-editor/renderChainDiagram.js +106 -0
  105. package/dist/features/code-editor/CodeEditorModal.js +84 -0
  106. package/dist/features/code-editor/CodeEditorPreview.js +14 -0
  107. package/dist/features/code-editor/useEditorKeyboard.js +150 -0
  108. package/dist/features/code-editor/useModalDimensions.js +9 -0
  109. package/dist/features/commands/commands.js +132 -0
  110. package/dist/features/commands/useCommandHandlers.js +280 -0
  111. package/dist/features/commands/useContextualActions.js +78 -0
  112. package/dist/features/commands/useGlobalShortcuts.js +154 -0
  113. package/dist/features/forms/FormRouter.js +46 -0
  114. package/dist/features/overlays/ContextHelpModal.js +8 -0
  115. package/dist/features/overlays/DiagramModal.js +45 -0
  116. package/dist/features/overlays/ExportModal.js +51 -0
  117. package/dist/features/overlays/HelpGuideModal.js +23 -0
  118. package/dist/features/overlays/HelpModal.js +59 -0
  119. package/dist/features/overlays/OverlayStack.js +10 -0
  120. package/dist/features/overlays/ProjectsModal.js +42 -0
  121. package/dist/features/overlays/TaskPickerModal.js +62 -0
  122. package/dist/features/overlays/WelcomeScreen.js +74 -0
  123. package/dist/features/overlays/useOverlayStack.js +55 -0
  124. package/dist/features/state/useAppState.js +151 -0
  125. package/dist/logger.js +25 -0
  126. package/dist/loop-config.js +152 -0
  127. package/dist/shared/clipboard.js +111 -0
  128. package/dist/shared/config/constants.js +104 -0
  129. package/dist/shared/config/paths.js +67 -0
  130. package/dist/shared/container/index.js +24 -0
  131. package/dist/shared/fs-utils.js +44 -0
  132. package/dist/shared/hooks/useBreakpoint.js +11 -0
  133. package/dist/shared/hooks/useDaemonSettings.js +41 -0
  134. package/dist/shared/hooks/useInject.js +6 -0
  135. package/dist/shared/hooks/useLogStream.js +37 -0
  136. package/dist/shared/hooks/useLoopFormValidation.js +131 -0
  137. package/dist/shared/hooks/useLoopPolling.js +30 -0
  138. package/dist/shared/hooks/useMouseScroll.js +56 -0
  139. package/dist/shared/hooks/useUndoRedo.js +77 -0
  140. package/dist/shared/i18n/en.json +709 -0
  141. package/dist/shared/i18n/index.js +11 -0
  142. package/dist/shared/providers/InversifyProvider.js +6 -0
  143. package/dist/shared/services/export-service.js +43 -0
  144. package/dist/shared/services/log-service.js +28 -0
  145. package/dist/shared/services/loop-service.js +78 -0
  146. package/dist/shared/services/project-service.js +39 -0
  147. package/dist/shared/services/settings-service.js +102 -0
  148. package/dist/shared/services/task-service.js +41 -0
  149. package/dist/shared/services/types.js +8 -0
  150. package/dist/shared/sleep.js +17 -0
  151. package/dist/shared/tail.js +4 -0
  152. package/dist/shared/ui/Button.js +18 -0
  153. package/dist/shared/ui/DebugPanel.js +9 -0
  154. package/dist/shared/ui/FocusableButton.js +18 -0
  155. package/dist/shared/ui/FocusableInput.js +72 -0
  156. package/dist/shared/ui/FocusableList.js +48 -0
  157. package/dist/shared/ui/Modal.js +13 -0
  158. package/dist/shared/ui/SelectModal.js +78 -0
  159. package/dist/shared/ui/Toast.js +37 -0
  160. package/dist/shared/ui/format.js +117 -0
  161. package/dist/shared/ui/hooks/useHoverState.js +11 -0
  162. package/dist/shared/ui/state.js +9 -0
  163. package/dist/shared/ui/theme.js +96 -0
  164. package/dist/shared/utils/log-lines.js +13 -0
  165. package/dist/shared/utils/paste.js +37 -0
  166. package/dist/shared/utils/syntax.js +128 -0
  167. package/dist/shared/utils/validation.js +56 -0
  168. package/dist/types.js +1 -0
  169. package/dist/visual-evidence/capture.js +51 -0
  170. package/dist/visual-evidence/cli.js +85 -0
  171. package/dist/visual-evidence/evidence-required.js +71 -0
  172. package/dist/visual-evidence/index.js +10 -0
  173. package/dist/visual-evidence/launch.js +61 -0
  174. package/dist/visual-evidence/manifest.js +32 -0
  175. package/dist/visual-evidence/openspec-resolver.js +45 -0
  176. package/dist/visual-evidence/publish.js +126 -0
  177. package/dist/visual-evidence/run.js +121 -0
  178. package/dist/visual-evidence/scenario-registry.js +132 -0
  179. package/dist/widgets/command-input/CommandDropdown.js +69 -0
  180. package/dist/widgets/command-input/CommandInput.js +133 -0
  181. package/dist/widgets/command-input/HintBar.js +9 -0
  182. package/dist/widgets/command-input/InputModes.js +72 -0
  183. package/dist/widgets/commands-browser/CommandsBrowserModal.js +85 -0
  184. package/dist/widgets/header/Header.js +54 -0
  185. package/dist/widgets/header/TabBar.js +20 -0
  186. package/dist/widgets/left-panel/LeftPanel.js +34 -0
  187. package/dist/widgets/left-panel/Navigator.js +107 -0
  188. package/dist/widgets/left-panel/ProjectsPage.js +76 -0
  189. package/dist/widgets/left-panel/ProjectsPageParts.js +34 -0
  190. package/dist/widgets/left-panel/TaskBrowser.js +132 -0
  191. package/dist/widgets/log-modal/LogModal.js +211 -0
  192. package/dist/widgets/loop-form/CreateForm.js +82 -0
  193. package/dist/widgets/loop-form/TextField.js +9 -0
  194. package/dist/widgets/loop-form/WizardForm.js +199 -0
  195. package/dist/widgets/loop-form/useCreateSteps.js +135 -0
  196. package/dist/widgets/loop-form/useHandleComplete.js +94 -0
  197. package/dist/widgets/project-form/ProjectForm.js +83 -0
  198. package/dist/widgets/right-panel/Inspector.js +41 -0
  199. package/dist/widgets/right-panel/RightPanel.js +38 -0
  200. package/dist/widgets/right-panel/RunHistory.js +187 -0
  201. package/dist/widgets/task-form/TaskForm.js +272 -0
  202. package/package.json +119 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Quique Fdez Guerra
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,534 @@
1
+ <div align="center">
2
+
3
+
4
+ # loop-task
5
+
6
+ **Loop engineering for your terminal. Run any command on a cadence.**
7
+
8
+ `@plainconceptsplatform/loop-task` is a cross-platform CLI that runs shell commands at human-readable intervals. Create loops in the background, manage them from an interactive TUI board, or run them in the foreground. It is the **heartbeat** primitive for [loop engineering](#loop-engineering): instead of running a task by hand every time, you schedule it once and let it run.
9
+
10
+ **[loop.ckgrafico.com](https://loop.ckgrafico.com)**
11
+
12
+ [![npm version](https://img.shields.io/npm/v/@plainconceptsplatform/loop-task?style=flat-square&color=black)](https://www.npmjs.com/package/@plainconceptsplatform/loop-task)
13
+ [![npm downloads](https://img.shields.io/npm/dm/@plainconceptsplatform/loop-task?style=flat-square&color=black)](https://www.npmjs.com/package/@plainconceptsplatform/loop-task)
14
+ [![license](https://img.shields.io/npm/l/@plainconceptsplatform/loop-task?style=flat-square&color=black)](./LICENSE)
15
+ [![node](https://img.shields.io/node/v/@plainconceptsplatform/loop-task?style=flat-square&color=black)](https://nodejs.org)
16
+
17
+ </div>
18
+
19
+ ## Loop engineering
20
+
21
+ **Loop engineering** is designing systems that run work on a cadence instead of triggering each run yourself. A *loop* is a recurring goal: you define a purpose, give it an interval, and let it iterate. It applies to ordinary engineering work just as much as to AI agents: health checks, sync jobs, test watches, data pulls, deploy polls, and report generation are all loops.
22
+
23
+ `loop-task` is that heartbeat as a tiny local primitive. Some examples:
24
+
25
+ <div align="center">
26
+ <img src="https://raw.githubusercontent.com/plainconceptsplatform/loop-task/refs/heads/main/demo.gif" alt="loop-task demo" width="700" />
27
+ </div>
28
+
29
+ ```bash
30
+ # Run the test suite every 30 minutes
31
+ loop-task new 30m -- npm test
32
+
33
+ # Poll a deploy every 10 seconds until you stop it
34
+ loop-task new 10s -- curl -sf https://example.com/health
35
+
36
+ # Re-sync a data export once an hour, scoped to a project
37
+ loop-task new 1h --project etl -- ./scripts/sync.sh
38
+
39
+ # Have a coding agent chip away at a backlog every 30 minutes
40
+ loop-task new 30m -- opencode run "find missing translations and translate them, 3 max"
41
+ ```
42
+
43
+ No cron files to maintain and no daemon to babysit: loops persist across reboots, run in the background, and you watch them from a terminal board. The idea is described well in Addy Osmani's [Loop Engineering](https://addyosmani.com/blog/loop-engineering/), where scheduled automations are the first of the five pieces of a working loop.
44
+
45
+ > **Stay in control.** A loop running unattended is also a loop failing unattended. Use `--max-runs`, watch the run history on the board, and review what each loop produces. The leverage moves to the loop; the responsibility stays with you.
46
+
47
+ ## Quick start
48
+
49
+ ```bash
50
+ npm install -g @plainconceptsplatform/loop-task
51
+ loop-task # open the board
52
+ loop-task start # start the daemon, restore persisted loops
53
+ loop-task new 30m -- npm test # create a background loop
54
+ loop-task run --now 10s -- echo hi # run a loop in the foreground
55
+ loop-task stop <id> # stop a frozen loop and kill its child process
56
+ loop-task restart # kill daemon + all loops, restart fresh
57
+ loop-task api # show HTTP API endpoints (Swagger, OpenAPI)
58
+ ```
59
+
60
+ Or run it directly:
61
+
62
+ ```bash
63
+ npx @plainconceptsplatform/loop-task
64
+ npx @plainconceptsplatform/loop-task new 30m -- npm test
65
+ ```
66
+
67
+ ## Agent skills
68
+
69
+ Install the public Loop Task skills with:
70
+
71
+ ```bash
72
+ npx skills add plainconceptsplatform/loop-task
73
+ ```
74
+
75
+ The repository also contains internal development skills under `.agents/skills/`.
76
+ They are marked internal automatically by the repository pre-commit hook and are
77
+ not intended for installation by Loop Task users.
78
+
79
+ ## Requirements
80
+
81
+ - **Node.js >= 20** - required for all commands including the board
82
+
83
+ ```bash
84
+ npm install -g @plainconceptsplatform/loop-task
85
+ ```
86
+
87
+ All commands (`start`, `new`, `run`, `board`) work with Node alone.
88
+
89
+ ## Concepts
90
+
91
+ ### Loops
92
+
93
+ A **loop** is a schedule - it defines *when* something runs. Loops trigger **tasks**.
94
+
95
+ | Field | Description |
96
+ | ----- | ----------- |
97
+ | **Interval** | How often to run (`30s`, `5m`, `1h`, `1d`, `1w`) |
98
+ | **Task** | Inline command or a reference to a previously defined task |
99
+ | **Description** | Optional label shown in the list; defaults to the task name |
100
+ | **Run immediately?** | Run once now, then every interval, or wait the first interval |
101
+ | **Max runs** | Stop after N runs, or leave blank to run forever |
102
+
103
+ ### Tasks
104
+
105
+ A **task** is an executable unit - it defines *what* runs. Tasks can chain to other tasks on success or failure.
106
+
107
+ | Field | Description |
108
+ | ----- | ----------- |
109
+ | **Name** | A short label for the task |
110
+ | **Command** | The full command line |
111
+ | **On success** | Optional task to run when this one exits with code 0 |
112
+ | **On failure** | Optional task to run when this one exits with a non-zero code |
113
+
114
+ Tasks are reusable - the same task can be referenced by multiple loops or by other tasks' success/failure chains.
115
+
116
+ ### Projects
117
+
118
+ A **project** is an organizational scope for loops. Every loop belongs to exactly one project. The board shows only loops in the currently selected project.
119
+
120
+ | Field | Description |
121
+ | ----- | ----------- |
122
+ | **Name** | A short label for the project |
123
+ | **Color** | One of six colors: white, cyan, orange, green, red, yellow |
124
+
125
+ Key behaviors:
126
+ - **Default project** - always present, cannot be renamed or deleted. New loops are assigned here when no other project is selected.
127
+ - **Color bullets** - each loop in the navigator displays a colored bullet (●) matching its project color.
128
+ - **Project filter** - the board shows only loops belonging to the currently active project. The selection persists across sessions via `localStorage`.
129
+
130
+ To use projects from the board:
131
+ - Press `c` to open the **Project selector** (switch between projects)
132
+ - Click **Manage Projects** in the filter bar (or use the keyboard shortcut) to open the **Manage Projects** page
133
+ - From the Manage Projects page: `n` creates a new project, `e` renames the selected project, `d` deletes it, `Esc` returns to the board
134
+
135
+ From the CLI:
136
+ - `loop-task project list` - list all projects
137
+ - `loop-task project new <name> [--color <color>]` - create a project
138
+ - `loop-task project rename <id|name> <new-name>` - rename a project
139
+ - `loop-task project color <id|name> <color>` - change a project's color
140
+ - `loop-task project delete <id|name>` - delete a project (loops move to Default)
141
+ - `loop-task new <interval> --project <name> -- <command>` - create a loop assigned to a project
142
+
143
+ Colors can be a name (`white`, `cyan`, `green`, `yellow`, `orange`, `pink`) or a `#rrggbb` hex value.
144
+
145
+ ## Commands
146
+
147
+ | Command | Description |
148
+ | ------- | ----------- |
149
+ | `loop-task` | Open the interactive board |
150
+ | `loop-task start` | Start the background daemon, restore persisted loops |
151
+ | `loop-task new <interval> -- <command>` | Create a background loop (creates an inline task) |
152
+ | `loop-task new <interval> --project <name> -- <command>` | Create a loop assigned to a project |
153
+ | `loop-task run <interval> -- <command>` | Run a loop in the foreground |
154
+ | `loop-task stop <id>` | Stop a loop and interrupt its running child process |
155
+ | `loop-task restart` | Kill the daemon and all running loops, then restart fresh |
156
+ | `loop-task status [--json]` | Show status of all loops (JSON optional for scripting) |
157
+ | `loop-task export [file]` | Export all configs to JSON file (or stdout) |
158
+ | `loop-task import <file>` | Import configs from file (triggers hot-reload) |
159
+ | `loop-task api` | Show HTTP API endpoints (base URL, Swagger UI, OpenAPI spec) |
160
+ | `loop-task http-host [address]` | Show or set the HTTP API bind host (default `0.0.0.0`; `local` to lock to loopback) |
161
+ | `loop-task project list` | List all projects |
162
+ | `loop-task project new <name> [--color <color>]` | Create a project |
163
+ | `loop-task project rename <id\|name> <new-name>` | Rename a project |
164
+ | `loop-task project color <id\|name> <color>` | Change project color |
165
+ | `loop-task project delete <id\|name>` | Delete a project (loops move to Default) |
166
+
167
+ ### Options (for `new` and `run`)
168
+
169
+ | Option | Description |
170
+ | ------ | ----------- |
171
+ | `--now` | Run immediately before waiting |
172
+ | `--max-runs <n>` | Stop after N executions |
173
+ | `--cwd <dir>` | Working directory for the command |
174
+ | `--verbose` | Show execution details |
175
+ | `-h, --help` | Display help |
176
+ | `-V, --version` | Display version |
177
+
178
+ ## Examples
179
+
180
+ ```bash
181
+ # Run tests every 30 minutes
182
+ loop-task new 30m -- npm test
183
+
184
+ # Run immediately, then every hour
185
+ loop-task new --now 1h -- npm test
186
+
187
+ # Run up to 5 times, then stop
188
+ loop-task run --max-runs 5 5m -- npm test
189
+
190
+ # Agent workflow - schedule an AI task every 30 minutes
191
+ loop-task new 30m --now -- opencode run "search missing translations and translate them, 3 maximum" --model "opencode/big-pickle"
192
+ # Run in a specific directory
193
+ loop-task new 30m --cwd ./packages/api -- npm test
194
+
195
+ # Verbose mode
196
+ loop-task run --verbose 30m -- npm test
197
+ ```
198
+
199
+ When the command has its own flags, use `--` to stop argument parsing:
200
+
201
+ ```bash
202
+ loop-task new 30m -- node -e "console.log('hello')"
203
+ ```
204
+
205
+ ## The board
206
+
207
+ The board is the primary way to manage loops and tasks. It shows all loops, their status, run history, and logs in a single terminal interface.
208
+
209
+ ### Board controls
210
+
211
+ ```
212
+ ↑/↓, j/k move selection
213
+ Enter edit selected loop
214
+ e edit loop
215
+ d/del delete loop
216
+ p pause (when waiting) / play (when idle/paused)
217
+ s stop loop (resets schedule)
218
+ n create a new loop
219
+ t create a new task
220
+ o cycle sort mode (order by)
221
+ ←/→ switch between panels
222
+ / search loops
223
+ h toggle help
224
+ esc quit
225
+ ```
226
+
227
+ Destructive actions (pause, force run, delete) prompt a confirmation before executing.
228
+
229
+ ### Copy & paste in the command bar
230
+
231
+ The bottom command bar is a normal terminal input, so use your terminal's own clipboard gestures, they work in every terminal (including the VS Code integrated terminal, where Ctrl+C/V are captured by the editor):
232
+
233
+ - **Paste** with **Ctrl+Shift+V** (Windows/Linux), **Cmd+V** (macOS), or **right-click**. Multi-line pastes collapse to a single line.
234
+ - **Ctrl+U** clears the command bar (select-all + delete).
235
+ - **`c`** copies the selected loop/task's command to the clipboard.
236
+ - To copy arbitrary text, drag-select in the terminal and use its native copy (Ctrl+Shift+C / right-click).
237
+
238
+ ### Pause vs Stop
239
+
240
+ - **Pause** (`p`) - temporarily halts the loop. Resuming continues the original schedule (e.g., a loop that runs every 6h at :00 paused at 12:00 and resumed at 14:00 will still fire at 16:00).
241
+ - **Stop** (`s`) - halts the loop and clears the schedule. Playing starts a fresh interval from now (e.g., the same loop stopped at 12:00 and played at 14:00 will fire at 20:00).
242
+
243
+ ## How it works
244
+
245
+ ```
246
+ loop-task (board) ──IPC──► daemon ──► loop 1 ──► task (command)
247
+ HTTP client (curl) ──HTTP──► ├──► loop 2 ──► task ──► on-success task
248
+ browser (Swagger) ──HTTP──► └──► loop 3 ──► task ──► on-failure task
249
+ ```
250
+
251
+ - The **daemon** is a background process that manages all loops and tasks. It starts automatically when you run `loop-task start` or any command that needs it.
252
+ - The **board** is a terminal UI that connects to the daemon via IPC.
253
+ - **Loops** define schedules and reference tasks. **Tasks** define commands and optional success/failure chains.
254
+ - Loops and tasks **persist to disk** - they survive daemon restarts and system reboots. When the daemon starts, it restores all loops and accounts for elapsed time.
255
+
256
+ ### Lifecycle
257
+
258
+ 1. `loop-task start` or `loop-task new ...` spawns the daemon if not running
259
+ 2. The daemon creates a loop and a task, and persists their state to disk
260
+ 3. `loop-task` opens the board for interactive management
261
+ 4. Closing the board or terminal does **not** stop loops - the daemon keeps running
262
+ 5. After a reboot, `loop-task start` restores all persisted loops with correct timing
263
+
264
+ ## Supported intervals
265
+
266
+ | Format | Description |
267
+ | ------ | ----------- |
268
+ | `10s` | 10 seconds |
269
+ | `5m` | 5 minutes |
270
+ | `1h` | 1 hour |
271
+ | `1d` | 1 day |
272
+ | `1w` | 1 week |
273
+
274
+ ## Behavior
275
+
276
+ - **No overlapping** - waits for the command to finish before starting the next interval
277
+ - **Resilient** - continues looping even if a command exits with a non-zero code
278
+ - **Persistent** - loop and task state is saved after every run; survives restarts
279
+ - **Graceful shutdown** - background loops are daemon-managed; foreground loops finish the current execution on Ctrl+C
280
+
281
+ ## Chain Context Sharing
282
+
283
+ When tasks are arranged in a chain (on-success or on-failure), context flows between them automatically. This lets later tasks reference output from earlier ones without custom glue.
284
+
285
+ ### How it works
286
+
287
+ 1. **Auto-capture** - stdout and stderr from every task in the chain are captured before the next task starts.
288
+ 2. **Parse rules** - stdout is parsed by content type:
289
+ - **JSON object** (`{"key": "value"}`) - each key is merged into the shared context.
290
+ - **JSONL** (one JSON object per line) - each line's keys are merged in order.
291
+ - **Plain text** - stored under a single `output` key.
292
+ - **Empty output** - no change to context.
293
+ 3. **Template interpolation** - use `{{key}}` in the command or arguments of any task. Before spawning, `{{key}}` is replaced with the current value of `key` from the shared context.
294
+ 4. **Merge semantics** - keys accumulate across the chain. Task 1 produces `{ "id": "42" }`, task 2 can use `{{id}}` and also add `{ "status": "ok" }`. Task 3 sees both.
295
+ 5. **Latest command output** - `{{output}}` always contains stdout plus stderr from the immediately preceding task. JSON fields such as `{{number}}`, `{{title}}`, and `{{body}}` remain available until a later task explicitly emits the same key.
296
+ 6. **Context lifecycle** - context is built fresh each loop iteration and exists only in memory. It is never persisted to disk.
297
+
298
+ ### Example: Issue Refinement Chain
299
+
300
+ A four-task chain that finds an issue, marks it in-progress, rewrites it with AI, and relabels it - all without re-querying:
301
+
302
+ **Task 1** (primary): Find an issue to refine
303
+
304
+ ```bash
305
+ gh issue list --label "to refine" --json number,title,body --jq '{number: .[0].number, title: .[0].title, body: .[0].body}'
306
+ ```
307
+
308
+ stdout: `{"number":123,"title":"Fix login","body":"It doesn't work"}`
309
+ context: `{ number: 123, title: "Fix login", body: "It doesn't work" }`
310
+
311
+ **Task 2** (chain, onSuccess): Mark as in-progress
312
+
313
+ ```bash
314
+ gh issue edit {{number}} --add-label "refining" --remove-label "to refine"
315
+ ```
316
+
317
+ interpolated: `gh issue edit 123 --add-label "refining" --remove-label "to refine"`
318
+
319
+ **Task 3** (chain, onSuccess): Rewrite with AI (edits the issue directly)
320
+
321
+ ```bash
322
+ opencode run "Rewrite this GitHub issue as a detailed user story using project context. Update the issue title and body directly using gh issue edit. Issue number: {{number}} Original title: {{title}} Original body: {{body}}" --model "opencode/big-pickle"
323
+ ```
324
+
325
+ interpolated: `opencode run "Rewrite this GitHub issue as a detailed user story using project context. Update the issue title and body directly using gh issue edit. Issue number: 123 Original title: Fix login Original body: It doesn't work" --model "opencode/big-pickle"`
326
+
327
+ **Task 4** (chain, onSuccess): Relabel as ready to implement
328
+
329
+ ```bash
330
+ gh issue edit {{number}} --remove-label "refining" --add-label "to implement"
331
+ ```
332
+
333
+ interpolated: `gh issue edit 123 --remove-label "refining" --add-label "to implement"`
334
+
335
+ ### How it works
336
+
337
+ 1. Task 1 queries the issue and emits a JSON object with `number`, `title`, and `body` via `--jq`. The primary task cannot use `{{key}}` interpolation because the chain context is empty when it runs.
338
+ 2. Task 2 receives `{{number}}` interpolated from task 1's context. It relabels the issue from "to refine" to "refining" - no re-query needed.
339
+ 3. Task 3 runs opencode, which finds the issue by the "refining" label and rewrites it in place using `gh issue edit`. The AI agent edits the issue directly - no need to parse its stdout as JSON.
340
+ 4. Task 4 receives `{{number}}` (still 123 from task 1) and relabels the issue as "to implement" - no re-query needed.
341
+
342
+ ### Wrapping values with --jq
343
+
344
+ To avoid the plain-text `output` clobbering, wrap any value in a named JSON key using `--jq` (requires `--json` before `--jq`):
345
+
346
+ ```bash
347
+ gh issue list --label "to refine" --json number,title --jq '{number: .[0].number, title: .[0].title}'
348
+ ```
349
+
350
+ This stores `{ "number": 123, "title": "Fix login" }` in context instead of overwriting `output`.
351
+
352
+ ### Example: Issue Implementation Chain
353
+
354
+ A four-task chain that finds an issue to implement, marks it in-progress, runs an AI agent to implement it, then closes it - all without re-querying:
355
+
356
+ **Task 1** (primary): Find an issue to implement (or exit if one is already in progress)
357
+
358
+ ```bash
359
+ gh issue list --label "implementing" --json number --jq 'length == 0' | grep -q true && gh issue list --label "to implement" --json number,title,body --jq '{number: .[0].number, title: .[0].title, body: .[0].body}'
360
+ ```
361
+
362
+ stdout: `{"number":456,"title":"Add dark mode toggle","body":"Users want a dark theme"}`
363
+ context: `{ number: 456, title: "Add dark mode toggle", body: "Users want a dark theme" }`
364
+
365
+ If an issue with the "implementing" label already exists, `length == 0` returns `false`, `grep -q true` fails, and the `&&` short-circuits - the chain does not fire. The loop waits for the next iteration.
366
+
367
+ **Task 2** (chain, onSuccess): Mark as in-progress
368
+
369
+ ```bash
370
+ gh issue edit {{number}} --add-label "implementing" --remove-label "to implement"
371
+ ```
372
+
373
+ interpolated: `gh issue edit 456 --add-label "implementing" --remove-label "to implement"`
374
+
375
+ **Task 3** (chain, onSuccess): Implement with AI agent
376
+
377
+ ```bash
378
+ git fetch origin && git checkout main && git reset --hard origin/main && opencode run "Implement this GitHub issue using /ob-autopilot and return only JSON with fields title and body after implementation is completed, merged to main, pushed to origin and the issue has been referenced in GitHub. Issue title: {{title}} Issue body: {{body}}" --model "opencode/big-pickle"
379
+ ```
380
+
381
+ interpolated: `git fetch origin && git checkout main && git reset --hard origin/main && opencode run "Implement this GitHub issue using /ob-autopilot ... Issue title: Add dark mode toggle Issue body: Users want a dark theme" --model "opencode/big-pickle"`
382
+
383
+ stdout: `{"title":"Add dark mode toggle","body":"Implemented dark mode toggle with CSS variables..."}`
384
+ context: `{ number: 456, title: "Add dark mode toggle", body: "Implemented dark mode toggle..." }`
385
+
386
+ **Task 4** (chain, onSuccess): Verify sync and close the issue
387
+
388
+ ```bash
389
+ git push && git fetch origin && [ "$(git rev-parse HEAD)" = "$(git rev-parse origin/main)" ] && gh issue edit {{number}} --remove-label "implementing" && gh issue close {{number}}
390
+ ```
391
+
392
+ interpolated: `git push && git fetch origin && [ "$(git rev-parse HEAD)" = "$(git rev-parse origin/main)" ] && gh issue edit 456 --remove-label "implementing" && gh issue close 456`
393
+
394
+ The `git rev-parse` check ensures local and remote are in sync before closing - if the push failed or remote is ahead, the command fails and the issue stays open.
395
+
396
+ ## Docker
397
+
398
+ ```bash
399
+ # Run the board in a container
400
+ docker run -it -v ~/.loop-cli:/root/.loop-cli loop-task
401
+
402
+ # Check status
403
+ docker run -v ~/.loop-cli:/root/.loop-cli loop-task status --json
404
+
405
+ # Create a background loop
406
+ docker run -v ~/.loop-cli:/root/.loop-cli loop-task new 30m -- npm test
407
+ ```
408
+
409
+ ## HTTP API
410
+
411
+ The daemon exposes a REST + SSE API on `localhost:8845` (configurable via `LOOP_CLI_HTTP_PORT`). It starts automatically with the daemon, no extra flags needed.
412
+
413
+ ### Quick reference
414
+
415
+ ```bash
416
+ # List all loops
417
+ curl http://127.0.0.1:8845/api/loops
418
+
419
+ # Create a loop
420
+ curl -X POST http://127.0.0.1:8845/api/loops \
421
+ -H "Content-Type: application/json" \
422
+ -d '{"command":"echo hi","intervalHuman":"5m","description":"test"}'
423
+
424
+ # Get a single loop
425
+ curl http://127.0.0.1:8845/api/loops/abc123
426
+
427
+ # Pause / resume / trigger / stop / delete
428
+ curl -X POST http://127.0.0.1:8845/api/loops/abc123/pause
429
+ curl -X POST http://127.0.0.1:8845/api/loops/abc123/resume
430
+ curl -X POST http://127.0.0.1:8845/api/loops/abc123/trigger
431
+ curl -X POST http://127.0.0.1:8845/api/loops/abc123/stop
432
+ curl -X DELETE http://127.0.0.1:8845/api/loops/abc123
433
+
434
+ # Fetch logs (last 50 lines)
435
+ curl http://127.0.0.1:8845/api/loops/abc123/logs?tail=50
436
+
437
+ # Stream logs in real-time (SSE)
438
+ curl -N http://127.0.0.1:8845/api/loops/abc123/logs/stream
439
+
440
+ # Subscribe to daemon events (SSE)
441
+ curl -N http://127.0.0.1:8845/api/events
442
+
443
+ # Tasks and projects
444
+ curl http://127.0.0.1:8845/api/tasks
445
+ curl http://127.0.0.1:8845/api/projects
446
+ ```
447
+
448
+ ### Interactive docs
449
+
450
+ - **Swagger UI**: `http://127.0.0.1:8845/api/docs`
451
+ - **OpenAPI 3.0 spec**: `http://127.0.0.1:8845/api/openapi.json`
452
+
453
+ ### From the CLI/TUI
454
+
455
+ - `loop-task api`, prints all API endpoints to stdout
456
+ - Board: press **Ctrl+G** or type `api`, shows a toast with API info
457
+
458
+ ### Response format
459
+
460
+ All responses use a consistent JSON envelope:
461
+
462
+ ```json
463
+ {"ok": true, "data": ...} // success (200/201)
464
+ {"ok": false, "error": {"message": "..."}} // error (400/404/405/500)
465
+ ```
466
+
467
+ If the port is already in use, the daemon skips the HTTP server and continues with IPC only.
468
+
469
+ ### Network access (remote / VMs)
470
+
471
+ The HTTP API (8845) and MCP server (8846) bind to **`0.0.0.0` (all interfaces) by default**, so a daemon on a VM or homelab box is reachable f, other machines out of the box — connect over SSH, [Tailscale](https://tailscale.com), or your LAN. `loop-task http-host` governs the bind for both.
472
+
473
+ > **The HTTP API is unauthenticated.** Anything that can reach the bound port can create and trigger loops, i.e. run commands on the host. Because the default is `0.0.0.0`, **securing access is your responsibility at the network layer**: run the box behind a VPN (Tailscale), reach it only via an SSH tunnel, and/or block the port at the cloud firewall so it isn't exposed to the public internet.
474
+
475
+ Change the bind host at any time (persisted in daemon settings, not an env var, and the daemon **rebinds live**, no restart):
476
+
477
+ ```bash
478
+ loop-task http-host # show the current bind host + reachable URL
479
+ loop-task http-host local # 127.0.0.1, loopback only (lock it down)
480
+ loop-task http-host 100.99.155.102 # bind a single interface (e.g. a Tailscale IP)
481
+ loop-task http-host all # 0.0.0.0, all interfaces (the default)
482
+ ```
483
+
484
+ **Locking it down:** if the box isn't behind a firewall/VPN you trust, either set `loop-task http-host local` and reach it through an SSH tunnel (`ssh -L 8845:127.0.0.1:8845 <host>`), or expose it tailnet-only with `tailscale serve --bg 8845` (HTTPS at `https://<node>.<tailnet>.ts.net`, restrict further with Tailscale ACLs; never `tailscale funnel`).
485
+
486
+ ## Development
487
+
488
+ Requires [Node.js](https://nodejs.org) >= 20. Uses [pnpm](https://pnpm.io) for package management.
489
+
490
+ ```bash
491
+ pnpm install
492
+ npm run build
493
+ ```
494
+
495
+ Run locally:
496
+
497
+ ```bash
498
+ tsx src/cli.ts # board
499
+ node dist/entry.js new --now 30m -- npm test # background loop
500
+ node dist/entry.js run --now --max-runs 1 10s -- echo hello # foreground
501
+ ```
502
+
503
+ Quality gates:
504
+
505
+ ```bash
506
+ pnpm run typecheck # tsc --noEmit
507
+ pnpm run lint # eslint src/ tests/
508
+ pnpm run test # vitest run
509
+ pnpm run build # tsc -p tsconfig.build.json
510
+ ```
511
+
512
+ ### Testing the board in a browser (ttyd)
513
+
514
+ > **Agents: Do NOT use ttyd unless the user explicitly asks you to check the CLI in a browser.** It is never the default. Do not start a ttyd server on your own for "manual pass" tasks or visual QA, those are for the human. Reach for ttyd only when the user says "check the board in the browser", "use ttyd", or similar.
515
+
516
+ The board is an interactive TUI, so it needs a real terminal, you can't drive it from a piped/captured shell (and neither can an AI agent). [`ttyd`](https://github.com/tsl0922/ttyd) shares a terminal over HTTP, which makes the board reachable from a browser and scriptable by browser-automation agents, but only when explicitly requested.
517
+
518
+ Install ttyd (see the [ttyd README](https://github.com/tsl0922/ttyd#installation), e.g. `winget install tsl0922.ttyd`, `brew install ttyd`, or `apt install ttyd`), then serve the board from an interactive terminal:
519
+
520
+ ```bash
521
+ # Point -w at the repo (absolute path) and run the dev board:
522
+ ttyd -W -w "C:\Projects\Personal\loop-cli" -p 7681 pnpm run dev
523
+
524
+ # The built entry works too (after `npm run build`), with the same -w:
525
+ ttyd -W -w "C:\Projects\Personal\loop-cli" -p 7681 node dist/entry.js
526
+ ```
527
+
528
+ Open `http://localhost:7681` in a browser and use the board as normal. `-W` makes it writable so keystrokes reach the TUI. Handy for demos, for testing on a machine without a good local terminal, and for letting an AI agent drive the board (navigate, send keys, screenshot; ttyd renders via xterm.js on a `<canvas>`, so read state from screenshots, not page text).
529
+
530
+ > **Windows note:** always pass `-w "<absolute repo path>"`. Without it, ttyd gives the spawned command no valid working directory and it fails with `CreateProcessW failed with error 267`, for *every* command (`pnpm`, `npx`, `node` all fail the same way; it is not a `.cmd`-shim issue). On macOS/Linux `-w` is optional but harmless. Start ttyd from a real interactive terminal; a detached/console-less launch can crash its ConPTY on Windows.
531
+
532
+ ## License
533
+
534
+ MIT