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,147 @@
1
+ export const DEFAULT_HOOK_TIMEOUT_MS = 120_000;
2
+ export const HOOK_TIMEOUT_ENV = 'OWFLOW_HOOK_TIMEOUT_MS';
3
+ export const MAX_STREAM_BUFFER_BYTES = 4 * 1024 * 1024;
4
+ export class HookSpawnError extends Error {
5
+ originalError;
6
+ constructor(originalError) {
7
+ super(originalError instanceof Error ? originalError.message : String(originalError));
8
+ this.originalError = originalError;
9
+ this.name = 'HookSpawnError';
10
+ }
11
+ }
12
+ export const buildExecutionArgs = (entry) => {
13
+ const resolvedLower = (entry.interpreter.resolved ?? '').toLowerCase();
14
+ const isPowerShell = entry.path.endsWith('.ps1') || entry.interpreter.expected === 'pwsh' || resolvedLower.includes('powershell') || resolvedLower.includes('pwsh');
15
+ if (isPowerShell) {
16
+ return [entry.interpreter.resolved, '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', entry.path];
17
+ }
18
+ return [entry.interpreter.resolved, entry.path];
19
+ };
20
+ export const readStream = async (stream, onChunk, maxBytes = MAX_STREAM_BUFFER_BYTES) => {
21
+ if (!stream)
22
+ return '';
23
+ const reader = stream.getReader();
24
+ const decoder = new TextDecoder();
25
+ let result = '';
26
+ let totalBytes = 0;
27
+ try {
28
+ while (true) {
29
+ const { done, value } = await reader.read();
30
+ if (done)
31
+ break;
32
+ totalBytes += value.byteLength;
33
+ const chunk = decoder.decode(value, { stream: true });
34
+ if (chunk) {
35
+ if (result.length < maxBytes) {
36
+ result += chunk.slice(0, maxBytes - result.length);
37
+ }
38
+ onChunk?.(chunk);
39
+ }
40
+ if (totalBytes >= maxBytes) {
41
+ try {
42
+ await reader.cancel('Stream limit exceeded');
43
+ }
44
+ catch { }
45
+ break;
46
+ }
47
+ }
48
+ const tail = decoder.decode();
49
+ if (tail) {
50
+ if (result.length < maxBytes) {
51
+ result += tail.slice(0, maxBytes - result.length);
52
+ }
53
+ onChunk?.(tail);
54
+ }
55
+ }
56
+ catch { }
57
+ return result;
58
+ };
59
+ export const resolveHookTimeoutMs = () => {
60
+ const rawTimeout = process.env[HOOK_TIMEOUT_ENV];
61
+ if (rawTimeout === undefined)
62
+ return DEFAULT_HOOK_TIMEOUT_MS;
63
+ const parsed = Number(rawTimeout);
64
+ if (Number.isFinite(parsed) && parsed <= 0)
65
+ return 0;
66
+ if (Number.isFinite(parsed))
67
+ return parsed;
68
+ return DEFAULT_HOOK_TIMEOUT_MS;
69
+ };
70
+ export const executeHookProcess = async (options) => {
71
+ const { entry, directory, stdinText, event, startedAt = Date.now(), timeoutMs = resolveHookTimeoutMs(), activeProcs } = options;
72
+ if (process.env.OWFLOW_DEBUG === '1') {
73
+ console.error(`[owflow:debug] [${event}] Spawning ${entry.path} (stdin: ${stdinText.length} bytes)`);
74
+ }
75
+ let proc;
76
+ try {
77
+ proc = Bun.spawn(buildExecutionArgs(entry), {
78
+ cwd: directory,
79
+ stdin: Buffer.from(stdinText),
80
+ stdout: 'pipe',
81
+ stderr: 'pipe',
82
+ });
83
+ activeProcs?.add(proc);
84
+ }
85
+ catch (spawnError) {
86
+ throw new HookSpawnError(spawnError);
87
+ }
88
+ let timedOut = false;
89
+ let timer;
90
+ let rawStdout = '';
91
+ let rawStderr = '';
92
+ const ioPromise = Promise.all([
93
+ readStream(proc.stdout),
94
+ readStream(proc.stderr),
95
+ proc.exited,
96
+ ]);
97
+ let exitCode = null;
98
+ try {
99
+ const res = timeoutMs > 0
100
+ ? await Promise.race([
101
+ ioPromise,
102
+ new Promise((resolve) => {
103
+ timer = setTimeout(() => {
104
+ timedOut = true;
105
+ try {
106
+ proc.kill();
107
+ }
108
+ catch { }
109
+ resolve('timeout');
110
+ }, Math.floor(timeoutMs));
111
+ timer.unref?.();
112
+ }),
113
+ ])
114
+ : await ioPromise;
115
+ if (res !== 'timeout')
116
+ [rawStdout, rawStderr, exitCode] = res;
117
+ }
118
+ finally {
119
+ if (timer !== undefined)
120
+ clearTimeout(timer);
121
+ activeProcs?.delete(proc);
122
+ }
123
+ const finishedAt = Date.now();
124
+ const durationMs = Math.max(0, finishedAt - startedAt);
125
+ const signal = proc.signalCode ?? null;
126
+ const ok = !timedOut && exitCode === 0 && signal === null;
127
+ if (process.env.OWFLOW_DEBUG === '1') {
128
+ console.error(`[owflow:debug] [${event}] Finished ${entry.path} in ${durationMs}ms (exitCode: ${exitCode}, ok: ${ok})`);
129
+ }
130
+ const reason = ok
131
+ ? undefined
132
+ : timedOut
133
+ ? `process timed out after ${Math.floor(timeoutMs)}ms`
134
+ : rawStderr.trim() || rawStdout.trim() || (signal ? `process terminated by signal ${signal}` : `process exited with code ${exitCode}`);
135
+ return {
136
+ startedAt,
137
+ finishedAt,
138
+ durationMs,
139
+ ok,
140
+ timedOut,
141
+ exitCode,
142
+ signal,
143
+ rawStdout,
144
+ rawStderr,
145
+ reason,
146
+ };
147
+ };
@@ -0,0 +1,27 @@
1
+ import type { HookRunLogRecord, HookRuntimeEntry } from '../../types/hook-types.js';
2
+ export declare const STATUS_DEBOUNCE_MS = 50;
3
+ export declare const MAX_LOG_FILE_BYTES: number;
4
+ export declare const NON_SESSION_STREAM_CAP_BYTES: number;
5
+ export interface StatusRecorderOptions {
6
+ directory: string;
7
+ statusFile?: string;
8
+ logFile?: string;
9
+ }
10
+ export declare class StatusRecorder {
11
+ readonly statusFile: string;
12
+ readonly logFile?: string;
13
+ readonly directory: string;
14
+ private readonly statuses;
15
+ private readonly writtenRunRecords;
16
+ private statusTimer;
17
+ constructor(options: StatusRecorderOptions);
18
+ getStatus(event: string): HookRuntimeEntry | undefined;
19
+ setStatus(event: string, entry: HookRuntimeEntry): void;
20
+ deleteStatus(event: string): void;
21
+ getAllStatuses(): ReadonlyMap<string, HookRuntimeEntry>;
22
+ flushStatusFileAtomic: (file: string, workspace: string, hooks: ReadonlyMap<string, HookRuntimeEntry>) => void;
23
+ flushStatus(): void;
24
+ schedulePendingStatus(): void;
25
+ appendLog(record: HookRunLogRecord): void;
26
+ dispose(): void;
27
+ }
@@ -0,0 +1,111 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { appendWithRotation, resolveHooksLatestSymlink, resolveHooksLogFile, resolveHooksStatusFile } from '../hooks-persistence.js';
4
+ export const STATUS_DEBOUNCE_MS = 50;
5
+ export const MAX_LOG_FILE_BYTES = 4 * 1024 * 1024;
6
+ export const NON_SESSION_STREAM_CAP_BYTES = 16 * 1024;
7
+ export class StatusRecorder {
8
+ statusFile;
9
+ logFile;
10
+ directory;
11
+ statuses = new Map();
12
+ writtenRunRecords = new Set();
13
+ statusTimer;
14
+ constructor(options) {
15
+ this.directory = options.directory;
16
+ this.statusFile = options.statusFile ?? resolveHooksStatusFile(options.directory);
17
+ this.logFile = options.logFile;
18
+ resolveHooksLatestSymlink(this.directory);
19
+ this.flushStatus();
20
+ }
21
+ getStatus(event) {
22
+ return this.statuses.get(event);
23
+ }
24
+ setStatus(event, entry) {
25
+ this.statuses.set(event, entry);
26
+ }
27
+ deleteStatus(event) {
28
+ this.statuses.delete(event);
29
+ }
30
+ getAllStatuses() {
31
+ return this.statuses;
32
+ }
33
+ flushStatusFileAtomic = (file, workspace, hooks) => {
34
+ const nonce = crypto.randomUUID();
35
+ const tmpFile = `${file}.${process.pid}.${nonce}.tmp`;
36
+ try {
37
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
38
+ try {
39
+ if (fs.lstatSync(file).isSymbolicLink())
40
+ return;
41
+ }
42
+ catch (err) {
43
+ if (err?.code !== 'ENOENT')
44
+ return;
45
+ }
46
+ const snapshot = {
47
+ version: 1,
48
+ pid: process.pid,
49
+ workspace,
50
+ updatedAt: Date.now(),
51
+ hooks: Object.fromEntries(hooks),
52
+ };
53
+ const flags = fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_TRUNC | (fs.constants.O_NOFOLLOW ?? 0);
54
+ const fd = fs.openSync(tmpFile, flags, 0o600);
55
+ try {
56
+ fs.writeFileSync(fd, JSON.stringify(snapshot), { encoding: 'utf8' });
57
+ }
58
+ finally {
59
+ fs.closeSync(fd);
60
+ }
61
+ fs.renameSync(tmpFile, file);
62
+ }
63
+ catch {
64
+ fs.unlinkSync(tmpFile);
65
+ // Best-effort status persistence never affects hook execution.
66
+ }
67
+ };
68
+ flushStatus() {
69
+ if (this.statusTimer !== undefined) {
70
+ clearTimeout(this.statusTimer);
71
+ this.statusTimer = undefined;
72
+ }
73
+ this.flushStatusFileAtomic(this.statusFile, this.directory, this.statuses);
74
+ }
75
+ schedulePendingStatus() {
76
+ if (this.statusTimer !== undefined) {
77
+ clearTimeout(this.statusTimer);
78
+ }
79
+ this.statusTimer = setTimeout(() => {
80
+ this.statusTimer = undefined;
81
+ this.flushStatusFileAtomic(this.statusFile, this.directory, this.statuses);
82
+ }, STATUS_DEBOUNCE_MS);
83
+ this.statusTimer.unref?.();
84
+ }
85
+ appendLog(record) {
86
+ try {
87
+ // Distinguish distinct runs by runId and terminal status
88
+ const recordKey = `${record.runId}:${record.status}`;
89
+ if (this.writtenRunRecords.has(recordKey))
90
+ return;
91
+ const targetLogFile = this.logFile ?? resolveHooksLogFile(this.directory, record.name);
92
+ appendWithRotation(targetLogFile, JSON.stringify(record));
93
+ if (!this.logFile) {
94
+ resolveHooksLatestSymlink(this.directory, targetLogFile);
95
+ }
96
+ this.writtenRunRecords.add(recordKey);
97
+ // Bounded set eviction to prevent memory growth across thousands of runs
98
+ if (this.writtenRunRecords.size > 1000) {
99
+ const oldest = this.writtenRunRecords.values().next().value;
100
+ if (oldest)
101
+ this.writtenRunRecords.delete(oldest);
102
+ }
103
+ }
104
+ catch {
105
+ // Best-effort log persistence never affects hook execution.
106
+ }
107
+ }
108
+ dispose() {
109
+ this.flushStatus();
110
+ }
111
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Owflow OpenCode Plugin
3
+ * Registers skills/agents/commands via OpenCode's JS event system.
4
+ */
5
+ import type { Plugin } from '@opencode-ai/plugin';
6
+ declare const OwflowPlugin: Plugin;
7
+ export default OwflowPlugin;
package/dist/index.js CHANGED
@@ -2,20 +2,54 @@
2
2
  * Owflow OpenCode Plugin
3
3
  * Registers skills/agents/commands via OpenCode's JS event system.
4
4
  */
5
- import path from "node:path";
6
- import fs from "node:fs";
7
- import { PLUGIN_VERSION } from "./utils/plugin-info.js";
8
- import { verify_template } from "./tools/verify_template.js";
9
- import { fork_task } from "./tools/fork_task.js";
10
- import { rereadOrchestratorState } from "./hooks/session-compaction.js";
11
- import { guardAgainstDestructiveActions } from "./hooks/before-tool.js";
12
- import { configureSkills } from "./configuration/skills-config.js";
13
- import { configureCommands } from "./configuration/commands-config.js";
14
- import { configureAgents } from "./configuration/agents-config.js";
5
+ import { PLUGIN_VERSION } from './utils/plugin-info.js';
6
+ import { verify_template } from './tools/verify_template.js';
7
+ import { fork_task } from './tools/fork_task.js';
8
+ import { rereadOrchestratorState } from './hooks/internal/session-compaction.js';
9
+ import { guardAgainstDestructiveActions } from './hooks/internal/before-tool.js';
10
+ import { configureSkills } from './configuration/skills-config.js';
11
+ import { configureCommands } from './configuration/commands-config.js';
12
+ import { configureAgents } from './configuration/agents-config.js';
13
+ import { HookEvent } from './types/hook-types.js';
14
+ import { renderSnapshot } from './hooks/hooks-diagnostics.js';
15
+ import { writeHookCrashLog } from './hooks/hooks-persistence.js';
16
+ import HooksWrapper from './hooks/hooks-wrapper.js';
15
17
  // Only the plugin factory method should be exported from this file due to OpenCode plugin handling
16
- const OwflowPlugin = async ({ $, directory }) => {
18
+ const MAX_TRACKED_SESSIONS = 256;
19
+ const OwflowPlugin = async ({ $, client, directory }) => {
17
20
  const agentBySession = new Map();
18
- return {
21
+ const hooksDisabled = process.env.OWFLOW_HOOKS_DISABLED === '1';
22
+ let configLoaded = false;
23
+ if (hooksDisabled) {
24
+ console.log('[owflow] User hooks disabled (OWFLOW_HOOKS_DISABLED=1); built-in hooks remain active.');
25
+ }
26
+ const endSession = async (sessionID) => {
27
+ if (typeof client?.session?.abort !== 'function')
28
+ return;
29
+ try {
30
+ await client.session.abort({ path: { id: sessionID } });
31
+ }
32
+ catch (error) {
33
+ const message = error instanceof Error ? error.message : String(error);
34
+ console.warn(`[owflow] failed to end session ${sessionID} after a gate hook failed: ${message}`);
35
+ }
36
+ };
37
+ let wrapper;
38
+ let snapshot = { entries: [], bypassed: true };
39
+ if (!hooksDisabled) {
40
+ try {
41
+ wrapper = new HooksWrapper(directory, endSession);
42
+ snapshot = wrapper.snapshot;
43
+ }
44
+ catch (error) {
45
+ writeHookCrashLog(directory, 'startup_digest_failed', error);
46
+ console.error(`[owflow] ERROR: user hooks failed to load (${error.message}).`);
47
+ console.error(`[owflow] Built-in destructive command guard remains ACTIVE.`);
48
+ console.error(`[owflow] User hooks bypassed until fixed. See crash log.`);
49
+ wrapper = undefined;
50
+ }
51
+ }
52
+ const hooks = {
19
53
  tool: {
20
54
  verify_template,
21
55
  fork_task,
@@ -24,51 +58,88 @@ const OwflowPlugin = async ({ $, directory }) => {
24
58
  * Register owflow's skills, commands, and agents so OpenCode discovers
25
59
  * them without requiring manual config file edits.
26
60
  */
27
- config: async (config) => {
28
- console.log(`[owflow] Starting plugin installation (v${PLUGIN_VERSION})...`);
29
- let typedConfig = config;
30
- // Get small_model config for alias resolution
31
- // Try from config parameter first, fallback to reading opencode.json
32
- let smallModel = typedConfig.small_model ?? "";
33
- if (!smallModel) {
34
- try {
35
- // Check project-level opencode.json
36
- const projectConfigPath = path.join(directory, "opencode.json");
37
- if (fs.existsSync(projectConfigPath)) {
38
- const rawConfig = fs.readFileSync(projectConfigPath, "utf8");
39
- const opencodeConfig = JSON.parse(rawConfig);
40
- smallModel = opencodeConfig.small_model;
41
- }
42
- }
43
- catch (error) {
44
- console.warn(`Couldn't read or parse opencode.json config - small_model remains undefined`);
45
- }
61
+ [HookEvent.Config]: async (config) => {
62
+ if (!configLoaded) {
63
+ console.log(`[owflow] Starting plugin installation (v${PLUGIN_VERSION})...`);
64
+ for (const line of renderSnapshot(snapshot))
65
+ console.log(line);
46
66
  }
67
+ let typedConfig = config;
47
68
  configureSkills(typedConfig);
48
69
  configureCommands(typedConfig);
49
- configureAgents(typedConfig, smallModel);
50
- config = typedConfig;
51
- console.log(`[owflow] Plugin installation completed successfully (v${PLUGIN_VERSION}).`);
70
+ configureAgents(typedConfig);
71
+ // User scripts run after built-in config mutations; failing closed on rejection.
72
+ if (wrapper !== undefined) {
73
+ await wrapper.dispatch(HookEvent.Config, typedConfig);
74
+ }
75
+ if (!configLoaded) {
76
+ console.log(`[owflow] Plugin installation completed successfully (v${PLUGIN_VERSION}).`);
77
+ configLoaded = true;
78
+ }
52
79
  },
53
- "experimental.session.compacting": async (input, output) => {
80
+ [HookEvent.ExperimentalSessionCompacting]: async (input, output) => {
54
81
  await rereadOrchestratorState(directory, output);
82
+ if (wrapper !== undefined) {
83
+ await wrapper.dispatch(HookEvent.ExperimentalSessionCompacting, input, output);
84
+ }
55
85
  },
56
- "chat.message": async (input, _output) => {
86
+ [HookEvent.ChatMessage]: async (input, output) => {
57
87
  try {
58
- agentBySession.set(input.sessionID, input.agent ?? "main");
88
+ if (agentBySession.has(input.sessionID)) {
89
+ agentBySession.delete(input.sessionID);
90
+ }
91
+ agentBySession.set(input.sessionID, input.agent ?? 'main');
92
+ while (agentBySession.size > MAX_TRACKED_SESSIONS) {
93
+ const oldest = agentBySession.keys().next().value;
94
+ if (oldest !== undefined)
95
+ agentBySession.delete(oldest);
96
+ }
59
97
  }
60
98
  catch {
61
99
  // ignore
62
100
  }
101
+ // Dispatch deliberately outside the swallowing catch so gate throws
102
+ // propagate even when the platform's input object misbehaves.
103
+ if (wrapper !== undefined) {
104
+ await wrapper.dispatch(HookEvent.ChatMessage, input, output);
105
+ }
63
106
  },
64
- "tool.execute.before": async (input, output) => {
107
+ [HookEvent.ToolExecuteBefore]: async (input, output) => {
65
108
  try {
66
109
  await guardAgainstDestructiveActions(input, output, agentBySession);
110
+ if (wrapper !== undefined) {
111
+ await wrapper.dispatch(HookEvent.ToolExecuteBefore, input, output);
112
+ }
67
113
  }
68
114
  catch (e) {
69
- // Re-throw only intentional blocks; swallow unexpected errors
70
- if (e.message?.startsWith("Blocked:"))
115
+ // Re-throw only intentional blocks; log and swallow unexpected errors
116
+ if (e.message?.startsWith('Blocked:'))
71
117
  throw e;
118
+ const message = e instanceof Error ? e.message : String(e);
119
+ console.warn(`[owflow] hooks: unexpected error in tool.execute.before (failing open): ${message}`);
120
+ }
121
+ },
122
+ };
123
+ // No wrapper - no support for user defined hooks, return owflow hook configs
124
+ if (wrapper === undefined)
125
+ return hooks;
126
+ // Return full plugin configuration with user hooks support
127
+ const dispatchHandlers = wrapper.createDispatchHandlers();
128
+ const userDispose = dispatchHandlers.dispose;
129
+ return {
130
+ ...hooks,
131
+ ...dispatchHandlers,
132
+ dispose: async () => {
133
+ try {
134
+ await userDispose?.();
135
+ }
136
+ catch (error) {
137
+ const msg = error instanceof Error ? error.message : String(error);
138
+ console.warn(`[owflow] error during user dispose hook: ${msg}`);
139
+ }
140
+ finally {
141
+ wrapper.dispose();
142
+ agentBySession.clear();
72
143
  }
73
144
  },
74
145
  };