@evo-dev/core 0.0.1-alpha → 0.0.1-alpha.1
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/assets/agents/review/code-reviewer/examples.md +1 -1
- package/assets/agents/review/code-reviewer/prompt.md +1 -1
- package/assets/agents/review/code-reviewer/verification.md +1 -1
- package/assets/skills/coding/knowledge-distillation/SKILL.md +248 -0
- package/assets/skills/coding/knowledge-distillation/manifest.json +10 -0
- package/assets/skills/coding/knowledge-distillation/references/knowledge-distillation-methods.md +122 -0
- package/assets/workflows/rd-bug-fix/WORKFLOW.json +1 -1
- package/assets/workflows/rd-code-review/WORKFLOW.json +1 -1
- package/assets/workflows/rd-docs-update/WORKFLOW.json +1 -1
- package/assets/workflows/rd-feature-implementation/WORKFLOW.json +1 -1
- package/assets/workflows/rd-refactor/WORKFLOW.json +1 -1
- package/assets/workflows/rd-release-readiness/WORKFLOW.json +1 -1
- package/assets/workflows/rd-security-boundary-review/WORKFLOW.json +2 -2
- package/assets/workflows/rd-test-generation/WORKFLOW.json +1 -1
- package/dist/config/index.js +242 -36
- package/dist/index.js +5045 -934
- package/dist/plugins/index.js +32 -32
- package/package.json +1 -1
- package/src/agents/index.ts +28 -49
- package/src/config/index.ts +2 -0
- package/src/config/paths.ts +30 -0
- package/src/config/settings.ts +52 -0
- package/src/config/store.ts +150 -0
- package/src/daemon/index.ts +376 -3
- package/src/evolution/index.ts +2356 -0
- package/src/hooks/index.ts +255 -238
- package/src/index.ts +4 -0
- package/src/pack/index.ts +13 -13
- package/src/plugins/capabilities.ts +40 -42
- package/src/plugins/index.ts +0 -1
- package/src/plugins/types.ts +4 -0
- package/src/protected-zones/index.ts +29 -11
- package/src/runtime-logs/index.ts +324 -0
- package/src/sync/orchestrator.ts +6 -0
- package/src/task/index.ts +3 -3
- package/src/team/index.ts +2398 -0
- package/src/team/mcp.ts +401 -0
- package/src/workflow/index.ts +6 -6
package/src/hooks/index.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
|
+
import { enqueueEvolutionTrigger, resolveEvolutionTriggerDecision } from "../evolution/index.ts";
|
|
5
|
+
import { resolveTraceTeamContext } from "../runtime-logs/index.ts";
|
|
4
6
|
import {
|
|
5
7
|
type TaskContract,
|
|
6
8
|
createTaskContract,
|
|
7
9
|
routeTaskContract,
|
|
8
10
|
writeTaskContract,
|
|
9
11
|
} from "../task/index.ts";
|
|
12
|
+
import { recordTeamAgentNativeSession, resolveTeamRunPaths } from "../team/index.ts";
|
|
10
13
|
|
|
11
14
|
export const CANONICAL_HOOK_EVENT_TYPES = [
|
|
12
15
|
"SessionStart",
|
|
@@ -49,15 +52,15 @@ export type CommandRiskClass =
|
|
|
49
52
|
| "credential-sensitive"
|
|
50
53
|
| "path-escaping"
|
|
51
54
|
| "unknown";
|
|
52
|
-
export type HookDecisionAction = "allow" | "warn"
|
|
55
|
+
export type HookDecisionAction = "allow" | "warn";
|
|
53
56
|
|
|
54
57
|
export interface HookSettings {
|
|
55
58
|
enabled: boolean;
|
|
56
59
|
targets: Record<CodeAgentHookTarget, HookTargetSettings>;
|
|
57
60
|
observability: {
|
|
58
|
-
metadataOnly:
|
|
59
|
-
rawPayloadStorage:
|
|
60
|
-
appendEvents:
|
|
61
|
+
metadataOnly: boolean;
|
|
62
|
+
rawPayloadStorage: boolean;
|
|
63
|
+
appendEvents: boolean;
|
|
61
64
|
};
|
|
62
65
|
learning: {
|
|
63
66
|
emitCandidates: false;
|
|
@@ -78,7 +81,7 @@ export interface HookEventV1 {
|
|
|
78
81
|
pluginId: string;
|
|
79
82
|
agent: string;
|
|
80
83
|
sessionIdHash: string | null;
|
|
81
|
-
rawPayloadStored:
|
|
84
|
+
rawPayloadStored: boolean;
|
|
82
85
|
};
|
|
83
86
|
time: {
|
|
84
87
|
occurredAt: string | null;
|
|
@@ -87,7 +90,7 @@ export interface HookEventV1 {
|
|
|
87
90
|
scope: {
|
|
88
91
|
taskId: string | null;
|
|
89
92
|
projectId: string | null;
|
|
90
|
-
cwdPolicy: "metadata-only";
|
|
93
|
+
cwdPolicy: "metadata-only" | "raw-local";
|
|
91
94
|
projectContextOptedIn: false;
|
|
92
95
|
};
|
|
93
96
|
payload: {
|
|
@@ -95,11 +98,11 @@ export interface HookEventV1 {
|
|
|
95
98
|
metadata: Record<string, string | number | boolean | string[]>;
|
|
96
99
|
redactions: string[];
|
|
97
100
|
redactionCount: number;
|
|
98
|
-
rawContentIncluded:
|
|
101
|
+
rawContentIncluded: boolean;
|
|
99
102
|
};
|
|
100
103
|
policy: {
|
|
101
104
|
classification: "local-private";
|
|
102
|
-
allowedUses: Array<"observability" | "
|
|
105
|
+
allowedUses: Array<"observability" | "workflow-suggestion">;
|
|
103
106
|
learningAllowed: false;
|
|
104
107
|
externalUploadAllowed: false;
|
|
105
108
|
};
|
|
@@ -128,7 +131,7 @@ export interface HookInstallDryRunPlan {
|
|
|
128
131
|
reason: string;
|
|
129
132
|
}>;
|
|
130
133
|
warnings: string[];
|
|
131
|
-
|
|
134
|
+
advisories: string[];
|
|
132
135
|
}
|
|
133
136
|
|
|
134
137
|
export interface HookRuntimeSessionBinding {
|
|
@@ -148,6 +151,7 @@ export interface HookRuntimeResult {
|
|
|
148
151
|
enabled: boolean;
|
|
149
152
|
output: Record<string, unknown> | null;
|
|
150
153
|
stateWrites: string[];
|
|
154
|
+
warnings: string[];
|
|
151
155
|
summary: string;
|
|
152
156
|
}
|
|
153
157
|
|
|
@@ -158,33 +162,34 @@ export interface HandleHookRuntimeInput {
|
|
|
158
162
|
event: HookEventV1;
|
|
159
163
|
rawPayload: Record<string, unknown>;
|
|
160
164
|
receivedAt?: string;
|
|
165
|
+
environment?: Record<string, string | undefined>;
|
|
161
166
|
}
|
|
162
167
|
|
|
163
168
|
const DEFAULT_EVENT_SETTINGS: Record<CanonicalHookEventType, boolean> = {
|
|
164
|
-
SessionStart:
|
|
165
|
-
UserPromptSubmit:
|
|
166
|
-
UserPromptExpansion:
|
|
167
|
-
PreToolUse:
|
|
168
|
-
PermissionRequest:
|
|
169
|
-
PostToolUse:
|
|
170
|
-
PostToolUseFailure:
|
|
171
|
-
PostToolBatch:
|
|
172
|
-
PermissionDenied:
|
|
173
|
-
SubagentStart:
|
|
174
|
-
Stop:
|
|
175
|
-
StopFailure:
|
|
176
|
-
TeammateIdle:
|
|
177
|
-
SubagentStop:
|
|
178
|
-
TaskCreated:
|
|
179
|
-
TaskCompleted:
|
|
180
|
-
PreCompact:
|
|
181
|
-
PostCompact:
|
|
182
|
-
SessionEnd:
|
|
183
|
-
ConfigChange:
|
|
184
|
-
CwdChanged:
|
|
185
|
-
FileChanged:
|
|
186
|
-
WorktreeCreate:
|
|
187
|
-
WorktreeRemove:
|
|
169
|
+
SessionStart: true,
|
|
170
|
+
UserPromptSubmit: true,
|
|
171
|
+
UserPromptExpansion: true,
|
|
172
|
+
PreToolUse: true,
|
|
173
|
+
PermissionRequest: true,
|
|
174
|
+
PostToolUse: true,
|
|
175
|
+
PostToolUseFailure: true,
|
|
176
|
+
PostToolBatch: true,
|
|
177
|
+
PermissionDenied: true,
|
|
178
|
+
SubagentStart: true,
|
|
179
|
+
Stop: true,
|
|
180
|
+
StopFailure: true,
|
|
181
|
+
TeammateIdle: true,
|
|
182
|
+
SubagentStop: true,
|
|
183
|
+
TaskCreated: true,
|
|
184
|
+
TaskCompleted: true,
|
|
185
|
+
PreCompact: true,
|
|
186
|
+
PostCompact: true,
|
|
187
|
+
SessionEnd: true,
|
|
188
|
+
ConfigChange: true,
|
|
189
|
+
CwdChanged: true,
|
|
190
|
+
FileChanged: true,
|
|
191
|
+
WorktreeCreate: true,
|
|
192
|
+
WorktreeRemove: true,
|
|
188
193
|
};
|
|
189
194
|
|
|
190
195
|
const SENSITIVE_TEXT_PATTERN =
|
|
@@ -193,20 +198,20 @@ const SOURCE_LIKE_PATTERN = /\b(function|class|import|export|const|let|var)\b.*[
|
|
|
193
198
|
|
|
194
199
|
export function createDefaultHookSettings(): HookSettings {
|
|
195
200
|
return {
|
|
196
|
-
enabled:
|
|
201
|
+
enabled: true,
|
|
197
202
|
targets: {
|
|
198
203
|
claude: {
|
|
199
|
-
enabled:
|
|
204
|
+
enabled: true,
|
|
200
205
|
events: { ...DEFAULT_EVENT_SETTINGS },
|
|
201
206
|
},
|
|
202
207
|
codex: {
|
|
203
|
-
enabled:
|
|
208
|
+
enabled: true,
|
|
204
209
|
events: { ...DEFAULT_EVENT_SETTINGS },
|
|
205
210
|
},
|
|
206
211
|
},
|
|
207
212
|
observability: {
|
|
208
|
-
metadataOnly:
|
|
209
|
-
rawPayloadStorage:
|
|
213
|
+
metadataOnly: false,
|
|
214
|
+
rawPayloadStorage: true,
|
|
210
215
|
appendEvents: false,
|
|
211
216
|
},
|
|
212
217
|
learning: {
|
|
@@ -220,6 +225,17 @@ export function parseHookSettings(value: unknown): HookSettings {
|
|
|
220
225
|
const defaults = createDefaultHookSettings();
|
|
221
226
|
if (value === undefined || value === null) return defaults;
|
|
222
227
|
if (!isRecord(value)) throw new Error("Invalid hooks settings; expected object.");
|
|
228
|
+
const observability = isRecord(value.observability) ? value.observability : undefined;
|
|
229
|
+
optionalBoolean(
|
|
230
|
+
observability?.metadataOnly,
|
|
231
|
+
defaults.observability.metadataOnly,
|
|
232
|
+
"hooks.observability.metadataOnly",
|
|
233
|
+
);
|
|
234
|
+
optionalBoolean(
|
|
235
|
+
observability?.rawPayloadStorage,
|
|
236
|
+
defaults.observability.rawPayloadStorage,
|
|
237
|
+
"hooks.observability.rawPayloadStorage",
|
|
238
|
+
);
|
|
223
239
|
|
|
224
240
|
return {
|
|
225
241
|
enabled: optionalBoolean(value.enabled, defaults.enabled, "hooks.enabled"),
|
|
@@ -228,9 +244,13 @@ export function parseHookSettings(value: unknown): HookSettings {
|
|
|
228
244
|
codex: parseHookTargetSettings(value.targets, defaults.targets.codex, "codex"),
|
|
229
245
|
},
|
|
230
246
|
observability: {
|
|
231
|
-
metadataOnly:
|
|
232
|
-
rawPayloadStorage:
|
|
233
|
-
appendEvents:
|
|
247
|
+
metadataOnly: false,
|
|
248
|
+
rawPayloadStorage: true,
|
|
249
|
+
appendEvents: optionalBoolean(
|
|
250
|
+
observability?.appendEvents,
|
|
251
|
+
defaults.observability.appendEvents,
|
|
252
|
+
"hooks.observability.appendEvents",
|
|
253
|
+
),
|
|
234
254
|
},
|
|
235
255
|
learning: {
|
|
236
256
|
emitCandidates: false,
|
|
@@ -257,7 +277,7 @@ export function normalizeHookEvent(input: NormalizeHookEventInput): HookEventV1
|
|
|
257
277
|
pluginId: sanitizeScalar(input.pluginId, redactions),
|
|
258
278
|
agent: sanitizeScalar(input.agent ?? "unknown", redactions),
|
|
259
279
|
sessionIdHash,
|
|
260
|
-
rawPayloadStored:
|
|
280
|
+
rawPayloadStored: true,
|
|
261
281
|
},
|
|
262
282
|
time: {
|
|
263
283
|
occurredAt: optionalSanitizedString(
|
|
@@ -269,7 +289,7 @@ export function normalizeHookEvent(input: NormalizeHookEventInput): HookEventV1
|
|
|
269
289
|
scope: {
|
|
270
290
|
taskId: optionalSanitizedString(input.payload.taskId, redactions),
|
|
271
291
|
projectId: optionalSanitizedString(input.payload.projectId, redactions),
|
|
272
|
-
cwdPolicy: "
|
|
292
|
+
cwdPolicy: "raw-local",
|
|
273
293
|
projectContextOptedIn: false,
|
|
274
294
|
},
|
|
275
295
|
payload: {
|
|
@@ -277,11 +297,11 @@ export function normalizeHookEvent(input: NormalizeHookEventInput): HookEventV1
|
|
|
277
297
|
metadata,
|
|
278
298
|
redactions,
|
|
279
299
|
redactionCount: redactions.length,
|
|
280
|
-
rawContentIncluded:
|
|
300
|
+
rawContentIncluded: true,
|
|
281
301
|
},
|
|
282
302
|
policy: {
|
|
283
303
|
classification: "local-private",
|
|
284
|
-
allowedUses: ["observability", "
|
|
304
|
+
allowedUses: ["observability", "workflow-suggestion"],
|
|
285
305
|
learningAllowed: false,
|
|
286
306
|
externalUploadAllowed: false,
|
|
287
307
|
},
|
|
@@ -318,7 +338,8 @@ export function formatHookInstallDryRun(plan: HookInstallDryRunPlan): string {
|
|
|
318
338
|
(eventType) => ` - ${eventType}: ${plan.settings.targets[plan.target].events[eventType]}`,
|
|
319
339
|
),
|
|
320
340
|
"Boundaries:",
|
|
321
|
-
" -
|
|
341
|
+
" - trace: local raw payload storage=required, external upload=false",
|
|
342
|
+
" - derived evidence/knowledge/observability surfaces: metadata-only until review/export",
|
|
322
343
|
" - learning: emitCandidates=false, writeMemory=false",
|
|
323
344
|
" - external upload: false",
|
|
324
345
|
" - protected project writes: .claude/.codex/CLAUDE.md/AGENTS.md not targeted",
|
|
@@ -331,10 +352,10 @@ export function formatHookInstallDryRun(plan: HookInstallDryRunPlan): string {
|
|
|
331
352
|
...(plan.warnings.length === 0
|
|
332
353
|
? [" - none"]
|
|
333
354
|
: plan.warnings.map((warning) => ` - ${warning}`)),
|
|
334
|
-
"
|
|
335
|
-
...(plan.
|
|
355
|
+
"Advisories:",
|
|
356
|
+
...(plan.advisories.length === 0
|
|
336
357
|
? [" - none"]
|
|
337
|
-
: plan.
|
|
358
|
+
: plan.advisories.map((advisory) => ` - ${advisory}`)),
|
|
338
359
|
].join("\n");
|
|
339
360
|
}
|
|
340
361
|
|
|
@@ -382,35 +403,45 @@ export async function handleHookRuntime(input: HandleHookRuntimeInput): Promise<
|
|
|
382
403
|
enabled,
|
|
383
404
|
output: null,
|
|
384
405
|
stateWrites: [],
|
|
406
|
+
warnings: [],
|
|
385
407
|
summary: `Hook ${input.event.type} ignored because EvoDev hooks are disabled.`,
|
|
386
408
|
};
|
|
387
409
|
}
|
|
388
410
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
if (input.event.type === "
|
|
411
|
+
const diagnostics = await recordTeamNativeSessionFromHook(input);
|
|
412
|
+
let result: HookRuntimeResult | undefined;
|
|
413
|
+
if (input.event.type === "SessionStart") result = await handleSessionStart(input);
|
|
414
|
+
else if (input.event.type === "UserPromptSubmit") result = await handleUserPromptSubmit(input);
|
|
415
|
+
else if (input.event.type === "PreToolUse") result = await handlePreToolUse(input);
|
|
392
416
|
if (input.event.type === "PostToolUse" || input.event.type === "PostToolUseFailure") {
|
|
393
|
-
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
if (
|
|
417
|
+
result = await handlePostToolUse(input);
|
|
418
|
+
} else if (input.event.type === "PostToolBatch") {
|
|
419
|
+
result = await handleAdditionalContext(input, "PostToolBatch");
|
|
420
|
+
} else if (
|
|
397
421
|
input.event.type === "SubagentStart" ||
|
|
398
422
|
input.event.type === "TaskCreated" ||
|
|
399
423
|
input.event.type === "PermissionRequest"
|
|
400
424
|
) {
|
|
401
|
-
|
|
402
|
-
}
|
|
403
|
-
if (
|
|
425
|
+
result = await handlePreToolUse(input);
|
|
426
|
+
} else if (
|
|
404
427
|
input.event.type === "Stop" ||
|
|
405
428
|
input.event.type === "SubagentStop" ||
|
|
406
429
|
input.event.type === "TaskCompleted" ||
|
|
407
430
|
input.event.type === "TeammateIdle"
|
|
408
431
|
) {
|
|
409
|
-
|
|
432
|
+
result = await handleCompletionObservation(input);
|
|
433
|
+
} else if (input.event.type === "PreCompact") {
|
|
434
|
+
result = await handlePreCompact(input);
|
|
435
|
+
} else if (input.event.type === "SessionEnd") {
|
|
436
|
+
result = await handleSessionEnd(input);
|
|
437
|
+
} else if (result === undefined) {
|
|
438
|
+
result = await handleAdditionalContext(input, input.event.type);
|
|
410
439
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
440
|
+
const evolutionDiagnostics = await recordEvolutionTriggerFromHook(input);
|
|
441
|
+
return appendRuntimeDiagnostics(
|
|
442
|
+
appendRuntimeDiagnostics(result, diagnostics),
|
|
443
|
+
evolutionDiagnostics,
|
|
444
|
+
);
|
|
414
445
|
}
|
|
415
446
|
|
|
416
447
|
export function formatHookRuntimeOutput(result: HookRuntimeResult): string {
|
|
@@ -429,6 +460,128 @@ function isHookEventEnabled(
|
|
|
429
460
|
);
|
|
430
461
|
}
|
|
431
462
|
|
|
463
|
+
interface HookRuntimeDiagnostics {
|
|
464
|
+
stateWrites: string[];
|
|
465
|
+
warnings: string[];
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
async function recordTeamNativeSessionFromHook(
|
|
469
|
+
input: HandleHookRuntimeInput,
|
|
470
|
+
): Promise<HookRuntimeDiagnostics> {
|
|
471
|
+
const environment = input.environment ?? {};
|
|
472
|
+
const runId = optionalPayloadString(environment.EVODEV_TEAM_RUN_ID);
|
|
473
|
+
const roleId = optionalPayloadString(environment.EVODEV_TEAM_ROLE_ID);
|
|
474
|
+
const sessionId = optionalPayloadString(
|
|
475
|
+
input.rawPayload.session_id ?? input.rawPayload.sessionId,
|
|
476
|
+
);
|
|
477
|
+
if (runId === null || roleId === null || sessionId === null) {
|
|
478
|
+
return { stateWrites: [], warnings: [] };
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
try {
|
|
482
|
+
await recordTeamAgentNativeSession({
|
|
483
|
+
homeDir: input.homeDir,
|
|
484
|
+
runId,
|
|
485
|
+
roleId,
|
|
486
|
+
sessionId,
|
|
487
|
+
now:
|
|
488
|
+
input.receivedAt === undefined || input.receivedAt === "dry-run"
|
|
489
|
+
? undefined
|
|
490
|
+
: new Date(input.receivedAt),
|
|
491
|
+
});
|
|
492
|
+
return {
|
|
493
|
+
stateWrites: [resolveTeamRunPaths(input.homeDir).agentPath(runId, roleId)],
|
|
494
|
+
warnings: [],
|
|
495
|
+
};
|
|
496
|
+
} catch {
|
|
497
|
+
return {
|
|
498
|
+
stateWrites: [],
|
|
499
|
+
warnings: [
|
|
500
|
+
`Team native session metadata could not be recorded for run ${safeDiagnosticId(runId)} role ${safeDiagnosticId(roleId)}.`,
|
|
501
|
+
],
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async function recordEvolutionTriggerFromHook(
|
|
507
|
+
input: HandleHookRuntimeInput,
|
|
508
|
+
): Promise<HookRuntimeDiagnostics> {
|
|
509
|
+
const decision = resolveEvolutionTriggerDecision({
|
|
510
|
+
eventType: input.event.type,
|
|
511
|
+
summary: input.event.payload.summary,
|
|
512
|
+
});
|
|
513
|
+
if (!decision.shouldQueue) return { stateWrites: [], warnings: [] };
|
|
514
|
+
|
|
515
|
+
const team = resolveTraceTeamContext({
|
|
516
|
+
homeDir: input.homeDir,
|
|
517
|
+
environment: input.environment,
|
|
518
|
+
payload: input.rawPayload,
|
|
519
|
+
});
|
|
520
|
+
if (team === null) {
|
|
521
|
+
return {
|
|
522
|
+
stateWrites: [],
|
|
523
|
+
warnings:
|
|
524
|
+
decision.strength === "strong"
|
|
525
|
+
? [
|
|
526
|
+
`Evolution trigger ${input.event.type} was not queued because team context is missing.`,
|
|
527
|
+
]
|
|
528
|
+
: [],
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
try {
|
|
533
|
+
const trigger = await enqueueEvolutionTrigger({
|
|
534
|
+
homeDir: input.homeDir,
|
|
535
|
+
projectKey: team.projectKey,
|
|
536
|
+
runId: team.runId,
|
|
537
|
+
roleId: team.roleId,
|
|
538
|
+
taskId: input.event.scope.taskId,
|
|
539
|
+
eventType: input.event.type,
|
|
540
|
+
eventId: input.event.eventId,
|
|
541
|
+
summary: input.event.payload.summary,
|
|
542
|
+
now:
|
|
543
|
+
input.receivedAt === undefined || input.receivedAt === "dry-run"
|
|
544
|
+
? undefined
|
|
545
|
+
: input.receivedAt,
|
|
546
|
+
});
|
|
547
|
+
if (trigger === null) return { stateWrites: [], warnings: [] };
|
|
548
|
+
return {
|
|
549
|
+
stateWrites: [
|
|
550
|
+
join(
|
|
551
|
+
input.homeDir,
|
|
552
|
+
".evodev",
|
|
553
|
+
"state",
|
|
554
|
+
"evolution",
|
|
555
|
+
trigger.projectKey,
|
|
556
|
+
trigger.runId,
|
|
557
|
+
"triggers",
|
|
558
|
+
`${trigger.id}.json`,
|
|
559
|
+
),
|
|
560
|
+
],
|
|
561
|
+
warnings: [],
|
|
562
|
+
};
|
|
563
|
+
} catch {
|
|
564
|
+
return {
|
|
565
|
+
stateWrites: [],
|
|
566
|
+
warnings: [`Evolution trigger ${input.event.type} could not be queued.`],
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function appendRuntimeDiagnostics(
|
|
572
|
+
result: HookRuntimeResult | undefined,
|
|
573
|
+
diagnostics: HookRuntimeDiagnostics,
|
|
574
|
+
): HookRuntimeResult {
|
|
575
|
+
if (result === undefined) {
|
|
576
|
+
throw new Error("Hook runtime did not produce a result.");
|
|
577
|
+
}
|
|
578
|
+
return {
|
|
579
|
+
...result,
|
|
580
|
+
stateWrites: [...new Set([...diagnostics.stateWrites, ...result.stateWrites])],
|
|
581
|
+
warnings: [...new Set([...diagnostics.warnings, ...result.warnings])],
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
|
|
432
585
|
async function handleSessionStart(input: HandleHookRuntimeInput): Promise<HookRuntimeResult> {
|
|
433
586
|
const binding = await readSessionBinding(input.homeDir, input.rawPayload);
|
|
434
587
|
const context = [
|
|
@@ -463,88 +616,30 @@ async function handleUserPromptSubmit(input: HandleHookRuntimeInput): Promise<Ho
|
|
|
463
616
|
await writeJsonFile(paths.bindingPath, binding);
|
|
464
617
|
|
|
465
618
|
const context = [
|
|
466
|
-
`EvoDev
|
|
619
|
+
`EvoDev observed this request before ${formatHookTargetName(input.target)} execution.`,
|
|
620
|
+
"Advisory only: EvoDev does not grant, deny, or stop model/tool execution.",
|
|
467
621
|
`Task Contract: ${paths.contractPath}`,
|
|
468
|
-
`
|
|
469
|
-
`
|
|
622
|
+
`Suggested mode: ${contract.route.mode ?? "unknown"}`,
|
|
623
|
+
`Suggested workflow: ${contract.route.workflowId ?? "none"}`,
|
|
470
624
|
`Reason: ${contract.route.rationale}`,
|
|
471
|
-
"
|
|
625
|
+
"Use the suggestion only if it fits the user request.",
|
|
472
626
|
].join(" ");
|
|
473
627
|
|
|
474
628
|
return createRuntimeResult(input, hookOutput(input.event.type, { additionalContext: context }), {
|
|
475
|
-
summary: "User prompt
|
|
629
|
+
summary: "User prompt observed and advisory context prepared.",
|
|
476
630
|
stateWrites: [paths.contractPath, paths.bindingPath],
|
|
477
631
|
});
|
|
478
632
|
}
|
|
479
633
|
|
|
480
634
|
async function handlePreToolUse(input: HandleHookRuntimeInput): Promise<HookRuntimeResult> {
|
|
481
|
-
const contract = await readActiveContract(input.homeDir, input.rawPayload);
|
|
482
635
|
const commandClass =
|
|
483
636
|
typeof input.event.payload.metadata.commandClass === "string"
|
|
484
637
|
? input.event.payload.metadata.commandClass
|
|
485
638
|
: undefined;
|
|
486
|
-
const protectedPath = findProtectedPath(input.rawPayload);
|
|
487
|
-
|
|
488
|
-
if (contract === null) {
|
|
489
|
-
return createRuntimeResult(
|
|
490
|
-
input,
|
|
491
|
-
hookOutput(input.event.type, {
|
|
492
|
-
permissionDecision: "ask",
|
|
493
|
-
permissionDecisionReason: "EvoDev requires an active Task Contract before tool use.",
|
|
494
|
-
additionalContext:
|
|
495
|
-
"Submit the user prompt through UserPromptSubmit to create the Task Contract first.",
|
|
496
|
-
}),
|
|
497
|
-
{ summary: "Tool use requires user confirmation because no active contract exists." },
|
|
498
|
-
);
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
if (protectedPath !== null) {
|
|
502
|
-
return createRuntimeResult(
|
|
503
|
-
input,
|
|
504
|
-
hookOutput(input.event.type, {
|
|
505
|
-
permissionDecision: "deny",
|
|
506
|
-
permissionDecisionReason: `EvoDev blocked access to protected project asset: ${protectedPath}`,
|
|
507
|
-
}),
|
|
508
|
-
{ summary: "Protected path access denied." },
|
|
509
|
-
);
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
if (
|
|
513
|
-
commandClass === "delete" ||
|
|
514
|
-
commandClass === "publish" ||
|
|
515
|
-
commandClass === "credential-sensitive" ||
|
|
516
|
-
commandClass === "path-escaping"
|
|
517
|
-
) {
|
|
518
|
-
return createRuntimeResult(
|
|
519
|
-
input,
|
|
520
|
-
hookOutput(input.event.type, {
|
|
521
|
-
permissionDecision: "deny",
|
|
522
|
-
permissionDecisionReason: `EvoDev blocked high-risk tool action: ${commandClass}`,
|
|
523
|
-
}),
|
|
524
|
-
{ summary: "High-risk tool action denied." },
|
|
525
|
-
);
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
if (commandClass === "network" || commandClass === "unknown" || commandClass === "write") {
|
|
529
|
-
return createRuntimeResult(
|
|
530
|
-
input,
|
|
531
|
-
hookOutput(input.event.type, {
|
|
532
|
-
permissionDecision: "ask",
|
|
533
|
-
permissionDecisionReason: `EvoDev requires confirmation for ${commandClass} action under ${contract.route.mode ?? "unrouted"} mode.`,
|
|
534
|
-
additionalContext: `Active Task Contract: ${contract.taskId}. Confirm scope before continuing.`,
|
|
535
|
-
}),
|
|
536
|
-
{ summary: "Tool use requires user confirmation." },
|
|
537
|
-
);
|
|
538
|
-
}
|
|
539
639
|
|
|
540
|
-
return createRuntimeResult(
|
|
541
|
-
input
|
|
542
|
-
|
|
543
|
-
permissionDecision: "allow",
|
|
544
|
-
permissionDecisionReason: `EvoDev allowed ${commandClass ?? "metadata-only"} action under Task Contract ${contract.taskId}.`,
|
|
545
|
-
}),
|
|
546
|
-
{ summary: "Tool use allowed by EvoDev policy." },
|
|
547
|
-
);
|
|
640
|
+
return createRuntimeResult(input, null, {
|
|
641
|
+
summary: `${input.event.type} observed (${commandClass ?? "metadata-only"}); no permission decision emitted.`,
|
|
642
|
+
});
|
|
548
643
|
}
|
|
549
644
|
|
|
550
645
|
async function handlePostToolUse(input: HandleHookRuntimeInput): Promise<HookRuntimeResult> {
|
|
@@ -574,62 +669,25 @@ async function handlePostToolUse(input: HandleHookRuntimeInput): Promise<HookRun
|
|
|
574
669
|
};
|
|
575
670
|
await writeTaskContract(binding.contractPath, nextContract, { overwrite: true });
|
|
576
671
|
|
|
577
|
-
return createRuntimeResult(
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
}),
|
|
582
|
-
{
|
|
583
|
-
summary: "Post-tool metadata evidence recorded.",
|
|
584
|
-
stateWrites: [binding.contractPath],
|
|
585
|
-
},
|
|
586
|
-
);
|
|
672
|
+
return createRuntimeResult(input, null, {
|
|
673
|
+
summary: "Post-tool metadata evidence recorded.",
|
|
674
|
+
stateWrites: [binding.contractPath],
|
|
675
|
+
});
|
|
587
676
|
}
|
|
588
677
|
|
|
589
|
-
async function
|
|
678
|
+
async function handleCompletionObservation(
|
|
679
|
+
input: HandleHookRuntimeInput,
|
|
680
|
+
): Promise<HookRuntimeResult> {
|
|
590
681
|
const contract = await readActiveContract(input.homeDir, input.rawPayload);
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
contract.route.mode === "rigorous" &&
|
|
597
|
-
contract.evidence.items.length === 0
|
|
598
|
-
) {
|
|
599
|
-
return createRuntimeResult(
|
|
600
|
-
input,
|
|
601
|
-
{
|
|
602
|
-
decision: "block",
|
|
603
|
-
reason: "EvoDev rigorous mode requires metadata-only evidence before stopping.",
|
|
604
|
-
...hookOutput(input.event.type, {
|
|
605
|
-
additionalContext:
|
|
606
|
-
"Run the required verification or record metadata-only evidence before finishing.",
|
|
607
|
-
}),
|
|
608
|
-
},
|
|
609
|
-
{ summary: "Completion blocked until evidence exists." },
|
|
610
|
-
);
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
return createRuntimeResult(
|
|
614
|
-
input,
|
|
615
|
-
hookOutput(input.event.type, {
|
|
616
|
-
additionalContext: `EvoDev completion gate checked Task Contract ${contract.taskId}. Evidence items: ${contract.evidence.items.length}.`,
|
|
617
|
-
}),
|
|
618
|
-
{ summary: "Completion gate checked." },
|
|
619
|
-
);
|
|
682
|
+
const suffix =
|
|
683
|
+
contract === null ? "without an active Task Contract" : `for Task Contract ${contract.taskId}`;
|
|
684
|
+
return createRuntimeResult(input, null, {
|
|
685
|
+
summary: `${input.event.type} observed ${suffix}; no completion control enforced.`,
|
|
686
|
+
});
|
|
620
687
|
}
|
|
621
688
|
|
|
622
689
|
async function handlePreCompact(input: HandleHookRuntimeInput): Promise<HookRuntimeResult> {
|
|
623
|
-
|
|
624
|
-
return createRuntimeResult(
|
|
625
|
-
input,
|
|
626
|
-
hookOutput(input.event.type, {
|
|
627
|
-
additionalContext: binding?.contractPath
|
|
628
|
-
? `Preserve EvoDev Task Contract reference across compaction: ${binding.contractPath}`
|
|
629
|
-
: "No EvoDev Task Contract is active for this session.",
|
|
630
|
-
}),
|
|
631
|
-
{ summary: "PreCompact context prepared." },
|
|
632
|
-
);
|
|
690
|
+
return createRuntimeResult(input, null, { summary: "PreCompact observed." });
|
|
633
691
|
}
|
|
634
692
|
|
|
635
693
|
async function handleSessionEnd(input: HandleHookRuntimeInput): Promise<HookRuntimeResult> {
|
|
@@ -656,20 +714,16 @@ function handleAdditionalContext(
|
|
|
656
714
|
eventName: CanonicalHookEventType,
|
|
657
715
|
): Promise<HookRuntimeResult> {
|
|
658
716
|
return Promise.resolve(
|
|
659
|
-
createRuntimeResult(
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
additionalContext: `EvoDev processed ${eventName} as metadata-only hook context.`,
|
|
663
|
-
}),
|
|
664
|
-
{ summary: `${eventName} processed as metadata-only context.` },
|
|
665
|
-
),
|
|
717
|
+
createRuntimeResult(input, null, {
|
|
718
|
+
summary: `${eventName} observed as metadata-only hook context.`,
|
|
719
|
+
}),
|
|
666
720
|
);
|
|
667
721
|
}
|
|
668
722
|
|
|
669
723
|
function createRuntimeResult(
|
|
670
724
|
input: HandleHookRuntimeInput,
|
|
671
725
|
output: Record<string, unknown> | null,
|
|
672
|
-
options: { summary: string; stateWrites?: string[] },
|
|
726
|
+
options: { summary: string; stateWrites?: string[]; warnings?: string[] },
|
|
673
727
|
): HookRuntimeResult {
|
|
674
728
|
return {
|
|
675
729
|
target: input.target,
|
|
@@ -677,6 +731,7 @@ function createRuntimeResult(
|
|
|
677
731
|
enabled: true,
|
|
678
732
|
output,
|
|
679
733
|
stateWrites: options.stateWrites ?? [],
|
|
734
|
+
warnings: options.warnings ?? [],
|
|
680
735
|
summary: options.summary,
|
|
681
736
|
};
|
|
682
737
|
}
|
|
@@ -701,14 +756,14 @@ function createHookTaskContract(
|
|
|
701
756
|
const targetName = formatHookTargetName(input.target);
|
|
702
757
|
const contract = createTaskContract({
|
|
703
758
|
title: `${targetName} hook task ${sessionKey}`,
|
|
704
|
-
summary: `${targetName} prompt classified as ${classification.kind}; raw prompt
|
|
759
|
+
summary: `${targetName} prompt classified as ${classification.kind}; raw prompt is local trace only.`,
|
|
705
760
|
projectId: null,
|
|
706
761
|
});
|
|
707
762
|
|
|
708
763
|
return {
|
|
709
764
|
...contract,
|
|
710
765
|
currentState: {
|
|
711
|
-
summary: `UserPromptSubmit received through ${targetName} hooks; raw prompt omitted.`,
|
|
766
|
+
summary: `UserPromptSubmit received through ${targetName} hooks; raw prompt omitted from Task Contract.`,
|
|
712
767
|
evidenceRefs: [],
|
|
713
768
|
},
|
|
714
769
|
targetState: {
|
|
@@ -811,34 +866,14 @@ async function writeJsonFile(path: string, value: unknown): Promise<void> {
|
|
|
811
866
|
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
812
867
|
}
|
|
813
868
|
|
|
814
|
-
function findProtectedPath(payload: Record<string, unknown>): string | null {
|
|
815
|
-
const candidates = collectStringValues(payload).filter((value) => value.length < 500);
|
|
816
|
-
for (const candidate of candidates) {
|
|
817
|
-
if (
|
|
818
|
-
candidate === "CLAUDE.md" ||
|
|
819
|
-
candidate === "AGENTS.md" ||
|
|
820
|
-
candidate.includes("/CLAUDE.md") ||
|
|
821
|
-
candidate.includes("/AGENTS.md") ||
|
|
822
|
-
candidate.includes(".claude/") ||
|
|
823
|
-
candidate.includes(".codex/")
|
|
824
|
-
) {
|
|
825
|
-
return candidate;
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
return null;
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
function collectStringValues(value: unknown): string[] {
|
|
832
|
-
if (typeof value === "string") return [value];
|
|
833
|
-
if (Array.isArray(value)) return value.flatMap((item) => collectStringValues(item));
|
|
834
|
-
if (isRecord(value)) return Object.values(value).flatMap((item) => collectStringValues(item));
|
|
835
|
-
return [];
|
|
836
|
-
}
|
|
837
|
-
|
|
838
869
|
function optionalPayloadString(value: unknown): string | null {
|
|
839
870
|
return typeof value === "string" && value.length > 0 ? value.slice(0, 300) : null;
|
|
840
871
|
}
|
|
841
872
|
|
|
873
|
+
function safeDiagnosticId(value: string): string {
|
|
874
|
+
return value.replace(/[^A-Za-z0-9._-]/g, "-").slice(0, 120) || "unknown";
|
|
875
|
+
}
|
|
876
|
+
|
|
842
877
|
function optionalPayloadNumber(value: unknown): number | null {
|
|
843
878
|
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
844
879
|
}
|
|
@@ -893,7 +928,7 @@ function extractMetadata(
|
|
|
893
928
|
redactions: string[],
|
|
894
929
|
): Record<string, string | number | boolean | string[]> {
|
|
895
930
|
const metadata: Record<string, string | number | boolean | string[]> = {
|
|
896
|
-
rawContentIncluded:
|
|
931
|
+
rawContentIncluded: true,
|
|
897
932
|
};
|
|
898
933
|
const toolInput = isRecord(payload.tool_input)
|
|
899
934
|
? payload.tool_input
|
|
@@ -932,30 +967,12 @@ function extractMetadata(
|
|
|
932
967
|
}
|
|
933
968
|
|
|
934
969
|
function decideHookPolicy(commandClass: string | undefined): HookEventV1["decision"] {
|
|
935
|
-
if (
|
|
936
|
-
commandClass === "delete" ||
|
|
937
|
-
commandClass === "network" ||
|
|
938
|
-
commandClass === "publish" ||
|
|
939
|
-
commandClass === "credential-sensitive" ||
|
|
940
|
-
commandClass === "path-escaping" ||
|
|
941
|
-
commandClass === "unknown"
|
|
942
|
-
) {
|
|
943
|
-
return {
|
|
944
|
-
action: "ask-user",
|
|
945
|
-
reason: `Command risk requires explicit confirmation: ${commandClass}`,
|
|
946
|
-
requiresUserConfirmation: true,
|
|
947
|
-
};
|
|
948
|
-
}
|
|
949
|
-
if (commandClass === "write") {
|
|
950
|
-
return {
|
|
951
|
-
action: "warn",
|
|
952
|
-
reason: "Write-like command requires scope review.",
|
|
953
|
-
requiresUserConfirmation: true,
|
|
954
|
-
};
|
|
955
|
-
}
|
|
956
970
|
return {
|
|
957
971
|
action: "allow",
|
|
958
|
-
reason:
|
|
972
|
+
reason:
|
|
973
|
+
commandClass === undefined
|
|
974
|
+
? "Observed metadata only; EvoDev does not stop hook execution."
|
|
975
|
+
: `Observed ${commandClass} tool action; EvoDev does not stop hook execution.`,
|
|
959
976
|
requiresUserConfirmation: false,
|
|
960
977
|
};
|
|
961
978
|
}
|
|
@@ -966,7 +983,7 @@ function summarizeEvent(
|
|
|
966
983
|
): string {
|
|
967
984
|
if (type === "PreToolUse") return `Tool use requested (${metadata.commandClass ?? "unknown"}).`;
|
|
968
985
|
if (type === "PostToolUse") return "Tool use completed with metadata-only result.";
|
|
969
|
-
if (type === "UserPromptSubmit") return "User prompt submitted
|
|
986
|
+
if (type === "UserPromptSubmit") return "User prompt submitted.";
|
|
970
987
|
if (type === "SubagentStop") return "Subagent stopped; transcript omitted.";
|
|
971
988
|
return `${type} received; metadata-only dry-run.`;
|
|
972
989
|
}
|