@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.
Files changed (41) 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 +249 -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/workflows/rd-bug-fix/WORKFLOW.json +1 -1
  8. package/assets/workflows/rd-code-review/WORKFLOW.json +1 -1
  9. package/assets/workflows/rd-docs-update/WORKFLOW.json +1 -1
  10. package/assets/workflows/rd-feature-implementation/WORKFLOW.json +1 -1
  11. package/assets/workflows/rd-refactor/WORKFLOW.json +1 -1
  12. package/assets/workflows/rd-release-readiness/WORKFLOW.json +1 -1
  13. package/assets/workflows/rd-security-boundary-review/WORKFLOW.json +2 -2
  14. package/assets/workflows/rd-test-generation/WORKFLOW.json +1 -1
  15. package/dist/config/index.js +968 -39
  16. package/dist/index.js +10914 -1476
  17. package/dist/plugins/index.js +32 -32
  18. package/package.json +5 -1
  19. package/src/agents/index.ts +84 -49
  20. package/src/code-agent-traces/index.ts +521 -0
  21. package/src/config/index.ts +5 -0
  22. package/src/config/paths.ts +30 -0
  23. package/src/config/settings.ts +130 -0
  24. package/src/config/store.ts +152 -0
  25. package/src/daemon/index.ts +465 -3
  26. package/src/evolution/index.ts +2827 -0
  27. package/src/hooks/index.ts +543 -247
  28. package/src/index.ts +6 -0
  29. package/src/knowledge/index.ts +4784 -0
  30. package/src/pack/index.ts +13 -13
  31. package/src/plugins/capabilities.ts +40 -42
  32. package/src/plugins/index.ts +0 -1
  33. package/src/plugins/types.ts +4 -0
  34. package/src/protected-zones/index.ts +29 -11
  35. package/src/runtime-logs/index.ts +798 -0
  36. package/src/sync/orchestrator.ts +6 -0
  37. package/src/task/index.ts +3 -3
  38. package/src/team/index.ts +3069 -0
  39. package/src/team/mcp.ts +405 -0
  40. package/src/team/prompts.ts +141 -0
  41. package/src/workflow/index.ts +6 -6
@@ -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" | "ask-user" | "block";
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: true;
59
- rawPayloadStorage: false;
60
- appendEvents: false;
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: false;
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: false;
115
+ rawContentIncluded: boolean;
99
116
  };
100
117
  policy: {
101
118
  classification: "local-private";
102
- allowedUses: Array<"observability" | "safety-check" | "workflow-suggestion">;
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
- blockers: string[];
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: 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
+ ]);
193
229
 
194
230
  export function createDefaultHookSettings(): HookSettings {
195
231
  return {
196
- enabled: false,
232
+ enabled: true,
197
233
  targets: {
198
234
  claude: {
199
- enabled: false,
235
+ enabled: true,
200
236
  events: { ...DEFAULT_EVENT_SETTINGS },
201
237
  },
202
238
  codex: {
203
- enabled: false,
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: false,
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", "safety-check", "workflow-suggestion"],
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
- " - observability: metadata-only, appendEvents=false, rawPayloadStorage=false",
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
- "Blockers:",
335
- ...(plan.blockers.length === 0
386
+ "Advisories:",
387
+ ...(plan.advisories.length === 0
336
388
  ? [" - none"]
337
- : plan.blockers.map((blocker) => ` - ${blocker}`)),
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
- 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);
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
- return handlePostToolUse(input);
394
- }
395
- if (input.event.type === "PostToolBatch") return handleAdditionalContext(input, "PostToolBatch");
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
- return handlePreToolUse(input);
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
- return handleCompletionGate(input);
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
- if (input.event.type === "PreCompact") return handlePreCompact(input);
412
- if (input.event.type === "SessionEnd") return handleSessionEnd(input);
413
- return handleAdditionalContext(input, input.event.type);
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
- 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 }), {
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 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(" ");
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, hookOutput(input.event.type, { additionalContext: context }), {
475
- summary: "User prompt routed through Task Contract.",
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
- 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
- );
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
- 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
- );
891
+ return createRuntimeResult(input, null, {
892
+ summary: "Post-tool metadata evidence recorded.",
893
+ stateWrites: [binding.contractPath],
894
+ });
587
895
  }
588
896
 
589
- async function handleCompletionGate(input: HandleHookRuntimeInput): Promise<HookRuntimeResult> {
897
+ async function handleCompletionObservation(
898
+ input: HandleHookRuntimeInput,
899
+ ): Promise<HookRuntimeResult> {
590
900
  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
- );
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
- 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
- );
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
- input,
661
- hookOutput(eventName, {
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: "No policy violation detected.",
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; raw prompt redacted.";
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
  }