@evo-dev/core 0.0.1-alpha.1 → 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.
@@ -2,6 +2,12 @@ import { createHash } from "node:crypto";
2
2
  import { mkdir, readFile, writeFile } from "node:fs/promises";
3
3
  import { dirname, join } from "node:path";
4
4
  import { enqueueEvolutionTrigger, resolveEvolutionTriggerDecision } from "../evolution/index.ts";
5
+ import {
6
+ createScopedKnowledgeContextPack,
7
+ formatScopedKnowledgePromptBlock,
8
+ hasContextInjectionReceipt,
9
+ writeContextInjectionReceipt,
10
+ } from "../knowledge/index.ts";
5
11
  import { resolveTraceTeamContext } from "../runtime-logs/index.ts";
6
12
  import {
7
13
  type TaskContract,
@@ -9,7 +15,15 @@ import {
9
15
  routeTaskContract,
10
16
  writeTaskContract,
11
17
  } from "../task/index.ts";
12
- import { recordTeamAgentNativeSession, resolveTeamRunPaths } from "../team/index.ts";
18
+ import {
19
+ type TeamMessageRecord,
20
+ createTeamRoleRuntimeContext,
21
+ markTeamMessagesDelivered,
22
+ readPendingTeamMessagesForRole,
23
+ recordTeamAgentNativeSession,
24
+ resolveTeamRunPaths,
25
+ updateTeamAgentHookState,
26
+ } from "../team/index.ts";
13
27
 
14
28
  export const CANONICAL_HOOK_EVENT_TYPES = [
15
29
  "SessionStart",
@@ -142,6 +156,7 @@ export interface HookRuntimeSessionBinding {
142
156
  contractPath: string | null;
143
157
  cwd: string | null;
144
158
  route: TaskContract["route"] | null;
159
+ teamRuntimeContextDeliveredAt?: string | null;
145
160
  updatedAt: string;
146
161
  }
147
162
 
@@ -159,10 +174,12 @@ export interface HandleHookRuntimeInput {
159
174
  target: CodeAgentHookTarget;
160
175
  homeDir: string;
161
176
  settings: HookSettings;
177
+ teamRuntimeDisplayMode?: "normal" | "development";
162
178
  event: HookEventV1;
163
179
  rawPayload: Record<string, unknown>;
164
180
  receivedAt?: string;
165
181
  environment?: Record<string, string | undefined>;
182
+ runtimeInjectionEnabled?: boolean;
166
183
  }
167
184
 
168
185
  const DEFAULT_EVENT_SETTINGS: Record<CanonicalHookEventType, boolean> = {
@@ -195,6 +212,20 @@ 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
+ ]);
198
229
 
199
230
  export function createDefaultHookSettings(): HookSettings {
200
231
  return {
@@ -210,8 +241,8 @@ export function createDefaultHookSettings(): HookSettings {
210
241
  },
211
242
  },
212
243
  observability: {
213
- metadataOnly: false,
214
- rawPayloadStorage: true,
244
+ metadataOnly: true,
245
+ rawPayloadStorage: false,
215
246
  appendEvents: false,
216
247
  },
217
248
  learning: {
@@ -244,8 +275,8 @@ export function parseHookSettings(value: unknown): HookSettings {
244
275
  codex: parseHookTargetSettings(value.targets, defaults.targets.codex, "codex"),
245
276
  },
246
277
  observability: {
247
- metadataOnly: false,
248
- rawPayloadStorage: true,
278
+ metadataOnly: true,
279
+ rawPayloadStorage: false,
249
280
  appendEvents: optionalBoolean(
250
281
  observability?.appendEvents,
251
282
  defaults.observability.appendEvents,
@@ -277,7 +308,7 @@ export function normalizeHookEvent(input: NormalizeHookEventInput): HookEventV1
277
308
  pluginId: sanitizeScalar(input.pluginId, redactions),
278
309
  agent: sanitizeScalar(input.agent ?? "unknown", redactions),
279
310
  sessionIdHash,
280
- rawPayloadStored: true,
311
+ rawPayloadStored: false,
281
312
  },
282
313
  time: {
283
314
  occurredAt: optionalSanitizedString(
@@ -289,7 +320,7 @@ export function normalizeHookEvent(input: NormalizeHookEventInput): HookEventV1
289
320
  scope: {
290
321
  taskId: optionalSanitizedString(input.payload.taskId, redactions),
291
322
  projectId: optionalSanitizedString(input.payload.projectId, redactions),
292
- cwdPolicy: "raw-local",
323
+ cwdPolicy: "metadata-only",
293
324
  projectContextOptedIn: false,
294
325
  },
295
326
  payload: {
@@ -297,7 +328,7 @@ export function normalizeHookEvent(input: NormalizeHookEventInput): HookEventV1
297
328
  metadata,
298
329
  redactions,
299
330
  redactionCount: redactions.length,
300
- rawContentIncluded: true,
331
+ rawContentIncluded: false,
301
332
  },
302
333
  policy: {
303
334
  classification: "local-private",
@@ -338,8 +369,8 @@ export function formatHookInstallDryRun(plan: HookInstallDryRunPlan): string {
338
369
  (eventType) => ` - ${eventType}: ${plan.settings.targets[plan.target].events[eventType]}`,
339
370
  ),
340
371
  "Boundaries:",
341
- " - trace: local raw payload storage=required, external upload=false",
342
- " - derived evidence/knowledge/observability surfaces: metadata-only until review/export",
372
+ " - execution events: metadata-only; raw payload/prompt/output/source storage=false",
373
+ " - historical raw trace files may remain for read-only migration compatibility",
343
374
  " - learning: emitCandidates=false, writeMemory=false",
344
375
  " - external upload: false",
345
376
  " - protected project writes: .claude/.codex/CLAUDE.md/AGENTS.md not targeted",
@@ -438,9 +469,21 @@ export async function handleHookRuntime(input: HandleHookRuntimeInput): Promise<
438
469
  result = await handleAdditionalContext(input, input.event.type);
439
470
  }
440
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);
441
475
  return appendRuntimeDiagnostics(
442
- appendRuntimeDiagnostics(result, diagnostics),
443
- evolutionDiagnostics,
476
+ appendRuntimeDiagnostics(
477
+ appendRuntimeDiagnostics(
478
+ appendRuntimeDiagnostics(
479
+ appendRuntimeDiagnostics(result, diagnostics),
480
+ evolutionDiagnostics,
481
+ ),
482
+ messageDiagnostics,
483
+ ),
484
+ scopedContextDiagnostics,
485
+ ),
486
+ teamStateDiagnostics,
444
487
  );
445
488
  }
446
489
 
@@ -568,6 +611,152 @@ async function recordEvolutionTriggerFromHook(
568
611
  }
569
612
  }
570
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
+
571
760
  function appendRuntimeDiagnostics(
572
761
  result: HookRuntimeResult | undefined,
573
762
  diagnostics: HookRuntimeDiagnostics,
@@ -583,15 +772,7 @@ function appendRuntimeDiagnostics(
583
772
  }
584
773
 
585
774
  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 }), {
775
+ return createRuntimeResult(input, null, {
595
776
  summary: "Session context prepared.",
596
777
  });
597
778
  }
@@ -601,6 +782,17 @@ async function handleUserPromptSubmit(input: HandleHookRuntimeInput): Promise<Ho
601
782
  const sessionKey = resolveHookSessionKey(input.rawPayload);
602
783
  const paths = resolveHookRuntimeSessionPaths({ homeDir: input.homeDir, sessionKey });
603
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);
604
796
  const binding: HookRuntimeSessionBinding = {
605
797
  version: 1,
606
798
  target: input.target,
@@ -609,28 +801,55 @@ async function handleUserPromptSubmit(input: HandleHookRuntimeInput): Promise<Ho
609
801
  contractPath: paths.contractPath,
610
802
  cwd: optionalPayloadString(input.rawPayload.cwd),
611
803
  route: contract.route,
804
+ teamRuntimeContextDeliveredAt,
612
805
  updatedAt: input.receivedAt ?? new Date().toISOString(),
613
806
  };
614
807
 
615
808
  await writeTaskContract(paths.contractPath, contract, { overwrite: true });
616
809
  await writeJsonFile(paths.bindingPath, binding);
617
810
 
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(" ");
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
+ }
627
825
 
628
- return createRuntimeResult(input, hookOutput(input.event.type, { additionalContext: context }), {
629
- summary: "User prompt observed and advisory context prepared.",
826
+ return createRuntimeResult(input, output, {
827
+ summary: "User prompt observed; Task Contract prepared.",
630
828
  stateWrites: [paths.contractPath, paths.bindingPath],
631
829
  });
632
830
  }
633
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
+
634
853
  async function handlePreToolUse(input: HandleHookRuntimeInput): Promise<HookRuntimeResult> {
635
854
  const commandClass =
636
855
  typeof input.event.payload.metadata.commandClass === "string"
@@ -748,6 +967,68 @@ function hookOutput(
748
967
  };
749
968
  }
750
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
+
751
1032
  function createHookTaskContract(
752
1033
  input: HandleHookRuntimeInput,
753
1034
  classification: ReturnType<typeof classifyUserPrompt>,
@@ -756,7 +1037,7 @@ function createHookTaskContract(
756
1037
  const targetName = formatHookTargetName(input.target);
757
1038
  const contract = createTaskContract({
758
1039
  title: `${targetName} hook task ${sessionKey}`,
759
- summary: `${targetName} prompt classified as ${classification.kind}; raw prompt is local trace only.`,
1040
+ summary: `${targetName} prompt classified as ${classification.kind}; raw prompt is not stored by EvoDev.`,
760
1041
  projectId: null,
761
1042
  });
762
1043
 
@@ -927,9 +1208,7 @@ function extractMetadata(
927
1208
  payload: Record<string, unknown>,
928
1209
  redactions: string[],
929
1210
  ): Record<string, string | number | boolean | string[]> {
930
- const metadata: Record<string, string | number | boolean | string[]> = {
931
- rawContentIncluded: true,
932
- };
1211
+ const metadata: Record<string, string | number | boolean | string[]> = {};
933
1212
  const toolInput = isRecord(payload.tool_input)
934
1213
  ? payload.tool_input
935
1214
  : isRecord(payload.toolInput)
package/src/index.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  export * from "./agents/index.ts";
2
2
  export * from "./assets/index.ts";
3
+ export * from "./code-agent-traces/index.ts";
3
4
  export * from "./config/index.ts";
4
5
  export * from "./daemon/index.ts";
5
6
  export * from "./evolution/index.ts";
6
7
  export * from "./hooks/index.ts";
8
+ export * from "./knowledge/index.ts";
7
9
  export * from "./learning/index.ts";
8
10
  export * from "./observability/index.ts";
9
11
  export * from "./pack/index.ts";