@evo-dev/core 0.0.1-alpha → 0.0.1-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/assets/agents/review/code-reviewer/examples.md +1 -1
  2. package/assets/agents/review/code-reviewer/prompt.md +1 -1
  3. package/assets/agents/review/code-reviewer/verification.md +1 -1
  4. package/assets/skills/coding/knowledge-distillation/SKILL.md +251 -0
  5. package/assets/skills/coding/knowledge-distillation/manifest.json +10 -0
  6. package/assets/skills/coding/knowledge-distillation/references/knowledge-distillation-methods.md +126 -0
  7. package/assets/team/agents/code-reviewer.md +48 -0
  8. package/assets/team/agents/docs-maintainer.md +51 -0
  9. package/assets/team/agents/implementation-engineer.md +51 -0
  10. package/assets/team/agents/product-scope-analyst.md +58 -0
  11. package/assets/team/agents/release-engineer.md +55 -0
  12. package/assets/team/agents/security-boundary-reviewer.md +50 -0
  13. package/assets/team/agents/solution-architect.md +51 -0
  14. package/assets/team/agents/verification-engineer.md +51 -0
  15. package/assets/team/team.md +102 -0
  16. package/assets/workflows/rd-bug-fix/WORKFLOW.json +1 -1
  17. package/assets/workflows/rd-code-review/WORKFLOW.json +1 -1
  18. package/assets/workflows/rd-docs-update/WORKFLOW.json +1 -1
  19. package/assets/workflows/rd-feature-implementation/WORKFLOW.json +1 -1
  20. package/assets/workflows/rd-refactor/WORKFLOW.json +1 -1
  21. package/assets/workflows/rd-release-readiness/WORKFLOW.json +1 -1
  22. package/assets/workflows/rd-security-boundary-review/WORKFLOW.json +2 -2
  23. package/assets/workflows/rd-test-generation/WORKFLOW.json +1 -1
  24. package/dist/config/index.js +1115 -81
  25. package/dist/index.js +13796 -2196
  26. package/dist/plugins/index.js +32 -32
  27. package/package.json +5 -1
  28. package/src/agents/index.ts +63 -292
  29. package/src/code-agent-traces/index.ts +520 -0
  30. package/src/config/index.ts +7 -0
  31. package/src/config/paths.ts +30 -0
  32. package/src/config/settings.ts +201 -0
  33. package/src/config/store.ts +152 -0
  34. package/src/daemon/index.ts +462 -40
  35. package/src/evolution/candidates/index.ts +564 -0
  36. package/src/evolution/control/index.ts +20 -0
  37. package/src/evolution/evidence/analysis.ts +533 -0
  38. package/src/evolution/evidence/index.ts +3 -0
  39. package/src/evolution/evidence/session-memory/analysis.ts +281 -0
  40. package/src/evolution/evidence/session-memory/constants.ts +9 -0
  41. package/src/evolution/evidence/session-memory/index.ts +7 -0
  42. package/src/evolution/evidence/session-memory/paths.ts +29 -0
  43. package/src/evolution/evidence/session-memory/policy.ts +39 -0
  44. package/src/evolution/evidence/session-memory/segment.ts +202 -0
  45. package/src/evolution/evidence/session-memory/sensitivity.ts +335 -0
  46. package/src/evolution/evidence/session-memory/state-machine.ts +249 -0
  47. package/src/evolution/evidence/session-memory/storage.ts +379 -0
  48. package/src/evolution/evidence/session-memory/types.ts +221 -0
  49. package/src/evolution/evidence/session-memory/updater.ts +191 -0
  50. package/src/evolution/formatters.ts +169 -0
  51. package/src/evolution/index.ts +16 -0
  52. package/src/evolution/knowledge/index.ts +5427 -0
  53. package/src/evolution/paths.ts +44 -0
  54. package/src/evolution/processor/distillation.ts +518 -0
  55. package/src/evolution/processor/index.ts +3 -0
  56. package/src/evolution/processor/process.ts +528 -0
  57. package/src/{learning → evolution/review}/index.ts +10 -14
  58. package/src/evolution/schema.ts +568 -0
  59. package/src/evolution/shared.ts +758 -0
  60. package/src/evolution/triggers/classification.ts +102 -0
  61. package/src/evolution/triggers/index.ts +295 -0
  62. package/src/hooks/index.ts +652 -376
  63. package/src/index.ts +16 -3
  64. package/src/pack/index.ts +13 -13
  65. package/src/plugins/capabilities.ts +40 -42
  66. package/src/plugins/index.ts +0 -1
  67. package/src/plugins/types.ts +4 -0
  68. package/src/projects/index.ts +453 -0
  69. package/src/protected-zones/index.ts +29 -11
  70. package/src/runtime-logs/index.ts +790 -0
  71. package/src/sync/orchestrator.ts +6 -0
  72. package/src/team/index.ts +3642 -0
  73. package/src/team/mcp.ts +405 -0
  74. package/src/team/prompts.ts +141 -0
  75. package/src/utils/errors.ts +13 -0
  76. package/src/utils/fs.ts +40 -0
  77. package/src/utils/hash.ts +9 -0
  78. package/src/utils/ids.ts +12 -0
  79. package/src/utils/index.ts +7 -0
  80. package/src/utils/parsing.ts +11 -0
  81. package/src/utils/text.ts +18 -0
  82. package/src/utils/time.ts +5 -0
  83. package/src/workflow/index.ts +6 -24
  84. package/src/project/index.ts +0 -507
  85. package/src/task/index.ts +0 -840
@@ -1,12 +1,31 @@
1
- import { createHash } from "node:crypto";
2
1
  import { mkdir, readFile, writeFile } from "node:fs/promises";
3
2
  import { dirname, join } from "node:path";
4
3
  import {
5
- type TaskContract,
6
- createTaskContract,
7
- routeTaskContract,
8
- writeTaskContract,
9
- } from "../task/index.ts";
4
+ type SessionMemoryPolicySnapshot,
5
+ updateSessionMemoryFromHook,
6
+ } from "../evolution/evidence/session-memory/index.ts";
7
+ import {
8
+ createScopedKnowledgeContextPack,
9
+ formatScopedKnowledgePromptBlock,
10
+ hasContextInjectionReceipt,
11
+ writeContextInjectionReceipt,
12
+ } from "../evolution/knowledge/index.ts";
13
+ import {
14
+ enqueueEvolutionTrigger,
15
+ resolveEvolutionTriggerDecision,
16
+ } from "../evolution/triggers/index.ts";
17
+ import { recordDiscoveredProject } from "../projects/index.ts";
18
+ import { resolveTraceSessionKey, resolveTraceTeamContext } from "../runtime-logs/index.ts";
19
+ import {
20
+ type TeamMessageRecord,
21
+ createTeamRoleRuntimeContext,
22
+ markTeamMessagesDelivered,
23
+ readPendingTeamMessagesForRole,
24
+ recordTeamAgentNativeSession,
25
+ resolveTeamRunPaths,
26
+ updateTeamAgentHookState,
27
+ } from "../team/index.ts";
28
+ import { sha256Short } from "../utils/index.ts";
10
29
 
11
30
  export const CANONICAL_HOOK_EVENT_TYPES = [
12
31
  "SessionStart",
@@ -49,15 +68,15 @@ export type CommandRiskClass =
49
68
  | "credential-sensitive"
50
69
  | "path-escaping"
51
70
  | "unknown";
52
- export type HookDecisionAction = "allow" | "warn" | "ask-user" | "block";
71
+ export type HookDecisionAction = "allow" | "warn";
53
72
 
54
73
  export interface HookSettings {
55
74
  enabled: boolean;
56
75
  targets: Record<CodeAgentHookTarget, HookTargetSettings>;
57
76
  observability: {
58
- metadataOnly: true;
59
- rawPayloadStorage: false;
60
- appendEvents: false;
77
+ metadataOnly: boolean;
78
+ rawPayloadStorage: boolean;
79
+ appendEvents: boolean;
61
80
  };
62
81
  learning: {
63
82
  emitCandidates: false;
@@ -78,7 +97,7 @@ export interface HookEventV1 {
78
97
  pluginId: string;
79
98
  agent: string;
80
99
  sessionIdHash: string | null;
81
- rawPayloadStored: false;
100
+ rawPayloadStored: boolean;
82
101
  };
83
102
  time: {
84
103
  occurredAt: string | null;
@@ -87,7 +106,7 @@ export interface HookEventV1 {
87
106
  scope: {
88
107
  taskId: string | null;
89
108
  projectId: string | null;
90
- cwdPolicy: "metadata-only";
109
+ cwdPolicy: "metadata-only" | "raw-local";
91
110
  projectContextOptedIn: false;
92
111
  };
93
112
  payload: {
@@ -95,11 +114,11 @@ export interface HookEventV1 {
95
114
  metadata: Record<string, string | number | boolean | string[]>;
96
115
  redactions: string[];
97
116
  redactionCount: number;
98
- rawContentIncluded: false;
117
+ rawContentIncluded: boolean;
99
118
  };
100
119
  policy: {
101
120
  classification: "local-private";
102
- allowedUses: Array<"observability" | "safety-check" | "workflow-suggestion">;
121
+ allowedUses: Array<"observability" | "workflow-suggestion">;
103
122
  learningAllowed: false;
104
123
  externalUploadAllowed: false;
105
124
  };
@@ -128,17 +147,15 @@ export interface HookInstallDryRunPlan {
128
147
  reason: string;
129
148
  }>;
130
149
  warnings: string[];
131
- blockers: string[];
150
+ advisories: string[];
132
151
  }
133
152
 
134
153
  export interface HookRuntimeSessionBinding {
135
154
  version: 1;
136
155
  target: CodeAgentHookTarget;
137
156
  sessionKey: string;
138
- taskId: string | null;
139
- contractPath: string | null;
140
157
  cwd: string | null;
141
- route: TaskContract["route"] | null;
158
+ teamRuntimeContextDeliveredAt?: string | null;
142
159
  updatedAt: string;
143
160
  }
144
161
 
@@ -148,6 +165,7 @@ export interface HookRuntimeResult {
148
165
  enabled: boolean;
149
166
  output: Record<string, unknown> | null;
150
167
  stateWrites: string[];
168
+ warnings: string[];
151
169
  summary: string;
152
170
  }
153
171
 
@@ -155,52 +173,76 @@ export interface HandleHookRuntimeInput {
155
173
  target: CodeAgentHookTarget;
156
174
  homeDir: string;
157
175
  settings: HookSettings;
176
+ teamRuntimeDisplayMode?: "normal" | "development";
158
177
  event: HookEventV1;
159
178
  rawPayload: Record<string, unknown>;
160
179
  receivedAt?: string;
180
+ environment?: Record<string, string | undefined>;
181
+ runtimeInjectionEnabled?: boolean;
182
+ sessionMemoryPolicy?: Partial<SessionMemoryPolicySnapshot>;
161
183
  }
162
184
 
163
185
  const DEFAULT_EVENT_SETTINGS: Record<CanonicalHookEventType, boolean> = {
164
- SessionStart: false,
165
- UserPromptSubmit: false,
166
- UserPromptExpansion: false,
167
- PreToolUse: false,
168
- PermissionRequest: false,
169
- PostToolUse: false,
170
- PostToolUseFailure: false,
171
- PostToolBatch: false,
172
- PermissionDenied: false,
173
- SubagentStart: false,
174
- Stop: false,
175
- StopFailure: false,
176
- TeammateIdle: false,
177
- SubagentStop: false,
178
- TaskCreated: false,
179
- TaskCompleted: false,
180
- PreCompact: false,
181
- PostCompact: false,
182
- SessionEnd: false,
183
- ConfigChange: false,
184
- CwdChanged: false,
185
- FileChanged: false,
186
- WorktreeCreate: false,
187
- WorktreeRemove: false,
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
+ ]);
229
+ const COMPLETION_EVENTS_WITHOUT_DEVELOPMENT_DIAGNOSTICS = new Set<CanonicalHookEventType>([
230
+ "Stop",
231
+ "SubagentStop",
232
+ "TaskCompleted",
233
+ "TeammateIdle",
234
+ ]);
193
235
 
194
236
  export function createDefaultHookSettings(): HookSettings {
195
237
  return {
196
- enabled: false,
238
+ enabled: true,
197
239
  targets: {
198
240
  claude: {
199
- enabled: false,
241
+ enabled: true,
200
242
  events: { ...DEFAULT_EVENT_SETTINGS },
201
243
  },
202
244
  codex: {
203
- enabled: false,
245
+ enabled: true,
204
246
  events: { ...DEFAULT_EVENT_SETTINGS },
205
247
  },
206
248
  },
@@ -220,6 +262,17 @@ export function parseHookSettings(value: unknown): HookSettings {
220
262
  const defaults = createDefaultHookSettings();
221
263
  if (value === undefined || value === null) return defaults;
222
264
  if (!isRecord(value)) throw new Error("Invalid hooks settings; expected object.");
265
+ const observability = isRecord(value.observability) ? value.observability : undefined;
266
+ optionalBoolean(
267
+ observability?.metadataOnly,
268
+ defaults.observability.metadataOnly,
269
+ "hooks.observability.metadataOnly",
270
+ );
271
+ optionalBoolean(
272
+ observability?.rawPayloadStorage,
273
+ defaults.observability.rawPayloadStorage,
274
+ "hooks.observability.rawPayloadStorage",
275
+ );
223
276
 
224
277
  return {
225
278
  enabled: optionalBoolean(value.enabled, defaults.enabled, "hooks.enabled"),
@@ -230,7 +283,11 @@ export function parseHookSettings(value: unknown): HookSettings {
230
283
  observability: {
231
284
  metadataOnly: true,
232
285
  rawPayloadStorage: false,
233
- appendEvents: false,
286
+ appendEvents: optionalBoolean(
287
+ observability?.appendEvents,
288
+ defaults.observability.appendEvents,
289
+ "hooks.observability.appendEvents",
290
+ ),
234
291
  },
235
292
  learning: {
236
293
  emitCandidates: false,
@@ -281,7 +338,7 @@ export function normalizeHookEvent(input: NormalizeHookEventInput): HookEventV1
281
338
  },
282
339
  policy: {
283
340
  classification: "local-private",
284
- allowedUses: ["observability", "safety-check", "workflow-suggestion"],
341
+ allowedUses: ["observability", "workflow-suggestion"],
285
342
  learningAllowed: false,
286
343
  externalUploadAllowed: false,
287
344
  },
@@ -318,7 +375,8 @@ export function formatHookInstallDryRun(plan: HookInstallDryRunPlan): string {
318
375
  (eventType) => ` - ${eventType}: ${plan.settings.targets[plan.target].events[eventType]}`,
319
376
  ),
320
377
  "Boundaries:",
321
- " - observability: metadata-only, appendEvents=false, rawPayloadStorage=false",
378
+ " - execution events: metadata-only; raw payload/prompt/output/source storage=false",
379
+ " - historical raw trace files may remain for read-only migration compatibility",
322
380
  " - learning: emitCandidates=false, writeMemory=false",
323
381
  " - external upload: false",
324
382
  " - protected project writes: .claude/.codex/CLAUDE.md/AGENTS.md not targeted",
@@ -331,10 +389,10 @@ export function formatHookInstallDryRun(plan: HookInstallDryRunPlan): string {
331
389
  ...(plan.warnings.length === 0
332
390
  ? [" - none"]
333
391
  : plan.warnings.map((warning) => ` - ${warning}`)),
334
- "Blockers:",
335
- ...(plan.blockers.length === 0
392
+ "Advisories:",
393
+ ...(plan.advisories.length === 0
336
394
  ? [" - none"]
337
- : plan.blockers.map((blocker) => ` - ${blocker}`)),
395
+ : plan.advisories.map((advisory) => ` - ${advisory}`)),
338
396
  ].join("\n");
339
397
  }
340
398
 
@@ -364,12 +422,11 @@ export function formatHookEventDryRun(event: HookEventV1): string {
364
422
  export function resolveHookRuntimeSessionPaths(input: {
365
423
  homeDir: string;
366
424
  sessionKey: string;
367
- }): { sessionDir: string; bindingPath: string; contractPath: string } {
425
+ }): { sessionDir: string; bindingPath: string } {
368
426
  const sessionDir = join(input.homeDir, ".evodev", "STATE", "hooks", "sessions", input.sessionKey);
369
427
  return {
370
428
  sessionDir,
371
429
  bindingPath: join(sessionDir, "binding.json"),
372
- contractPath: join(sessionDir, "contract.json"),
373
430
  };
374
431
  }
375
432
 
@@ -382,35 +439,76 @@ export async function handleHookRuntime(input: HandleHookRuntimeInput): Promise<
382
439
  enabled,
383
440
  output: null,
384
441
  stateWrites: [],
442
+ warnings: [],
385
443
  summary: `Hook ${input.event.type} ignored because EvoDev hooks are disabled.`,
386
444
  };
387
445
  }
388
446
 
389
- if (input.event.type === "SessionStart") return handleSessionStart(input);
390
- if (input.event.type === "UserPromptSubmit") return handleUserPromptSubmit(input);
391
- if (input.event.type === "PreToolUse") return handlePreToolUse(input);
392
- if (input.event.type === "PostToolUse" || input.event.type === "PostToolUseFailure") {
393
- return handlePostToolUse(input);
447
+ if (isActiveClaudeStopHook(input)) {
448
+ return createRuntimeResult(input, null, {
449
+ summary: `${input.event.type} re-entry allowed to complete without hook output.`,
450
+ });
394
451
  }
395
- if (input.event.type === "PostToolBatch") return handleAdditionalContext(input, "PostToolBatch");
396
- if (
452
+
453
+ const diagnostics = await recordTeamNativeSessionFromHook(input);
454
+ const projectDiagnostics = await recordDiscoveredProjectFromHook(input);
455
+ let result: HookRuntimeResult | undefined;
456
+ if (input.event.type === "SessionStart") result = await handleSessionStart(input);
457
+ else if (input.event.type === "UserPromptSubmit") result = await handleUserPromptSubmit(input);
458
+ else if (input.event.type === "PreToolUse") result = await handlePreToolUse(input);
459
+ if (input.event.type === "PostToolUse" || input.event.type === "PostToolUseFailure") {
460
+ result = await handlePostToolUse(input);
461
+ } else if (input.event.type === "PostToolBatch") {
462
+ result = await handleAdditionalContext(input, "PostToolBatch");
463
+ } else if (
397
464
  input.event.type === "SubagentStart" ||
398
465
  input.event.type === "TaskCreated" ||
399
466
  input.event.type === "PermissionRequest"
400
467
  ) {
401
- return handlePreToolUse(input);
402
- }
403
- if (
468
+ result = await handlePreToolUse(input);
469
+ } else if (
404
470
  input.event.type === "Stop" ||
405
471
  input.event.type === "SubagentStop" ||
406
472
  input.event.type === "TaskCompleted" ||
407
473
  input.event.type === "TeammateIdle"
408
474
  ) {
409
- return handleCompletionGate(input);
475
+ result = await handleCompletionObservation(input);
476
+ } else if (input.event.type === "PreCompact") {
477
+ result = await handlePreCompact(input);
478
+ } else if (input.event.type === "SessionEnd") {
479
+ result = await handleSessionEnd(input);
480
+ } else if (result === undefined) {
481
+ result = await handleAdditionalContext(input, input.event.type);
410
482
  }
411
- if (input.event.type === "PreCompact") return handlePreCompact(input);
412
- if (input.event.type === "SessionEnd") return handleSessionEnd(input);
413
- return handleAdditionalContext(input, input.event.type);
483
+ const sessionMemoryDiagnostics = await recordSessionMemoryFromHook(input);
484
+ const evolutionDiagnostics =
485
+ sessionMemoryDiagnostics.queuedSegmentTriggerId !== null ||
486
+ shouldSuppressLegacyEvolutionTrigger(input.event.type)
487
+ ? { stateWrites: [], warnings: [] }
488
+ : await recordEvolutionTriggerFromHook(input);
489
+ const messageDiagnostics = await deliverPendingTeamMessagesFromHook(input, result);
490
+ const scopedContextDiagnostics = await injectScopedKnowledgeContextFromHook(input, result);
491
+ const teamStateDiagnostics = await recordTeamAgentHookStateFromHook(input);
492
+ const completed = appendRuntimeDiagnostics(
493
+ appendRuntimeDiagnostics(
494
+ appendRuntimeDiagnostics(
495
+ appendRuntimeDiagnostics(
496
+ appendRuntimeDiagnostics(
497
+ appendRuntimeDiagnostics(
498
+ appendRuntimeDiagnostics(result, diagnostics),
499
+ projectDiagnostics,
500
+ ),
501
+ sessionMemoryDiagnostics,
502
+ ),
503
+ evolutionDiagnostics,
504
+ ),
505
+ messageDiagnostics,
506
+ ),
507
+ scopedContextDiagnostics,
508
+ ),
509
+ teamStateDiagnostics,
510
+ );
511
+ return appendDevelopmentHookDiagnostics(input, completed);
414
512
  }
415
513
 
416
514
  export function formatHookRuntimeOutput(result: HookRuntimeResult): string {
@@ -429,207 +527,427 @@ function isHookEventEnabled(
429
527
  );
430
528
  }
431
529
 
530
+ interface HookRuntimeDiagnostics {
531
+ stateWrites: string[];
532
+ warnings: string[];
533
+ }
534
+
535
+ async function recordDiscoveredProjectFromHook(
536
+ input: HandleHookRuntimeInput,
537
+ ): Promise<HookRuntimeDiagnostics> {
538
+ if (
539
+ input.event.type !== "SessionStart" &&
540
+ input.event.type !== "UserPromptSubmit" &&
541
+ input.event.type !== "CwdChanged"
542
+ ) {
543
+ return { stateWrites: [], warnings: [] };
544
+ }
545
+ const cwd = optionalPayloadString(input.rawPayload.cwd);
546
+ if (cwd === null) return { stateWrites: [], warnings: [] };
547
+
548
+ try {
549
+ const result = await recordDiscoveredProject({
550
+ homeDir: input.homeDir,
551
+ cwd,
552
+ target: input.target,
553
+ sessionKey: resolveTraceSessionKey(input.rawPayload),
554
+ now:
555
+ input.receivedAt === undefined || input.receivedAt === "dry-run"
556
+ ? undefined
557
+ : input.receivedAt,
558
+ });
559
+ return result === null
560
+ ? { stateWrites: [], warnings: [] }
561
+ : { stateWrites: [result.path], warnings: [] };
562
+ } catch {
563
+ return {
564
+ stateWrites: [],
565
+ warnings: ["Local project discovery could not be updated at this hook safe point."],
566
+ };
567
+ }
568
+ }
569
+
570
+ async function recordTeamNativeSessionFromHook(
571
+ input: HandleHookRuntimeInput,
572
+ ): Promise<HookRuntimeDiagnostics> {
573
+ const environment = input.environment ?? {};
574
+ const runId = optionalPayloadString(environment.EVODEV_TEAM_RUN_ID);
575
+ const roleId = optionalPayloadString(environment.EVODEV_TEAM_ROLE_ID);
576
+ const sessionId = optionalPayloadString(
577
+ input.rawPayload.session_id ?? input.rawPayload.sessionId,
578
+ );
579
+ if (runId === null || roleId === null || sessionId === null) {
580
+ return { stateWrites: [], warnings: [] };
581
+ }
582
+
583
+ try {
584
+ await recordTeamAgentNativeSession({
585
+ homeDir: input.homeDir,
586
+ runId,
587
+ roleId,
588
+ sessionId,
589
+ now:
590
+ input.receivedAt === undefined || input.receivedAt === "dry-run"
591
+ ? undefined
592
+ : new Date(input.receivedAt),
593
+ });
594
+ return {
595
+ stateWrites: [resolveTeamRunPaths(input.homeDir).agentPath(runId, roleId)],
596
+ warnings: [],
597
+ };
598
+ } catch {
599
+ return {
600
+ stateWrites: [],
601
+ warnings: [
602
+ `Team native session metadata could not be recorded for run ${safeDiagnosticId(runId)} role ${safeDiagnosticId(roleId)}.`,
603
+ ],
604
+ };
605
+ }
606
+ }
607
+
608
+ async function recordEvolutionTriggerFromHook(
609
+ input: HandleHookRuntimeInput,
610
+ ): Promise<HookRuntimeDiagnostics> {
611
+ const decision = resolveEvolutionTriggerDecision({
612
+ eventType: input.event.type,
613
+ summary: input.event.payload.summary,
614
+ });
615
+ if (!decision.shouldQueue) return { stateWrites: [], warnings: [] };
616
+
617
+ const team = resolveTraceTeamContext({
618
+ homeDir: input.homeDir,
619
+ environment: input.environment,
620
+ payload: input.rawPayload,
621
+ });
622
+ if (team === null) {
623
+ return {
624
+ stateWrites: [],
625
+ warnings:
626
+ decision.strength === "strong"
627
+ ? [
628
+ `Evolution trigger ${input.event.type} was not queued because team context is missing.`,
629
+ ]
630
+ : [],
631
+ };
632
+ }
633
+
634
+ try {
635
+ const trigger = await enqueueEvolutionTrigger({
636
+ homeDir: input.homeDir,
637
+ projectKey: team.projectKey,
638
+ runId: team.runId,
639
+ roleId: team.roleId,
640
+ taskId: input.event.scope.taskId,
641
+ eventType: input.event.type,
642
+ eventId: input.event.eventId,
643
+ summary: input.event.payload.summary,
644
+ now:
645
+ input.receivedAt === undefined || input.receivedAt === "dry-run"
646
+ ? undefined
647
+ : input.receivedAt,
648
+ });
649
+ if (trigger === null) return { stateWrites: [], warnings: [] };
650
+ return {
651
+ stateWrites: [
652
+ join(
653
+ input.homeDir,
654
+ ".evodev",
655
+ "state",
656
+ "evolution",
657
+ trigger.projectKey,
658
+ trigger.runId,
659
+ "triggers",
660
+ `${trigger.id}.json`,
661
+ ),
662
+ ],
663
+ warnings: [],
664
+ };
665
+ } catch {
666
+ return {
667
+ stateWrites: [],
668
+ warnings: [`Evolution trigger ${input.event.type} could not be queued.`],
669
+ };
670
+ }
671
+ }
672
+
673
+ async function recordSessionMemoryFromHook(
674
+ input: HandleHookRuntimeInput,
675
+ ): Promise<HookRuntimeDiagnostics & { queuedSegmentTriggerId: string | null }> {
676
+ try {
677
+ const result = await updateSessionMemoryFromHook({
678
+ homeDir: input.homeDir,
679
+ target: input.target,
680
+ event: input.event,
681
+ rawPayload: input.rawPayload,
682
+ environment: input.environment,
683
+ receivedAt: input.receivedAt,
684
+ policy: input.sessionMemoryPolicy,
685
+ });
686
+ return {
687
+ stateWrites: result.stateWrites,
688
+ warnings: result.warnings,
689
+ queuedSegmentTriggerId: result.queuedSegmentTriggerId,
690
+ };
691
+ } catch {
692
+ return {
693
+ stateWrites: [],
694
+ warnings: ["Session Memory state could not be updated at this hook safe point."],
695
+ queuedSegmentTriggerId: null,
696
+ };
697
+ }
698
+ }
699
+
700
+ function shouldSuppressLegacyEvolutionTrigger(eventType: CanonicalHookEventType): boolean {
701
+ return eventType === "Stop" || eventType === "SubagentStop" || eventType === "SessionEnd";
702
+ }
703
+
704
+ async function deliverPendingTeamMessagesFromHook(
705
+ input: HandleHookRuntimeInput,
706
+ result: HookRuntimeResult | undefined,
707
+ ): Promise<HookRuntimeDiagnostics> {
708
+ if (result === undefined || !canDeliverTeamMessagesFromHook(input.target, input.event.type)) {
709
+ return { stateWrites: [], warnings: [] };
710
+ }
711
+ const team = resolveTraceTeamContext({
712
+ homeDir: input.homeDir,
713
+ environment: input.environment,
714
+ payload: input.rawPayload,
715
+ });
716
+ if (team === null) return { stateWrites: [], warnings: [] };
717
+
718
+ try {
719
+ const messages = await readPendingTeamMessagesForRole({
720
+ homeDir: input.homeDir,
721
+ runId: team.runId,
722
+ roleId: team.roleId,
723
+ limit: 5,
724
+ });
725
+ if (messages.length === 0) return { stateWrites: [], warnings: [] };
726
+ result.output = appendAdditionalContext(
727
+ result.output,
728
+ input.event.type,
729
+ formatTeamInboxContext(messages),
730
+ );
731
+ await markTeamMessagesDelivered({
732
+ homeDir: input.homeDir,
733
+ runId: team.runId,
734
+ roleId: team.roleId,
735
+ messageIds: messages.map((message) => message.messageId),
736
+ now:
737
+ input.receivedAt === undefined || input.receivedAt === "dry-run"
738
+ ? undefined
739
+ : new Date(input.receivedAt),
740
+ });
741
+ const teamPaths = resolveTeamRunPaths(input.homeDir);
742
+ return {
743
+ stateWrites: [teamPaths.eventsPath(team.runId), teamPaths.messageListPath(team.runId)],
744
+ warnings: [],
745
+ };
746
+ } catch {
747
+ return {
748
+ stateWrites: [],
749
+ warnings: [
750
+ `Team inbox messages could not be delivered for role ${safeDiagnosticId(team.roleId)}.`,
751
+ ],
752
+ };
753
+ }
754
+ }
755
+
756
+ async function injectScopedKnowledgeContextFromHook(
757
+ input: HandleHookRuntimeInput,
758
+ result: HookRuntimeResult | undefined,
759
+ ): Promise<HookRuntimeDiagnostics> {
760
+ if (result === undefined || !canDeliverTeamMessagesFromHook(input.target, input.event.type)) {
761
+ return { stateWrites: [], warnings: [] };
762
+ }
763
+ const team = resolveTraceTeamContext({
764
+ homeDir: input.homeDir,
765
+ environment: input.environment,
766
+ payload: input.rawPayload,
767
+ });
768
+ if (team === null || team.roleId !== "main") return { stateWrites: [], warnings: [] };
769
+
770
+ try {
771
+ if (input.runtimeInjectionEnabled === false) return { stateWrites: [], warnings: [] };
772
+ const pack = await createScopedKnowledgeContextPack({
773
+ homeDir: input.homeDir,
774
+ projectKey: team.projectKey,
775
+ roleId: team.roleId,
776
+ });
777
+ if (pack === null) return { stateWrites: [], warnings: [] };
778
+ const sessionKey = resolveHookSessionKey(input.rawPayload);
779
+ if (
780
+ await hasContextInjectionReceipt({
781
+ homeDir: input.homeDir,
782
+ sessionKey,
783
+ contextPackId: pack.id,
784
+ })
785
+ ) {
786
+ return { stateWrites: [], warnings: [] };
787
+ }
788
+ const receipt = await writeContextInjectionReceipt({
789
+ homeDir: input.homeDir,
790
+ sessionKey,
791
+ pack,
792
+ trigger: "hook-safe-point",
793
+ hookEventId: input.event.eventId,
794
+ injectedAt: input.receivedAt ?? new Date().toISOString(),
795
+ });
796
+ result.output = appendAdditionalContext(
797
+ result.output,
798
+ input.event.type,
799
+ formatScopedKnowledgePromptBlock(pack),
800
+ );
801
+ return { stateWrites: [receipt.path], warnings: [] };
802
+ } catch {
803
+ return {
804
+ stateWrites: [],
805
+ warnings: ["Scoped knowledge context could not be injected at this hook safe point."],
806
+ };
807
+ }
808
+ }
809
+
810
+ function canDeliverTeamMessagesFromHook(
811
+ target: CodeAgentHookTarget,
812
+ eventType: CanonicalHookEventType,
813
+ ): boolean {
814
+ if (!TEAM_MESSAGE_DELIVERY_EVENTS.has(eventType)) return false;
815
+ return target !== "codex" || !CODEX_STOP_EVENTS_WITHOUT_ADDITIONAL_CONTEXT.has(eventType);
816
+ }
817
+
818
+ async function recordTeamAgentHookStateFromHook(
819
+ input: HandleHookRuntimeInput,
820
+ ): Promise<HookRuntimeDiagnostics> {
821
+ const team = resolveTraceTeamContext({
822
+ homeDir: input.homeDir,
823
+ environment: input.environment,
824
+ payload: input.rawPayload,
825
+ });
826
+ if (team === null) return { stateWrites: [], warnings: [] };
827
+
828
+ try {
829
+ const result = await updateTeamAgentHookState({
830
+ homeDir: input.homeDir,
831
+ runId: team.runId,
832
+ roleId: team.roleId,
833
+ hookEvent: input.event.type,
834
+ now:
835
+ input.receivedAt === undefined || input.receivedAt === "dry-run"
836
+ ? undefined
837
+ : new Date(input.receivedAt),
838
+ });
839
+ return { stateWrites: [result.agentPath, result.statusPath], warnings: [] };
840
+ } catch {
841
+ return {
842
+ stateWrites: [],
843
+ warnings: [
844
+ `Team agent state could not be updated for hook role ${safeDiagnosticId(team.roleId)}.`,
845
+ ],
846
+ };
847
+ }
848
+ }
849
+
850
+ function appendRuntimeDiagnostics(
851
+ result: HookRuntimeResult | undefined,
852
+ diagnostics: HookRuntimeDiagnostics,
853
+ ): HookRuntimeResult {
854
+ if (result === undefined) {
855
+ throw new Error("Hook runtime did not produce a result.");
856
+ }
857
+ return {
858
+ ...result,
859
+ stateWrites: [...new Set([...diagnostics.stateWrites, ...result.stateWrites])],
860
+ warnings: [...new Set([...diagnostics.warnings, ...result.warnings])],
861
+ };
862
+ }
863
+
432
864
  async function handleSessionStart(input: HandleHookRuntimeInput): Promise<HookRuntimeResult> {
433
- const binding = await readSessionBinding(input.homeDir, input.rawPayload);
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 }), {
865
+ return createRuntimeResult(input, null, {
442
866
  summary: "Session context prepared.",
443
867
  });
444
868
  }
445
869
 
446
870
  async function handleUserPromptSubmit(input: HandleHookRuntimeInput): Promise<HookRuntimeResult> {
447
- const classification = classifyUserPrompt(input.rawPayload.prompt ?? input.rawPayload.userPrompt);
448
871
  const sessionKey = resolveHookSessionKey(input.rawPayload);
449
872
  const paths = resolveHookRuntimeSessionPaths({ homeDir: input.homeDir, sessionKey });
450
- const contract = routeTaskContract(createHookTaskContract(input, classification));
873
+ const previousBinding = await readSessionBinding(input.homeDir, input.rawPayload);
874
+ const teamRuntimeContext =
875
+ previousBinding?.teamRuntimeContextDeliveredAt === undefined ||
876
+ previousBinding.teamRuntimeContextDeliveredAt === null
877
+ ? await createTeamRuntimeContextForUserPrompt(input)
878
+ : null;
879
+ const teamRuntimeContextDeliveredAt =
880
+ teamRuntimeContext !== null
881
+ ? (input.receivedAt ?? new Date().toISOString())
882
+ : (previousBinding?.teamRuntimeContextDeliveredAt ?? null);
451
883
  const binding: HookRuntimeSessionBinding = {
452
884
  version: 1,
453
885
  target: input.target,
454
886
  sessionKey,
455
- taskId: contract.taskId,
456
- contractPath: paths.contractPath,
457
887
  cwd: optionalPayloadString(input.rawPayload.cwd),
458
- route: contract.route,
888
+ teamRuntimeContextDeliveredAt,
459
889
  updatedAt: input.receivedAt ?? new Date().toISOString(),
460
890
  };
461
891
 
462
- await writeTaskContract(paths.contractPath, contract, { overwrite: true });
463
892
  await writeJsonFile(paths.bindingPath, binding);
464
893
 
465
- const context = [
466
- `EvoDev routed this request before ${formatHookTargetName(input.target)} execution.`,
467
- `Task Contract: ${paths.contractPath}`,
468
- `Mode: ${contract.route.mode ?? "unknown"}`,
469
- `Workflow: ${contract.route.workflowId ?? "none"}`,
470
- `Reason: ${contract.route.rationale}`,
471
- "Follow the contract scope, anti-criteria, and verification plan before finishing.",
472
- ].join(" ");
473
-
474
- return createRuntimeResult(input, hookOutput(input.event.type, { additionalContext: context }), {
475
- summary: "User prompt routed through Task Contract.",
476
- stateWrites: [paths.contractPath, paths.bindingPath],
894
+ let output: Record<string, unknown> | null = null;
895
+ if (teamRuntimeContext !== null) {
896
+ output = appendAdditionalContext(output, input.event.type, teamRuntimeContext);
897
+ }
898
+
899
+ return createRuntimeResult(input, output, {
900
+ summary: "User prompt observed; session binding updated.",
901
+ stateWrites: [paths.bindingPath],
477
902
  });
478
903
  }
479
904
 
905
+ async function createTeamRuntimeContextForUserPrompt(
906
+ input: HandleHookRuntimeInput,
907
+ ): Promise<string | null> {
908
+ const team = resolveTraceTeamContext({
909
+ homeDir: input.homeDir,
910
+ environment: input.environment,
911
+ payload: input.rawPayload,
912
+ });
913
+ if (team === null || team.roleId !== "main") return null;
914
+
915
+ try {
916
+ return await createTeamRoleRuntimeContext({
917
+ homeDir: input.homeDir,
918
+ runId: team.runId,
919
+ roleId: team.roleId,
920
+ });
921
+ } catch {
922
+ return null;
923
+ }
924
+ }
925
+
480
926
  async function handlePreToolUse(input: HandleHookRuntimeInput): Promise<HookRuntimeResult> {
481
- const contract = await readActiveContract(input.homeDir, input.rawPayload);
482
927
  const commandClass =
483
928
  typeof input.event.payload.metadata.commandClass === "string"
484
929
  ? input.event.payload.metadata.commandClass
485
930
  : 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
931
 
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
-
540
- return createRuntimeResult(
541
- input,
542
- hookOutput(input.event.type, {
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
- );
932
+ return createRuntimeResult(input, null, {
933
+ summary: `${input.event.type} observed (${commandClass ?? "metadata-only"}); no permission decision emitted.`,
934
+ });
548
935
  }
549
936
 
550
937
  async function handlePostToolUse(input: HandleHookRuntimeInput): Promise<HookRuntimeResult> {
551
- const contract = await readActiveContract(input.homeDir, input.rawPayload);
552
- const binding = await readSessionBinding(input.homeDir, input.rawPayload);
553
- if (contract === null || binding?.contractPath === null || binding?.contractPath === undefined) {
554
- return handleAdditionalContext(input, input.event.type);
555
- }
556
-
557
- const status = optionalPayloadNumber(input.rawPayload.exit_code ?? input.rawPayload.exitCode);
558
- const nextContract: TaskContract = {
559
- ...contract,
560
- evidence: {
561
- metadataOnly: true,
562
- items: [
563
- ...contract.evidence.items,
564
- {
565
- type: "command-result",
566
- id: input.event.eventId,
567
- status: status === 0 ? "pass" : status === null ? "unknown" : "fail",
568
- summary: input.event.payload.summary,
569
- rawOutputStored: false,
570
- sourceContentStored: false,
571
- },
572
- ],
573
- },
574
- };
575
- await writeTaskContract(binding.contractPath, nextContract, { overwrite: true });
576
-
577
- return createRuntimeResult(
578
- input,
579
- hookOutput(input.event.type, {
580
- additionalContext: `EvoDev recorded metadata-only evidence for Task Contract ${contract.taskId}. Raw output was not stored.`,
581
- }),
582
- {
583
- summary: "Post-tool metadata evidence recorded.",
584
- stateWrites: [binding.contractPath],
585
- },
586
- );
938
+ return handleAdditionalContext(input, input.event.type);
587
939
  }
588
940
 
589
- async function handleCompletionGate(input: HandleHookRuntimeInput): Promise<HookRuntimeResult> {
590
- const contract = await readActiveContract(input.homeDir, input.rawPayload);
591
- if (contract === null) return handleAdditionalContext(input, input.event.type);
592
-
593
- const stopHookActive = input.rawPayload.stop_hook_active === true;
594
- if (
595
- !stopHookActive &&
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
- );
941
+ async function handleCompletionObservation(
942
+ input: HandleHookRuntimeInput,
943
+ ): Promise<HookRuntimeResult> {
944
+ return createRuntimeResult(input, null, {
945
+ summary: `${input.event.type} observed as metadata-only hook context.`,
946
+ });
620
947
  }
621
948
 
622
949
  async function handlePreCompact(input: HandleHookRuntimeInput): Promise<HookRuntimeResult> {
623
- const binding = await readSessionBinding(input.homeDir, input.rawPayload);
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
- );
950
+ return createRuntimeResult(input, null, { summary: "PreCompact observed." });
633
951
  }
634
952
 
635
953
  async function handleSessionEnd(input: HandleHookRuntimeInput): Promise<HookRuntimeResult> {
@@ -656,20 +974,16 @@ function handleAdditionalContext(
656
974
  eventName: CanonicalHookEventType,
657
975
  ): Promise<HookRuntimeResult> {
658
976
  return Promise.resolve(
659
- createRuntimeResult(
660
- input,
661
- hookOutput(eventName, {
662
- additionalContext: `EvoDev processed ${eventName} as metadata-only hook context.`,
663
- }),
664
- { summary: `${eventName} processed as metadata-only context.` },
665
- ),
977
+ createRuntimeResult(input, null, {
978
+ summary: `${eventName} observed as metadata-only hook context.`,
979
+ }),
666
980
  );
667
981
  }
668
982
 
669
983
  function createRuntimeResult(
670
984
  input: HandleHookRuntimeInput,
671
985
  output: Record<string, unknown> | null,
672
- options: { summary: string; stateWrites?: string[] },
986
+ options: { summary: string; stateWrites?: string[]; warnings?: string[] },
673
987
  ): HookRuntimeResult {
674
988
  return {
675
989
  target: input.target,
@@ -677,6 +991,7 @@ function createRuntimeResult(
677
991
  enabled: true,
678
992
  output,
679
993
  stateWrites: options.stateWrites ?? [],
994
+ warnings: options.warnings ?? [],
680
995
  summary: options.summary,
681
996
  };
682
997
  }
@@ -693,96 +1008,101 @@ function hookOutput(
693
1008
  };
694
1009
  }
695
1010
 
696
- function createHookTaskContract(
697
- input: HandleHookRuntimeInput,
698
- classification: ReturnType<typeof classifyUserPrompt>,
699
- ): TaskContract {
700
- const sessionKey = resolveHookSessionKey(input.rawPayload);
701
- const targetName = formatHookTargetName(input.target);
702
- const contract = createTaskContract({
703
- title: `${targetName} hook task ${sessionKey}`,
704
- summary: `${targetName} prompt classified as ${classification.kind}; raw prompt not stored.`,
705
- projectId: null,
706
- });
707
-
1011
+ function appendAdditionalContext(
1012
+ output: Record<string, unknown> | null,
1013
+ eventName: CanonicalHookEventType,
1014
+ context: string,
1015
+ ): Record<string, unknown> {
1016
+ if (output === null) return hookOutput(eventName, { additionalContext: context });
1017
+ const hookSpecificOutput = isRecord(output.hookSpecificOutput) ? output.hookSpecificOutput : {};
1018
+ const previous =
1019
+ typeof hookSpecificOutput.additionalContext === "string"
1020
+ ? hookSpecificOutput.additionalContext
1021
+ : "";
708
1022
  return {
709
- ...contract,
710
- currentState: {
711
- summary: `UserPromptSubmit received through ${targetName} hooks; raw prompt omitted.`,
712
- evidenceRefs: [],
713
- },
714
- targetState: {
715
- summary: `Complete the ${classification.kind} task through EvoDev-controlled workflow.`,
716
- nonGoals: ["Do not store raw prompts, transcripts, source content, secrets, or raw output."],
717
- constraints: [
718
- `prompt-kind:${classification.kind}`,
719
- ...classification.riskTerms.map((term) => `risk:${term}`),
720
- ],
721
- },
722
- scope: {
723
- ...contract.scope,
724
- requiresUserConfirmation: classification.riskTerms,
725
- },
726
- context: {
727
- ...contract.context,
728
- assumptions: [
729
- `hook-session:${sessionKey}`,
730
- `cwd:${optionalPayloadString(input.rawPayload.cwd) ?? "unknown"}`,
731
- ],
732
- openQuestions: classification.needsClarification
733
- ? ["User request may need clarification before broad changes."]
734
- : [],
1023
+ ...output,
1024
+ hookSpecificOutput: {
1025
+ ...hookSpecificOutput,
1026
+ hookEventName:
1027
+ typeof hookSpecificOutput.hookEventName === "string"
1028
+ ? hookSpecificOutput.hookEventName
1029
+ : eventName,
1030
+ additionalContext: previous === "" ? context : `${previous}\n\n${context}`,
735
1031
  },
736
1032
  };
737
1033
  }
738
1034
 
739
- function formatHookTargetName(target: CodeAgentHookTarget): string {
740
- return target === "codex" ? "Codex" : "Claude";
741
- }
742
-
743
- function classifyUserPrompt(value: unknown): {
744
- kind: string;
745
- riskTerms: string[];
746
- needsClarification: boolean;
747
- } {
748
- const text = typeof value === "string" ? value.toLowerCase() : "";
749
- const riskTerms = [
750
- "security",
751
- "release",
752
- "publish",
753
- "hook",
754
- "memory",
755
- "learning",
756
- "secret",
757
- "privacy",
758
- ].filter((term) => text.includes(term));
759
- let kind = "feature";
760
- if (/\bbug|fix|error|failed|failure\b/.test(text)) kind = "bugfix";
761
- if (/\brefactor|migration|migrate\b/.test(text)) kind = "refactor";
762
- if (/\breview|audit\b/.test(text)) kind = "review";
763
- if (/\btest|coverage\b/.test(text)) kind = "test";
764
- if (/\bdoc|readme|guide\b/.test(text)) kind = "docs";
765
- if (riskTerms.includes("security") || riskTerms.includes("privacy")) kind = "security";
766
- if (riskTerms.includes("release") || riskTerms.includes("publish")) kind = "release";
1035
+ function appendDevelopmentHookDiagnostics(
1036
+ input: HandleHookRuntimeInput,
1037
+ result: HookRuntimeResult,
1038
+ ): HookRuntimeResult {
1039
+ if (
1040
+ input.teamRuntimeDisplayMode !== "development" ||
1041
+ COMPLETION_EVENTS_WITHOUT_DEVELOPMENT_DIAGNOSTICS.has(input.event.type) ||
1042
+ !canDeliverTeamMessagesFromHook(input.target, input.event.type)
1043
+ ) {
1044
+ return result;
1045
+ }
767
1046
  return {
768
- kind,
769
- riskTerms,
770
- needsClarification: text.trim().length < 12 || /\bmaybe|unclear|not sure\b/.test(text),
1047
+ ...result,
1048
+ output: appendAdditionalContext(
1049
+ result.output,
1050
+ input.event.type,
1051
+ formatDevelopmentHookDiagnostics(input),
1052
+ ),
771
1053
  };
772
1054
  }
773
1055
 
774
- async function readActiveContract(
775
- homeDir: string,
776
- payload: Record<string, unknown>,
777
- ): Promise<TaskContract | null> {
778
- const binding = await readSessionBinding(homeDir, payload);
779
- if (binding?.contractPath === null || binding?.contractPath === undefined) return null;
780
- try {
781
- return JSON.parse(await readFile(binding.contractPath, "utf8")) as TaskContract;
782
- } catch (error) {
783
- if (isNotFoundError(error)) return null;
784
- throw error;
785
- }
1056
+ function isActiveClaudeStopHook(input: HandleHookRuntimeInput): boolean {
1057
+ return (
1058
+ input.target === "claude" &&
1059
+ (input.event.type === "Stop" || input.event.type === "SubagentStop") &&
1060
+ input.rawPayload.stop_hook_active === true
1061
+ );
1062
+ }
1063
+
1064
+ function formatDevelopmentHookDiagnostics(input: HandleHookRuntimeInput): string {
1065
+ const metadata = Object.entries(input.event.payload.metadata);
1066
+ const inputFields = Object.keys(input.rawPayload)
1067
+ .map((field) => field.replace(/[^A-Za-z0-9._-]/g, "-").slice(0, 80))
1068
+ .filter((field) => field.length > 0)
1069
+ .slice(0, 30);
1070
+ return [
1071
+ "EvoDev hook development diagnostics",
1072
+ `Target: ${input.target}`,
1073
+ `Event: ${input.event.type}`,
1074
+ `Summary: ${input.event.payload.summary}`,
1075
+ `Input fields: ${inputFields.join(", ") || "none"}`,
1076
+ "Normalized metadata:",
1077
+ ...(metadata.length === 0
1078
+ ? ["- none"]
1079
+ : metadata.map(([key, value]) => `- ${key}: ${formatMetadataValue(value)}`)),
1080
+ `Redactions: ${input.event.payload.redactions.join(", ") || "none"}`,
1081
+ "Raw payload included: false",
1082
+ ].join("\n");
1083
+ }
1084
+
1085
+ function formatTeamInboxContext(messages: TeamMessageRecord[]): string {
1086
+ const blocks = messages.map((message) =>
1087
+ [
1088
+ "[EvoDev team message]",
1089
+ "Instruction: read this queued team message at this safe point; reply through Teams MCP only when a response is needed.",
1090
+ `from: ${message.fromRoleId}`,
1091
+ `to: ${message.toRoleId}`,
1092
+ `type: ${message.type}`,
1093
+ `messageId: ${message.messageId}`,
1094
+ `cc: ${message.ccRoleIds.join(",") || "none"}`,
1095
+ "",
1096
+ truncateTeamMessageBody(message.body),
1097
+ "[/EvoDev team message]",
1098
+ ].join("\n"),
1099
+ );
1100
+ return ["EvoDev team inbox:", ...blocks].join("\n\n");
1101
+ }
1102
+
1103
+ function truncateTeamMessageBody(value: string): string {
1104
+ if (value.length <= 4000) return value;
1105
+ return `${value.slice(0, 4000)}...[truncated:${value.length - 4000}]`;
786
1106
  }
787
1107
 
788
1108
  async function readSessionBinding(
@@ -803,7 +1123,7 @@ function resolveHookSessionKey(payload: Record<string, unknown>): string {
803
1123
  const sessionId = optionalPayloadString(payload.session_id ?? payload.sessionId);
804
1124
  const cwd = optionalPayloadString(payload.cwd);
805
1125
  const source = sessionId ?? cwd ?? "local";
806
- return `session-${createHash("sha256").update(source).digest("hex").slice(0, 16)}`;
1126
+ return `session-${sha256Short(source)}`;
807
1127
  }
808
1128
 
809
1129
  async function writeJsonFile(path: string, value: unknown): Promise<void> {
@@ -811,36 +1131,12 @@ async function writeJsonFile(path: string, value: unknown): Promise<void> {
811
1131
  await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
812
1132
  }
813
1133
 
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
1134
  function optionalPayloadString(value: unknown): string | null {
839
1135
  return typeof value === "string" && value.length > 0 ? value.slice(0, 300) : null;
840
1136
  }
841
1137
 
842
- function optionalPayloadNumber(value: unknown): number | null {
843
- return typeof value === "number" && Number.isFinite(value) ? value : null;
1138
+ function safeDiagnosticId(value: string): string {
1139
+ return value.replace(/[^A-Za-z0-9._-]/g, "-").slice(0, 120) || "unknown";
844
1140
  }
845
1141
 
846
1142
  function isNotFoundError(error: unknown): boolean {
@@ -892,9 +1188,7 @@ function extractMetadata(
892
1188
  payload: Record<string, unknown>,
893
1189
  redactions: string[],
894
1190
  ): Record<string, string | number | boolean | string[]> {
895
- const metadata: Record<string, string | number | boolean | string[]> = {
896
- rawContentIncluded: false,
897
- };
1191
+ const metadata: Record<string, string | number | boolean | string[]> = {};
898
1192
  const toolInput = isRecord(payload.tool_input)
899
1193
  ? payload.tool_input
900
1194
  : isRecord(payload.toolInput)
@@ -932,30 +1226,12 @@ function extractMetadata(
932
1226
  }
933
1227
 
934
1228
  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
1229
  return {
957
1230
  action: "allow",
958
- reason: "No policy violation detected.",
1231
+ reason:
1232
+ commandClass === undefined
1233
+ ? "Observed metadata only; EvoDev does not stop hook execution."
1234
+ : `Observed ${commandClass} tool action; EvoDev does not stop hook execution.`,
959
1235
  requiresUserConfirmation: false,
960
1236
  };
961
1237
  }
@@ -966,7 +1242,7 @@ function summarizeEvent(
966
1242
  ): string {
967
1243
  if (type === "PreToolUse") return `Tool use requested (${metadata.commandClass ?? "unknown"}).`;
968
1244
  if (type === "PostToolUse") return "Tool use completed with metadata-only result.";
969
- if (type === "UserPromptSubmit") return "User prompt submitted; raw prompt redacted.";
1245
+ if (type === "UserPromptSubmit") return "User prompt submitted.";
970
1246
  if (type === "SubagentStop") return "Subagent stopped; transcript omitted.";
971
1247
  return `${type} received; metadata-only dry-run.`;
972
1248
  }
@@ -1001,7 +1277,7 @@ function stableEventId(
1001
1277
 
1002
1278
  function hashOptionalIdentifier(value: unknown): string | null {
1003
1279
  if (typeof value !== "string" || value.length === 0) return null;
1004
- return `sha256-${createHash("sha256").update(value).digest("hex").slice(0, 16)}`;
1280
+ return `sha256-${sha256Short(value)}`;
1005
1281
  }
1006
1282
 
1007
1283
  function optionalBoolean(value: unknown, fallback: boolean, path: string): boolean {