owflow 0.5.0 → 0.7.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 (195) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +448 -261
  3. package/dist/agents/bottleneck-analyzer.md +332 -329
  4. package/dist/agents/code-quality-pragmatist.md +353 -354
  5. package/dist/agents/code-reviewer.md +238 -239
  6. package/dist/agents/codebase-analysis-reporter.md +245 -246
  7. package/dist/agents/docs-operator.md +22 -23
  8. package/dist/agents/e2e-test-verifier.md +447 -448
  9. package/dist/agents/gap-analyzer.md +501 -502
  10. package/dist/agents/implementation-completeness-checker.md +219 -220
  11. package/dist/agents/implementation-planner.md +308 -309
  12. package/dist/agents/information-gatherer.md +690 -691
  13. package/dist/agents/production-readiness-checker.md +278 -279
  14. package/dist/agents/project-analyzer.md +403 -403
  15. package/dist/agents/reality-assessor.md +365 -366
  16. package/dist/agents/research-planner.md +449 -450
  17. package/dist/agents/research-synthesizer.md +402 -403
  18. package/dist/agents/solution-brainstormer.md +256 -257
  19. package/dist/agents/solution-designer.md +371 -372
  20. package/dist/agents/spec-auditor.md +297 -298
  21. package/dist/agents/specification-creator.md +333 -334
  22. package/dist/agents/task-classifier.md +449 -450
  23. package/dist/agents/task-group-implementer.md +283 -284
  24. package/dist/agents/test-suite-runner.md +194 -195
  25. package/dist/agents/user-docs-generator.md +508 -509
  26. package/dist/commands/reviews-code.md +90 -90
  27. package/dist/commands/reviews-pragmatic.md +99 -99
  28. package/dist/commands/reviews-production-readiness.md +113 -113
  29. package/dist/commands/reviews-reality-check.md +110 -110
  30. package/dist/commands/reviews-spec-audit.md +114 -114
  31. package/dist/commands/work.md +280 -280
  32. package/dist/configuration/agents-config.d.ts +2 -0
  33. package/dist/configuration/agents-config.js +8 -44
  34. package/dist/configuration/commands-config.d.ts +23 -0
  35. package/dist/configuration/commands-config.js +17 -17
  36. package/dist/configuration/skills-config.d.ts +2 -0
  37. package/dist/configuration/skills-config.js +3 -3
  38. package/dist/hooks/hooks-diagnostics.d.ts +3 -0
  39. package/dist/hooks/hooks-diagnostics.js +21 -0
  40. package/dist/hooks/hooks-persistence.d.ts +17 -0
  41. package/dist/hooks/hooks-persistence.js +207 -0
  42. package/dist/hooks/hooks-registry.d.ts +6 -0
  43. package/dist/hooks/hooks-registry.js +104 -0
  44. package/dist/hooks/hooks-wrapper.d.ts +25 -0
  45. package/dist/hooks/hooks-wrapper.js +318 -0
  46. package/dist/hooks/internal/before-tool.d.ts +6 -0
  47. package/dist/hooks/internal/before-tool.js +13 -0
  48. package/dist/hooks/internal/session-compaction.d.ts +9 -0
  49. package/dist/hooks/{session-compaction.js → internal/session-compaction.js} +9 -7
  50. package/dist/hooks/runtime/circuit-breaker.d.ts +23 -0
  51. package/dist/hooks/runtime/circuit-breaker.js +39 -0
  52. package/dist/hooks/runtime/failure-policy.d.ts +15 -0
  53. package/dist/hooks/runtime/failure-policy.js +49 -0
  54. package/dist/hooks/runtime/process-executor.d.ts +33 -0
  55. package/dist/hooks/runtime/process-executor.js +147 -0
  56. package/dist/hooks/runtime/status-recorder.d.ts +27 -0
  57. package/dist/hooks/runtime/status-recorder.js +111 -0
  58. package/dist/index.d.ts +7 -0
  59. package/dist/index.js +111 -40
  60. package/dist/skills/agents-md-generator/SKILL.md +223 -223
  61. package/dist/skills/codebase-analyzer/SKILL.md +162 -162
  62. package/dist/skills/codebase-analyzer/references/code-analysis.md +63 -63
  63. package/dist/skills/codebase-analyzer/references/combined.md +31 -31
  64. package/dist/skills/codebase-analyzer/references/context-discovery.md +63 -63
  65. package/dist/skills/codebase-analyzer/references/file-discovery.md +51 -51
  66. package/dist/skills/codebase-analyzer/references/migration-target.md +23 -23
  67. package/dist/skills/codebase-analyzer/references/pattern-mining.md +22 -22
  68. package/dist/skills/dev-analyze/SKILL.md +115 -115
  69. package/dist/skills/dev-bugfix/SKILL.md +452 -452
  70. package/dist/skills/dev-finalize/SKILL.md +118 -118
  71. package/dist/skills/dev-implement/SKILL.md +167 -167
  72. package/dist/skills/dev-plan/SKILL.md +147 -147
  73. package/dist/skills/dev-spec/SKILL.md +182 -182
  74. package/dist/skills/dev-tdd-red/SKILL.md +94 -94
  75. package/dist/skills/dev-verify/SKILL.md +153 -153
  76. package/dist/skills/development/SKILL.md +260 -260
  77. package/dist/skills/diagrams-mermaid/SKILL.md +183 -159
  78. package/dist/skills/diagrams-mermaid/references/reference.md +220 -148
  79. package/dist/skills/docs-manager/SKILL.md +379 -379
  80. package/dist/skills/docs-manager/docs/INDEX.md +199 -199
  81. package/dist/skills/docs-manager/docs/standards/backend/api.md +25 -25
  82. package/dist/skills/docs-manager/docs/standards/backend/migrations.md +22 -22
  83. package/dist/skills/docs-manager/docs/standards/backend/models.md +25 -25
  84. package/dist/skills/docs-manager/docs/standards/backend/queries.md +22 -22
  85. package/dist/skills/docs-manager/docs/standards/frontend/accessibility.md +25 -25
  86. package/dist/skills/docs-manager/docs/standards/frontend/components.md +28 -28
  87. package/dist/skills/docs-manager/docs/standards/frontend/css.md +16 -16
  88. package/dist/skills/docs-manager/docs/standards/frontend/responsive.md +28 -28
  89. package/dist/skills/docs-manager/docs/standards/global/coding-style.md +25 -25
  90. package/dist/skills/docs-manager/docs/standards/global/commenting.md +10 -10
  91. package/dist/skills/docs-manager/docs/standards/global/conventions.md +31 -31
  92. package/dist/skills/docs-manager/docs/standards/global/error-handling.md +22 -22
  93. package/dist/skills/docs-manager/docs/standards/global/minimal-implementation.md +22 -22
  94. package/dist/skills/docs-manager/docs/standards/global/validation.md +28 -28
  95. package/dist/skills/docs-manager/docs/standards/testing/test-writing.md +25 -25
  96. package/dist/skills/docs-manager/references/agents-md-template.md +29 -29
  97. package/dist/skills/docs-manager/references/index-md-template.md +73 -73
  98. package/dist/skills/flow-init/SKILL.md +234 -234
  99. package/dist/skills/flow-init/references/architecture-template.md +45 -45
  100. package/dist/skills/flow-init/references/roadmap-templates.md +93 -93
  101. package/dist/skills/flow-init/references/tech-stack-template.md +70 -70
  102. package/dist/skills/flow-init/references/vision-templates.md +75 -75
  103. package/dist/skills/goal-development/SKILL.md +96 -96
  104. package/dist/skills/goal-migration/SKILL.md +114 -114
  105. package/dist/skills/goal-performance/SKILL.md +126 -0
  106. package/dist/skills/goal-research/SKILL.md +118 -118
  107. package/dist/skills/html-renderer/SKILL.md +173 -173
  108. package/dist/skills/html-renderer/references/index.html +318 -318
  109. package/dist/skills/html-renderer/references/styles.css +388 -388
  110. package/dist/skills/html-renderer/references/tokens.json +104 -104
  111. package/dist/skills/implementation-plan-executor/SKILL.md +378 -378
  112. package/dist/skills/implementation-verifier/SKILL.md +307 -307
  113. package/dist/skills/migration/SKILL.md +268 -268
  114. package/dist/skills/migration-analyze/SKILL.md +126 -126
  115. package/dist/skills/migration-finalize/SKILL.md +161 -161
  116. package/dist/skills/migration-fix/SKILL.md +155 -155
  117. package/dist/skills/migration-implement/SKILL.md +131 -131
  118. package/dist/skills/migration-plan/SKILL.md +131 -131
  119. package/dist/skills/migration-spec/SKILL.md +135 -135
  120. package/dist/skills/migration-spec/references/migration-strategies.md +430 -430
  121. package/dist/skills/migration-target/SKILL.md +134 -134
  122. package/dist/skills/migration-target/references/migration-types.md +489 -489
  123. package/dist/skills/migration-verify/SKILL.md +184 -184
  124. package/dist/skills/orchestrator-framework/SKILL.md +65 -65
  125. package/dist/skills/orchestrator-framework/references/command-namespacing.md +7 -7
  126. package/dist/skills/orchestrator-framework/references/confirm-or-revise-exception.md +20 -20
  127. package/dist/skills/orchestrator-framework/references/delegation-rules.md +54 -54
  128. package/dist/skills/orchestrator-framework/references/dispatcher-handoff.md +27 -27
  129. package/dist/skills/orchestrator-framework/references/fix-loop-contract.md +65 -65
  130. package/dist/skills/orchestrator-framework/references/gate-contract.md +122 -117
  131. package/dist/skills/orchestrator-framework/references/orchestrator-creation-checklist.md +51 -51
  132. package/dist/skills/orchestrator-framework/references/orchestrator-patterns.md +235 -235
  133. package/dist/skills/performance/SKILL.md +267 -437
  134. package/dist/skills/performance-analyze/SKILL.md +129 -0
  135. package/dist/skills/{performance → performance-analyze}/references/performance-optimization-guide.md +271 -365
  136. package/dist/skills/performance-finalize/SKILL.md +117 -0
  137. package/dist/skills/performance-implement/SKILL.md +114 -0
  138. package/dist/skills/performance-plan/SKILL.md +112 -0
  139. package/dist/skills/performance-spec/SKILL.md +145 -0
  140. package/dist/skills/performance-verify/SKILL.md +164 -0
  141. package/dist/skills/research/SKILL.md +249 -249
  142. package/dist/skills/research-brainstorm/SKILL.md +120 -120
  143. package/dist/skills/research-brainstorm/references/brainstorming-techniques.md +84 -84
  144. package/dist/skills/research-converge/SKILL.md +130 -130
  145. package/dist/skills/research-design/SKILL.md +146 -146
  146. package/dist/skills/research-design/references/design-techniques.md +40 -40
  147. package/dist/skills/research-finalize/SKILL.md +125 -125
  148. package/dist/skills/research-fork/SKILL.md +190 -190
  149. package/dist/skills/research-gather/SKILL.md +133 -133
  150. package/dist/skills/research-plan/SKILL.md +141 -141
  151. package/dist/skills/research-plan/references/research-methodologies.md +710 -710
  152. package/dist/skills/research-quick/SKILL.md +139 -139
  153. package/dist/skills/research-synthesize/SKILL.md +127 -127
  154. package/dist/skills/rule-reviewer/SKILL.md +334 -334
  155. package/dist/skills/standards-discover/SKILL.md +260 -260
  156. package/dist/skills/standards-discover/references/aggregation-strategy.md +76 -76
  157. package/dist/skills/standards-discover/references/code-pattern-prompt.md +68 -68
  158. package/dist/skills/standards-discover/references/config-analyzer-prompt.md +67 -67
  159. package/dist/skills/standards-discover/references/docs-extractor-prompt.md +65 -65
  160. package/dist/skills/standards-discover/references/external-analyzer-prompt.md +75 -75
  161. package/dist/skills/standards-update/SKILL.md +207 -207
  162. package/dist/templates/orchestrator-state-base.yml +17 -17
  163. package/dist/templates/orchestrator-state-development.yml +95 -95
  164. package/dist/templates/orchestrator-state-migration.yml +95 -95
  165. package/dist/templates/orchestrator-state-performance.yml +64 -53
  166. package/dist/templates/orchestrator-state-research.yml +67 -67
  167. package/dist/tools/fork_task.d.ts +17 -0
  168. package/dist/tools/fork_task.js +21 -11
  169. package/dist/tools/verify_template.d.ts +11 -0
  170. package/dist/tools/verify_template.js +32 -17
  171. package/dist/tui/hooks-dialogs.d.ts +20 -0
  172. package/dist/tui/hooks-dialogs.js +181 -0
  173. package/dist/tui/hooks-presenter.d.ts +15 -0
  174. package/dist/tui/hooks-presenter.js +70 -0
  175. package/dist/tui/hooks-sidebar.d.ts +10 -0
  176. package/dist/tui/hooks-sidebar.js +104 -0
  177. package/dist/tui/primitives.d.ts +21 -0
  178. package/dist/tui/primitives.js +47 -0
  179. package/dist/tui.d.ts +12 -0
  180. package/dist/tui.js +129 -0
  181. package/dist/types/hook-types.d.ts +125 -0
  182. package/dist/types/hook-types.js +76 -0
  183. package/dist/types/opencode-types.d.ts +13 -0
  184. package/dist/utils/file-util.d.ts +2 -0
  185. package/dist/utils/file-util.js +6 -6
  186. package/dist/utils/fork-task.d.ts +27 -0
  187. package/dist/utils/fork-task.js +18 -23
  188. package/dist/utils/plugin-info.d.ts +3 -0
  189. package/dist/utils/plugin-info.js +8 -10
  190. package/dist/utils/workflow-paths.d.ts +17 -0
  191. package/dist/utils/workflow-paths.js +18 -0
  192. package/dist/utils/yaml-structure.d.ts +7 -0
  193. package/dist/utils/yaml-structure.js +7 -11
  194. package/package.json +68 -48
  195. package/dist/hooks/before-tool.js +0 -19
@@ -0,0 +1,318 @@
1
+ import path from 'node:path';
2
+ import { HookEvent, HookRunState, isHostEventName, } from '../types/hook-types.js';
3
+ import { hooksDirectory } from '../utils/workflow-paths.js';
4
+ import { scanHooks } from './hooks-registry.js';
5
+ import { CircuitBreakerTracker } from './runtime/circuit-breaker.js';
6
+ import { SessionAbortController, buildCircuitBreakerError, buildInterpreterUnavailableError, buildProcessFailureError, buildSpawnError, } from './runtime/failure-policy.js';
7
+ import { HookSpawnError, executeHookProcess } from './runtime/process-executor.js';
8
+ import { NON_SESSION_STREAM_CAP_BYTES, StatusRecorder } from './runtime/status-recorder.js';
9
+ import { writeHookCrashLog } from './hooks-persistence.js';
10
+ export { CircuitBreakerTracker } from './runtime/circuit-breaker.js';
11
+ export { SessionAbortController, buildCircuitBreakerError, buildInterpreterUnavailableError, buildSpawnError, buildProcessFailureError, } from './runtime/failure-policy.js';
12
+ export { HookSpawnError, executeHookProcess } from './runtime/process-executor.js';
13
+ export { NON_SESSION_STREAM_CAP_BYTES, StatusRecorder } from './runtime/status-recorder.js';
14
+ class HooksWrapper {
15
+ PASSTHROUGH_EVENTS = [
16
+ HookEvent.ToolExecuteAfter,
17
+ HookEvent.ToolDefinition,
18
+ HookEvent.ChatParams,
19
+ HookEvent.ChatHeaders,
20
+ HookEvent.CommandExecuteBefore,
21
+ HookEvent.ShellEnv,
22
+ HookEvent.ExperimentalChatMessagesTransform,
23
+ HookEvent.ExperimentalChatSystemTransform,
24
+ HookEvent.ExperimentalCompactionAutocontinue,
25
+ HookEvent.ExperimentalTextComplete,
26
+ HookEvent.Dispose,
27
+ ];
28
+ IDENTITY_KEYS = ['id', 'sessionID', 'messageID', 'partID', 'callID', 'providerID', 'type', 'name', 'tool'];
29
+ snapshot;
30
+ statusRecorder;
31
+ abortController;
32
+ circuitBreaker;
33
+ activeProcs;
34
+ directory;
35
+ runCounter = 0;
36
+ constructor(directory, endSession, options = {}) {
37
+ this.snapshot = scanHooks(hooksDirectory(directory));
38
+ this.directory = directory;
39
+ this.statusRecorder = new StatusRecorder({
40
+ directory,
41
+ statusFile: options.statusFile,
42
+ logFile: options.logFile,
43
+ });
44
+ this.abortController = new SessionAbortController(endSession);
45
+ this.circuitBreaker = new CircuitBreakerTracker();
46
+ this.activeProcs = new Set();
47
+ }
48
+ sessionIDFrom = (input) => {
49
+ const rec = input;
50
+ const candidate = rec?.sessionID ?? rec?.event?.properties?.sessionID ?? rec?.event?.properties?.part?.sessionID ?? rec?.event?.properties?.info?.sessionID;
51
+ return typeof candidate === 'string' && candidate.length > 0 ? candidate : undefined;
52
+ };
53
+ // shortcut: JSON.stringify replacer strips class-instance internals to id/name/type/sessionID instead of a recursive AST projector;
54
+ // ceiling: host objects with throwing getters or deep circular refs;
55
+ // upgrade: explicit per-event context projection.
56
+ buildStdinPayload = (event, input, output, sessionID) => {
57
+ const context = {};
58
+ for (const src of [input, output]) {
59
+ if (typeof src === 'object' && src !== null && !Array.isArray(src)) {
60
+ Object.assign(context, src);
61
+ }
62
+ }
63
+ const seen = new WeakSet();
64
+ return JSON.stringify({
65
+ event,
66
+ ...(sessionID ? { session: { sessionID } } : {}),
67
+ context,
68
+ }, (_key, value) => {
69
+ if (typeof value === 'bigint')
70
+ return value.toString();
71
+ if (typeof value === 'object' && value !== null) {
72
+ if (seen.has(value))
73
+ return '[Circular]';
74
+ seen.add(value);
75
+ if (!Array.isArray(value)) {
76
+ const proto = Object.getPrototypeOf(value);
77
+ if (proto !== Object.prototype && proto !== null) {
78
+ const picked = {};
79
+ for (const k of this.IDENTITY_KEYS) {
80
+ if (value[k] !== undefined)
81
+ picked[k] = value[k];
82
+ }
83
+ return picked;
84
+ }
85
+ }
86
+ }
87
+ return value;
88
+ });
89
+ };
90
+ dispose = () => {
91
+ this.statusRecorder.dispose();
92
+ for (const proc of this.activeProcs) {
93
+ try {
94
+ proc.kill();
95
+ }
96
+ catch { }
97
+ }
98
+ this.activeProcs.clear();
99
+ };
100
+ dispatch = async (event, input, output) => {
101
+ const byEvent = Map.groupBy(this.snapshot.entries, (e) => e.event);
102
+ const sessionID = this.sessionIDFrom(input);
103
+ if (event === HookEvent.ChatMessage && sessionID) {
104
+ this.abortController.clearSession(sessionID);
105
+ }
106
+ const hooks = byEvent.get(event);
107
+ if (!hooks || hooks.length === 0)
108
+ return;
109
+ const stdinText = this.buildStdinPayload(event, input, output, sessionID);
110
+ for (const entry of hooks) {
111
+ this.runCounter += 1;
112
+ const runId = `${event}#${this.runCounter}`;
113
+ const startedAt = Date.now();
114
+ const name = path.basename(entry.path, path.extname(entry.path));
115
+ const cbCheck = this.circuitBreaker.check(event, entry.path, sessionID, startedAt);
116
+ if (cbCheck.tripped) {
117
+ const reason = cbCheck.reason;
118
+ this.statusRecorder.setStatus(event, {
119
+ status: HookRunState.FAILURE,
120
+ startedAt,
121
+ finishedAt: startedAt,
122
+ durationMs: 0,
123
+ reason,
124
+ ...(sessionID ? { sessionID } : {}),
125
+ });
126
+ this.statusRecorder.flushStatus();
127
+ this.statusRecorder.appendLog({
128
+ version: 1,
129
+ runId,
130
+ pid: process.pid,
131
+ workspace: this.directory,
132
+ event,
133
+ hook: entry.path,
134
+ name,
135
+ ...(sessionID ? { sessionID } : {}),
136
+ startedAt,
137
+ finishedAt: startedAt,
138
+ durationMs: 0,
139
+ status: HookRunState.FAILURE,
140
+ reason,
141
+ });
142
+ throw buildCircuitBreakerError(entry.path, reason);
143
+ }
144
+ if (!entry.interpreter.available || !entry.interpreter.resolved) {
145
+ const label = entry.interpreter.resolved ?? entry.interpreter.expected;
146
+ const reason = `${label} is not available`;
147
+ this.statusRecorder.setStatus(event, {
148
+ status: HookRunState.FAILURE,
149
+ startedAt,
150
+ finishedAt: startedAt,
151
+ durationMs: 0,
152
+ reason,
153
+ ...(sessionID ? { sessionID } : {}),
154
+ });
155
+ this.statusRecorder.flushStatus();
156
+ this.statusRecorder.appendLog({
157
+ version: 1,
158
+ runId,
159
+ pid: process.pid,
160
+ workspace: this.directory,
161
+ event,
162
+ hook: entry.path,
163
+ name,
164
+ ...(sessionID ? { sessionID } : {}),
165
+ startedAt,
166
+ finishedAt: startedAt,
167
+ durationMs: 0,
168
+ status: HookRunState.FAILURE,
169
+ reason,
170
+ });
171
+ await this.abortController.abortSession(event, sessionID, entry.path);
172
+ throw buildInterpreterUnavailableError(entry.path, label);
173
+ }
174
+ const previousStatus = this.statusRecorder.getStatus(event);
175
+ this.statusRecorder.setStatus(event, {
176
+ status: HookRunState.PENDING,
177
+ startedAt,
178
+ ...(sessionID ? { sessionID } : {}),
179
+ });
180
+ this.statusRecorder.schedulePendingStatus();
181
+ let execResult;
182
+ try {
183
+ execResult = await executeHookProcess({
184
+ entry,
185
+ directory: this.directory,
186
+ stdinText,
187
+ event,
188
+ startedAt,
189
+ activeProcs: this.activeProcs,
190
+ });
191
+ }
192
+ catch (spawnError) {
193
+ if (spawnError instanceof HookSpawnError) {
194
+ const finishedAt = Date.now();
195
+ const spawnErrMsg = spawnError.originalError instanceof Error ? spawnError.originalError.message : String(spawnError.originalError);
196
+ const reason = `failed to spawn ${entry.path}: ${spawnErrMsg}`;
197
+ this.statusRecorder.setStatus(event, {
198
+ status: HookRunState.FAILURE,
199
+ startedAt,
200
+ finishedAt,
201
+ durationMs: 0,
202
+ reason,
203
+ ...(sessionID ? { sessionID } : {}),
204
+ });
205
+ this.statusRecorder.flushStatus();
206
+ this.statusRecorder.appendLog({
207
+ version: 1,
208
+ runId,
209
+ pid: process.pid,
210
+ workspace: this.directory,
211
+ event,
212
+ hook: entry.path,
213
+ name,
214
+ ...(sessionID ? { sessionID } : {}),
215
+ startedAt,
216
+ finishedAt,
217
+ durationMs: 0,
218
+ status: HookRunState.FAILURE,
219
+ reason,
220
+ });
221
+ await this.abortController.abortSession(event, sessionID, entry.path);
222
+ throw buildSpawnError(entry.path, spawnErrMsg, event);
223
+ }
224
+ throw spawnError;
225
+ }
226
+ const { ok, exitCode, signal, reason, durationMs, finishedAt, rawStdout, rawStderr } = execResult;
227
+ if (ok) {
228
+ this.circuitBreaker.recordSuccess(event, entry.path, sessionID);
229
+ }
230
+ else {
231
+ this.circuitBreaker.recordFailure(event, entry.path, sessionID, startedAt, finishedAt);
232
+ }
233
+ // Silent successful catch-all `event` runs leave no status/log trace so hot bus events don't flood.
234
+ if (ok && event === HookEvent.Event && !rawStdout.trim() && !rawStderr.trim()) {
235
+ if (previousStatus)
236
+ this.statusRecorder.setStatus(event, previousStatus);
237
+ else
238
+ this.statusRecorder.deleteStatus(event);
239
+ this.statusRecorder.flushStatus();
240
+ continue;
241
+ }
242
+ this.statusRecorder.setStatus(event, {
243
+ status: ok ? HookRunState.SUCCESS : HookRunState.FAILURE,
244
+ startedAt,
245
+ finishedAt,
246
+ durationMs,
247
+ ...(reason ? { reason } : {}),
248
+ ...(sessionID ? { sessionID } : {}),
249
+ });
250
+ this.statusRecorder.flushStatus();
251
+ const loggedStdout = !sessionID && rawStdout.length > NON_SESSION_STREAM_CAP_BYTES ? rawStdout.slice(0, NON_SESSION_STREAM_CAP_BYTES) : rawStdout;
252
+ const loggedStderr = !sessionID && rawStderr.length > NON_SESSION_STREAM_CAP_BYTES ? rawStderr.slice(0, NON_SESSION_STREAM_CAP_BYTES) : rawStderr;
253
+ this.statusRecorder.appendLog({
254
+ version: 1,
255
+ runId,
256
+ pid: process.pid,
257
+ workspace: this.directory,
258
+ event,
259
+ hook: entry.path,
260
+ name,
261
+ ...(sessionID ? { sessionID } : {}),
262
+ startedAt,
263
+ finishedAt,
264
+ durationMs,
265
+ status: ok ? HookRunState.SUCCESS : HookRunState.FAILURE,
266
+ exitCode,
267
+ ...(signal ? { signal } : {}),
268
+ ...(reason ? { reason } : {}),
269
+ ...(loggedStdout ? { stdout: loggedStdout } : {}),
270
+ ...(loggedStderr ? { stderr: loggedStderr } : {}),
271
+ });
272
+ if (!ok) {
273
+ await this.abortController.abortSession(event, sessionID, entry.path);
274
+ throw buildProcessFailureError(entry.path, reason, event);
275
+ }
276
+ }
277
+ };
278
+ createDispatchHandlers = () => {
279
+ const handlers = Object.fromEntries(this.PASSTHROUGH_EVENTS.map((event) => [event, (input, output) => this.dispatch(event, input, output)]));
280
+ const chains = new Map();
281
+ const runSerialized = (key, task) => {
282
+ const next = (chains.get(key) ?? Promise.resolve()).catch(() => { }).then(task);
283
+ const tracked = next.finally(() => {
284
+ if (chains.get(key) === tracked)
285
+ chains.delete(key);
286
+ });
287
+ tracked.catch(() => { });
288
+ chains.set(key, tracked);
289
+ return next;
290
+ };
291
+ return {
292
+ ...handlers,
293
+ [HookEvent.Event]: async (input) => {
294
+ const type = input?.event?.type;
295
+ if (typeof type === 'string' && isHostEventName(type) && !chains.has(type)) {
296
+ await runSerialized(type, async () => {
297
+ try {
298
+ await this.dispatch(type, input);
299
+ }
300
+ catch (error) {
301
+ writeHookCrashLog(this.directory, `reactive_host_event_${type}`, error);
302
+ }
303
+ });
304
+ }
305
+ await runSerialized(HookEvent.Event, async () => {
306
+ try {
307
+ await this.dispatch(HookEvent.Event, input);
308
+ }
309
+ catch (error) {
310
+ writeHookCrashLog(this.directory, 'catch_all_event_hook', error);
311
+ }
312
+ });
313
+ },
314
+ };
315
+ };
316
+ }
317
+ export { HooksWrapper };
318
+ export default HooksWrapper;
@@ -0,0 +1,6 @@
1
+ export declare const guardAgainstDestructiveActions: (input: {
2
+ tool: string;
3
+ sessionID: string;
4
+ }, output: {
5
+ args: any;
6
+ }, agentBySessionMap: Map<string, string>) => Promise<void>;
@@ -0,0 +1,13 @@
1
+ export const guardAgainstDestructiveActions = async (input, output, agentBySessionMap) => {
2
+ if (input.tool !== 'bash')
3
+ return;
4
+ const agentName = agentBySessionMap.get(input.sessionID) ?? 'main';
5
+ const WHITELIST = ['task-group-implementer', 'test-suite-runner', 'e2e-test-verifier', 'user-docs-generator', 'docs-operator'];
6
+ if (WHITELIST.includes(agentName))
7
+ return;
8
+ const cmd = output?.args?.command ?? '';
9
+ const DESTRUCTIVE = /git\s+stash|git\s+reset(?:\s+.*?)?\s+--hard|git\s+checkout\s+--\s+\.|git\s+checkout\s+\.\s*(?:$|\s)|git\s+clean|git\s+push\s+(?:.*?\s)?(?:--force(?:-with-lease)?|-f|\+[a-z0-9_\-\/]+)|rm\s+.*?(?:-[a-z]*[rf][a-z]*\s+-[a-z]*[rf]|-[rf]{2}|--recursive\s+--force|--force\s+--recursive)/i;
10
+ if (DESTRUCTIVE.test(cmd)) {
11
+ throw new Error(`Blocked: destructive command not permitted for agent "${agentName}"`);
12
+ }
13
+ };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Post-compaction reminder.
3
+ * Injects orchestrator state reminder into the compaction context so the
4
+ * AI knows to re-read orchestrator-state.yml after the context is rebuilt.
5
+ */
6
+ export declare const rereadOrchestratorState: (directory: string, output: {
7
+ context: string[];
8
+ prompt?: string | undefined;
9
+ }) => Promise<void>;
@@ -1,19 +1,21 @@
1
- import { $ } from "bun";
1
+ import fs from 'node:fs';
2
+ import { tasksDirectory } from '../../utils/workflow-paths.js';
2
3
  /**
3
4
  * Post-compaction reminder.
4
5
  * Injects orchestrator state reminder into the compaction context so the
5
6
  * AI knows to re-read orchestrator-state.yml after the context is rebuilt.
6
7
  */
7
8
  export const rereadOrchestratorState = async (directory, output) => {
8
- const tasksDir = `${directory}/.owflow/tasks`;
9
+ const tasksDir = tasksDirectory(directory);
9
10
  try {
10
- await $ `test -d ${tasksDir}`;
11
- output.context.push(`## owflow Workflow State
12
- If an orchestrator workflow was active before compaction, you MUST re-read
13
- orchestrator-state.yml in that task's directory to verify completed_phases
11
+ if (fs.existsSync(tasksDir) && fs.statSync(tasksDir).isDirectory()) {
12
+ output.context.push(`## owflow Workflow State
13
+ If an orchestrator workflow was active before compaction, you MUST re-read
14
+ orchestrator-state.yml in that task's directory to verify completed_phases
14
15
  and determine the next phase. Use the question tool at Phase Gates.`);
16
+ }
15
17
  }
16
18
  catch {
17
- // No .owflow/tasks directory — not a owflow project, skip injection
19
+ // No tasks directory — not an owflow project, skip injection
18
20
  }
19
21
  };
@@ -0,0 +1,23 @@
1
+ export declare const CIRCUIT_BREAKER_THRESHOLD = 4;
2
+ export declare const CIRCUIT_BREAKER_RAPID_GAP_MS = 500;
3
+ export declare const CIRCUIT_BREAKER_COOLDOWN_MS = 30000;
4
+ export interface CircuitBreakerEntry {
5
+ rapidFailures: number;
6
+ lastFinishedAt: number;
7
+ trippedUntil: number;
8
+ }
9
+ export interface CircuitBreakerCheckResult {
10
+ tripped: boolean;
11
+ reason?: string;
12
+ }
13
+ export declare class CircuitBreakerTracker {
14
+ private readonly breakers;
15
+ private key;
16
+ check(event: string, scriptPath: string, sessionID: string | undefined, now?: number): CircuitBreakerCheckResult;
17
+ recordSuccess(event: string, scriptPath: string, sessionID: string | undefined): void;
18
+ recordFailure(event: string, scriptPath: string, sessionID: string | undefined, startedAt: number, finishedAt: number): {
19
+ trippedUntil: number;
20
+ rapidFailures: number;
21
+ };
22
+ clear(): void;
23
+ }
@@ -0,0 +1,39 @@
1
+ export const CIRCUIT_BREAKER_THRESHOLD = 4;
2
+ export const CIRCUIT_BREAKER_RAPID_GAP_MS = 500;
3
+ export const CIRCUIT_BREAKER_COOLDOWN_MS = 30_000;
4
+ export class CircuitBreakerTracker {
5
+ breakers = new Map();
6
+ key(event, scriptPath, sessionID) {
7
+ return `${event}::${scriptPath}::${sessionID ?? 'global'}`;
8
+ }
9
+ check(event, scriptPath, sessionID, now = Date.now()) {
10
+ const breaker = this.breakers.get(this.key(event, scriptPath, sessionID));
11
+ if (breaker && now < breaker.trippedUntil) {
12
+ const reason = `Circuit breaker active for ${scriptPath} after ${CIRCUIT_BREAKER_THRESHOLD} rapid consecutive failures (cooldown until ${new Date(breaker.trippedUntil).toISOString()})`;
13
+ return { tripped: true, reason };
14
+ }
15
+ return { tripped: false };
16
+ }
17
+ recordSuccess(event, scriptPath, sessionID) {
18
+ this.breakers.delete(this.key(event, scriptPath, sessionID));
19
+ }
20
+ recordFailure(event, scriptPath, sessionID, startedAt, finishedAt) {
21
+ const k = this.key(event, scriptPath, sessionID);
22
+ const prev = this.breakers.get(k);
23
+ const isRapid = prev !== undefined && startedAt - prev.lastFinishedAt <= CIRCUIT_BREAKER_RAPID_GAP_MS;
24
+ const rapidFailures = isRapid ? prev.rapidFailures + 1 : 1;
25
+ const trippedUntil = rapidFailures >= CIRCUIT_BREAKER_THRESHOLD ? finishedAt + CIRCUIT_BREAKER_COOLDOWN_MS : 0;
26
+ this.breakers.set(k, {
27
+ rapidFailures,
28
+ lastFinishedAt: finishedAt,
29
+ trippedUntil,
30
+ });
31
+ if (trippedUntil > 0) {
32
+ console.warn(`[owflow] hooks: circuit breaker tripped for ${scriptPath} on event "${event}" after ${rapidFailures} rapid failures; pausing for ${CIRCUIT_BREAKER_COOLDOWN_MS}ms.`);
33
+ }
34
+ return { trippedUntil, rapidFailures };
35
+ }
36
+ clear() {
37
+ this.breakers.clear();
38
+ }
39
+ }
@@ -0,0 +1,15 @@
1
+ import { type HookEventName } from '../../types/hook-types.js';
2
+ export declare const ABORT_COOLDOWN_MS = 2000;
3
+ export declare const canAbortSessionForEvent: (event: HookEventName) => boolean;
4
+ export declare class SessionAbortController {
5
+ private readonly endSession?;
6
+ private readonly abortingSessions;
7
+ constructor(endSession?: ((sessionID: string) => Promise<void> | void) | undefined);
8
+ clearSession(sessionID: string): void;
9
+ abortSession(event: HookEventName, sessionID: string | undefined, scriptPath: string): Promise<void>;
10
+ dispose(): void;
11
+ }
12
+ export declare const buildCircuitBreakerError: (scriptPath: string, reason: string) => Error;
13
+ export declare const buildInterpreterUnavailableError: (scriptPath: string, label: string) => Error;
14
+ export declare const buildSpawnError: (scriptPath: string, spawnErrMsg: string, event: HookEventName) => Error;
15
+ export declare const buildProcessFailureError: (scriptPath: string, reason: string | undefined, event: HookEventName) => Error;
@@ -0,0 +1,49 @@
1
+ import { HookEvent, isHostEventName } from '../../types/hook-types.js';
2
+ export const ABORT_COOLDOWN_MS = 2_000;
3
+ export const canAbortSessionForEvent = (event) => !isHostEventName(event) && event !== HookEvent.Event;
4
+ export class SessionAbortController {
5
+ endSession;
6
+ abortingSessions = new Map();
7
+ constructor(endSession) {
8
+ this.endSession = endSession;
9
+ }
10
+ clearSession(sessionID) {
11
+ this.abortingSessions.delete(sessionID);
12
+ }
13
+ async abortSession(event, sessionID, scriptPath) {
14
+ if (!this.endSession || !sessionID || !canAbortSessionForEvent(event))
15
+ return;
16
+ const now = Date.now();
17
+ const lastAbort = this.abortingSessions.get(sessionID);
18
+ if (lastAbort !== undefined && now - lastAbort < ABORT_COOLDOWN_MS)
19
+ return;
20
+ this.abortingSessions.set(sessionID, now);
21
+ try {
22
+ await this.endSession(sessionID);
23
+ }
24
+ catch (error) {
25
+ const msg = error instanceof Error ? error.message : String(error);
26
+ console.warn(`[owflow] hooks: failed to end session ${sessionID} after ${scriptPath} failed: ${msg}`);
27
+ }
28
+ }
29
+ dispose() {
30
+ this.abortingSessions.clear();
31
+ }
32
+ }
33
+ export const buildCircuitBreakerError = (scriptPath, reason) => new Error(`Blocked: ${reason}. Hint: fix ${scriptPath} or wait for cooldown.`);
34
+ export const buildInterpreterUnavailableError = (scriptPath, label) => new Error(`Blocked: hook ${scriptPath} cannot run: the ${label} interpreter is not available. ` +
35
+ `Hint: install ${label} and restart OpenCode, or remove the script to stop failing closed.`);
36
+ export const buildSpawnError = (scriptPath, spawnErrMsg, event) => {
37
+ const failPosture = canAbortSessionForEvent(event) ? 'failure ends the current session; fail closed' : 'failure recorded; fail closed';
38
+ return new Error(`Blocked: failed to spawn hook ${scriptPath}: ${spawnErrMsg}. Hint: fix the failing script ${scriptPath} (${failPosture}), or remove it and restart OpenCode to proceed.`);
39
+ };
40
+ export const buildProcessFailureError = (scriptPath, reason, event) => {
41
+ const firstLine = reason
42
+ ?.split('\n')
43
+ .find((l) => l.trim().length > 0)
44
+ ?.trim();
45
+ const summary = firstLine ? (firstLine.length > 200 ? `${firstLine.slice(0, 197)}...` : firstLine) : reason || `hook ${scriptPath} failed`;
46
+ const body = summary.startsWith('Blocked:') ? summary : `Blocked: ${summary}`;
47
+ const failPosture = canAbortSessionForEvent(event) ? 'failure ends the current session; fail closed' : 'failure recorded; fail closed';
48
+ return new Error(`${body.endsWith('.') ? body : `${body}.`} Hint: fix the failing script ${scriptPath} (${failPosture}), or remove it and restart OpenCode to proceed.`);
49
+ };
@@ -0,0 +1,33 @@
1
+ import type { HookEventName, HookRegistryEntry } from '../../types/hook-types.js';
2
+ export declare const DEFAULT_HOOK_TIMEOUT_MS = 120000;
3
+ export declare const HOOK_TIMEOUT_ENV = "OWFLOW_HOOK_TIMEOUT_MS";
4
+ export declare const MAX_STREAM_BUFFER_BYTES: number;
5
+ export declare class HookSpawnError extends Error {
6
+ readonly originalError: unknown;
7
+ constructor(originalError: unknown);
8
+ }
9
+ export declare const buildExecutionArgs: (entry: HookRegistryEntry) => string[];
10
+ export declare const readStream: (stream: ReadableStream<Uint8Array> | null | undefined, onChunk?: (chunk: string) => void, maxBytes?: number) => Promise<string>;
11
+ export declare const resolveHookTimeoutMs: () => number;
12
+ export interface ExecuteHookProcessOptions {
13
+ entry: HookRegistryEntry;
14
+ directory: string;
15
+ stdinText: string;
16
+ event: HookEventName;
17
+ startedAt?: number;
18
+ timeoutMs?: number;
19
+ activeProcs?: Set<ReturnType<typeof Bun.spawn>>;
20
+ }
21
+ export interface HookProcessResult {
22
+ startedAt: number;
23
+ finishedAt: number;
24
+ durationMs: number;
25
+ ok: boolean;
26
+ timedOut: boolean;
27
+ exitCode: number | null;
28
+ signal: string | null;
29
+ rawStdout: string;
30
+ rawStderr: string;
31
+ reason?: string;
32
+ }
33
+ export declare const executeHookProcess: (options: ExecuteHookProcessOptions) => Promise<HookProcessResult>;