@evo-dev/core 0.0.1-alpha.5 → 0.0.1-alpha.7

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.
@@ -14,13 +14,8 @@ import {
14
14
  enqueueEvolutionTrigger,
15
15
  resolveEvolutionTriggerDecision,
16
16
  } from "../evolution/triggers/index.ts";
17
- import { resolveTraceTeamContext } from "../runtime-logs/index.ts";
18
- import {
19
- type TaskContract,
20
- createTaskContract,
21
- routeTaskContract,
22
- writeTaskContract,
23
- } from "../task/index.ts";
17
+ import { recordDiscoveredProject } from "../projects/index.ts";
18
+ import { resolveTraceSessionKey, resolveTraceTeamContext } from "../runtime-logs/index.ts";
24
19
  import {
25
20
  type TeamMessageRecord,
26
21
  createTeamRoleRuntimeContext,
@@ -159,10 +154,7 @@ export interface HookRuntimeSessionBinding {
159
154
  version: 1;
160
155
  target: CodeAgentHookTarget;
161
156
  sessionKey: string;
162
- taskId: string | null;
163
- contractPath: string | null;
164
157
  cwd: string | null;
165
- route: TaskContract["route"] | null;
166
158
  teamRuntimeContextDeliveredAt?: string | null;
167
159
  updatedAt: string;
168
160
  }
@@ -234,6 +226,12 @@ const CODEX_STOP_EVENTS_WITHOUT_ADDITIONAL_CONTEXT = new Set<CanonicalHookEventT
234
226
  "Stop",
235
227
  "SubagentStop",
236
228
  ]);
229
+ const COMPLETION_EVENTS_WITHOUT_DEVELOPMENT_DIAGNOSTICS = new Set<CanonicalHookEventType>([
230
+ "Stop",
231
+ "SubagentStop",
232
+ "TaskCompleted",
233
+ "TeammateIdle",
234
+ ]);
237
235
 
238
236
  export function createDefaultHookSettings(): HookSettings {
239
237
  return {
@@ -424,12 +422,11 @@ export function formatHookEventDryRun(event: HookEventV1): string {
424
422
  export function resolveHookRuntimeSessionPaths(input: {
425
423
  homeDir: string;
426
424
  sessionKey: string;
427
- }): { sessionDir: string; bindingPath: string; contractPath: string } {
425
+ }): { sessionDir: string; bindingPath: string } {
428
426
  const sessionDir = join(input.homeDir, ".evodev", "STATE", "hooks", "sessions", input.sessionKey);
429
427
  return {
430
428
  sessionDir,
431
429
  bindingPath: join(sessionDir, "binding.json"),
432
- contractPath: join(sessionDir, "contract.json"),
433
430
  };
434
431
  }
435
432
 
@@ -447,7 +444,14 @@ export async function handleHookRuntime(input: HandleHookRuntimeInput): Promise<
447
444
  };
448
445
  }
449
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
+
450
453
  const diagnostics = await recordTeamNativeSessionFromHook(input);
454
+ const projectDiagnostics = await recordDiscoveredProjectFromHook(input);
451
455
  let result: HookRuntimeResult | undefined;
452
456
  if (input.event.type === "SessionStart") result = await handleSessionStart(input);
453
457
  else if (input.event.type === "UserPromptSubmit") result = await handleUserPromptSubmit(input);
@@ -485,12 +489,15 @@ export async function handleHookRuntime(input: HandleHookRuntimeInput): Promise<
485
489
  const messageDiagnostics = await deliverPendingTeamMessagesFromHook(input, result);
486
490
  const scopedContextDiagnostics = await injectScopedKnowledgeContextFromHook(input, result);
487
491
  const teamStateDiagnostics = await recordTeamAgentHookStateFromHook(input);
488
- return appendRuntimeDiagnostics(
492
+ const completed = appendRuntimeDiagnostics(
489
493
  appendRuntimeDiagnostics(
490
494
  appendRuntimeDiagnostics(
491
495
  appendRuntimeDiagnostics(
492
496
  appendRuntimeDiagnostics(
493
- appendRuntimeDiagnostics(result, diagnostics),
497
+ appendRuntimeDiagnostics(
498
+ appendRuntimeDiagnostics(result, diagnostics),
499
+ projectDiagnostics,
500
+ ),
494
501
  sessionMemoryDiagnostics,
495
502
  ),
496
503
  evolutionDiagnostics,
@@ -501,6 +508,7 @@ export async function handleHookRuntime(input: HandleHookRuntimeInput): Promise<
501
508
  ),
502
509
  teamStateDiagnostics,
503
510
  );
511
+ return appendDevelopmentHookDiagnostics(input, completed);
504
512
  }
505
513
 
506
514
  export function formatHookRuntimeOutput(result: HookRuntimeResult): string {
@@ -524,6 +532,41 @@ interface HookRuntimeDiagnostics {
524
532
  warnings: string[];
525
533
  }
526
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
+
527
570
  async function recordTeamNativeSessionFromHook(
528
571
  input: HandleHookRuntimeInput,
529
572
  ): Promise<HookRuntimeDiagnostics> {
@@ -825,17 +868,14 @@ async function handleSessionStart(input: HandleHookRuntimeInput): Promise<HookRu
825
868
  }
826
869
 
827
870
  async function handleUserPromptSubmit(input: HandleHookRuntimeInput): Promise<HookRuntimeResult> {
828
- const classification = classifyUserPrompt(input.rawPayload.prompt ?? input.rawPayload.userPrompt);
829
871
  const sessionKey = resolveHookSessionKey(input.rawPayload);
830
872
  const paths = resolveHookRuntimeSessionPaths({ homeDir: input.homeDir, sessionKey });
831
- const contract = routeTaskContract(createHookTaskContract(input, classification));
832
873
  const previousBinding = await readSessionBinding(input.homeDir, input.rawPayload);
833
874
  const teamRuntimeContext =
834
875
  previousBinding?.teamRuntimeContextDeliveredAt === undefined ||
835
876
  previousBinding.teamRuntimeContextDeliveredAt === null
836
877
  ? await createTeamRuntimeContextForUserPrompt(input)
837
878
  : null;
838
- const shouldShowDiagnostics = input.teamRuntimeDisplayMode === "development";
839
879
  const teamRuntimeContextDeliveredAt =
840
880
  teamRuntimeContext !== null
841
881
  ? (input.receivedAt ?? new Date().toISOString())
@@ -844,35 +884,21 @@ async function handleUserPromptSubmit(input: HandleHookRuntimeInput): Promise<Ho
844
884
  version: 1,
845
885
  target: input.target,
846
886
  sessionKey,
847
- taskId: contract.taskId,
848
- contractPath: paths.contractPath,
849
887
  cwd: optionalPayloadString(input.rawPayload.cwd),
850
- route: contract.route,
851
888
  teamRuntimeContextDeliveredAt,
852
889
  updatedAt: input.receivedAt ?? new Date().toISOString(),
853
890
  };
854
891
 
855
- await writeTaskContract(paths.contractPath, contract, { overwrite: true });
856
892
  await writeJsonFile(paths.bindingPath, binding);
857
893
 
858
- const visibleContext = createUserPromptVisibleContext({
859
- classification,
860
- contract,
861
- contractPath: paths.contractPath,
862
- });
863
- let output =
864
- visibleContext === null || !shouldShowDiagnostics
865
- ? null
866
- : hookOutput(input.event.type, {
867
- additionalContext: visibleContext,
868
- });
894
+ let output: Record<string, unknown> | null = null;
869
895
  if (teamRuntimeContext !== null) {
870
896
  output = appendAdditionalContext(output, input.event.type, teamRuntimeContext);
871
897
  }
872
898
 
873
899
  return createRuntimeResult(input, output, {
874
- summary: "User prompt observed; Task Contract prepared.",
875
- stateWrites: [paths.contractPath, paths.bindingPath],
900
+ summary: "User prompt observed; session binding updated.",
901
+ stateWrites: [paths.bindingPath],
876
902
  });
877
903
  }
878
904
 
@@ -909,46 +935,14 @@ async function handlePreToolUse(input: HandleHookRuntimeInput): Promise<HookRunt
909
935
  }
910
936
 
911
937
  async function handlePostToolUse(input: HandleHookRuntimeInput): Promise<HookRuntimeResult> {
912
- const contract = await readActiveContract(input.homeDir, input.rawPayload);
913
- const binding = await readSessionBinding(input.homeDir, input.rawPayload);
914
- if (contract === null || binding?.contractPath === null || binding?.contractPath === undefined) {
915
- return handleAdditionalContext(input, input.event.type);
916
- }
917
-
918
- const status = optionalPayloadNumber(input.rawPayload.exit_code ?? input.rawPayload.exitCode);
919
- const nextContract: TaskContract = {
920
- ...contract,
921
- evidence: {
922
- metadataOnly: true,
923
- items: [
924
- ...contract.evidence.items,
925
- {
926
- type: "command-result",
927
- id: input.event.eventId,
928
- status: status === 0 ? "pass" : status === null ? "unknown" : "fail",
929
- summary: input.event.payload.summary,
930
- rawOutputStored: false,
931
- sourceContentStored: false,
932
- },
933
- ],
934
- },
935
- };
936
- await writeTaskContract(binding.contractPath, nextContract, { overwrite: true });
937
-
938
- return createRuntimeResult(input, null, {
939
- summary: "Post-tool metadata evidence recorded.",
940
- stateWrites: [binding.contractPath],
941
- });
938
+ return handleAdditionalContext(input, input.event.type);
942
939
  }
943
940
 
944
941
  async function handleCompletionObservation(
945
942
  input: HandleHookRuntimeInput,
946
943
  ): Promise<HookRuntimeResult> {
947
- const contract = await readActiveContract(input.homeDir, input.rawPayload);
948
- const suffix =
949
- contract === null ? "without an active Task Contract" : `for Task Contract ${contract.taskId}`;
950
944
  return createRuntimeResult(input, null, {
951
- summary: `${input.event.type} observed ${suffix}; no completion control enforced.`,
945
+ summary: `${input.event.type} observed as metadata-only hook context.`,
952
946
  });
953
947
  }
954
948
 
@@ -1038,6 +1032,56 @@ function appendAdditionalContext(
1038
1032
  };
1039
1033
  }
1040
1034
 
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
+ }
1046
+ return {
1047
+ ...result,
1048
+ output: appendAdditionalContext(
1049
+ result.output,
1050
+ input.event.type,
1051
+ formatDevelopmentHookDiagnostics(input),
1052
+ ),
1053
+ };
1054
+ }
1055
+
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
+
1041
1085
  function formatTeamInboxContext(messages: TeamMessageRecord[]): string {
1042
1086
  const blocks = messages.map((message) =>
1043
1087
  [
@@ -1061,113 +1105,6 @@ function truncateTeamMessageBody(value: string): string {
1061
1105
  return `${value.slice(0, 4000)}...[truncated:${value.length - 4000}]`;
1062
1106
  }
1063
1107
 
1064
- function createUserPromptVisibleContext(input: {
1065
- classification: ReturnType<typeof classifyUserPrompt>;
1066
- contract: TaskContract;
1067
- contractPath: string;
1068
- }): string | null {
1069
- if (!input.classification.needsClarification) return null;
1070
- return [
1071
- "EvoDev advisory: clarification may be needed before broad changes.",
1072
- `Task Contract: ${input.contractPath}`,
1073
- `Suggested mode: ${input.contract.route.mode ?? "unknown"}`,
1074
- `Suggested workflow: ${input.contract.route.workflowId ?? "none"}`,
1075
- `Reason: ${input.contract.route.rationale}`,
1076
- ].join(" ");
1077
- }
1078
-
1079
- function createHookTaskContract(
1080
- input: HandleHookRuntimeInput,
1081
- classification: ReturnType<typeof classifyUserPrompt>,
1082
- ): TaskContract {
1083
- const sessionKey = resolveHookSessionKey(input.rawPayload);
1084
- const targetName = formatHookTargetName(input.target);
1085
- const contract = createTaskContract({
1086
- title: `${targetName} hook task ${sessionKey}`,
1087
- summary: `${targetName} prompt classified as ${classification.kind}; raw prompt is not stored by EvoDev.`,
1088
- projectId: null,
1089
- });
1090
-
1091
- return {
1092
- ...contract,
1093
- currentState: {
1094
- summary: `UserPromptSubmit received through ${targetName} hooks; raw prompt omitted from Task Contract.`,
1095
- evidenceRefs: [],
1096
- },
1097
- targetState: {
1098
- summary: `Complete the ${classification.kind} task through EvoDev-controlled workflow.`,
1099
- nonGoals: ["Do not store raw prompts, transcripts, source content, secrets, or raw output."],
1100
- constraints: [
1101
- `prompt-kind:${classification.kind}`,
1102
- ...classification.riskTerms.map((term) => `risk:${term}`),
1103
- ],
1104
- },
1105
- scope: {
1106
- ...contract.scope,
1107
- requiresUserConfirmation: classification.riskTerms,
1108
- },
1109
- context: {
1110
- ...contract.context,
1111
- assumptions: [
1112
- `hook-session:${sessionKey}`,
1113
- `cwd:${optionalPayloadString(input.rawPayload.cwd) ?? "unknown"}`,
1114
- ],
1115
- openQuestions: classification.needsClarification
1116
- ? ["User request may need clarification before broad changes."]
1117
- : [],
1118
- },
1119
- };
1120
- }
1121
-
1122
- function formatHookTargetName(target: CodeAgentHookTarget): string {
1123
- return target === "codex" ? "Codex" : "Claude";
1124
- }
1125
-
1126
- function classifyUserPrompt(value: unknown): {
1127
- kind: string;
1128
- riskTerms: string[];
1129
- needsClarification: boolean;
1130
- } {
1131
- const text = typeof value === "string" ? value.toLowerCase() : "";
1132
- const riskTerms = [
1133
- "security",
1134
- "release",
1135
- "publish",
1136
- "hook",
1137
- "memory",
1138
- "learning",
1139
- "secret",
1140
- "privacy",
1141
- ].filter((term) => text.includes(term));
1142
- let kind = "feature";
1143
- if (/\bbug|fix|error|failed|failure\b/.test(text)) kind = "bugfix";
1144
- if (/\brefactor|migration|migrate\b/.test(text)) kind = "refactor";
1145
- if (/\breview|audit\b/.test(text)) kind = "review";
1146
- if (/\btest|coverage\b/.test(text)) kind = "test";
1147
- if (/\bdoc|readme|guide\b/.test(text)) kind = "docs";
1148
- if (riskTerms.includes("security") || riskTerms.includes("privacy")) kind = "security";
1149
- if (riskTerms.includes("release") || riskTerms.includes("publish")) kind = "release";
1150
- return {
1151
- kind,
1152
- riskTerms,
1153
- needsClarification: text.trim().length < 12 || /\bmaybe|unclear|not sure\b/.test(text),
1154
- };
1155
- }
1156
-
1157
- async function readActiveContract(
1158
- homeDir: string,
1159
- payload: Record<string, unknown>,
1160
- ): Promise<TaskContract | null> {
1161
- const binding = await readSessionBinding(homeDir, payload);
1162
- if (binding?.contractPath === null || binding?.contractPath === undefined) return null;
1163
- try {
1164
- return JSON.parse(await readFile(binding.contractPath, "utf8")) as TaskContract;
1165
- } catch (error) {
1166
- if (isNotFoundError(error)) return null;
1167
- throw error;
1168
- }
1169
- }
1170
-
1171
1108
  async function readSessionBinding(
1172
1109
  homeDir: string,
1173
1110
  payload: Record<string, unknown>,
@@ -1202,10 +1139,6 @@ function safeDiagnosticId(value: string): string {
1202
1139
  return value.replace(/[^A-Za-z0-9._-]/g, "-").slice(0, 120) || "unknown";
1203
1140
  }
1204
1141
 
1205
- function optionalPayloadNumber(value: unknown): number | null {
1206
- return typeof value === "number" && Number.isFinite(value) ? value : null;
1207
- }
1208
-
1209
1142
  function isNotFoundError(error: unknown): boolean {
1210
1143
  return (
1211
1144
  error instanceof Error && "code" in error && (error as NodeJS.ErrnoException).code === "ENOENT"
package/src/index.ts CHANGED
@@ -18,11 +18,10 @@ export * from "./hooks/index.ts";
18
18
  export * from "./observability/index.ts";
19
19
  export * from "./pack/index.ts";
20
20
  export * from "./plugins/index.ts";
21
- export * from "./project/index.ts";
22
21
  export * from "./protected-zones/index.ts";
22
+ export * from "./projects/index.ts";
23
23
  export * from "./runtime-logs/index.ts";
24
24
  export * from "./sync/index.ts";
25
- export * from "./task/index.ts";
26
25
  export * from "./team/index.ts";
27
26
  export * from "./team/mcp.ts";
28
27
  export * from "./workflow/index.ts";