@evo-dev/core 0.0.1-alpha.1 → 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 (66) hide show
  1. package/assets/skills/coding/knowledge-distillation/SKILL.md +117 -114
  2. package/assets/skills/coding/knowledge-distillation/references/knowledge-distillation-methods.md +11 -7
  3. package/assets/team/agents/code-reviewer.md +48 -0
  4. package/assets/team/agents/docs-maintainer.md +51 -0
  5. package/assets/team/agents/implementation-engineer.md +51 -0
  6. package/assets/team/agents/product-scope-analyst.md +58 -0
  7. package/assets/team/agents/release-engineer.md +55 -0
  8. package/assets/team/agents/security-boundary-reviewer.md +50 -0
  9. package/assets/team/agents/solution-architect.md +51 -0
  10. package/assets/team/agents/verification-engineer.md +51 -0
  11. package/assets/team/team.md +102 -0
  12. package/dist/config/index.js +925 -97
  13. package/dist/index.js +13107 -5618
  14. package/package.json +5 -1
  15. package/src/agents/index.ts +56 -264
  16. package/src/code-agent-traces/index.ts +520 -0
  17. package/src/config/index.ts +5 -0
  18. package/src/config/paths.ts +1 -1
  19. package/src/config/settings.ts +149 -0
  20. package/src/config/store.ts +2 -0
  21. package/src/daemon/index.ts +99 -50
  22. package/src/evolution/candidates/index.ts +564 -0
  23. package/src/evolution/control/index.ts +20 -0
  24. package/src/evolution/evidence/analysis.ts +533 -0
  25. package/src/evolution/evidence/index.ts +3 -0
  26. package/src/evolution/evidence/session-memory/analysis.ts +281 -0
  27. package/src/evolution/evidence/session-memory/constants.ts +9 -0
  28. package/src/evolution/evidence/session-memory/index.ts +7 -0
  29. package/src/evolution/evidence/session-memory/paths.ts +29 -0
  30. package/src/evolution/evidence/session-memory/policy.ts +39 -0
  31. package/src/evolution/evidence/session-memory/segment.ts +202 -0
  32. package/src/evolution/evidence/session-memory/sensitivity.ts +335 -0
  33. package/src/evolution/evidence/session-memory/state-machine.ts +249 -0
  34. package/src/evolution/evidence/session-memory/storage.ts +379 -0
  35. package/src/evolution/evidence/session-memory/types.ts +221 -0
  36. package/src/evolution/evidence/session-memory/updater.ts +191 -0
  37. package/src/evolution/formatters.ts +169 -0
  38. package/src/evolution/index.ts +16 -2356
  39. package/src/evolution/knowledge/index.ts +5427 -0
  40. package/src/evolution/paths.ts +44 -0
  41. package/src/evolution/processor/distillation.ts +518 -0
  42. package/src/evolution/processor/index.ts +3 -0
  43. package/src/evolution/processor/process.ts +528 -0
  44. package/src/{learning → evolution/review}/index.ts +10 -14
  45. package/src/evolution/schema.ts +568 -0
  46. package/src/evolution/shared.ts +758 -0
  47. package/src/evolution/triggers/classification.ts +102 -0
  48. package/src/evolution/triggers/index.ts +295 -0
  49. package/src/hooks/index.ts +438 -179
  50. package/src/index.ts +12 -3
  51. package/src/projects/index.ts +453 -0
  52. package/src/runtime-logs/index.ts +490 -24
  53. package/src/team/index.ts +1429 -185
  54. package/src/team/mcp.ts +9 -5
  55. package/src/team/prompts.ts +141 -0
  56. package/src/utils/errors.ts +13 -0
  57. package/src/utils/fs.ts +40 -0
  58. package/src/utils/hash.ts +9 -0
  59. package/src/utils/ids.ts +12 -0
  60. package/src/utils/index.ts +7 -0
  61. package/src/utils/parsing.ts +11 -0
  62. package/src/utils/text.ts +18 -0
  63. package/src/utils/time.ts +5 -0
  64. package/src/workflow/index.ts +3 -21
  65. package/src/project/index.ts +0 -507
  66. package/src/task/index.ts +0 -840
@@ -1,15 +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
- import { enqueueEvolutionTrigger, resolveEvolutionTriggerDecision } from "../evolution/index.ts";
5
- import { resolveTraceTeamContext } from "../runtime-logs/index.ts";
6
3
  import {
7
- type TaskContract,
8
- createTaskContract,
9
- routeTaskContract,
10
- writeTaskContract,
11
- } from "../task/index.ts";
12
- import { recordTeamAgentNativeSession, resolveTeamRunPaths } from "../team/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";
13
29
 
14
30
  export const CANONICAL_HOOK_EVENT_TYPES = [
15
31
  "SessionStart",
@@ -138,10 +154,8 @@ export interface HookRuntimeSessionBinding {
138
154
  version: 1;
139
155
  target: CodeAgentHookTarget;
140
156
  sessionKey: string;
141
- taskId: string | null;
142
- contractPath: string | null;
143
157
  cwd: string | null;
144
- route: TaskContract["route"] | null;
158
+ teamRuntimeContextDeliveredAt?: string | null;
145
159
  updatedAt: string;
146
160
  }
147
161
 
@@ -159,10 +173,13 @@ export interface HandleHookRuntimeInput {
159
173
  target: CodeAgentHookTarget;
160
174
  homeDir: string;
161
175
  settings: HookSettings;
176
+ teamRuntimeDisplayMode?: "normal" | "development";
162
177
  event: HookEventV1;
163
178
  rawPayload: Record<string, unknown>;
164
179
  receivedAt?: string;
165
180
  environment?: Record<string, string | undefined>;
181
+ runtimeInjectionEnabled?: boolean;
182
+ sessionMemoryPolicy?: Partial<SessionMemoryPolicySnapshot>;
166
183
  }
167
184
 
168
185
  const DEFAULT_EVENT_SETTINGS: Record<CanonicalHookEventType, boolean> = {
@@ -195,6 +212,26 @@ const DEFAULT_EVENT_SETTINGS: Record<CanonicalHookEventType, boolean> = {
195
212
  const SENSITIVE_TEXT_PATTERN =
196
213
  /https?:\/\/\S+|\b(secret|token|password|passwd|private|internal|api[_-]?key|apikey|credential|credentials|\.env)\b/i;
197
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
+ ]);
198
235
 
199
236
  export function createDefaultHookSettings(): HookSettings {
200
237
  return {
@@ -210,8 +247,8 @@ export function createDefaultHookSettings(): HookSettings {
210
247
  },
211
248
  },
212
249
  observability: {
213
- metadataOnly: false,
214
- rawPayloadStorage: true,
250
+ metadataOnly: true,
251
+ rawPayloadStorage: false,
215
252
  appendEvents: false,
216
253
  },
217
254
  learning: {
@@ -244,8 +281,8 @@ export function parseHookSettings(value: unknown): HookSettings {
244
281
  codex: parseHookTargetSettings(value.targets, defaults.targets.codex, "codex"),
245
282
  },
246
283
  observability: {
247
- metadataOnly: false,
248
- rawPayloadStorage: true,
284
+ metadataOnly: true,
285
+ rawPayloadStorage: false,
249
286
  appendEvents: optionalBoolean(
250
287
  observability?.appendEvents,
251
288
  defaults.observability.appendEvents,
@@ -277,7 +314,7 @@ export function normalizeHookEvent(input: NormalizeHookEventInput): HookEventV1
277
314
  pluginId: sanitizeScalar(input.pluginId, redactions),
278
315
  agent: sanitizeScalar(input.agent ?? "unknown", redactions),
279
316
  sessionIdHash,
280
- rawPayloadStored: true,
317
+ rawPayloadStored: false,
281
318
  },
282
319
  time: {
283
320
  occurredAt: optionalSanitizedString(
@@ -289,7 +326,7 @@ export function normalizeHookEvent(input: NormalizeHookEventInput): HookEventV1
289
326
  scope: {
290
327
  taskId: optionalSanitizedString(input.payload.taskId, redactions),
291
328
  projectId: optionalSanitizedString(input.payload.projectId, redactions),
292
- cwdPolicy: "raw-local",
329
+ cwdPolicy: "metadata-only",
293
330
  projectContextOptedIn: false,
294
331
  },
295
332
  payload: {
@@ -297,7 +334,7 @@ export function normalizeHookEvent(input: NormalizeHookEventInput): HookEventV1
297
334
  metadata,
298
335
  redactions,
299
336
  redactionCount: redactions.length,
300
- rawContentIncluded: true,
337
+ rawContentIncluded: false,
301
338
  },
302
339
  policy: {
303
340
  classification: "local-private",
@@ -338,8 +375,8 @@ export function formatHookInstallDryRun(plan: HookInstallDryRunPlan): string {
338
375
  (eventType) => ` - ${eventType}: ${plan.settings.targets[plan.target].events[eventType]}`,
339
376
  ),
340
377
  "Boundaries:",
341
- " - trace: local raw payload storage=required, external upload=false",
342
- " - derived evidence/knowledge/observability surfaces: metadata-only until review/export",
378
+ " - execution events: metadata-only; raw payload/prompt/output/source storage=false",
379
+ " - historical raw trace files may remain for read-only migration compatibility",
343
380
  " - learning: emitCandidates=false, writeMemory=false",
344
381
  " - external upload: false",
345
382
  " - protected project writes: .claude/.codex/CLAUDE.md/AGENTS.md not targeted",
@@ -385,12 +422,11 @@ export function formatHookEventDryRun(event: HookEventV1): string {
385
422
  export function resolveHookRuntimeSessionPaths(input: {
386
423
  homeDir: string;
387
424
  sessionKey: string;
388
- }): { sessionDir: string; bindingPath: string; contractPath: string } {
425
+ }): { sessionDir: string; bindingPath: string } {
389
426
  const sessionDir = join(input.homeDir, ".evodev", "STATE", "hooks", "sessions", input.sessionKey);
390
427
  return {
391
428
  sessionDir,
392
429
  bindingPath: join(sessionDir, "binding.json"),
393
- contractPath: join(sessionDir, "contract.json"),
394
430
  };
395
431
  }
396
432
 
@@ -408,7 +444,14 @@ export async function handleHookRuntime(input: HandleHookRuntimeInput): Promise<
408
444
  };
409
445
  }
410
446
 
447
+ if (isActiveClaudeStopHook(input)) {
448
+ return createRuntimeResult(input, null, {
449
+ summary: `${input.event.type} re-entry allowed to complete without hook output.`,
450
+ });
451
+ }
452
+
411
453
  const diagnostics = await recordTeamNativeSessionFromHook(input);
454
+ const projectDiagnostics = await recordDiscoveredProjectFromHook(input);
412
455
  let result: HookRuntimeResult | undefined;
413
456
  if (input.event.type === "SessionStart") result = await handleSessionStart(input);
414
457
  else if (input.event.type === "UserPromptSubmit") result = await handleUserPromptSubmit(input);
@@ -437,11 +480,35 @@ export async function handleHookRuntime(input: HandleHookRuntimeInput): Promise<
437
480
  } else if (result === undefined) {
438
481
  result = await handleAdditionalContext(input, input.event.type);
439
482
  }
440
- const evolutionDiagnostics = await recordEvolutionTriggerFromHook(input);
441
- return appendRuntimeDiagnostics(
442
- appendRuntimeDiagnostics(result, diagnostics),
443
- evolutionDiagnostics,
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,
444
510
  );
511
+ return appendDevelopmentHookDiagnostics(input, completed);
445
512
  }
446
513
 
447
514
  export function formatHookRuntimeOutput(result: HookRuntimeResult): string {
@@ -465,6 +532,41 @@ interface HookRuntimeDiagnostics {
465
532
  warnings: string[];
466
533
  }
467
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
+
468
570
  async function recordTeamNativeSessionFromHook(
469
571
  input: HandleHookRuntimeInput,
470
572
  ): Promise<HookRuntimeDiagnostics> {
@@ -568,6 +670,183 @@ async function recordEvolutionTriggerFromHook(
568
670
  }
569
671
  }
570
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
+
571
850
  function appendRuntimeDiagnostics(
572
851
  result: HookRuntimeResult | undefined,
573
852
  diagnostics: HookRuntimeDiagnostics,
@@ -583,54 +862,67 @@ function appendRuntimeDiagnostics(
583
862
  }
584
863
 
585
864
  async function handleSessionStart(input: HandleHookRuntimeInput): Promise<HookRuntimeResult> {
586
- const binding = await readSessionBinding(input.homeDir, input.rawPayload);
587
- const context = [
588
- "EvoDev session initialized.",
589
- binding?.contractPath
590
- ? `Active Task Contract: ${binding.contractPath}`
591
- : "No active Task Contract yet.",
592
- "User prompts will be routed through EvoDev before execution.",
593
- ].join(" ");
594
- return createRuntimeResult(input, hookOutput(input.event.type, { additionalContext: context }), {
865
+ return createRuntimeResult(input, null, {
595
866
  summary: "Session context prepared.",
596
867
  });
597
868
  }
598
869
 
599
870
  async function handleUserPromptSubmit(input: HandleHookRuntimeInput): Promise<HookRuntimeResult> {
600
- const classification = classifyUserPrompt(input.rawPayload.prompt ?? input.rawPayload.userPrompt);
601
871
  const sessionKey = resolveHookSessionKey(input.rawPayload);
602
872
  const paths = resolveHookRuntimeSessionPaths({ homeDir: input.homeDir, sessionKey });
603
- 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);
604
883
  const binding: HookRuntimeSessionBinding = {
605
884
  version: 1,
606
885
  target: input.target,
607
886
  sessionKey,
608
- taskId: contract.taskId,
609
- contractPath: paths.contractPath,
610
887
  cwd: optionalPayloadString(input.rawPayload.cwd),
611
- route: contract.route,
888
+ teamRuntimeContextDeliveredAt,
612
889
  updatedAt: input.receivedAt ?? new Date().toISOString(),
613
890
  };
614
891
 
615
- await writeTaskContract(paths.contractPath, contract, { overwrite: true });
616
892
  await writeJsonFile(paths.bindingPath, binding);
617
893
 
618
- const context = [
619
- `EvoDev observed this request before ${formatHookTargetName(input.target)} execution.`,
620
- "Advisory only: EvoDev does not grant, deny, or stop model/tool execution.",
621
- `Task Contract: ${paths.contractPath}`,
622
- `Suggested mode: ${contract.route.mode ?? "unknown"}`,
623
- `Suggested workflow: ${contract.route.workflowId ?? "none"}`,
624
- `Reason: ${contract.route.rationale}`,
625
- "Use the suggestion only if it fits the user request.",
626
- ].join(" ");
627
-
628
- return createRuntimeResult(input, hookOutput(input.event.type, { additionalContext: context }), {
629
- summary: "User prompt observed and advisory context prepared.",
630
- 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],
631
902
  });
632
903
  }
633
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
+
634
926
  async function handlePreToolUse(input: HandleHookRuntimeInput): Promise<HookRuntimeResult> {
635
927
  const commandClass =
636
928
  typeof input.event.payload.metadata.commandClass === "string"
@@ -643,46 +935,14 @@ async function handlePreToolUse(input: HandleHookRuntimeInput): Promise<HookRunt
643
935
  }
644
936
 
645
937
  async function handlePostToolUse(input: HandleHookRuntimeInput): Promise<HookRuntimeResult> {
646
- const contract = await readActiveContract(input.homeDir, input.rawPayload);
647
- const binding = await readSessionBinding(input.homeDir, input.rawPayload);
648
- if (contract === null || binding?.contractPath === null || binding?.contractPath === undefined) {
649
- return handleAdditionalContext(input, input.event.type);
650
- }
651
-
652
- const status = optionalPayloadNumber(input.rawPayload.exit_code ?? input.rawPayload.exitCode);
653
- const nextContract: TaskContract = {
654
- ...contract,
655
- evidence: {
656
- metadataOnly: true,
657
- items: [
658
- ...contract.evidence.items,
659
- {
660
- type: "command-result",
661
- id: input.event.eventId,
662
- status: status === 0 ? "pass" : status === null ? "unknown" : "fail",
663
- summary: input.event.payload.summary,
664
- rawOutputStored: false,
665
- sourceContentStored: false,
666
- },
667
- ],
668
- },
669
- };
670
- await writeTaskContract(binding.contractPath, nextContract, { overwrite: true });
671
-
672
- return createRuntimeResult(input, null, {
673
- summary: "Post-tool metadata evidence recorded.",
674
- stateWrites: [binding.contractPath],
675
- });
938
+ return handleAdditionalContext(input, input.event.type);
676
939
  }
677
940
 
678
941
  async function handleCompletionObservation(
679
942
  input: HandleHookRuntimeInput,
680
943
  ): Promise<HookRuntimeResult> {
681
- const contract = await readActiveContract(input.homeDir, input.rawPayload);
682
- const suffix =
683
- contract === null ? "without an active Task Contract" : `for Task Contract ${contract.taskId}`;
684
944
  return createRuntimeResult(input, null, {
685
- summary: `${input.event.type} observed ${suffix}; no completion control enforced.`,
945
+ summary: `${input.event.type} observed as metadata-only hook context.`,
686
946
  });
687
947
  }
688
948
 
@@ -748,96 +1008,101 @@ function hookOutput(
748
1008
  };
749
1009
  }
750
1010
 
751
- function createHookTaskContract(
752
- input: HandleHookRuntimeInput,
753
- classification: ReturnType<typeof classifyUserPrompt>,
754
- ): TaskContract {
755
- const sessionKey = resolveHookSessionKey(input.rawPayload);
756
- const targetName = formatHookTargetName(input.target);
757
- const contract = createTaskContract({
758
- title: `${targetName} hook task ${sessionKey}`,
759
- summary: `${targetName} prompt classified as ${classification.kind}; raw prompt is local trace only.`,
760
- projectId: null,
761
- });
762
-
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
+ : "";
763
1022
  return {
764
- ...contract,
765
- currentState: {
766
- summary: `UserPromptSubmit received through ${targetName} hooks; raw prompt omitted from Task Contract.`,
767
- evidenceRefs: [],
768
- },
769
- targetState: {
770
- summary: `Complete the ${classification.kind} task through EvoDev-controlled workflow.`,
771
- nonGoals: ["Do not store raw prompts, transcripts, source content, secrets, or raw output."],
772
- constraints: [
773
- `prompt-kind:${classification.kind}`,
774
- ...classification.riskTerms.map((term) => `risk:${term}`),
775
- ],
776
- },
777
- scope: {
778
- ...contract.scope,
779
- requiresUserConfirmation: classification.riskTerms,
780
- },
781
- context: {
782
- ...contract.context,
783
- assumptions: [
784
- `hook-session:${sessionKey}`,
785
- `cwd:${optionalPayloadString(input.rawPayload.cwd) ?? "unknown"}`,
786
- ],
787
- openQuestions: classification.needsClarification
788
- ? ["User request may need clarification before broad changes."]
789
- : [],
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}`,
790
1031
  },
791
1032
  };
792
1033
  }
793
1034
 
794
- function formatHookTargetName(target: CodeAgentHookTarget): string {
795
- return target === "codex" ? "Codex" : "Claude";
796
- }
797
-
798
- function classifyUserPrompt(value: unknown): {
799
- kind: string;
800
- riskTerms: string[];
801
- needsClarification: boolean;
802
- } {
803
- const text = typeof value === "string" ? value.toLowerCase() : "";
804
- const riskTerms = [
805
- "security",
806
- "release",
807
- "publish",
808
- "hook",
809
- "memory",
810
- "learning",
811
- "secret",
812
- "privacy",
813
- ].filter((term) => text.includes(term));
814
- let kind = "feature";
815
- if (/\bbug|fix|error|failed|failure\b/.test(text)) kind = "bugfix";
816
- if (/\brefactor|migration|migrate\b/.test(text)) kind = "refactor";
817
- if (/\breview|audit\b/.test(text)) kind = "review";
818
- if (/\btest|coverage\b/.test(text)) kind = "test";
819
- if (/\bdoc|readme|guide\b/.test(text)) kind = "docs";
820
- if (riskTerms.includes("security") || riskTerms.includes("privacy")) kind = "security";
821
- 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
+ }
822
1046
  return {
823
- kind,
824
- riskTerms,
825
- 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
+ ),
826
1053
  };
827
1054
  }
828
1055
 
829
- async function readActiveContract(
830
- homeDir: string,
831
- payload: Record<string, unknown>,
832
- ): Promise<TaskContract | null> {
833
- const binding = await readSessionBinding(homeDir, payload);
834
- if (binding?.contractPath === null || binding?.contractPath === undefined) return null;
835
- try {
836
- return JSON.parse(await readFile(binding.contractPath, "utf8")) as TaskContract;
837
- } catch (error) {
838
- if (isNotFoundError(error)) return null;
839
- throw error;
840
- }
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}]`;
841
1106
  }
842
1107
 
843
1108
  async function readSessionBinding(
@@ -858,7 +1123,7 @@ function resolveHookSessionKey(payload: Record<string, unknown>): string {
858
1123
  const sessionId = optionalPayloadString(payload.session_id ?? payload.sessionId);
859
1124
  const cwd = optionalPayloadString(payload.cwd);
860
1125
  const source = sessionId ?? cwd ?? "local";
861
- return `session-${createHash("sha256").update(source).digest("hex").slice(0, 16)}`;
1126
+ return `session-${sha256Short(source)}`;
862
1127
  }
863
1128
 
864
1129
  async function writeJsonFile(path: string, value: unknown): Promise<void> {
@@ -874,10 +1139,6 @@ function safeDiagnosticId(value: string): string {
874
1139
  return value.replace(/[^A-Za-z0-9._-]/g, "-").slice(0, 120) || "unknown";
875
1140
  }
876
1141
 
877
- function optionalPayloadNumber(value: unknown): number | null {
878
- return typeof value === "number" && Number.isFinite(value) ? value : null;
879
- }
880
-
881
1142
  function isNotFoundError(error: unknown): boolean {
882
1143
  return (
883
1144
  error instanceof Error && "code" in error && (error as NodeJS.ErrnoException).code === "ENOENT"
@@ -927,9 +1188,7 @@ function extractMetadata(
927
1188
  payload: Record<string, unknown>,
928
1189
  redactions: string[],
929
1190
  ): Record<string, string | number | boolean | string[]> {
930
- const metadata: Record<string, string | number | boolean | string[]> = {
931
- rawContentIncluded: true,
932
- };
1191
+ const metadata: Record<string, string | number | boolean | string[]> = {};
933
1192
  const toolInput = isRecord(payload.tool_input)
934
1193
  ? payload.tool_input
935
1194
  : isRecord(payload.toolInput)
@@ -1018,7 +1277,7 @@ function stableEventId(
1018
1277
 
1019
1278
  function hashOptionalIdentifier(value: unknown): string | null {
1020
1279
  if (typeof value !== "string" || value.length === 0) return null;
1021
- return `sha256-${createHash("sha256").update(value).digest("hex").slice(0, 16)}`;
1280
+ return `sha256-${sha256Short(value)}`;
1022
1281
  }
1023
1282
 
1024
1283
  function optionalBoolean(value: unknown, fallback: boolean, path: string): boolean {