@evo-dev/core 0.0.1-alpha.1 → 0.0.1-alpha.11
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.
- package/assets/skills/coding/knowledge-distillation/SKILL.md +117 -114
- package/assets/skills/coding/knowledge-distillation/references/knowledge-distillation-methods.md +11 -7
- package/assets/team/agents/code-reviewer.md +48 -0
- package/assets/team/agents/docs-maintainer.md +51 -0
- package/assets/team/agents/implementation-engineer.md +51 -0
- package/assets/team/agents/product-scope-analyst.md +58 -0
- package/assets/team/agents/release-engineer.md +55 -0
- package/assets/team/agents/security-boundary-reviewer.md +50 -0
- package/assets/team/agents/solution-architect.md +51 -0
- package/assets/team/agents/verification-engineer.md +51 -0
- package/assets/team/team.md +102 -0
- package/dist/config/index.js +925 -97
- package/dist/index.js +13107 -5618
- package/package.json +5 -1
- package/src/agents/index.ts +56 -264
- package/src/code-agent-traces/index.ts +520 -0
- package/src/config/index.ts +5 -0
- package/src/config/paths.ts +1 -1
- package/src/config/settings.ts +149 -0
- package/src/config/store.ts +2 -0
- package/src/daemon/index.ts +99 -50
- package/src/evolution/candidates/index.ts +564 -0
- package/src/evolution/control/index.ts +20 -0
- package/src/evolution/evidence/analysis.ts +533 -0
- package/src/evolution/evidence/index.ts +3 -0
- package/src/evolution/evidence/session-memory/analysis.ts +281 -0
- package/src/evolution/evidence/session-memory/constants.ts +9 -0
- package/src/evolution/evidence/session-memory/index.ts +7 -0
- package/src/evolution/evidence/session-memory/paths.ts +29 -0
- package/src/evolution/evidence/session-memory/policy.ts +39 -0
- package/src/evolution/evidence/session-memory/segment.ts +202 -0
- package/src/evolution/evidence/session-memory/sensitivity.ts +335 -0
- package/src/evolution/evidence/session-memory/state-machine.ts +249 -0
- package/src/evolution/evidence/session-memory/storage.ts +379 -0
- package/src/evolution/evidence/session-memory/types.ts +221 -0
- package/src/evolution/evidence/session-memory/updater.ts +191 -0
- package/src/evolution/formatters.ts +169 -0
- package/src/evolution/index.ts +16 -2356
- package/src/evolution/knowledge/index.ts +5427 -0
- package/src/evolution/paths.ts +44 -0
- package/src/evolution/processor/distillation.ts +518 -0
- package/src/evolution/processor/index.ts +3 -0
- package/src/evolution/processor/process.ts +528 -0
- package/src/{learning → evolution/review}/index.ts +10 -14
- package/src/evolution/schema.ts +568 -0
- package/src/evolution/shared.ts +758 -0
- package/src/evolution/triggers/classification.ts +102 -0
- package/src/evolution/triggers/index.ts +295 -0
- package/src/hooks/index.ts +438 -179
- package/src/index.ts +12 -3
- package/src/projects/index.ts +453 -0
- package/src/runtime-logs/index.ts +490 -24
- package/src/team/index.ts +1429 -185
- package/src/team/mcp.ts +9 -5
- package/src/team/prompts.ts +141 -0
- package/src/utils/errors.ts +13 -0
- package/src/utils/fs.ts +40 -0
- package/src/utils/hash.ts +9 -0
- package/src/utils/ids.ts +12 -0
- package/src/utils/index.ts +7 -0
- package/src/utils/parsing.ts +11 -0
- package/src/utils/text.ts +18 -0
- package/src/utils/time.ts +5 -0
- package/src/workflow/index.ts +3 -21
- package/src/project/index.ts +0 -507
- package/src/task/index.ts +0 -840
package/src/hooks/index.ts
CHANGED
|
@@ -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
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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:
|
|
214
|
-
rawPayloadStorage:
|
|
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:
|
|
248
|
-
rawPayloadStorage:
|
|
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:
|
|
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: "
|
|
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:
|
|
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
|
-
" -
|
|
342
|
-
" -
|
|
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
|
|
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
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
const
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
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
|
-
...
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
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
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
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
|
-
|
|
824
|
-
|
|
825
|
-
|
|
1047
|
+
...result,
|
|
1048
|
+
output: appendAdditionalContext(
|
|
1049
|
+
result.output,
|
|
1050
|
+
input.event.type,
|
|
1051
|
+
formatDevelopmentHookDiagnostics(input),
|
|
1052
|
+
),
|
|
826
1053
|
};
|
|
827
1054
|
}
|
|
828
1055
|
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
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-${
|
|
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-${
|
|
1280
|
+
return `sha256-${sha256Short(value)}`;
|
|
1022
1281
|
}
|
|
1023
1282
|
|
|
1024
1283
|
function optionalBoolean(value: unknown, fallback: boolean, path: string): boolean {
|