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,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
|
+
}
|
package/dist/index.d.ts
ADDED
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
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
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
|
|
18
|
+
const MAX_TRACKED_SESSIONS = 256;
|
|
19
|
+
const OwflowPlugin = async ({ $, client, directory }) => {
|
|
17
20
|
const agentBySession = new Map();
|
|
18
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
50
|
-
config
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
86
|
+
[HookEvent.ChatMessage]: async (input, output) => {
|
|
57
87
|
try {
|
|
58
|
-
agentBySession.
|
|
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
|
-
|
|
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(
|
|
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
|
};
|