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.
- package/LICENSE +21 -21
- package/README.md +448 -261
- package/dist/agents/bottleneck-analyzer.md +332 -329
- package/dist/agents/code-quality-pragmatist.md +353 -354
- package/dist/agents/code-reviewer.md +238 -239
- package/dist/agents/codebase-analysis-reporter.md +245 -246
- package/dist/agents/docs-operator.md +22 -23
- package/dist/agents/e2e-test-verifier.md +447 -448
- package/dist/agents/gap-analyzer.md +501 -502
- package/dist/agents/implementation-completeness-checker.md +219 -220
- package/dist/agents/implementation-planner.md +308 -309
- package/dist/agents/information-gatherer.md +690 -691
- package/dist/agents/production-readiness-checker.md +278 -279
- package/dist/agents/project-analyzer.md +403 -403
- package/dist/agents/reality-assessor.md +365 -366
- package/dist/agents/research-planner.md +449 -450
- package/dist/agents/research-synthesizer.md +402 -403
- package/dist/agents/solution-brainstormer.md +256 -257
- package/dist/agents/solution-designer.md +371 -372
- package/dist/agents/spec-auditor.md +297 -298
- package/dist/agents/specification-creator.md +333 -334
- package/dist/agents/task-classifier.md +449 -450
- package/dist/agents/task-group-implementer.md +283 -284
- package/dist/agents/test-suite-runner.md +194 -195
- package/dist/agents/user-docs-generator.md +508 -509
- package/dist/commands/reviews-code.md +90 -90
- package/dist/commands/reviews-pragmatic.md +99 -99
- package/dist/commands/reviews-production-readiness.md +113 -113
- package/dist/commands/reviews-reality-check.md +110 -110
- package/dist/commands/reviews-spec-audit.md +114 -114
- package/dist/commands/work.md +280 -280
- package/dist/configuration/agents-config.d.ts +2 -0
- package/dist/configuration/agents-config.js +8 -44
- package/dist/configuration/commands-config.d.ts +23 -0
- package/dist/configuration/commands-config.js +17 -17
- package/dist/configuration/skills-config.d.ts +2 -0
- package/dist/configuration/skills-config.js +3 -3
- package/dist/hooks/hooks-diagnostics.d.ts +3 -0
- package/dist/hooks/hooks-diagnostics.js +21 -0
- package/dist/hooks/hooks-persistence.d.ts +17 -0
- package/dist/hooks/hooks-persistence.js +207 -0
- package/dist/hooks/hooks-registry.d.ts +6 -0
- package/dist/hooks/hooks-registry.js +104 -0
- package/dist/hooks/hooks-wrapper.d.ts +25 -0
- package/dist/hooks/hooks-wrapper.js +318 -0
- package/dist/hooks/internal/before-tool.d.ts +6 -0
- package/dist/hooks/internal/before-tool.js +13 -0
- package/dist/hooks/internal/session-compaction.d.ts +9 -0
- package/dist/hooks/{session-compaction.js → internal/session-compaction.js} +9 -7
- package/dist/hooks/runtime/circuit-breaker.d.ts +23 -0
- package/dist/hooks/runtime/circuit-breaker.js +39 -0
- package/dist/hooks/runtime/failure-policy.d.ts +15 -0
- package/dist/hooks/runtime/failure-policy.js +49 -0
- package/dist/hooks/runtime/process-executor.d.ts +33 -0
- package/dist/hooks/runtime/process-executor.js +147 -0
- package/dist/hooks/runtime/status-recorder.d.ts +27 -0
- package/dist/hooks/runtime/status-recorder.js +111 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +111 -40
- package/dist/skills/agents-md-generator/SKILL.md +223 -223
- package/dist/skills/codebase-analyzer/SKILL.md +162 -162
- package/dist/skills/codebase-analyzer/references/code-analysis.md +63 -63
- package/dist/skills/codebase-analyzer/references/combined.md +31 -31
- package/dist/skills/codebase-analyzer/references/context-discovery.md +63 -63
- package/dist/skills/codebase-analyzer/references/file-discovery.md +51 -51
- package/dist/skills/codebase-analyzer/references/migration-target.md +23 -23
- package/dist/skills/codebase-analyzer/references/pattern-mining.md +22 -22
- package/dist/skills/dev-analyze/SKILL.md +115 -115
- package/dist/skills/dev-bugfix/SKILL.md +452 -452
- package/dist/skills/dev-finalize/SKILL.md +118 -118
- package/dist/skills/dev-implement/SKILL.md +167 -167
- package/dist/skills/dev-plan/SKILL.md +147 -147
- package/dist/skills/dev-spec/SKILL.md +182 -182
- package/dist/skills/dev-tdd-red/SKILL.md +94 -94
- package/dist/skills/dev-verify/SKILL.md +153 -153
- package/dist/skills/development/SKILL.md +260 -260
- package/dist/skills/diagrams-mermaid/SKILL.md +183 -159
- package/dist/skills/diagrams-mermaid/references/reference.md +220 -148
- package/dist/skills/docs-manager/SKILL.md +379 -379
- package/dist/skills/docs-manager/docs/INDEX.md +199 -199
- package/dist/skills/docs-manager/docs/standards/backend/api.md +25 -25
- package/dist/skills/docs-manager/docs/standards/backend/migrations.md +22 -22
- package/dist/skills/docs-manager/docs/standards/backend/models.md +25 -25
- package/dist/skills/docs-manager/docs/standards/backend/queries.md +22 -22
- package/dist/skills/docs-manager/docs/standards/frontend/accessibility.md +25 -25
- package/dist/skills/docs-manager/docs/standards/frontend/components.md +28 -28
- package/dist/skills/docs-manager/docs/standards/frontend/css.md +16 -16
- package/dist/skills/docs-manager/docs/standards/frontend/responsive.md +28 -28
- package/dist/skills/docs-manager/docs/standards/global/coding-style.md +25 -25
- package/dist/skills/docs-manager/docs/standards/global/commenting.md +10 -10
- package/dist/skills/docs-manager/docs/standards/global/conventions.md +31 -31
- package/dist/skills/docs-manager/docs/standards/global/error-handling.md +22 -22
- package/dist/skills/docs-manager/docs/standards/global/minimal-implementation.md +22 -22
- package/dist/skills/docs-manager/docs/standards/global/validation.md +28 -28
- package/dist/skills/docs-manager/docs/standards/testing/test-writing.md +25 -25
- package/dist/skills/docs-manager/references/agents-md-template.md +29 -29
- package/dist/skills/docs-manager/references/index-md-template.md +73 -73
- package/dist/skills/flow-init/SKILL.md +234 -234
- package/dist/skills/flow-init/references/architecture-template.md +45 -45
- package/dist/skills/flow-init/references/roadmap-templates.md +93 -93
- package/dist/skills/flow-init/references/tech-stack-template.md +70 -70
- package/dist/skills/flow-init/references/vision-templates.md +75 -75
- package/dist/skills/goal-development/SKILL.md +96 -96
- package/dist/skills/goal-migration/SKILL.md +114 -114
- package/dist/skills/goal-performance/SKILL.md +126 -0
- package/dist/skills/goal-research/SKILL.md +118 -118
- package/dist/skills/html-renderer/SKILL.md +173 -173
- package/dist/skills/html-renderer/references/index.html +318 -318
- package/dist/skills/html-renderer/references/styles.css +388 -388
- package/dist/skills/html-renderer/references/tokens.json +104 -104
- package/dist/skills/implementation-plan-executor/SKILL.md +378 -378
- package/dist/skills/implementation-verifier/SKILL.md +307 -307
- package/dist/skills/migration/SKILL.md +268 -268
- package/dist/skills/migration-analyze/SKILL.md +126 -126
- package/dist/skills/migration-finalize/SKILL.md +161 -161
- package/dist/skills/migration-fix/SKILL.md +155 -155
- package/dist/skills/migration-implement/SKILL.md +131 -131
- package/dist/skills/migration-plan/SKILL.md +131 -131
- package/dist/skills/migration-spec/SKILL.md +135 -135
- package/dist/skills/migration-spec/references/migration-strategies.md +430 -430
- package/dist/skills/migration-target/SKILL.md +134 -134
- package/dist/skills/migration-target/references/migration-types.md +489 -489
- package/dist/skills/migration-verify/SKILL.md +184 -184
- package/dist/skills/orchestrator-framework/SKILL.md +65 -65
- package/dist/skills/orchestrator-framework/references/command-namespacing.md +7 -7
- package/dist/skills/orchestrator-framework/references/confirm-or-revise-exception.md +20 -20
- package/dist/skills/orchestrator-framework/references/delegation-rules.md +54 -54
- package/dist/skills/orchestrator-framework/references/dispatcher-handoff.md +27 -27
- package/dist/skills/orchestrator-framework/references/fix-loop-contract.md +65 -65
- package/dist/skills/orchestrator-framework/references/gate-contract.md +122 -117
- package/dist/skills/orchestrator-framework/references/orchestrator-creation-checklist.md +51 -51
- package/dist/skills/orchestrator-framework/references/orchestrator-patterns.md +235 -235
- package/dist/skills/performance/SKILL.md +267 -437
- package/dist/skills/performance-analyze/SKILL.md +129 -0
- package/dist/skills/{performance → performance-analyze}/references/performance-optimization-guide.md +271 -365
- package/dist/skills/performance-finalize/SKILL.md +117 -0
- package/dist/skills/performance-implement/SKILL.md +114 -0
- package/dist/skills/performance-plan/SKILL.md +112 -0
- package/dist/skills/performance-spec/SKILL.md +145 -0
- package/dist/skills/performance-verify/SKILL.md +164 -0
- package/dist/skills/research/SKILL.md +249 -249
- package/dist/skills/research-brainstorm/SKILL.md +120 -120
- package/dist/skills/research-brainstorm/references/brainstorming-techniques.md +84 -84
- package/dist/skills/research-converge/SKILL.md +130 -130
- package/dist/skills/research-design/SKILL.md +146 -146
- package/dist/skills/research-design/references/design-techniques.md +40 -40
- package/dist/skills/research-finalize/SKILL.md +125 -125
- package/dist/skills/research-fork/SKILL.md +190 -190
- package/dist/skills/research-gather/SKILL.md +133 -133
- package/dist/skills/research-plan/SKILL.md +141 -141
- package/dist/skills/research-plan/references/research-methodologies.md +710 -710
- package/dist/skills/research-quick/SKILL.md +139 -139
- package/dist/skills/research-synthesize/SKILL.md +127 -127
- package/dist/skills/rule-reviewer/SKILL.md +334 -334
- package/dist/skills/standards-discover/SKILL.md +260 -260
- package/dist/skills/standards-discover/references/aggregation-strategy.md +76 -76
- package/dist/skills/standards-discover/references/code-pattern-prompt.md +68 -68
- package/dist/skills/standards-discover/references/config-analyzer-prompt.md +67 -67
- package/dist/skills/standards-discover/references/docs-extractor-prompt.md +65 -65
- package/dist/skills/standards-discover/references/external-analyzer-prompt.md +75 -75
- package/dist/skills/standards-update/SKILL.md +207 -207
- package/dist/templates/orchestrator-state-base.yml +17 -17
- package/dist/templates/orchestrator-state-development.yml +95 -95
- package/dist/templates/orchestrator-state-migration.yml +95 -95
- package/dist/templates/orchestrator-state-performance.yml +64 -53
- package/dist/templates/orchestrator-state-research.yml +67 -67
- package/dist/tools/fork_task.d.ts +17 -0
- package/dist/tools/fork_task.js +21 -11
- package/dist/tools/verify_template.d.ts +11 -0
- package/dist/tools/verify_template.js +32 -17
- package/dist/tui/hooks-dialogs.d.ts +20 -0
- package/dist/tui/hooks-dialogs.js +181 -0
- package/dist/tui/hooks-presenter.d.ts +15 -0
- package/dist/tui/hooks-presenter.js +70 -0
- package/dist/tui/hooks-sidebar.d.ts +10 -0
- package/dist/tui/hooks-sidebar.js +104 -0
- package/dist/tui/primitives.d.ts +21 -0
- package/dist/tui/primitives.js +47 -0
- package/dist/tui.d.ts +12 -0
- package/dist/tui.js +129 -0
- package/dist/types/hook-types.d.ts +125 -0
- package/dist/types/hook-types.js +76 -0
- package/dist/types/opencode-types.d.ts +13 -0
- package/dist/utils/file-util.d.ts +2 -0
- package/dist/utils/file-util.js +6 -6
- package/dist/utils/fork-task.d.ts +27 -0
- package/dist/utils/fork-task.js +18 -23
- package/dist/utils/plugin-info.d.ts +3 -0
- package/dist/utils/plugin-info.js +8 -10
- package/dist/utils/workflow-paths.d.ts +17 -0
- package/dist/utils/workflow-paths.js +18 -0
- package/dist/utils/yaml-structure.d.ts +7 -0
- package/dist/utils/yaml-structure.js +7 -11
- package/package.json +68 -48
- 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,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
|
|
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 =
|
|
9
|
+
const tasksDir = tasksDirectory(directory);
|
|
9
10
|
try {
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
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>;
|