@evo-dev/core 0.0.1-alpha.5 → 0.0.1-alpha.6
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/dist/config/index.js +0 -36
- package/dist/index.js +710 -1590
- package/package.json +1 -1
- package/src/agents/index.ts +0 -264
- package/src/code-agent-traces/index.ts +8 -2
- package/src/daemon/index.ts +0 -40
- package/src/evolution/candidates/index.ts +77 -18
- package/src/evolution/evidence/session-memory/storage.ts +50 -0
- package/src/evolution/evidence/session-memory/updater.ts +8 -1
- package/src/evolution/review/index.ts +3 -9
- package/src/evolution/schema.ts +0 -1
- package/src/hooks/index.ts +91 -179
- package/src/index.ts +1 -2
- package/src/projects/index.ts +453 -0
- package/src/workflow/index.ts +3 -21
- package/src/project/index.ts +0 -507
- package/src/task/index.ts +0 -840
package/package.json
CHANGED
package/src/agents/index.ts
CHANGED
|
@@ -4,17 +4,8 @@ import {
|
|
|
4
4
|
createScopedKnowledgeContextPack,
|
|
5
5
|
formatScopedKnowledgePromptBlock,
|
|
6
6
|
} from "../evolution/knowledge/index.ts";
|
|
7
|
-
import type { TaskContract, TaskExecutionMode } from "../task/index.ts";
|
|
8
7
|
|
|
9
8
|
export type AgentPersistence = "named" | "dynamic";
|
|
10
|
-
export type AgentLens =
|
|
11
|
-
| "review"
|
|
12
|
-
| "qa"
|
|
13
|
-
| "security"
|
|
14
|
-
| "release"
|
|
15
|
-
| "architecture"
|
|
16
|
-
| "migration"
|
|
17
|
-
| "test";
|
|
18
9
|
export type AgentOutputSchemaId =
|
|
19
10
|
| "review-findings-v1"
|
|
20
11
|
| "verification-summary-v1"
|
|
@@ -61,34 +52,6 @@ export interface AgentProfile {
|
|
|
61
52
|
};
|
|
62
53
|
}
|
|
63
54
|
|
|
64
|
-
export interface AgentPlannedInvocation {
|
|
65
|
-
profile: AgentProfile;
|
|
66
|
-
lens: AgentLens;
|
|
67
|
-
reason: string;
|
|
68
|
-
plannedOnly: true;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export interface AgentMergePlan {
|
|
72
|
-
strategy: "none" | "single-output" | "dedupe-preserve-conflicts";
|
|
73
|
-
outputSchema: AgentOutputSchemaId | null;
|
|
74
|
-
dedupeBy: string[];
|
|
75
|
-
conflictPolicy: string[];
|
|
76
|
-
advisoryCategories: string[];
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export interface AgentComposeDryRunPlan {
|
|
80
|
-
ok: boolean;
|
|
81
|
-
taskId: string;
|
|
82
|
-
mode: TaskExecutionMode | null;
|
|
83
|
-
workflowId: string | null;
|
|
84
|
-
agents: AgentPlannedInvocation[];
|
|
85
|
-
permissions: AgentPermissions;
|
|
86
|
-
mergePlan: AgentMergePlan;
|
|
87
|
-
warnings: string[];
|
|
88
|
-
advisories: string[];
|
|
89
|
-
rationale: string;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
55
|
export interface AgentContextDryRunBundle {
|
|
93
56
|
profile: AgentProfile;
|
|
94
57
|
context: {
|
|
@@ -138,17 +101,6 @@ const DEFAULT_AGENT_PERMISSIONS: AgentPermissions = {
|
|
|
138
101
|
};
|
|
139
102
|
const FORBIDDEN_INPUTS = ["rawPrompts", "secrets", "rawCommandLogs", "sourceCorpus"];
|
|
140
103
|
const SAFE_AGENT_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
|
|
141
|
-
const PRIVACY_ADVISORY_PATTERN =
|
|
142
|
-
/raw prompt|rawprompt|source corpus|sourcecorpus|secret|\.env|raw command|raw log|internal url|private url|network|write files|run commands|spawn agents|write memory/i;
|
|
143
|
-
const LENS_TO_EXPERTISE: Record<AgentLens, string[]> = {
|
|
144
|
-
review: ["code-review"],
|
|
145
|
-
qa: ["qa", "verification"],
|
|
146
|
-
security: ["security", "privacy"],
|
|
147
|
-
release: ["release", "packaging"],
|
|
148
|
-
architecture: ["architecture"],
|
|
149
|
-
migration: ["migration", "impact-analysis"],
|
|
150
|
-
test: ["testing"],
|
|
151
|
-
};
|
|
152
104
|
|
|
153
105
|
export function createDefaultAgentPermissions(): AgentPermissions {
|
|
154
106
|
return { ...DEFAULT_AGENT_PERMISSIONS };
|
|
@@ -202,67 +154,6 @@ export async function readAgentProfile(path: string): Promise<AgentProfile> {
|
|
|
202
154
|
return parseAgentProfile(JSON.parse(await readFile(path, "utf8")));
|
|
203
155
|
}
|
|
204
156
|
|
|
205
|
-
export function composeAgentDryRun(input: {
|
|
206
|
-
contract: TaskContract;
|
|
207
|
-
workflowId?: string;
|
|
208
|
-
lenses?: AgentLens[];
|
|
209
|
-
}): AgentComposeDryRunPlan {
|
|
210
|
-
const mode = input.contract.route.mode;
|
|
211
|
-
const workflowId = input.workflowId ?? input.contract.route.workflowId;
|
|
212
|
-
const warnings: string[] = [];
|
|
213
|
-
const permissions = createDefaultAgentPermissions();
|
|
214
|
-
const requestedLenses = dedupeLenses(input.lenses ?? []);
|
|
215
|
-
const advisories = collectPrivacyBoundaryAdvisories(input.contract, requestedLenses, workflowId);
|
|
216
|
-
|
|
217
|
-
if (mode === "minimal") {
|
|
218
|
-
return {
|
|
219
|
-
ok: advisories.length === 0,
|
|
220
|
-
taskId: input.contract.taskId,
|
|
221
|
-
mode,
|
|
222
|
-
workflowId,
|
|
223
|
-
agents: [],
|
|
224
|
-
permissions,
|
|
225
|
-
mergePlan: createMergePlan([]),
|
|
226
|
-
warnings,
|
|
227
|
-
advisories,
|
|
228
|
-
rationale: "Minimal mode does not force dynamic agents by default.",
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const selectedLenses = selectLenses(
|
|
233
|
-
mode,
|
|
234
|
-
requestedLenses,
|
|
235
|
-
workflowId,
|
|
236
|
-
input.contract.route.requiredReview,
|
|
237
|
-
);
|
|
238
|
-
const agents = selectedLenses.map((lens) => ({
|
|
239
|
-
profile: createDynamicProfile(lens, input.contract, permissions),
|
|
240
|
-
lens,
|
|
241
|
-
reason: createLensReason(lens, mode, workflowId),
|
|
242
|
-
plannedOnly: true as const,
|
|
243
|
-
}));
|
|
244
|
-
|
|
245
|
-
if (mode === "standard" && agents.length > 1) {
|
|
246
|
-
warnings.push("Standard mode advisory selected more than one reviewer/triager.");
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
return {
|
|
250
|
-
ok: advisories.length === 0,
|
|
251
|
-
taskId: input.contract.taskId,
|
|
252
|
-
mode,
|
|
253
|
-
workflowId,
|
|
254
|
-
agents,
|
|
255
|
-
permissions,
|
|
256
|
-
mergePlan: createMergePlan(agents),
|
|
257
|
-
warnings,
|
|
258
|
-
advisories,
|
|
259
|
-
rationale:
|
|
260
|
-
agents.length === 0
|
|
261
|
-
? "No dynamic agents selected; deterministic verification may be sufficient."
|
|
262
|
-
: `Selected ${agents.length} planned-only agent(s) based on mode/workflow/lenses.`,
|
|
263
|
-
};
|
|
264
|
-
}
|
|
265
|
-
|
|
266
157
|
export function loadAgentContextDryRun(profile: AgentProfile): AgentContextDryRunBundle {
|
|
267
158
|
const parsed = parseAgentProfile(profile);
|
|
268
159
|
return {
|
|
@@ -376,47 +267,6 @@ export function mergeReviewFindings(outputs: ReviewFindingOutput[]): {
|
|
|
376
267
|
return { acceptedFindings: [...byKey.values()], unresolvedConflicts: conflicts };
|
|
377
268
|
}
|
|
378
269
|
|
|
379
|
-
export function formatAgentComposeDryRun(plan: AgentComposeDryRunPlan): string {
|
|
380
|
-
return [
|
|
381
|
-
"EvoDev agent compose dry-run",
|
|
382
|
-
"",
|
|
383
|
-
`Task: ${plan.taskId}`,
|
|
384
|
-
`Mode: ${plan.mode ?? "not routed"}`,
|
|
385
|
-
`Workflow: ${plan.workflowId ?? "none"}`,
|
|
386
|
-
`Rationale: ${plan.rationale}`,
|
|
387
|
-
"No-write/no-spawn: true",
|
|
388
|
-
"Privacy: local-private, metadata-only, raw prompts/source/secrets/logs forbidden",
|
|
389
|
-
"Agents:",
|
|
390
|
-
...(plan.agents.length === 0
|
|
391
|
-
? [" - none"]
|
|
392
|
-
: plan.agents.map(
|
|
393
|
-
(agent) =>
|
|
394
|
-
` - ${agent.profile.id} [${agent.lens}] traits=${agent.profile.traits.expertise.join(",")} schema=${agent.profile.output.schema} plannedOnly=${agent.plannedOnly}`,
|
|
395
|
-
)),
|
|
396
|
-
"Inputs:",
|
|
397
|
-
...(plan.agents.length === 0
|
|
398
|
-
? [" - none"]
|
|
399
|
-
: [
|
|
400
|
-
` - required: ${plan.agents[0].profile.inputs.required.join(",")}`,
|
|
401
|
-
` - optional: ${plan.agents[0].profile.inputs.optional.join(",")}`,
|
|
402
|
-
` - forbidden: ${plan.agents[0].profile.inputs.forbidden.join(",")}`,
|
|
403
|
-
]),
|
|
404
|
-
"Permissions:",
|
|
405
|
-
...Object.entries(plan.permissions).map(([key, value]) => ` - ${key}: ${value}`),
|
|
406
|
-
"Merge plan:",
|
|
407
|
-
` - strategy: ${plan.mergePlan.strategy}`,
|
|
408
|
-
` - outputSchema: ${plan.mergePlan.outputSchema ?? "none"}`,
|
|
409
|
-
"Warnings:",
|
|
410
|
-
...(plan.warnings.length === 0
|
|
411
|
-
? [" - none"]
|
|
412
|
-
: plan.warnings.map((warning) => ` - ${warning}`)),
|
|
413
|
-
"Advisories:",
|
|
414
|
-
...(plan.advisories.length === 0
|
|
415
|
-
? [" - none"]
|
|
416
|
-
: plan.advisories.map((advisory) => ` - ${advisory}`)),
|
|
417
|
-
].join("\n");
|
|
418
|
-
}
|
|
419
|
-
|
|
420
270
|
export function formatAgentContextDryRun(bundle: AgentContextDryRunBundle): string {
|
|
421
271
|
return [
|
|
422
272
|
"EvoDev agent load-context dry-run",
|
|
@@ -436,116 +286,6 @@ export function formatAgentContextDryRun(bundle: AgentContextDryRunBundle): stri
|
|
|
436
286
|
].join("\n");
|
|
437
287
|
}
|
|
438
288
|
|
|
439
|
-
function createDynamicProfile(
|
|
440
|
-
lens: AgentLens,
|
|
441
|
-
contract: TaskContract,
|
|
442
|
-
permissions: AgentPermissions,
|
|
443
|
-
): AgentProfile {
|
|
444
|
-
return {
|
|
445
|
-
version: 1,
|
|
446
|
-
id: `dynamic-${lens}-${contract.taskId}`.slice(0, 100),
|
|
447
|
-
name: `Dynamic ${lens} reviewer`,
|
|
448
|
-
persistence: "dynamic",
|
|
449
|
-
description: `Task-scoped ${lens} profile for metadata-only dry-run planning.`,
|
|
450
|
-
traits: {
|
|
451
|
-
expertise: LENS_TO_EXPERTISE[lens],
|
|
452
|
-
stance: ["skeptical-reviewer"],
|
|
453
|
-
approach: ["evidence-first", "advisory"],
|
|
454
|
-
domain: ["software-rd"],
|
|
455
|
-
},
|
|
456
|
-
inputs: {
|
|
457
|
-
required: ["taskContract", "scope", "evidenceSummary"],
|
|
458
|
-
optional: ["workflowPlan"],
|
|
459
|
-
forbidden: FORBIDDEN_INPUTS,
|
|
460
|
-
},
|
|
461
|
-
permissions,
|
|
462
|
-
output: {
|
|
463
|
-
schema: "review-findings-v1",
|
|
464
|
-
requiredFields: ["summary", "findings", "confidence", "evidenceRefs"],
|
|
465
|
-
},
|
|
466
|
-
privacy: {
|
|
467
|
-
classification: "local-private",
|
|
468
|
-
metadataOnly: true,
|
|
469
|
-
rawPromptStored: false,
|
|
470
|
-
sourceContentStored: false,
|
|
471
|
-
},
|
|
472
|
-
};
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
function selectLenses(
|
|
476
|
-
mode: TaskExecutionMode | null,
|
|
477
|
-
requested: AgentLens[],
|
|
478
|
-
workflowId: string | null,
|
|
479
|
-
requiredReview: string[],
|
|
480
|
-
): AgentLens[] {
|
|
481
|
-
if (requested.length > 0) return mode === "standard" ? requested.slice(0, 1) : requested;
|
|
482
|
-
if (mode === "standard") return requiredReview.length > 0 ? ["review"] : [];
|
|
483
|
-
if (mode === "rigorous") {
|
|
484
|
-
if (workflowId?.includes("security")) return ["security", "review"];
|
|
485
|
-
if (workflowId?.includes("release")) return ["release", "security"];
|
|
486
|
-
if (workflowId?.includes("migration")) return ["migration", "architecture"];
|
|
487
|
-
if (workflowId?.includes("architecture")) return ["architecture", "review"];
|
|
488
|
-
return requiredReview.length > 0 ? ["review", "qa"] : [];
|
|
489
|
-
}
|
|
490
|
-
return [];
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
function createMergePlan(agents: AgentPlannedInvocation[]): AgentMergePlan {
|
|
494
|
-
if (agents.length === 0)
|
|
495
|
-
return {
|
|
496
|
-
strategy: "none",
|
|
497
|
-
outputSchema: null,
|
|
498
|
-
dedupeBy: [],
|
|
499
|
-
conflictPolicy: [],
|
|
500
|
-
advisoryCategories: [],
|
|
501
|
-
};
|
|
502
|
-
if (agents.length === 1)
|
|
503
|
-
return {
|
|
504
|
-
strategy: "single-output",
|
|
505
|
-
outputSchema: agents[0].profile.output.schema,
|
|
506
|
-
dedupeBy: [],
|
|
507
|
-
conflictPolicy: [],
|
|
508
|
-
advisoryCategories: [],
|
|
509
|
-
};
|
|
510
|
-
return {
|
|
511
|
-
strategy: "dedupe-preserve-conflicts",
|
|
512
|
-
outputSchema: "review-findings-v1",
|
|
513
|
-
dedupeBy: ["category", "path", "line", "title"],
|
|
514
|
-
conflictPolicy: ["preserve dissent", "keep higher severity unless evidence refutes"],
|
|
515
|
-
advisoryCategories: ["security", "privacy", "release"],
|
|
516
|
-
};
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
function createLensReason(
|
|
520
|
-
lens: AgentLens,
|
|
521
|
-
mode: TaskExecutionMode | null,
|
|
522
|
-
workflowId: string | null,
|
|
523
|
-
): string {
|
|
524
|
-
return `${lens} lens selected for ${mode ?? "unrouted"} mode${workflowId ? ` and workflow ${workflowId}` : ""}.`;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
function collectPrivacyBoundaryAdvisories(
|
|
528
|
-
contract: TaskContract,
|
|
529
|
-
lenses: AgentLens[],
|
|
530
|
-
workflowId: string | null,
|
|
531
|
-
): string[] {
|
|
532
|
-
const text = [
|
|
533
|
-
contract.source.summary,
|
|
534
|
-
contract.currentState.summary,
|
|
535
|
-
contract.targetState.summary,
|
|
536
|
-
contract.route.rationale,
|
|
537
|
-
workflowId ?? "",
|
|
538
|
-
...contract.scope.allowedOperations,
|
|
539
|
-
...contract.scope.requiresUserConfirmation,
|
|
540
|
-
...lenses,
|
|
541
|
-
].join(" ");
|
|
542
|
-
return PRIVACY_ADVISORY_PATTERN.test(text)
|
|
543
|
-
? [
|
|
544
|
-
"Agent planning detected privacy/risky context requiring raw data, writes, commands, network, spawning, or memory.",
|
|
545
|
-
]
|
|
546
|
-
: [];
|
|
547
|
-
}
|
|
548
|
-
|
|
549
289
|
function validateRequiredObject(
|
|
550
290
|
output: unknown,
|
|
551
291
|
fields: string[],
|
|
@@ -569,10 +309,6 @@ function isKnownOutputSchema(value: unknown): value is AgentOutputSchemaId {
|
|
|
569
309
|
);
|
|
570
310
|
}
|
|
571
311
|
|
|
572
|
-
function dedupeLenses(lenses: AgentLens[]): AgentLens[] {
|
|
573
|
-
return [...new Set(lenses)];
|
|
574
|
-
}
|
|
575
|
-
|
|
576
312
|
function severityRank(severity: AgentFindingSeverity): number {
|
|
577
313
|
return { info: 0, low: 1, medium: 2, high: 3, critical: 4 }[severity];
|
|
578
314
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, isAbsolute, join, normalize, relative } from "node:path";
|
|
3
3
|
import { resolveEvoDevPaths } from "../config/paths.ts";
|
|
4
|
+
import { resolveProjectWorkspaceFromCwd } from "../projects/index.ts";
|
|
4
5
|
import { resolveTraceSessionKey, resolveTraceTeamContext } from "../runtime-logs/index.ts";
|
|
5
6
|
import { normalizeTimestamp, sha256Short } from "../utils/index.ts";
|
|
6
7
|
|
|
@@ -264,6 +265,11 @@ export async function recordCodeAgentTraceRefFromHook(
|
|
|
264
265
|
payload: input.payload,
|
|
265
266
|
environment,
|
|
266
267
|
});
|
|
268
|
+
const cwd = optionalString(input.payload.cwd);
|
|
269
|
+
const workspace =
|
|
270
|
+
team === null && cwd !== null
|
|
271
|
+
? await resolveProjectWorkspaceFromCwd({ homeDir: input.homeDir, cwd })
|
|
272
|
+
: null;
|
|
267
273
|
const source =
|
|
268
274
|
sessionIdFromPayload !== null || tracePathFromPayload !== null ? "hook-payload" : "environment";
|
|
269
275
|
|
|
@@ -271,10 +277,10 @@ export async function recordCodeAgentTraceRefFromHook(
|
|
|
271
277
|
target: input.target,
|
|
272
278
|
sessionKey: resolveTraceSessionKey(input.payload),
|
|
273
279
|
nativeSessionId,
|
|
274
|
-
projectKey: team?.projectKey ?? null,
|
|
280
|
+
projectKey: team?.projectKey ?? workspace?.projectKey ?? null,
|
|
275
281
|
runId: team?.runId ?? null,
|
|
276
282
|
roleId: team?.roleId ?? null,
|
|
277
|
-
cwd
|
|
283
|
+
cwd,
|
|
278
284
|
discoveredAt: input.now,
|
|
279
285
|
source,
|
|
280
286
|
tracePath: normalizedPath,
|
package/src/daemon/index.ts
CHANGED
|
@@ -251,8 +251,6 @@ export async function handleDaemonRequest(
|
|
|
251
251
|
}
|
|
252
252
|
if (input.method !== "GET") return notFound(warnings);
|
|
253
253
|
|
|
254
|
-
if (input.path === "/tasks")
|
|
255
|
-
return ok(await collectTaskSummaries(input.homeDir, warnings), warnings);
|
|
256
254
|
if (input.path === "/observability/events")
|
|
257
255
|
return ok(await collectObservabilitySummaries(input.homeDir, warnings), warnings);
|
|
258
256
|
if (input.path === "/memory/candidates")
|
|
@@ -407,33 +405,6 @@ function isAllowedLocalOrigin(origin: string | null): boolean {
|
|
|
407
405
|
}
|
|
408
406
|
}
|
|
409
407
|
|
|
410
|
-
async function collectTaskSummaries(homeDir: string, warnings: string[]): Promise<unknown[]> {
|
|
411
|
-
const root = join(homeDir, ".evodev", "STATE", "tasks");
|
|
412
|
-
if (!(await pathExists(root))) {
|
|
413
|
-
warnings.push("Task store not found; returning empty tasks.");
|
|
414
|
-
return [];
|
|
415
|
-
}
|
|
416
|
-
const contracts = await collectNamedFiles(root, "contract.json");
|
|
417
|
-
const summaries: unknown[] = [];
|
|
418
|
-
for (const file of contracts) {
|
|
419
|
-
try {
|
|
420
|
-
const contract = JSON.parse(await readFile(file, "utf8"));
|
|
421
|
-
summaries.push(
|
|
422
|
-
sanitizeMetadata({
|
|
423
|
-
taskId: contract.taskId,
|
|
424
|
-
status: contract.status,
|
|
425
|
-
mode: contract.route?.mode ?? null,
|
|
426
|
-
workflowId: contract.route?.workflowId ?? null,
|
|
427
|
-
verificationStatus: contract.verification?.status ?? null,
|
|
428
|
-
}),
|
|
429
|
-
);
|
|
430
|
-
} catch {
|
|
431
|
-
warnings.push(`Skipped unreadable task contract: ${file}`);
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
return summaries;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
408
|
async function collectObservabilitySummaries(
|
|
438
409
|
homeDir: string,
|
|
439
410
|
warnings: string[],
|
|
@@ -841,17 +812,6 @@ function describeError(error: unknown): string {
|
|
|
841
812
|
return error instanceof Error ? error.message : String(error);
|
|
842
813
|
}
|
|
843
814
|
|
|
844
|
-
async function collectNamedFiles(root: string, name: string): Promise<string[]> {
|
|
845
|
-
const entries = await readdir(root, { withFileTypes: true });
|
|
846
|
-
const files: string[] = [];
|
|
847
|
-
for (const entry of entries) {
|
|
848
|
-
const path = join(root, entry.name);
|
|
849
|
-
if (entry.isDirectory()) files.push(...(await collectNamedFiles(path, name)));
|
|
850
|
-
else if (entry.isFile() && entry.name === name) files.push(path);
|
|
851
|
-
}
|
|
852
|
-
return files;
|
|
853
|
-
}
|
|
854
|
-
|
|
855
815
|
function ok(data: unknown, warnings: string[]): { status: number; body: DaemonResponseBody } {
|
|
856
816
|
return { status: 200, body: { ok: true, data: sanitizeMetadata(data), warnings } };
|
|
857
817
|
}
|
|
@@ -411,29 +411,88 @@ export async function updateEvolutionRepoProposalReviewState(input: {
|
|
|
411
411
|
},
|
|
412
412
|
};
|
|
413
413
|
validateEvolutionRepoProposal(next);
|
|
414
|
-
await
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
)
|
|
414
|
+
await writeRepoProposalAndIndex({ homeDir: input.homeDir, proposal: next, changedAt });
|
|
415
|
+
return { path, record: next, changed: true };
|
|
416
|
+
},
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
export async function markEvolutionRepoProposalApplied(input: {
|
|
421
|
+
homeDir: string;
|
|
422
|
+
proposalId: string;
|
|
423
|
+
projectKey?: string;
|
|
424
|
+
expectedReviewState?: EvolutionRepoProposal["reviewState"];
|
|
425
|
+
now?: string | Date;
|
|
426
|
+
}): Promise<{ path: string; record: EvolutionRepoProposal; changed: boolean }> {
|
|
427
|
+
return await withEvolutionReviewDecisionLock(
|
|
428
|
+
{ homeDir: input.homeDir, kind: "repo-proposal", itemId: input.proposalId },
|
|
429
|
+
async () => {
|
|
430
|
+
const record = await readEvolutionRepoProposalById(input);
|
|
431
|
+
if (!hasConcreteRepoProposalChanges(record)) {
|
|
432
|
+
throw new Error("Repo proposal has no concrete repository changes to apply.");
|
|
433
|
+
}
|
|
434
|
+
if (
|
|
435
|
+
input.expectedReviewState !== undefined &&
|
|
436
|
+
record.reviewState !== input.expectedReviewState
|
|
437
|
+
) {
|
|
438
|
+
throw new Error("Repo proposal review state changed before it was marked applied.");
|
|
439
|
+
}
|
|
440
|
+
const paths = resolveEvolutionPaths({
|
|
441
|
+
homeDir: input.homeDir,
|
|
442
|
+
projectKey: record.projectKey,
|
|
443
|
+
runId: record.provenance.runId,
|
|
444
|
+
});
|
|
445
|
+
const path = join(paths.repoProposalsDir, `${record.id}.json`);
|
|
446
|
+
if (record.reviewState === "applied") return { path, record, changed: false };
|
|
447
|
+
if (record.reviewState !== "accepted") {
|
|
448
|
+
throw new Error("Only accepted repo proposals can be marked applied.");
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const changedAt = normalizeTimestamp(input.now);
|
|
452
|
+
const next: EvolutionRepoProposal = {
|
|
453
|
+
...record,
|
|
454
|
+
reviewState: "applied",
|
|
455
|
+
reviewStateChangedAt: changedAt,
|
|
456
|
+
};
|
|
457
|
+
validateEvolutionRepoProposal(next);
|
|
458
|
+
await writeRepoProposalAndIndex({ homeDir: input.homeDir, proposal: next, changedAt });
|
|
432
459
|
return { path, record: next, changed: true };
|
|
433
460
|
},
|
|
434
461
|
);
|
|
435
462
|
}
|
|
436
463
|
|
|
464
|
+
async function writeRepoProposalAndIndex(input: {
|
|
465
|
+
homeDir: string;
|
|
466
|
+
proposal: EvolutionRepoProposal;
|
|
467
|
+
changedAt: string;
|
|
468
|
+
}): Promise<void> {
|
|
469
|
+
const paths = resolveEvolutionPaths({
|
|
470
|
+
homeDir: input.homeDir,
|
|
471
|
+
projectKey: input.proposal.projectKey,
|
|
472
|
+
runId: input.proposal.provenance.runId,
|
|
473
|
+
});
|
|
474
|
+
await writeJson(join(paths.repoProposalsDir, `${input.proposal.id}.json`), input.proposal, {
|
|
475
|
+
overwrite: true,
|
|
476
|
+
});
|
|
477
|
+
const allRunProposals = await readJsonFiles(paths.repoProposalsDir, parseRepoProposal);
|
|
478
|
+
await writeJson(
|
|
479
|
+
paths.repoProposalsIndexPath,
|
|
480
|
+
{
|
|
481
|
+
schemaVersion: 1,
|
|
482
|
+
projectKey: input.proposal.projectKey,
|
|
483
|
+
runId: input.proposal.provenance.runId,
|
|
484
|
+
updatedAt: input.changedAt,
|
|
485
|
+
proposals: allRunProposals.map((proposal) => ({
|
|
486
|
+
id: proposal.id,
|
|
487
|
+
kind: proposal.kind,
|
|
488
|
+
title: proposal.title,
|
|
489
|
+
reviewState: proposal.reviewState,
|
|
490
|
+
})),
|
|
491
|
+
},
|
|
492
|
+
{ overwrite: true },
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
|
|
437
496
|
function parseKnowledgeReviewHistoryRecord(value: unknown): EvolutionKnowledgeReviewHistoryRecord {
|
|
438
497
|
if (!isRecord(value)) throw new Error("Knowledge review history record must be an object.");
|
|
439
498
|
const allowedKeys = new Set([
|
|
@@ -55,6 +55,34 @@ export async function listSessionEvidenceSegments(input: {
|
|
|
55
55
|
return segments.sort((left, right) => right.createdAt.localeCompare(left.createdAt));
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
export async function listSessionMemoryStates(input: {
|
|
59
|
+
homeDir: string;
|
|
60
|
+
projectKey?: string;
|
|
61
|
+
}): Promise<SessionMemoryStateV1[]> {
|
|
62
|
+
const rootDir = join(resolveEvoDevPaths(input.homeDir).stateDir, "session-memory");
|
|
63
|
+
const projectDirs =
|
|
64
|
+
input.projectKey === undefined ? await listDirectoryNames(rootDir) : [input.projectKey];
|
|
65
|
+
const states: SessionMemoryStateV1[] = [];
|
|
66
|
+
for (const projectKey of projectDirs) {
|
|
67
|
+
const projectDir = join(rootDir, projectKey);
|
|
68
|
+
for (const sessionKey of await listDirectoryNames(projectDir)) {
|
|
69
|
+
try {
|
|
70
|
+
const state = parseSessionMemoryState(
|
|
71
|
+
JSON.parse(await readFile(join(projectDir, sessionKey, "state.json"), "utf8")) as unknown,
|
|
72
|
+
);
|
|
73
|
+
if (state.projectKey === projectKey && state.sessionKey === sessionKey) states.push(state);
|
|
74
|
+
} catch {
|
|
75
|
+
// Invalid local state is omitted from listings and remains available for diagnostics.
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return states.sort(
|
|
80
|
+
(left, right) =>
|
|
81
|
+
right.updatedAt.localeCompare(left.updatedAt) ||
|
|
82
|
+
left.sessionKey.localeCompare(right.sessionKey),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
58
86
|
export async function readSessionEvidenceSegment(input: {
|
|
59
87
|
homeDir: string;
|
|
60
88
|
projectKey: string;
|
|
@@ -241,6 +269,28 @@ function parseSessionEvidenceSegment(value: unknown): SessionEvidenceSegmentV1 {
|
|
|
241
269
|
return segment as SessionEvidenceSegmentV1;
|
|
242
270
|
}
|
|
243
271
|
|
|
272
|
+
function parseSessionMemoryState(value: unknown): SessionMemoryStateV1 {
|
|
273
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
274
|
+
throw new SessionMemoryEvidenceError("invalid", "Session Memory state must be an object.");
|
|
275
|
+
}
|
|
276
|
+
const state = value as Partial<SessionMemoryStateV1>;
|
|
277
|
+
if (
|
|
278
|
+
state.schemaVersion !== 1 ||
|
|
279
|
+
state.kind !== "session-memory-state" ||
|
|
280
|
+
typeof state.projectKey !== "string" ||
|
|
281
|
+
typeof state.sessionKey !== "string" ||
|
|
282
|
+
!(state.runId === null || typeof state.runId === "string") ||
|
|
283
|
+
!(state.roleId === null || typeof state.roleId === "string") ||
|
|
284
|
+
typeof state.createdAt !== "string" ||
|
|
285
|
+
typeof state.updatedAt !== "string" ||
|
|
286
|
+
typeof state.counters !== "object" ||
|
|
287
|
+
state.counters === null
|
|
288
|
+
) {
|
|
289
|
+
throw new SessionMemoryEvidenceError("invalid", "Session Memory state fields are invalid.");
|
|
290
|
+
}
|
|
291
|
+
return state as SessionMemoryStateV1;
|
|
292
|
+
}
|
|
293
|
+
|
|
244
294
|
async function listDirectoryNames(path: string): Promise<string[]> {
|
|
245
295
|
try {
|
|
246
296
|
return (await readdir(path, { withFileTypes: true }))
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
+
import { resolveProjectWorkspaceFromCwd } from "../../../projects/index.ts";
|
|
2
3
|
import {
|
|
3
4
|
resolveProjectLogKey,
|
|
4
5
|
resolveTraceSessionKey,
|
|
@@ -42,9 +43,15 @@ export async function updateSessionMemoryFromHook(
|
|
|
42
43
|
environment: input.environment,
|
|
43
44
|
payload: input.rawPayload,
|
|
44
45
|
});
|
|
46
|
+
const cwd = optionalString(input.rawPayload.cwd);
|
|
47
|
+
const workspace =
|
|
48
|
+
team === null && cwd !== null
|
|
49
|
+
? await resolveProjectWorkspaceFromCwd({ homeDir: input.homeDir, cwd })
|
|
50
|
+
: null;
|
|
45
51
|
const projectKey = sanitizeStorageId(
|
|
46
52
|
team?.projectKey ??
|
|
47
|
-
|
|
53
|
+
workspace?.projectKey ??
|
|
54
|
+
resolveProjectLogKey(input.homeDir, cwd ?? input.homeDir),
|
|
48
55
|
"project",
|
|
49
56
|
);
|
|
50
57
|
const sessionKey = sanitizeStorageId(resolveTraceSessionKey(input.rawPayload), "session");
|
|
@@ -31,7 +31,6 @@ export interface LearningCandidate {
|
|
|
31
31
|
};
|
|
32
32
|
provenance: {
|
|
33
33
|
taskId: string | null;
|
|
34
|
-
taskContractRef: string | null;
|
|
35
34
|
workflowRunId: string | null;
|
|
36
35
|
evidenceRefs: string[];
|
|
37
36
|
sourceType:
|
|
@@ -198,7 +197,6 @@ export function createLearningCandidate(input: {
|
|
|
198
197
|
},
|
|
199
198
|
provenance: {
|
|
200
199
|
taskId: sanitizeNullableId(input.provenance.taskId),
|
|
201
|
-
taskContractRef: sanitizeNullableText(input.provenance.taskContractRef),
|
|
202
200
|
workflowRunId: sanitizeNullableId(input.provenance.workflowRunId),
|
|
203
201
|
evidenceRefs: input.provenance.evidenceRefs.map(sanitizeText),
|
|
204
202
|
sourceType: input.provenance.sourceType,
|
|
@@ -626,13 +624,9 @@ async function parseJsonOrJsonlFile<T>(
|
|
|
626
624
|
}
|
|
627
625
|
|
|
628
626
|
function candidatePathFields(candidate: LearningCandidate): Array<[string, string]> {
|
|
629
|
-
return
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
`provenance.evidenceRefs[${index}]`,
|
|
633
|
-
ref,
|
|
634
|
-
]),
|
|
635
|
-
].filter((entry): entry is [string, string] => typeof entry[1] === "string");
|
|
627
|
+
return candidate.provenance.evidenceRefs
|
|
628
|
+
.map((ref, index): [string, string] => [`provenance.evidenceRefs[${index}]`, ref])
|
|
629
|
+
.filter((entry): entry is [string, string] => typeof entry[1] === "string");
|
|
636
630
|
}
|
|
637
631
|
|
|
638
632
|
function isCandidateStale(candidate: LearningCandidate, now: string | undefined): boolean {
|