@evo-dev/core 0.0.1-alpha → 0.0.1-alpha.2
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 +249 -0
- package/assets/skills/coding/knowledge-distillation/manifest.json +10 -0
- package/assets/skills/coding/knowledge-distillation/references/knowledge-distillation-methods.md +126 -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 +968 -39
- package/dist/index.js +10914 -1476
- package/dist/plugins/index.js +32 -32
- package/package.json +5 -1
- package/src/agents/index.ts +84 -49
- package/src/code-agent-traces/index.ts +521 -0
- package/src/config/index.ts +5 -0
- package/src/config/paths.ts +30 -0
- package/src/config/settings.ts +130 -0
- package/src/config/store.ts +152 -0
- package/src/daemon/index.ts +465 -3
- package/src/evolution/index.ts +2827 -0
- package/src/hooks/index.ts +543 -247
- package/src/index.ts +6 -0
- package/src/knowledge/index.ts +4784 -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 +798 -0
- package/src/sync/orchestrator.ts +6 -0
- package/src/task/index.ts +3 -3
- package/src/team/index.ts +3069 -0
- package/src/team/mcp.ts +405 -0
- package/src/team/prompts.ts +141 -0
- package/src/workflow/index.ts +6 -6
package/src/hooks/index.ts
CHANGED
|
@@ -1,12 +1,29 @@
|
|
|
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 {
|
|
6
|
+
createScopedKnowledgeContextPack,
|
|
7
|
+
formatScopedKnowledgePromptBlock,
|
|
8
|
+
hasContextInjectionReceipt,
|
|
9
|
+
writeContextInjectionReceipt,
|
|
10
|
+
} from "../knowledge/index.ts";
|
|
11
|
+
import { resolveTraceTeamContext } from "../runtime-logs/index.ts";
|
|
4
12
|
import {
|
|
5
13
|
type TaskContract,
|
|
6
14
|
createTaskContract,
|
|
7
15
|
routeTaskContract,
|
|
8
16
|
writeTaskContract,
|
|
9
17
|
} from "../task/index.ts";
|
|
18
|
+
import {
|
|
19
|
+
type TeamMessageRecord,
|
|
20
|
+
createTeamRoleRuntimeContext,
|
|
21
|
+
markTeamMessagesDelivered,
|
|
22
|
+
readPendingTeamMessagesForRole,
|
|
23
|
+
recordTeamAgentNativeSession,
|
|
24
|
+
resolveTeamRunPaths,
|
|
25
|
+
updateTeamAgentHookState,
|
|
26
|
+
} from "../team/index.ts";
|
|
10
27
|
|
|
11
28
|
export const CANONICAL_HOOK_EVENT_TYPES = [
|
|
12
29
|
"SessionStart",
|
|
@@ -49,15 +66,15 @@ export type CommandRiskClass =
|
|
|
49
66
|
| "credential-sensitive"
|
|
50
67
|
| "path-escaping"
|
|
51
68
|
| "unknown";
|
|
52
|
-
export type HookDecisionAction = "allow" | "warn"
|
|
69
|
+
export type HookDecisionAction = "allow" | "warn";
|
|
53
70
|
|
|
54
71
|
export interface HookSettings {
|
|
55
72
|
enabled: boolean;
|
|
56
73
|
targets: Record<CodeAgentHookTarget, HookTargetSettings>;
|
|
57
74
|
observability: {
|
|
58
|
-
metadataOnly:
|
|
59
|
-
rawPayloadStorage:
|
|
60
|
-
appendEvents:
|
|
75
|
+
metadataOnly: boolean;
|
|
76
|
+
rawPayloadStorage: boolean;
|
|
77
|
+
appendEvents: boolean;
|
|
61
78
|
};
|
|
62
79
|
learning: {
|
|
63
80
|
emitCandidates: false;
|
|
@@ -78,7 +95,7 @@ export interface HookEventV1 {
|
|
|
78
95
|
pluginId: string;
|
|
79
96
|
agent: string;
|
|
80
97
|
sessionIdHash: string | null;
|
|
81
|
-
rawPayloadStored:
|
|
98
|
+
rawPayloadStored: boolean;
|
|
82
99
|
};
|
|
83
100
|
time: {
|
|
84
101
|
occurredAt: string | null;
|
|
@@ -87,7 +104,7 @@ export interface HookEventV1 {
|
|
|
87
104
|
scope: {
|
|
88
105
|
taskId: string | null;
|
|
89
106
|
projectId: string | null;
|
|
90
|
-
cwdPolicy: "metadata-only";
|
|
107
|
+
cwdPolicy: "metadata-only" | "raw-local";
|
|
91
108
|
projectContextOptedIn: false;
|
|
92
109
|
};
|
|
93
110
|
payload: {
|
|
@@ -95,11 +112,11 @@ export interface HookEventV1 {
|
|
|
95
112
|
metadata: Record<string, string | number | boolean | string[]>;
|
|
96
113
|
redactions: string[];
|
|
97
114
|
redactionCount: number;
|
|
98
|
-
rawContentIncluded:
|
|
115
|
+
rawContentIncluded: boolean;
|
|
99
116
|
};
|
|
100
117
|
policy: {
|
|
101
118
|
classification: "local-private";
|
|
102
|
-
allowedUses: Array<"observability" | "
|
|
119
|
+
allowedUses: Array<"observability" | "workflow-suggestion">;
|
|
103
120
|
learningAllowed: false;
|
|
104
121
|
externalUploadAllowed: false;
|
|
105
122
|
};
|
|
@@ -128,7 +145,7 @@ export interface HookInstallDryRunPlan {
|
|
|
128
145
|
reason: string;
|
|
129
146
|
}>;
|
|
130
147
|
warnings: string[];
|
|
131
|
-
|
|
148
|
+
advisories: string[];
|
|
132
149
|
}
|
|
133
150
|
|
|
134
151
|
export interface HookRuntimeSessionBinding {
|
|
@@ -139,6 +156,7 @@ export interface HookRuntimeSessionBinding {
|
|
|
139
156
|
contractPath: string | null;
|
|
140
157
|
cwd: string | null;
|
|
141
158
|
route: TaskContract["route"] | null;
|
|
159
|
+
teamRuntimeContextDeliveredAt?: string | null;
|
|
142
160
|
updatedAt: string;
|
|
143
161
|
}
|
|
144
162
|
|
|
@@ -148,6 +166,7 @@ export interface HookRuntimeResult {
|
|
|
148
166
|
enabled: boolean;
|
|
149
167
|
output: Record<string, unknown> | null;
|
|
150
168
|
stateWrites: string[];
|
|
169
|
+
warnings: string[];
|
|
151
170
|
summary: string;
|
|
152
171
|
}
|
|
153
172
|
|
|
@@ -155,52 +174,69 @@ export interface HandleHookRuntimeInput {
|
|
|
155
174
|
target: CodeAgentHookTarget;
|
|
156
175
|
homeDir: string;
|
|
157
176
|
settings: HookSettings;
|
|
177
|
+
teamRuntimeDisplayMode?: "normal" | "development";
|
|
158
178
|
event: HookEventV1;
|
|
159
179
|
rawPayload: Record<string, unknown>;
|
|
160
180
|
receivedAt?: string;
|
|
181
|
+
environment?: Record<string, string | undefined>;
|
|
182
|
+
runtimeInjectionEnabled?: boolean;
|
|
161
183
|
}
|
|
162
184
|
|
|
163
185
|
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:
|
|
186
|
+
SessionStart: true,
|
|
187
|
+
UserPromptSubmit: true,
|
|
188
|
+
UserPromptExpansion: true,
|
|
189
|
+
PreToolUse: true,
|
|
190
|
+
PermissionRequest: true,
|
|
191
|
+
PostToolUse: true,
|
|
192
|
+
PostToolUseFailure: true,
|
|
193
|
+
PostToolBatch: true,
|
|
194
|
+
PermissionDenied: true,
|
|
195
|
+
SubagentStart: true,
|
|
196
|
+
Stop: true,
|
|
197
|
+
StopFailure: true,
|
|
198
|
+
TeammateIdle: true,
|
|
199
|
+
SubagentStop: true,
|
|
200
|
+
TaskCreated: true,
|
|
201
|
+
TaskCompleted: true,
|
|
202
|
+
PreCompact: true,
|
|
203
|
+
PostCompact: true,
|
|
204
|
+
SessionEnd: true,
|
|
205
|
+
ConfigChange: true,
|
|
206
|
+
CwdChanged: true,
|
|
207
|
+
FileChanged: true,
|
|
208
|
+
WorktreeCreate: true,
|
|
209
|
+
WorktreeRemove: true,
|
|
188
210
|
};
|
|
189
211
|
|
|
190
212
|
const SENSITIVE_TEXT_PATTERN =
|
|
191
213
|
/https?:\/\/\S+|\b(secret|token|password|passwd|private|internal|api[_-]?key|apikey|credential|credentials|\.env)\b/i;
|
|
192
214
|
const SOURCE_LIKE_PATTERN = /\b(function|class|import|export|const|let|var)\b.*[{};]/s;
|
|
215
|
+
const TEAM_MESSAGE_DELIVERY_EVENTS = new Set<CanonicalHookEventType>([
|
|
216
|
+
"SessionStart",
|
|
217
|
+
"UserPromptSubmit",
|
|
218
|
+
"PostToolUse",
|
|
219
|
+
"PostToolUseFailure",
|
|
220
|
+
"Stop",
|
|
221
|
+
"TeammateIdle",
|
|
222
|
+
"SubagentStop",
|
|
223
|
+
"TaskCompleted",
|
|
224
|
+
]);
|
|
225
|
+
const CODEX_STOP_EVENTS_WITHOUT_ADDITIONAL_CONTEXT = new Set<CanonicalHookEventType>([
|
|
226
|
+
"Stop",
|
|
227
|
+
"SubagentStop",
|
|
228
|
+
]);
|
|
193
229
|
|
|
194
230
|
export function createDefaultHookSettings(): HookSettings {
|
|
195
231
|
return {
|
|
196
|
-
enabled:
|
|
232
|
+
enabled: true,
|
|
197
233
|
targets: {
|
|
198
234
|
claude: {
|
|
199
|
-
enabled:
|
|
235
|
+
enabled: true,
|
|
200
236
|
events: { ...DEFAULT_EVENT_SETTINGS },
|
|
201
237
|
},
|
|
202
238
|
codex: {
|
|
203
|
-
enabled:
|
|
239
|
+
enabled: true,
|
|
204
240
|
events: { ...DEFAULT_EVENT_SETTINGS },
|
|
205
241
|
},
|
|
206
242
|
},
|
|
@@ -220,6 +256,17 @@ export function parseHookSettings(value: unknown): HookSettings {
|
|
|
220
256
|
const defaults = createDefaultHookSettings();
|
|
221
257
|
if (value === undefined || value === null) return defaults;
|
|
222
258
|
if (!isRecord(value)) throw new Error("Invalid hooks settings; expected object.");
|
|
259
|
+
const observability = isRecord(value.observability) ? value.observability : undefined;
|
|
260
|
+
optionalBoolean(
|
|
261
|
+
observability?.metadataOnly,
|
|
262
|
+
defaults.observability.metadataOnly,
|
|
263
|
+
"hooks.observability.metadataOnly",
|
|
264
|
+
);
|
|
265
|
+
optionalBoolean(
|
|
266
|
+
observability?.rawPayloadStorage,
|
|
267
|
+
defaults.observability.rawPayloadStorage,
|
|
268
|
+
"hooks.observability.rawPayloadStorage",
|
|
269
|
+
);
|
|
223
270
|
|
|
224
271
|
return {
|
|
225
272
|
enabled: optionalBoolean(value.enabled, defaults.enabled, "hooks.enabled"),
|
|
@@ -230,7 +277,11 @@ export function parseHookSettings(value: unknown): HookSettings {
|
|
|
230
277
|
observability: {
|
|
231
278
|
metadataOnly: true,
|
|
232
279
|
rawPayloadStorage: false,
|
|
233
|
-
appendEvents:
|
|
280
|
+
appendEvents: optionalBoolean(
|
|
281
|
+
observability?.appendEvents,
|
|
282
|
+
defaults.observability.appendEvents,
|
|
283
|
+
"hooks.observability.appendEvents",
|
|
284
|
+
),
|
|
234
285
|
},
|
|
235
286
|
learning: {
|
|
236
287
|
emitCandidates: false,
|
|
@@ -281,7 +332,7 @@ export function normalizeHookEvent(input: NormalizeHookEventInput): HookEventV1
|
|
|
281
332
|
},
|
|
282
333
|
policy: {
|
|
283
334
|
classification: "local-private",
|
|
284
|
-
allowedUses: ["observability", "
|
|
335
|
+
allowedUses: ["observability", "workflow-suggestion"],
|
|
285
336
|
learningAllowed: false,
|
|
286
337
|
externalUploadAllowed: false,
|
|
287
338
|
},
|
|
@@ -318,7 +369,8 @@ export function formatHookInstallDryRun(plan: HookInstallDryRunPlan): string {
|
|
|
318
369
|
(eventType) => ` - ${eventType}: ${plan.settings.targets[plan.target].events[eventType]}`,
|
|
319
370
|
),
|
|
320
371
|
"Boundaries:",
|
|
321
|
-
" -
|
|
372
|
+
" - execution events: metadata-only; raw payload/prompt/output/source storage=false",
|
|
373
|
+
" - historical raw trace files may remain for read-only migration compatibility",
|
|
322
374
|
" - learning: emitCandidates=false, writeMemory=false",
|
|
323
375
|
" - external upload: false",
|
|
324
376
|
" - protected project writes: .claude/.codex/CLAUDE.md/AGENTS.md not targeted",
|
|
@@ -331,10 +383,10 @@ export function formatHookInstallDryRun(plan: HookInstallDryRunPlan): string {
|
|
|
331
383
|
...(plan.warnings.length === 0
|
|
332
384
|
? [" - none"]
|
|
333
385
|
: plan.warnings.map((warning) => ` - ${warning}`)),
|
|
334
|
-
"
|
|
335
|
-
...(plan.
|
|
386
|
+
"Advisories:",
|
|
387
|
+
...(plan.advisories.length === 0
|
|
336
388
|
? [" - none"]
|
|
337
|
-
: plan.
|
|
389
|
+
: plan.advisories.map((advisory) => ` - ${advisory}`)),
|
|
338
390
|
].join("\n");
|
|
339
391
|
}
|
|
340
392
|
|
|
@@ -382,35 +434,57 @@ export async function handleHookRuntime(input: HandleHookRuntimeInput): Promise<
|
|
|
382
434
|
enabled,
|
|
383
435
|
output: null,
|
|
384
436
|
stateWrites: [],
|
|
437
|
+
warnings: [],
|
|
385
438
|
summary: `Hook ${input.event.type} ignored because EvoDev hooks are disabled.`,
|
|
386
439
|
};
|
|
387
440
|
}
|
|
388
441
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
if (input.event.type === "
|
|
442
|
+
const diagnostics = await recordTeamNativeSessionFromHook(input);
|
|
443
|
+
let result: HookRuntimeResult | undefined;
|
|
444
|
+
if (input.event.type === "SessionStart") result = await handleSessionStart(input);
|
|
445
|
+
else if (input.event.type === "UserPromptSubmit") result = await handleUserPromptSubmit(input);
|
|
446
|
+
else if (input.event.type === "PreToolUse") result = await handlePreToolUse(input);
|
|
392
447
|
if (input.event.type === "PostToolUse" || input.event.type === "PostToolUseFailure") {
|
|
393
|
-
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
if (
|
|
448
|
+
result = await handlePostToolUse(input);
|
|
449
|
+
} else if (input.event.type === "PostToolBatch") {
|
|
450
|
+
result = await handleAdditionalContext(input, "PostToolBatch");
|
|
451
|
+
} else if (
|
|
397
452
|
input.event.type === "SubagentStart" ||
|
|
398
453
|
input.event.type === "TaskCreated" ||
|
|
399
454
|
input.event.type === "PermissionRequest"
|
|
400
455
|
) {
|
|
401
|
-
|
|
402
|
-
}
|
|
403
|
-
if (
|
|
456
|
+
result = await handlePreToolUse(input);
|
|
457
|
+
} else if (
|
|
404
458
|
input.event.type === "Stop" ||
|
|
405
459
|
input.event.type === "SubagentStop" ||
|
|
406
460
|
input.event.type === "TaskCompleted" ||
|
|
407
461
|
input.event.type === "TeammateIdle"
|
|
408
462
|
) {
|
|
409
|
-
|
|
463
|
+
result = await handleCompletionObservation(input);
|
|
464
|
+
} else if (input.event.type === "PreCompact") {
|
|
465
|
+
result = await handlePreCompact(input);
|
|
466
|
+
} else if (input.event.type === "SessionEnd") {
|
|
467
|
+
result = await handleSessionEnd(input);
|
|
468
|
+
} else if (result === undefined) {
|
|
469
|
+
result = await handleAdditionalContext(input, input.event.type);
|
|
410
470
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
471
|
+
const evolutionDiagnostics = await recordEvolutionTriggerFromHook(input);
|
|
472
|
+
const messageDiagnostics = await deliverPendingTeamMessagesFromHook(input, result);
|
|
473
|
+
const scopedContextDiagnostics = await injectScopedKnowledgeContextFromHook(input, result);
|
|
474
|
+
const teamStateDiagnostics = await recordTeamAgentHookStateFromHook(input);
|
|
475
|
+
return appendRuntimeDiagnostics(
|
|
476
|
+
appendRuntimeDiagnostics(
|
|
477
|
+
appendRuntimeDiagnostics(
|
|
478
|
+
appendRuntimeDiagnostics(
|
|
479
|
+
appendRuntimeDiagnostics(result, diagnostics),
|
|
480
|
+
evolutionDiagnostics,
|
|
481
|
+
),
|
|
482
|
+
messageDiagnostics,
|
|
483
|
+
),
|
|
484
|
+
scopedContextDiagnostics,
|
|
485
|
+
),
|
|
486
|
+
teamStateDiagnostics,
|
|
487
|
+
);
|
|
414
488
|
}
|
|
415
489
|
|
|
416
490
|
export function formatHookRuntimeOutput(result: HookRuntimeResult): string {
|
|
@@ -429,16 +503,276 @@ function isHookEventEnabled(
|
|
|
429
503
|
);
|
|
430
504
|
}
|
|
431
505
|
|
|
506
|
+
interface HookRuntimeDiagnostics {
|
|
507
|
+
stateWrites: string[];
|
|
508
|
+
warnings: string[];
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
async function recordTeamNativeSessionFromHook(
|
|
512
|
+
input: HandleHookRuntimeInput,
|
|
513
|
+
): Promise<HookRuntimeDiagnostics> {
|
|
514
|
+
const environment = input.environment ?? {};
|
|
515
|
+
const runId = optionalPayloadString(environment.EVODEV_TEAM_RUN_ID);
|
|
516
|
+
const roleId = optionalPayloadString(environment.EVODEV_TEAM_ROLE_ID);
|
|
517
|
+
const sessionId = optionalPayloadString(
|
|
518
|
+
input.rawPayload.session_id ?? input.rawPayload.sessionId,
|
|
519
|
+
);
|
|
520
|
+
if (runId === null || roleId === null || sessionId === null) {
|
|
521
|
+
return { stateWrites: [], warnings: [] };
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
try {
|
|
525
|
+
await recordTeamAgentNativeSession({
|
|
526
|
+
homeDir: input.homeDir,
|
|
527
|
+
runId,
|
|
528
|
+
roleId,
|
|
529
|
+
sessionId,
|
|
530
|
+
now:
|
|
531
|
+
input.receivedAt === undefined || input.receivedAt === "dry-run"
|
|
532
|
+
? undefined
|
|
533
|
+
: new Date(input.receivedAt),
|
|
534
|
+
});
|
|
535
|
+
return {
|
|
536
|
+
stateWrites: [resolveTeamRunPaths(input.homeDir).agentPath(runId, roleId)],
|
|
537
|
+
warnings: [],
|
|
538
|
+
};
|
|
539
|
+
} catch {
|
|
540
|
+
return {
|
|
541
|
+
stateWrites: [],
|
|
542
|
+
warnings: [
|
|
543
|
+
`Team native session metadata could not be recorded for run ${safeDiagnosticId(runId)} role ${safeDiagnosticId(roleId)}.`,
|
|
544
|
+
],
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
async function recordEvolutionTriggerFromHook(
|
|
550
|
+
input: HandleHookRuntimeInput,
|
|
551
|
+
): Promise<HookRuntimeDiagnostics> {
|
|
552
|
+
const decision = resolveEvolutionTriggerDecision({
|
|
553
|
+
eventType: input.event.type,
|
|
554
|
+
summary: input.event.payload.summary,
|
|
555
|
+
});
|
|
556
|
+
if (!decision.shouldQueue) return { stateWrites: [], warnings: [] };
|
|
557
|
+
|
|
558
|
+
const team = resolveTraceTeamContext({
|
|
559
|
+
homeDir: input.homeDir,
|
|
560
|
+
environment: input.environment,
|
|
561
|
+
payload: input.rawPayload,
|
|
562
|
+
});
|
|
563
|
+
if (team === null) {
|
|
564
|
+
return {
|
|
565
|
+
stateWrites: [],
|
|
566
|
+
warnings:
|
|
567
|
+
decision.strength === "strong"
|
|
568
|
+
? [
|
|
569
|
+
`Evolution trigger ${input.event.type} was not queued because team context is missing.`,
|
|
570
|
+
]
|
|
571
|
+
: [],
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
try {
|
|
576
|
+
const trigger = await enqueueEvolutionTrigger({
|
|
577
|
+
homeDir: input.homeDir,
|
|
578
|
+
projectKey: team.projectKey,
|
|
579
|
+
runId: team.runId,
|
|
580
|
+
roleId: team.roleId,
|
|
581
|
+
taskId: input.event.scope.taskId,
|
|
582
|
+
eventType: input.event.type,
|
|
583
|
+
eventId: input.event.eventId,
|
|
584
|
+
summary: input.event.payload.summary,
|
|
585
|
+
now:
|
|
586
|
+
input.receivedAt === undefined || input.receivedAt === "dry-run"
|
|
587
|
+
? undefined
|
|
588
|
+
: input.receivedAt,
|
|
589
|
+
});
|
|
590
|
+
if (trigger === null) return { stateWrites: [], warnings: [] };
|
|
591
|
+
return {
|
|
592
|
+
stateWrites: [
|
|
593
|
+
join(
|
|
594
|
+
input.homeDir,
|
|
595
|
+
".evodev",
|
|
596
|
+
"state",
|
|
597
|
+
"evolution",
|
|
598
|
+
trigger.projectKey,
|
|
599
|
+
trigger.runId,
|
|
600
|
+
"triggers",
|
|
601
|
+
`${trigger.id}.json`,
|
|
602
|
+
),
|
|
603
|
+
],
|
|
604
|
+
warnings: [],
|
|
605
|
+
};
|
|
606
|
+
} catch {
|
|
607
|
+
return {
|
|
608
|
+
stateWrites: [],
|
|
609
|
+
warnings: [`Evolution trigger ${input.event.type} could not be queued.`],
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
async function deliverPendingTeamMessagesFromHook(
|
|
615
|
+
input: HandleHookRuntimeInput,
|
|
616
|
+
result: HookRuntimeResult | undefined,
|
|
617
|
+
): Promise<HookRuntimeDiagnostics> {
|
|
618
|
+
if (result === undefined || !canDeliverTeamMessagesFromHook(input.target, input.event.type)) {
|
|
619
|
+
return { stateWrites: [], warnings: [] };
|
|
620
|
+
}
|
|
621
|
+
const team = resolveTraceTeamContext({
|
|
622
|
+
homeDir: input.homeDir,
|
|
623
|
+
environment: input.environment,
|
|
624
|
+
payload: input.rawPayload,
|
|
625
|
+
});
|
|
626
|
+
if (team === null) return { stateWrites: [], warnings: [] };
|
|
627
|
+
|
|
628
|
+
try {
|
|
629
|
+
const messages = await readPendingTeamMessagesForRole({
|
|
630
|
+
homeDir: input.homeDir,
|
|
631
|
+
runId: team.runId,
|
|
632
|
+
roleId: team.roleId,
|
|
633
|
+
limit: 5,
|
|
634
|
+
});
|
|
635
|
+
if (messages.length === 0) return { stateWrites: [], warnings: [] };
|
|
636
|
+
result.output = appendAdditionalContext(
|
|
637
|
+
result.output,
|
|
638
|
+
input.event.type,
|
|
639
|
+
formatTeamInboxContext(messages),
|
|
640
|
+
);
|
|
641
|
+
await markTeamMessagesDelivered({
|
|
642
|
+
homeDir: input.homeDir,
|
|
643
|
+
runId: team.runId,
|
|
644
|
+
roleId: team.roleId,
|
|
645
|
+
messageIds: messages.map((message) => message.messageId),
|
|
646
|
+
now:
|
|
647
|
+
input.receivedAt === undefined || input.receivedAt === "dry-run"
|
|
648
|
+
? undefined
|
|
649
|
+
: new Date(input.receivedAt),
|
|
650
|
+
});
|
|
651
|
+
const teamPaths = resolveTeamRunPaths(input.homeDir);
|
|
652
|
+
return {
|
|
653
|
+
stateWrites: [teamPaths.eventsPath(team.runId), teamPaths.messageListPath(team.runId)],
|
|
654
|
+
warnings: [],
|
|
655
|
+
};
|
|
656
|
+
} catch {
|
|
657
|
+
return {
|
|
658
|
+
stateWrites: [],
|
|
659
|
+
warnings: [
|
|
660
|
+
`Team inbox messages could not be delivered for role ${safeDiagnosticId(team.roleId)}.`,
|
|
661
|
+
],
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
async function injectScopedKnowledgeContextFromHook(
|
|
667
|
+
input: HandleHookRuntimeInput,
|
|
668
|
+
result: HookRuntimeResult | undefined,
|
|
669
|
+
): Promise<HookRuntimeDiagnostics> {
|
|
670
|
+
if (result === undefined || !canDeliverTeamMessagesFromHook(input.target, input.event.type)) {
|
|
671
|
+
return { stateWrites: [], warnings: [] };
|
|
672
|
+
}
|
|
673
|
+
const team = resolveTraceTeamContext({
|
|
674
|
+
homeDir: input.homeDir,
|
|
675
|
+
environment: input.environment,
|
|
676
|
+
payload: input.rawPayload,
|
|
677
|
+
});
|
|
678
|
+
if (team === null || team.roleId !== "main") return { stateWrites: [], warnings: [] };
|
|
679
|
+
|
|
680
|
+
try {
|
|
681
|
+
if (input.runtimeInjectionEnabled === false) return { stateWrites: [], warnings: [] };
|
|
682
|
+
const pack = await createScopedKnowledgeContextPack({
|
|
683
|
+
homeDir: input.homeDir,
|
|
684
|
+
projectKey: team.projectKey,
|
|
685
|
+
roleId: team.roleId,
|
|
686
|
+
});
|
|
687
|
+
if (pack === null) return { stateWrites: [], warnings: [] };
|
|
688
|
+
const sessionKey = resolveHookSessionKey(input.rawPayload);
|
|
689
|
+
if (
|
|
690
|
+
await hasContextInjectionReceipt({
|
|
691
|
+
homeDir: input.homeDir,
|
|
692
|
+
sessionKey,
|
|
693
|
+
contextPackId: pack.id,
|
|
694
|
+
})
|
|
695
|
+
) {
|
|
696
|
+
return { stateWrites: [], warnings: [] };
|
|
697
|
+
}
|
|
698
|
+
const receipt = await writeContextInjectionReceipt({
|
|
699
|
+
homeDir: input.homeDir,
|
|
700
|
+
sessionKey,
|
|
701
|
+
pack,
|
|
702
|
+
trigger: "hook-safe-point",
|
|
703
|
+
hookEventId: input.event.eventId,
|
|
704
|
+
injectedAt: input.receivedAt ?? new Date().toISOString(),
|
|
705
|
+
});
|
|
706
|
+
result.output = appendAdditionalContext(
|
|
707
|
+
result.output,
|
|
708
|
+
input.event.type,
|
|
709
|
+
formatScopedKnowledgePromptBlock(pack),
|
|
710
|
+
);
|
|
711
|
+
return { stateWrites: [receipt.path], warnings: [] };
|
|
712
|
+
} catch {
|
|
713
|
+
return {
|
|
714
|
+
stateWrites: [],
|
|
715
|
+
warnings: ["Scoped knowledge context could not be injected at this hook safe point."],
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function canDeliverTeamMessagesFromHook(
|
|
721
|
+
target: CodeAgentHookTarget,
|
|
722
|
+
eventType: CanonicalHookEventType,
|
|
723
|
+
): boolean {
|
|
724
|
+
if (!TEAM_MESSAGE_DELIVERY_EVENTS.has(eventType)) return false;
|
|
725
|
+
return target !== "codex" || !CODEX_STOP_EVENTS_WITHOUT_ADDITIONAL_CONTEXT.has(eventType);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
async function recordTeamAgentHookStateFromHook(
|
|
729
|
+
input: HandleHookRuntimeInput,
|
|
730
|
+
): Promise<HookRuntimeDiagnostics> {
|
|
731
|
+
const team = resolveTraceTeamContext({
|
|
732
|
+
homeDir: input.homeDir,
|
|
733
|
+
environment: input.environment,
|
|
734
|
+
payload: input.rawPayload,
|
|
735
|
+
});
|
|
736
|
+
if (team === null) return { stateWrites: [], warnings: [] };
|
|
737
|
+
|
|
738
|
+
try {
|
|
739
|
+
const result = await updateTeamAgentHookState({
|
|
740
|
+
homeDir: input.homeDir,
|
|
741
|
+
runId: team.runId,
|
|
742
|
+
roleId: team.roleId,
|
|
743
|
+
hookEvent: input.event.type,
|
|
744
|
+
now:
|
|
745
|
+
input.receivedAt === undefined || input.receivedAt === "dry-run"
|
|
746
|
+
? undefined
|
|
747
|
+
: new Date(input.receivedAt),
|
|
748
|
+
});
|
|
749
|
+
return { stateWrites: [result.agentPath, result.statusPath], warnings: [] };
|
|
750
|
+
} catch {
|
|
751
|
+
return {
|
|
752
|
+
stateWrites: [],
|
|
753
|
+
warnings: [
|
|
754
|
+
`Team agent state could not be updated for hook role ${safeDiagnosticId(team.roleId)}.`,
|
|
755
|
+
],
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function appendRuntimeDiagnostics(
|
|
761
|
+
result: HookRuntimeResult | undefined,
|
|
762
|
+
diagnostics: HookRuntimeDiagnostics,
|
|
763
|
+
): HookRuntimeResult {
|
|
764
|
+
if (result === undefined) {
|
|
765
|
+
throw new Error("Hook runtime did not produce a result.");
|
|
766
|
+
}
|
|
767
|
+
return {
|
|
768
|
+
...result,
|
|
769
|
+
stateWrites: [...new Set([...diagnostics.stateWrites, ...result.stateWrites])],
|
|
770
|
+
warnings: [...new Set([...diagnostics.warnings, ...result.warnings])],
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
|
|
432
774
|
async function handleSessionStart(input: HandleHookRuntimeInput): Promise<HookRuntimeResult> {
|
|
433
|
-
|
|
434
|
-
const context = [
|
|
435
|
-
"EvoDev session initialized.",
|
|
436
|
-
binding?.contractPath
|
|
437
|
-
? `Active Task Contract: ${binding.contractPath}`
|
|
438
|
-
: "No active Task Contract yet.",
|
|
439
|
-
"User prompts will be routed through EvoDev before execution.",
|
|
440
|
-
].join(" ");
|
|
441
|
-
return createRuntimeResult(input, hookOutput(input.event.type, { additionalContext: context }), {
|
|
775
|
+
return createRuntimeResult(input, null, {
|
|
442
776
|
summary: "Session context prepared.",
|
|
443
777
|
});
|
|
444
778
|
}
|
|
@@ -448,6 +782,17 @@ async function handleUserPromptSubmit(input: HandleHookRuntimeInput): Promise<Ho
|
|
|
448
782
|
const sessionKey = resolveHookSessionKey(input.rawPayload);
|
|
449
783
|
const paths = resolveHookRuntimeSessionPaths({ homeDir: input.homeDir, sessionKey });
|
|
450
784
|
const contract = routeTaskContract(createHookTaskContract(input, classification));
|
|
785
|
+
const previousBinding = await readSessionBinding(input.homeDir, input.rawPayload);
|
|
786
|
+
const teamRuntimeContext =
|
|
787
|
+
previousBinding?.teamRuntimeContextDeliveredAt === undefined ||
|
|
788
|
+
previousBinding.teamRuntimeContextDeliveredAt === null
|
|
789
|
+
? await createTeamRuntimeContextForUserPrompt(input)
|
|
790
|
+
: null;
|
|
791
|
+
const shouldShowDiagnostics = input.teamRuntimeDisplayMode === "development";
|
|
792
|
+
const teamRuntimeContextDeliveredAt =
|
|
793
|
+
teamRuntimeContext !== null && shouldShowDiagnostics
|
|
794
|
+
? (input.receivedAt ?? new Date().toISOString())
|
|
795
|
+
: (previousBinding?.teamRuntimeContextDeliveredAt ?? null);
|
|
451
796
|
const binding: HookRuntimeSessionBinding = {
|
|
452
797
|
version: 1,
|
|
453
798
|
target: input.target,
|
|
@@ -456,95 +801,64 @@ async function handleUserPromptSubmit(input: HandleHookRuntimeInput): Promise<Ho
|
|
|
456
801
|
contractPath: paths.contractPath,
|
|
457
802
|
cwd: optionalPayloadString(input.rawPayload.cwd),
|
|
458
803
|
route: contract.route,
|
|
804
|
+
teamRuntimeContextDeliveredAt,
|
|
459
805
|
updatedAt: input.receivedAt ?? new Date().toISOString(),
|
|
460
806
|
};
|
|
461
807
|
|
|
462
808
|
await writeTaskContract(paths.contractPath, contract, { overwrite: true });
|
|
463
809
|
await writeJsonFile(paths.bindingPath, binding);
|
|
464
810
|
|
|
465
|
-
const
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
811
|
+
const visibleContext = createUserPromptVisibleContext({
|
|
812
|
+
classification,
|
|
813
|
+
contract,
|
|
814
|
+
contractPath: paths.contractPath,
|
|
815
|
+
});
|
|
816
|
+
let output =
|
|
817
|
+
visibleContext === null || !shouldShowDiagnostics
|
|
818
|
+
? null
|
|
819
|
+
: hookOutput(input.event.type, {
|
|
820
|
+
additionalContext: visibleContext,
|
|
821
|
+
});
|
|
822
|
+
if (teamRuntimeContext !== null && shouldShowDiagnostics) {
|
|
823
|
+
output = appendAdditionalContext(output, input.event.type, teamRuntimeContext);
|
|
824
|
+
}
|
|
473
825
|
|
|
474
|
-
return createRuntimeResult(input,
|
|
475
|
-
summary: "User prompt
|
|
826
|
+
return createRuntimeResult(input, output, {
|
|
827
|
+
summary: "User prompt observed; Task Contract prepared.",
|
|
476
828
|
stateWrites: [paths.contractPath, paths.bindingPath],
|
|
477
829
|
});
|
|
478
830
|
}
|
|
479
831
|
|
|
832
|
+
async function createTeamRuntimeContextForUserPrompt(
|
|
833
|
+
input: HandleHookRuntimeInput,
|
|
834
|
+
): Promise<string | null> {
|
|
835
|
+
const team = resolveTraceTeamContext({
|
|
836
|
+
homeDir: input.homeDir,
|
|
837
|
+
environment: input.environment,
|
|
838
|
+
payload: input.rawPayload,
|
|
839
|
+
});
|
|
840
|
+
if (team === null || team.roleId !== "main") return null;
|
|
841
|
+
|
|
842
|
+
try {
|
|
843
|
+
return await createTeamRoleRuntimeContext({
|
|
844
|
+
homeDir: input.homeDir,
|
|
845
|
+
runId: team.runId,
|
|
846
|
+
roleId: team.roleId,
|
|
847
|
+
});
|
|
848
|
+
} catch {
|
|
849
|
+
return null;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
480
853
|
async function handlePreToolUse(input: HandleHookRuntimeInput): Promise<HookRuntimeResult> {
|
|
481
|
-
const contract = await readActiveContract(input.homeDir, input.rawPayload);
|
|
482
854
|
const commandClass =
|
|
483
855
|
typeof input.event.payload.metadata.commandClass === "string"
|
|
484
856
|
? input.event.payload.metadata.commandClass
|
|
485
857
|
: 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
858
|
|
|
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
|
-
);
|
|
859
|
+
return createRuntimeResult(input, null, {
|
|
860
|
+
summary: `${input.event.type} observed (${commandClass ?? "metadata-only"}); no permission decision emitted.`,
|
|
861
|
+
});
|
|
548
862
|
}
|
|
549
863
|
|
|
550
864
|
async function handlePostToolUse(input: HandleHookRuntimeInput): Promise<HookRuntimeResult> {
|
|
@@ -574,62 +888,25 @@ async function handlePostToolUse(input: HandleHookRuntimeInput): Promise<HookRun
|
|
|
574
888
|
};
|
|
575
889
|
await writeTaskContract(binding.contractPath, nextContract, { overwrite: true });
|
|
576
890
|
|
|
577
|
-
return createRuntimeResult(
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
}),
|
|
582
|
-
{
|
|
583
|
-
summary: "Post-tool metadata evidence recorded.",
|
|
584
|
-
stateWrites: [binding.contractPath],
|
|
585
|
-
},
|
|
586
|
-
);
|
|
891
|
+
return createRuntimeResult(input, null, {
|
|
892
|
+
summary: "Post-tool metadata evidence recorded.",
|
|
893
|
+
stateWrites: [binding.contractPath],
|
|
894
|
+
});
|
|
587
895
|
}
|
|
588
896
|
|
|
589
|
-
async function
|
|
897
|
+
async function handleCompletionObservation(
|
|
898
|
+
input: HandleHookRuntimeInput,
|
|
899
|
+
): Promise<HookRuntimeResult> {
|
|
590
900
|
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
|
-
);
|
|
901
|
+
const suffix =
|
|
902
|
+
contract === null ? "without an active Task Contract" : `for Task Contract ${contract.taskId}`;
|
|
903
|
+
return createRuntimeResult(input, null, {
|
|
904
|
+
summary: `${input.event.type} observed ${suffix}; no completion control enforced.`,
|
|
905
|
+
});
|
|
620
906
|
}
|
|
621
907
|
|
|
622
908
|
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
|
-
);
|
|
909
|
+
return createRuntimeResult(input, null, { summary: "PreCompact observed." });
|
|
633
910
|
}
|
|
634
911
|
|
|
635
912
|
async function handleSessionEnd(input: HandleHookRuntimeInput): Promise<HookRuntimeResult> {
|
|
@@ -656,20 +933,16 @@ function handleAdditionalContext(
|
|
|
656
933
|
eventName: CanonicalHookEventType,
|
|
657
934
|
): Promise<HookRuntimeResult> {
|
|
658
935
|
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
|
-
),
|
|
936
|
+
createRuntimeResult(input, null, {
|
|
937
|
+
summary: `${eventName} observed as metadata-only hook context.`,
|
|
938
|
+
}),
|
|
666
939
|
);
|
|
667
940
|
}
|
|
668
941
|
|
|
669
942
|
function createRuntimeResult(
|
|
670
943
|
input: HandleHookRuntimeInput,
|
|
671
944
|
output: Record<string, unknown> | null,
|
|
672
|
-
options: { summary: string; stateWrites?: string[] },
|
|
945
|
+
options: { summary: string; stateWrites?: string[]; warnings?: string[] },
|
|
673
946
|
): HookRuntimeResult {
|
|
674
947
|
return {
|
|
675
948
|
target: input.target,
|
|
@@ -677,6 +950,7 @@ function createRuntimeResult(
|
|
|
677
950
|
enabled: true,
|
|
678
951
|
output,
|
|
679
952
|
stateWrites: options.stateWrites ?? [],
|
|
953
|
+
warnings: options.warnings ?? [],
|
|
680
954
|
summary: options.summary,
|
|
681
955
|
};
|
|
682
956
|
}
|
|
@@ -693,6 +967,68 @@ function hookOutput(
|
|
|
693
967
|
};
|
|
694
968
|
}
|
|
695
969
|
|
|
970
|
+
function appendAdditionalContext(
|
|
971
|
+
output: Record<string, unknown> | null,
|
|
972
|
+
eventName: CanonicalHookEventType,
|
|
973
|
+
context: string,
|
|
974
|
+
): Record<string, unknown> {
|
|
975
|
+
if (output === null) return hookOutput(eventName, { additionalContext: context });
|
|
976
|
+
const hookSpecificOutput = isRecord(output.hookSpecificOutput) ? output.hookSpecificOutput : {};
|
|
977
|
+
const previous =
|
|
978
|
+
typeof hookSpecificOutput.additionalContext === "string"
|
|
979
|
+
? hookSpecificOutput.additionalContext
|
|
980
|
+
: "";
|
|
981
|
+
return {
|
|
982
|
+
...output,
|
|
983
|
+
hookSpecificOutput: {
|
|
984
|
+
...hookSpecificOutput,
|
|
985
|
+
hookEventName:
|
|
986
|
+
typeof hookSpecificOutput.hookEventName === "string"
|
|
987
|
+
? hookSpecificOutput.hookEventName
|
|
988
|
+
: eventName,
|
|
989
|
+
additionalContext: previous === "" ? context : `${previous}\n\n${context}`,
|
|
990
|
+
},
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function formatTeamInboxContext(messages: TeamMessageRecord[]): string {
|
|
995
|
+
const blocks = messages.map((message) =>
|
|
996
|
+
[
|
|
997
|
+
"[EvoDev team message]",
|
|
998
|
+
"Instruction: read this queued team message at this safe point; reply through Teams MCP only when a response is needed.",
|
|
999
|
+
`from: ${message.fromRoleId}`,
|
|
1000
|
+
`to: ${message.toRoleId}`,
|
|
1001
|
+
`type: ${message.type}`,
|
|
1002
|
+
`messageId: ${message.messageId}`,
|
|
1003
|
+
`cc: ${message.ccRoleIds.join(",") || "none"}`,
|
|
1004
|
+
"",
|
|
1005
|
+
truncateTeamMessageBody(message.body),
|
|
1006
|
+
"[/EvoDev team message]",
|
|
1007
|
+
].join("\n"),
|
|
1008
|
+
);
|
|
1009
|
+
return ["EvoDev team inbox:", ...blocks].join("\n\n");
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
function truncateTeamMessageBody(value: string): string {
|
|
1013
|
+
if (value.length <= 4000) return value;
|
|
1014
|
+
return `${value.slice(0, 4000)}...[truncated:${value.length - 4000}]`;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
function createUserPromptVisibleContext(input: {
|
|
1018
|
+
classification: ReturnType<typeof classifyUserPrompt>;
|
|
1019
|
+
contract: TaskContract;
|
|
1020
|
+
contractPath: string;
|
|
1021
|
+
}): string | null {
|
|
1022
|
+
if (!input.classification.needsClarification) return null;
|
|
1023
|
+
return [
|
|
1024
|
+
"EvoDev advisory: clarification may be needed before broad changes.",
|
|
1025
|
+
`Task Contract: ${input.contractPath}`,
|
|
1026
|
+
`Suggested mode: ${input.contract.route.mode ?? "unknown"}`,
|
|
1027
|
+
`Suggested workflow: ${input.contract.route.workflowId ?? "none"}`,
|
|
1028
|
+
`Reason: ${input.contract.route.rationale}`,
|
|
1029
|
+
].join(" ");
|
|
1030
|
+
}
|
|
1031
|
+
|
|
696
1032
|
function createHookTaskContract(
|
|
697
1033
|
input: HandleHookRuntimeInput,
|
|
698
1034
|
classification: ReturnType<typeof classifyUserPrompt>,
|
|
@@ -701,14 +1037,14 @@ function createHookTaskContract(
|
|
|
701
1037
|
const targetName = formatHookTargetName(input.target);
|
|
702
1038
|
const contract = createTaskContract({
|
|
703
1039
|
title: `${targetName} hook task ${sessionKey}`,
|
|
704
|
-
summary: `${targetName} prompt classified as ${classification.kind}; raw prompt not stored.`,
|
|
1040
|
+
summary: `${targetName} prompt classified as ${classification.kind}; raw prompt is not stored by EvoDev.`,
|
|
705
1041
|
projectId: null,
|
|
706
1042
|
});
|
|
707
1043
|
|
|
708
1044
|
return {
|
|
709
1045
|
...contract,
|
|
710
1046
|
currentState: {
|
|
711
|
-
summary: `UserPromptSubmit received through ${targetName} hooks; raw prompt omitted.`,
|
|
1047
|
+
summary: `UserPromptSubmit received through ${targetName} hooks; raw prompt omitted from Task Contract.`,
|
|
712
1048
|
evidenceRefs: [],
|
|
713
1049
|
},
|
|
714
1050
|
targetState: {
|
|
@@ -811,34 +1147,14 @@ async function writeJsonFile(path: string, value: unknown): Promise<void> {
|
|
|
811
1147
|
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
812
1148
|
}
|
|
813
1149
|
|
|
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
1150
|
function optionalPayloadString(value: unknown): string | null {
|
|
839
1151
|
return typeof value === "string" && value.length > 0 ? value.slice(0, 300) : null;
|
|
840
1152
|
}
|
|
841
1153
|
|
|
1154
|
+
function safeDiagnosticId(value: string): string {
|
|
1155
|
+
return value.replace(/[^A-Za-z0-9._-]/g, "-").slice(0, 120) || "unknown";
|
|
1156
|
+
}
|
|
1157
|
+
|
|
842
1158
|
function optionalPayloadNumber(value: unknown): number | null {
|
|
843
1159
|
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
844
1160
|
}
|
|
@@ -892,9 +1208,7 @@ function extractMetadata(
|
|
|
892
1208
|
payload: Record<string, unknown>,
|
|
893
1209
|
redactions: string[],
|
|
894
1210
|
): Record<string, string | number | boolean | string[]> {
|
|
895
|
-
const metadata: Record<string, string | number | boolean | string[]> = {
|
|
896
|
-
rawContentIncluded: false,
|
|
897
|
-
};
|
|
1211
|
+
const metadata: Record<string, string | number | boolean | string[]> = {};
|
|
898
1212
|
const toolInput = isRecord(payload.tool_input)
|
|
899
1213
|
? payload.tool_input
|
|
900
1214
|
: isRecord(payload.toolInput)
|
|
@@ -932,30 +1246,12 @@ function extractMetadata(
|
|
|
932
1246
|
}
|
|
933
1247
|
|
|
934
1248
|
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
1249
|
return {
|
|
957
1250
|
action: "allow",
|
|
958
|
-
reason:
|
|
1251
|
+
reason:
|
|
1252
|
+
commandClass === undefined
|
|
1253
|
+
? "Observed metadata only; EvoDev does not stop hook execution."
|
|
1254
|
+
: `Observed ${commandClass} tool action; EvoDev does not stop hook execution.`,
|
|
959
1255
|
requiresUserConfirmation: false,
|
|
960
1256
|
};
|
|
961
1257
|
}
|
|
@@ -966,7 +1262,7 @@ function summarizeEvent(
|
|
|
966
1262
|
): string {
|
|
967
1263
|
if (type === "PreToolUse") return `Tool use requested (${metadata.commandClass ?? "unknown"}).`;
|
|
968
1264
|
if (type === "PostToolUse") return "Tool use completed with metadata-only result.";
|
|
969
|
-
if (type === "UserPromptSubmit") return "User prompt submitted
|
|
1265
|
+
if (type === "UserPromptSubmit") return "User prompt submitted.";
|
|
970
1266
|
if (type === "SubagentStop") return "Subagent stopped; transcript omitted.";
|
|
971
1267
|
return `${type} received; metadata-only dry-run.`;
|
|
972
1268
|
}
|