@evo-dev/core 0.0.1-alpha.4 → 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 +3 -39
- package/dist/index.js +1385 -2017
- 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 +367 -38
- package/src/evolution/evidence/session-memory/index.ts +1 -0
- package/src/evolution/evidence/session-memory/segment.ts +2 -2
- package/src/evolution/evidence/session-memory/storage.ts +173 -2
- package/src/evolution/evidence/session-memory/types.ts +3 -3
- package/src/evolution/evidence/session-memory/updater.ts +8 -1
- package/src/evolution/index.ts +4 -0
- package/src/evolution/knowledge/index.ts +9 -74
- package/src/evolution/paths.ts +3 -0
- package/src/evolution/processor/distillation.ts +42 -33
- package/src/evolution/processor/process.ts +3 -74
- package/src/evolution/review/index.ts +3 -9
- package/src/evolution/schema.ts +23 -2
- package/src/evolution/shared.ts +62 -2
- 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
|
@@ -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");
|
package/src/evolution/index.ts
CHANGED
|
@@ -6,7 +6,11 @@ export * from "./candidates/index.ts";
|
|
|
6
6
|
export * from "./triggers/index.ts";
|
|
7
7
|
export * from "./formatters.ts";
|
|
8
8
|
export {
|
|
9
|
+
createEvolutionRepoProposal,
|
|
10
|
+
hasConcreteRepoProposalChanges,
|
|
11
|
+
sanitizeProposedChange,
|
|
9
12
|
validateEvolutionDistillationBatch,
|
|
10
13
|
validateEvolutionEvidenceWindow,
|
|
11
14
|
validateEvolutionKnowledgeRecord,
|
|
15
|
+
validateEvolutionRepoProposal,
|
|
12
16
|
} from "./shared.ts";
|
|
@@ -1656,10 +1656,10 @@ export function decideOkfKnowledgeCandidate(
|
|
|
1656
1656
|
if (highRiskRequiresHuman) {
|
|
1657
1657
|
return {
|
|
1658
1658
|
...sanitized,
|
|
1659
|
-
decision: "
|
|
1659
|
+
decision: "no_write",
|
|
1660
1660
|
scores,
|
|
1661
|
-
reviewState: "
|
|
1662
|
-
decisionReason: "
|
|
1661
|
+
reviewState: "auto-stored/unreviewed",
|
|
1662
|
+
decisionReason: "No write: high-risk knowledge is not eligible for automatic activation.",
|
|
1663
1663
|
};
|
|
1664
1664
|
}
|
|
1665
1665
|
|
|
@@ -1677,12 +1677,12 @@ export function decideOkfKnowledgeCandidate(
|
|
|
1677
1677
|
if (reusable) {
|
|
1678
1678
|
return {
|
|
1679
1679
|
...sanitized,
|
|
1680
|
-
decision: "
|
|
1680
|
+
decision: "no_write",
|
|
1681
1681
|
scores,
|
|
1682
|
-
reviewState: "
|
|
1682
|
+
reviewState: "auto-stored/unreviewed",
|
|
1683
1683
|
decisionReason:
|
|
1684
1684
|
conflict?.reason ??
|
|
1685
|
-
|
|
1685
|
+
explainAutomaticNoWriteDecision(sanitized, scores, hasVerification, hasScopeTags),
|
|
1686
1686
|
};
|
|
1687
1687
|
}
|
|
1688
1688
|
|
|
@@ -1746,12 +1746,7 @@ export async function activateOkfKnowledgePlan(input: {
|
|
|
1746
1746
|
continue;
|
|
1747
1747
|
}
|
|
1748
1748
|
if (candidate.decision === "needs-human") {
|
|
1749
|
-
|
|
1750
|
-
homeDir: input.homeDir,
|
|
1751
|
-
plan: input.plan,
|
|
1752
|
-
candidate,
|
|
1753
|
-
});
|
|
1754
|
-
needsHumanCandidates.push(candidate.id);
|
|
1749
|
+
skippedCandidates.push(candidate.id);
|
|
1755
1750
|
continue;
|
|
1756
1751
|
}
|
|
1757
1752
|
if (candidate.targetStore !== "okf") {
|
|
@@ -3165,66 +3160,6 @@ function createOverlayUpdates(
|
|
|
3165
3160
|
];
|
|
3166
3161
|
}
|
|
3167
3162
|
|
|
3168
|
-
async function writeNeedsHumanKnowledgeCandidate(input: {
|
|
3169
|
-
homeDir: string;
|
|
3170
|
-
plan: OkfKnowledgePlan;
|
|
3171
|
-
candidate: OkfKnowledgePlanCandidate;
|
|
3172
|
-
}): Promise<string> {
|
|
3173
|
-
const root = join(resolveEvoDevPaths(input.homeDir).stateDir, "evolution");
|
|
3174
|
-
const targetDir = join(
|
|
3175
|
-
root,
|
|
3176
|
-
sanitizePlanStorageId("projectKey", input.plan.projectKey),
|
|
3177
|
-
sanitizePlanStorageId("runId", input.plan.runId),
|
|
3178
|
-
"review-candidates",
|
|
3179
|
-
);
|
|
3180
|
-
const targetPath = join(
|
|
3181
|
-
targetDir,
|
|
3182
|
-
`${sanitizePlanStorageId("candidateId", input.candidate.id)}.json`,
|
|
3183
|
-
);
|
|
3184
|
-
assertPathDescendant(root, targetPath, "reviewCandidatePath");
|
|
3185
|
-
const candidateSnapshot = sanitizeReviewQueueValue(input.candidate);
|
|
3186
|
-
await writeJson(
|
|
3187
|
-
targetPath,
|
|
3188
|
-
{
|
|
3189
|
-
schemaVersion: 1,
|
|
3190
|
-
kind: "evolution-review-candidate",
|
|
3191
|
-
id: input.candidate.id,
|
|
3192
|
-
projectKey: input.plan.projectKey,
|
|
3193
|
-
runId: input.plan.runId,
|
|
3194
|
-
createdAt: input.plan.createdAt,
|
|
3195
|
-
candidateKind: input.candidate.kind,
|
|
3196
|
-
title: sanitizeOkfText(input.candidate.title),
|
|
3197
|
-
targetStore: input.candidate.targetStore,
|
|
3198
|
-
targetPath: sanitizeOkfText(input.candidate.targetPath),
|
|
3199
|
-
stableKey: sanitizeOkfText(input.candidate.stableKey),
|
|
3200
|
-
reviewState: "needs-human",
|
|
3201
|
-
reasons: [sanitizeOkfText(input.candidate.decisionReason)],
|
|
3202
|
-
candidate: candidateSnapshot,
|
|
3203
|
-
provenance: {
|
|
3204
|
-
runId: input.plan.runId,
|
|
3205
|
-
evidenceWindowId: input.plan.evidenceWindowId,
|
|
3206
|
-
evidenceRefs: input.candidate.evidenceRefs.map(sanitizeOkfText),
|
|
3207
|
-
createdBy: "evodev",
|
|
3208
|
-
rawLogsStored: false,
|
|
3209
|
-
rawPromptsStored: false,
|
|
3210
|
-
sourceDumpsStored: false,
|
|
3211
|
-
rawCommandOutputStored: false,
|
|
3212
|
-
},
|
|
3213
|
-
privacy: {
|
|
3214
|
-
classification: "local-private",
|
|
3215
|
-
rawPromptsStored: false,
|
|
3216
|
-
rawLogsStored: false,
|
|
3217
|
-
sourceDumpsStored: false,
|
|
3218
|
-
rawCommandOutputStored: false,
|
|
3219
|
-
secretsStored: false,
|
|
3220
|
-
internalLinksStored: false,
|
|
3221
|
-
},
|
|
3222
|
-
},
|
|
3223
|
-
{ overwrite: true },
|
|
3224
|
-
);
|
|
3225
|
-
return targetPath;
|
|
3226
|
-
}
|
|
3227
|
-
|
|
3228
3163
|
function sanitizeOkfPlanCandidate(candidate: OkfKnowledgePlanCandidate): OkfKnowledgePlanCandidate {
|
|
3229
3164
|
return sanitizeReviewQueueValue(candidate) as OkfKnowledgePlanCandidate;
|
|
3230
3165
|
}
|
|
@@ -3292,7 +3227,7 @@ function hasHighRiskHumanReviewSignal(candidate: unknown): boolean {
|
|
|
3292
3227
|
return HUMAN_REVIEW_DOMAIN_PATTERN.test(values.join(" "));
|
|
3293
3228
|
}
|
|
3294
3229
|
|
|
3295
|
-
function
|
|
3230
|
+
function explainAutomaticNoWriteDecision(
|
|
3296
3231
|
candidate: OkfKnowledgePlanCandidate,
|
|
3297
3232
|
scores: OkfKnowledgeCandidateScores,
|
|
3298
3233
|
hasVerification: boolean,
|
|
@@ -3308,7 +3243,7 @@ function explainNeedsHumanDecision(
|
|
|
3308
3243
|
if (scores.privacyRisk > 2) reasons.push("privacy risk is above auto-accept");
|
|
3309
3244
|
if (scores.stability < 3) reasons.push("stability is below auto-accept");
|
|
3310
3245
|
if (scores.duplicationRisk > 2) reasons.push("duplication risk is above auto-accept");
|
|
3311
|
-
return `
|
|
3246
|
+
return `No write: ${reasons.join("; ") || "auto-accept gates were not all satisfied"}.`;
|
|
3312
3247
|
}
|
|
3313
3248
|
|
|
3314
3249
|
function resolveCandidateReviewStateForWrite(
|
package/src/evolution/paths.ts
CHANGED
|
@@ -12,12 +12,14 @@ export function resolveEvolutionPaths(input: {
|
|
|
12
12
|
const projectKey = sanitizeStorageId("projectKey", input.projectKey);
|
|
13
13
|
const runId = sanitizeStorageId("runId", input.runId);
|
|
14
14
|
const evolutionStateDir = join(paths.stateDir, "evolution");
|
|
15
|
+
const projectStateDir = join(evolutionStateDir, projectKey);
|
|
15
16
|
const runStateDir = join(evolutionStateDir, projectKey, runId);
|
|
16
17
|
const repoProposalsDir = join(runStateDir, "proposals");
|
|
17
18
|
const reviewCandidatesDir = join(runStateDir, "review-candidates");
|
|
18
19
|
const knowledgeProjectDir = join(paths.knowledgeDir, projectKey);
|
|
19
20
|
const evosCasesProjectDir = join(paths.evosCasesDir, projectKey);
|
|
20
21
|
assertPathDescendant(evolutionStateDir, runStateDir, "runStateDir");
|
|
22
|
+
assertPathDescendant(evolutionStateDir, projectStateDir, "projectStateDir");
|
|
21
23
|
assertPathDescendant(evolutionStateDir, reviewCandidatesDir, "reviewCandidatesDir");
|
|
22
24
|
assertPathDescendant(paths.knowledgeDir, knowledgeProjectDir, "knowledgeProjectDir");
|
|
23
25
|
assertPathDescendant(paths.evosCasesDir, evosCasesProjectDir, "evosCasesProjectDir");
|
|
@@ -35,6 +37,7 @@ export function resolveEvolutionPaths(input: {
|
|
|
35
37
|
knowledgeProjectDir,
|
|
36
38
|
knowledgeRecordsDir: join(knowledgeProjectDir, "records"),
|
|
37
39
|
knowledgeIndexPath: join(knowledgeProjectDir, "index.json"),
|
|
40
|
+
knowledgeReviewHistoryPath: join(projectStateDir, "knowledge-review-history.jsonl"),
|
|
38
41
|
evosCasesProjectDir,
|
|
39
42
|
evosIndexPath: paths.evosIndexPath,
|
|
40
43
|
};
|
|
@@ -26,8 +26,8 @@ import {
|
|
|
26
26
|
collectRoleIds,
|
|
27
27
|
createEvolutionEvosCase,
|
|
28
28
|
createEvolutionKnowledgeRecord,
|
|
29
|
-
createEvolutionRepoProposal,
|
|
30
29
|
createPrivacyFields,
|
|
30
|
+
hasConcreteRepoProposalChanges,
|
|
31
31
|
listDirectoryNames,
|
|
32
32
|
parseEvosCase,
|
|
33
33
|
parseKnowledgeRecord,
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
validateEvolutionDistillationBatch,
|
|
37
37
|
validateEvolutionEvidenceWindow,
|
|
38
38
|
validateEvolutionEvosCase,
|
|
39
|
+
validateEvolutionRepoProposal,
|
|
39
40
|
} from "../shared.ts";
|
|
40
41
|
import { chooseEvosTriggerKind, createTriggerSummary } from "../triggers/classification.ts";
|
|
41
42
|
|
|
@@ -97,10 +98,7 @@ export function createEvolutionDistillationBatch(input: {
|
|
|
97
98
|
relations: {
|
|
98
99
|
relatedKnowledgeIds: [],
|
|
99
100
|
evosCaseIds: [createStableId("evo", [input.evidenceWindow.id, "case"])],
|
|
100
|
-
proposalIds:
|
|
101
|
-
failedSignals.length === 0
|
|
102
|
-
? []
|
|
103
|
-
: [createStableId("proposal", [input.evidenceWindow.id, "engineering-practice"])],
|
|
101
|
+
proposalIds: [],
|
|
104
102
|
supersedes: [],
|
|
105
103
|
},
|
|
106
104
|
privacy: createPrivacyFields(),
|
|
@@ -146,34 +144,9 @@ export function createEvolutionDistillationBatch(input: {
|
|
|
146
144
|
}),
|
|
147
145
|
];
|
|
148
146
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
: [
|
|
153
|
-
createEvolutionRepoProposal({
|
|
154
|
-
id: createStableId("proposal", [input.evidenceWindow.id, "engineering-practice"]),
|
|
155
|
-
kind: "engineering-practice",
|
|
156
|
-
projectKey: input.evidenceWindow.projectKey,
|
|
157
|
-
title: `Review repeated failure signals for ${input.evidenceWindow.runId}`,
|
|
158
|
-
summary:
|
|
159
|
-
"Failure or issue signals appeared in redacted run evidence; review whether repository rules, tests, skills, or role agents need improvement.",
|
|
160
|
-
rationale:
|
|
161
|
-
"EvoDev stores this as a proposal only. It must not modify the user repository until an explicit apply command is implemented and invoked.",
|
|
162
|
-
roleTags: roleIds,
|
|
163
|
-
tags: ["proposal", "failure-signal"],
|
|
164
|
-
reviewState: "pending",
|
|
165
|
-
confidence: "low",
|
|
166
|
-
targetRepoPath: null,
|
|
167
|
-
plannedFiles: [],
|
|
168
|
-
apply: {
|
|
169
|
-
autoApply: false,
|
|
170
|
-
requiresExplicitCommand: true,
|
|
171
|
-
rollbackPlan: "No repository files are changed by this proposal.",
|
|
172
|
-
},
|
|
173
|
-
provenance: createProvenance(input.evidenceWindow, createdAt, sourceRefs),
|
|
174
|
-
privacy: createPrivacyFields(),
|
|
175
|
-
}),
|
|
176
|
-
];
|
|
147
|
+
// Failure metadata is evidence, not a repository change proposal. A proposal is only valid
|
|
148
|
+
// after another stage supplies concrete target files and proposed changes.
|
|
149
|
+
const repoProposals: EvolutionRepoProposal[] = [];
|
|
177
150
|
|
|
178
151
|
const batch: EvolutionDistillationBatch = {
|
|
179
152
|
schemaVersion: 1,
|
|
@@ -302,6 +275,42 @@ export async function writeEvolutionDistillationBatch(input: {
|
|
|
302
275
|
};
|
|
303
276
|
}
|
|
304
277
|
|
|
278
|
+
export async function writeEvolutionRepoProposal(input: {
|
|
279
|
+
homeDir: string;
|
|
280
|
+
proposal: EvolutionRepoProposal;
|
|
281
|
+
}): Promise<{ proposalPath: string; indexPath: string }> {
|
|
282
|
+
validateEvolutionRepoProposal(input.proposal);
|
|
283
|
+
if (!hasConcreteRepoProposalChanges(input.proposal)) {
|
|
284
|
+
throw new Error("Repo proposal must include a target repository and concrete file changes.");
|
|
285
|
+
}
|
|
286
|
+
const paths = resolveEvolutionPaths({
|
|
287
|
+
homeDir: input.homeDir,
|
|
288
|
+
projectKey: input.proposal.projectKey,
|
|
289
|
+
runId: input.proposal.provenance.runId,
|
|
290
|
+
});
|
|
291
|
+
const proposalPath = join(paths.repoProposalsDir, `${input.proposal.id}.json`);
|
|
292
|
+
await writeJson(proposalPath, input.proposal, { overwrite: false });
|
|
293
|
+
|
|
294
|
+
const allRunProposals = await readJsonFiles(paths.repoProposalsDir, parseRepoProposal);
|
|
295
|
+
await writeJson(
|
|
296
|
+
paths.repoProposalsIndexPath,
|
|
297
|
+
{
|
|
298
|
+
schemaVersion: 1,
|
|
299
|
+
projectKey: input.proposal.projectKey,
|
|
300
|
+
runId: input.proposal.provenance.runId,
|
|
301
|
+
updatedAt: input.proposal.provenance.createdAt,
|
|
302
|
+
proposals: allRunProposals.map((proposal) => ({
|
|
303
|
+
id: proposal.id,
|
|
304
|
+
kind: proposal.kind,
|
|
305
|
+
title: proposal.title,
|
|
306
|
+
reviewState: proposal.reviewState,
|
|
307
|
+
})),
|
|
308
|
+
},
|
|
309
|
+
{ overwrite: true },
|
|
310
|
+
);
|
|
311
|
+
return { proposalPath, indexPath: paths.repoProposalsIndexPath };
|
|
312
|
+
}
|
|
313
|
+
|
|
305
314
|
export async function activateEvolutionDistillationBatch(input: {
|
|
306
315
|
homeDir: string;
|
|
307
316
|
batch: EvolutionDistillationBatch;
|
|
@@ -1,22 +1,14 @@
|
|
|
1
1
|
import { mkdir, rm, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { resolveEvoDevPaths } from "../../config/paths.ts";
|
|
4
|
-
import {
|
|
5
|
-
createStableId,
|
|
6
|
-
isFileExistsError,
|
|
7
|
-
normalizeTimestamp,
|
|
8
|
-
writeJsonFile as writeJson,
|
|
9
|
-
} from "../../utils/index.ts";
|
|
4
|
+
import { createStableId, isFileExistsError, normalizeTimestamp } from "../../utils/index.ts";
|
|
10
5
|
import { analyzeEvolutionRun } from "../evidence/analysis.ts";
|
|
11
|
-
import { resolveEvolutionPaths } from "../paths.ts";
|
|
12
6
|
import type {
|
|
13
7
|
EvolutionProcessInput,
|
|
14
8
|
EvolutionProcessResult,
|
|
15
|
-
EvolutionReviewCandidate,
|
|
16
9
|
EvolutionTriggerStatus,
|
|
17
|
-
SegmentEvolutionTriggerRecord,
|
|
18
10
|
} from "../schema.ts";
|
|
19
|
-
import {
|
|
11
|
+
import { sanitizeText } from "../shared.ts";
|
|
20
12
|
import {
|
|
21
13
|
groupTriggersByRun,
|
|
22
14
|
listEvolutionTriggers,
|
|
@@ -103,16 +95,11 @@ export async function processEvolutionTriggers(
|
|
|
103
95
|
attempts: (current) => current.attempts + 1,
|
|
104
96
|
});
|
|
105
97
|
try {
|
|
106
|
-
await writeSegmentReviewCandidate({
|
|
107
|
-
homeDir: input.homeDir,
|
|
108
|
-
trigger,
|
|
109
|
-
now,
|
|
110
|
-
});
|
|
111
98
|
result.consumed += 1;
|
|
112
99
|
await updateSegmentTriggers(input.homeDir, [trigger], {
|
|
113
100
|
status: "consumed",
|
|
114
101
|
updatedAt: now,
|
|
115
|
-
processedBatchId: createStableId("segment-
|
|
102
|
+
processedBatchId: createStableId("segment-evidence", [trigger.id]),
|
|
116
103
|
lastError: null,
|
|
117
104
|
});
|
|
118
105
|
} catch (error) {
|
|
@@ -246,61 +233,3 @@ async function acquireEvolutionProcessLock(
|
|
|
246
233
|
async function releaseEvolutionProcessLock(lock: { path: string }): Promise<void> {
|
|
247
234
|
await rm(lock.path, { force: true });
|
|
248
235
|
}
|
|
249
|
-
|
|
250
|
-
async function writeSegmentReviewCandidate(input: {
|
|
251
|
-
homeDir: string;
|
|
252
|
-
trigger: SegmentEvolutionTriggerRecord;
|
|
253
|
-
now: string;
|
|
254
|
-
}): Promise<EvolutionReviewCandidate> {
|
|
255
|
-
const runId = input.trigger.runId ?? `session-${input.trigger.sessionKey}`;
|
|
256
|
-
const paths = resolveEvolutionPaths({
|
|
257
|
-
homeDir: input.homeDir,
|
|
258
|
-
projectKey: input.trigger.projectKey,
|
|
259
|
-
runId,
|
|
260
|
-
});
|
|
261
|
-
const candidate: EvolutionReviewCandidate = {
|
|
262
|
-
schemaVersion: 1,
|
|
263
|
-
kind: "evolution-review-candidate",
|
|
264
|
-
id: createStableId("review", [input.trigger.id]),
|
|
265
|
-
projectKey: input.trigger.projectKey,
|
|
266
|
-
runId: paths.runId,
|
|
267
|
-
createdAt: input.now,
|
|
268
|
-
candidateKind: "session-memory-segment",
|
|
269
|
-
title: sanitizeText(`Review session segment ${input.trigger.reason}`),
|
|
270
|
-
targetStore: "session-memory-segments",
|
|
271
|
-
targetPath: input.trigger.segmentPath,
|
|
272
|
-
stableKey: createStableId("segment-candidate", [
|
|
273
|
-
input.trigger.projectKey,
|
|
274
|
-
input.trigger.sessionKey,
|
|
275
|
-
input.trigger.segmentId,
|
|
276
|
-
]),
|
|
277
|
-
reviewState: "needs-human",
|
|
278
|
-
reasons: [input.trigger.reason, input.trigger.strength],
|
|
279
|
-
candidate: {
|
|
280
|
-
segmentId: input.trigger.segmentId,
|
|
281
|
-
sessionKey: input.trigger.sessionKey,
|
|
282
|
-
roleId: input.trigger.roleId,
|
|
283
|
-
summary: input.trigger.summary,
|
|
284
|
-
reviewAction:
|
|
285
|
-
"Open the referenced local session segment for raw evidence before approving memory or OKF changes.",
|
|
286
|
-
directOkfWrite: false,
|
|
287
|
-
localOnly: true,
|
|
288
|
-
},
|
|
289
|
-
provenance: {
|
|
290
|
-
runId: paths.runId,
|
|
291
|
-
evidenceWindowId: input.trigger.segmentId,
|
|
292
|
-
evidenceRefs: [input.trigger.id, input.trigger.segmentId],
|
|
293
|
-
createdBy: "evodev",
|
|
294
|
-
rawLogsStored: false,
|
|
295
|
-
rawPromptsStored: false,
|
|
296
|
-
sourceDumpsStored: false,
|
|
297
|
-
rawCommandOutputStored: false,
|
|
298
|
-
},
|
|
299
|
-
privacy: createPrivacyFields(),
|
|
300
|
-
};
|
|
301
|
-
validateEvolutionReviewCandidate(candidate);
|
|
302
|
-
await writeJson(join(paths.reviewCandidatesDir, `${candidate.id}.json`), candidate, {
|
|
303
|
-
overwrite: true,
|
|
304
|
-
});
|
|
305
|
-
return candidate;
|
|
306
|
-
}
|
|
@@ -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 {
|
package/src/evolution/schema.ts
CHANGED
|
@@ -4,7 +4,6 @@ export type EvolutionEvidenceSourceKind =
|
|
|
4
4
|
| "evodev-execution-event"
|
|
5
5
|
| "team-agent-trace"
|
|
6
6
|
| "session-trace"
|
|
7
|
-
| "task-contract"
|
|
8
7
|
| "verification"
|
|
9
8
|
| "review-summary"
|
|
10
9
|
| "user-feedback"
|
|
@@ -278,6 +277,19 @@ export interface EvolutionKnowledgeRecord {
|
|
|
278
277
|
};
|
|
279
278
|
}
|
|
280
279
|
|
|
280
|
+
export interface EvolutionKnowledgeReviewHistoryRecord {
|
|
281
|
+
version: 1;
|
|
282
|
+
kind: "evolution-knowledge-review-state-changed";
|
|
283
|
+
knowledgeId: string;
|
|
284
|
+
projectKey: string;
|
|
285
|
+
roleTags: string[];
|
|
286
|
+
artifactCreatedAt: string;
|
|
287
|
+
previousReviewState: EvolutionReviewState;
|
|
288
|
+
nextReviewState: EvolutionReviewState;
|
|
289
|
+
changedAt: string;
|
|
290
|
+
metadataOnly: true;
|
|
291
|
+
}
|
|
292
|
+
|
|
281
293
|
export interface EvolutionEvosCase {
|
|
282
294
|
schemaVersion: 1;
|
|
283
295
|
id: string;
|
|
@@ -327,13 +339,20 @@ export interface EvolutionRepoProposal {
|
|
|
327
339
|
roleTags: string[];
|
|
328
340
|
tags: string[];
|
|
329
341
|
reviewState: "pending" | "accepted" | "rejected" | "deferred" | "applied";
|
|
342
|
+
reviewStateChangedAt?: string;
|
|
330
343
|
confidence: EvolutionConfidence;
|
|
331
344
|
targetRepoPath: string | null;
|
|
332
345
|
plannedFiles: Array<{
|
|
333
346
|
relativePath: string;
|
|
334
347
|
action: "create" | "update";
|
|
335
348
|
reason: string;
|
|
349
|
+
proposedChange?: string;
|
|
336
350
|
}>;
|
|
351
|
+
lastDecision?: {
|
|
352
|
+
state: "accepted" | "rejected" | "deferred";
|
|
353
|
+
reason: string | null;
|
|
354
|
+
decidedAt: string;
|
|
355
|
+
};
|
|
337
356
|
apply: {
|
|
338
357
|
autoApply: false;
|
|
339
358
|
requiresExplicitCommand: true;
|
|
@@ -366,7 +385,8 @@ export interface EvolutionReviewCandidate {
|
|
|
366
385
|
targetStore: string;
|
|
367
386
|
targetPath: string;
|
|
368
387
|
stableKey: string;
|
|
369
|
-
reviewState: "needs-human";
|
|
388
|
+
reviewState: "needs-human" | "accepted" | "rejected" | "deferred";
|
|
389
|
+
reviewStateChangedAt?: string;
|
|
370
390
|
reasons: string[];
|
|
371
391
|
candidate: unknown;
|
|
372
392
|
provenance: {
|
|
@@ -455,6 +475,7 @@ export interface EvolutionResolvedPaths {
|
|
|
455
475
|
knowledgeProjectDir: string;
|
|
456
476
|
knowledgeRecordsDir: string;
|
|
457
477
|
knowledgeIndexPath: string;
|
|
478
|
+
knowledgeReviewHistoryPath: string;
|
|
458
479
|
evosCasesProjectDir: string;
|
|
459
480
|
evosIndexPath: string;
|
|
460
481
|
}
|
package/src/evolution/shared.ts
CHANGED
|
@@ -27,6 +27,7 @@ import type {
|
|
|
27
27
|
|
|
28
28
|
export const MAX_EVIDENCE_EVENTS = 200;
|
|
29
29
|
export const MAX_TEXT_LENGTH = 600;
|
|
30
|
+
export const MAX_PROPOSED_CHANGE_LENGTH = 16 * 1024;
|
|
30
31
|
export const PROCESS_LOCK_STALE_MS = 5 * 60 * 1000;
|
|
31
32
|
export const REVIEW_STATES: readonly EvolutionReviewState[] = [
|
|
32
33
|
"auto-accepted",
|
|
@@ -330,6 +331,9 @@ export function createEvolutionRepoProposal(
|
|
|
330
331
|
relativePath: sanitizeRelativePath(file.relativePath),
|
|
331
332
|
action: file.action,
|
|
332
333
|
reason: sanitizeText(file.reason),
|
|
334
|
+
...(file.proposedChange === undefined
|
|
335
|
+
? {}
|
|
336
|
+
: { proposedChange: sanitizeProposedChange(file.proposedChange) }),
|
|
333
337
|
})),
|
|
334
338
|
};
|
|
335
339
|
validateEvolutionRepoProposal(proposal);
|
|
@@ -357,6 +361,33 @@ export function validateEvolutionRepoProposal(proposal: EvolutionRepoProposal):
|
|
|
357
361
|
"deferred",
|
|
358
362
|
"applied",
|
|
359
363
|
]);
|
|
364
|
+
if (
|
|
365
|
+
proposal.reviewStateChangedAt !== undefined &&
|
|
366
|
+
Number.isNaN(new Date(proposal.reviewStateChangedAt).getTime())
|
|
367
|
+
) {
|
|
368
|
+
throw new Error("Repo proposal reviewStateChangedAt must be a valid timestamp.");
|
|
369
|
+
}
|
|
370
|
+
for (const file of proposal.plannedFiles) {
|
|
371
|
+
if (file.proposedChange !== undefined && file.proposedChange.trim() === "") {
|
|
372
|
+
throw new Error("Repo proposal proposedChange must not be empty.");
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if (proposal.lastDecision !== undefined) {
|
|
376
|
+
assertEnum("lastDecision.state", proposal.lastDecision.state, [
|
|
377
|
+
"accepted",
|
|
378
|
+
"rejected",
|
|
379
|
+
"deferred",
|
|
380
|
+
]);
|
|
381
|
+
if (
|
|
382
|
+
proposal.lastDecision.reason !== null &&
|
|
383
|
+
(proposal.lastDecision.reason.trim() === "" || proposal.lastDecision.reason.length > 500)
|
|
384
|
+
) {
|
|
385
|
+
throw new Error("Repo proposal decision reason must be 1-500 characters.");
|
|
386
|
+
}
|
|
387
|
+
if (Number.isNaN(new Date(proposal.lastDecision.decidedAt).getTime())) {
|
|
388
|
+
throw new Error("Repo proposal decision timestamp must be valid.");
|
|
389
|
+
}
|
|
390
|
+
}
|
|
360
391
|
if (proposal.apply.autoApply !== false || proposal.apply.requiresExplicitCommand !== true) {
|
|
361
392
|
throw new Error("Repo proposal must require explicit apply.");
|
|
362
393
|
}
|
|
@@ -365,6 +396,17 @@ export function validateEvolutionRepoProposal(proposal: EvolutionRepoProposal):
|
|
|
365
396
|
assertNoForbiddenRawFields(proposal);
|
|
366
397
|
}
|
|
367
398
|
|
|
399
|
+
export function hasConcreteRepoProposalChanges(proposal: EvolutionRepoProposal): boolean {
|
|
400
|
+
return (
|
|
401
|
+
proposal.targetRepoPath !== null &&
|
|
402
|
+
proposal.targetRepoPath.trim() !== "" &&
|
|
403
|
+
proposal.plannedFiles.length > 0 &&
|
|
404
|
+
proposal.plannedFiles.every(
|
|
405
|
+
(file) => file.proposedChange !== undefined && file.proposedChange.trim() !== "",
|
|
406
|
+
)
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
368
410
|
export function validateEvolutionReviewCandidate(candidate: EvolutionReviewCandidate): void {
|
|
369
411
|
if (!isRecord(candidate)) throw new Error("Review candidate must be an object.");
|
|
370
412
|
if (candidate.schemaVersion !== 1) {
|
|
@@ -382,8 +424,17 @@ export function validateEvolutionReviewCandidate(candidate: EvolutionReviewCandi
|
|
|
382
424
|
assertString("reviewCandidate.targetStore", candidate.targetStore);
|
|
383
425
|
assertString("reviewCandidate.targetPath", candidate.targetPath);
|
|
384
426
|
assertString("reviewCandidate.stableKey", candidate.stableKey);
|
|
385
|
-
|
|
386
|
-
|
|
427
|
+
assertEnum("reviewCandidate.reviewState", candidate.reviewState, [
|
|
428
|
+
"needs-human",
|
|
429
|
+
"accepted",
|
|
430
|
+
"rejected",
|
|
431
|
+
"deferred",
|
|
432
|
+
]);
|
|
433
|
+
if (
|
|
434
|
+
candidate.reviewStateChangedAt !== undefined &&
|
|
435
|
+
Number.isNaN(new Date(candidate.reviewStateChangedAt).getTime())
|
|
436
|
+
) {
|
|
437
|
+
throw new Error("Review candidate reviewStateChangedAt must be a valid timestamp.");
|
|
387
438
|
}
|
|
388
439
|
if (!Array.isArray(candidate.reasons))
|
|
389
440
|
throw new Error("Review candidate reasons must be an array.");
|
|
@@ -616,6 +667,15 @@ export function sanitizeText(value: string): string {
|
|
|
616
667
|
.slice(0, MAX_TEXT_LENGTH);
|
|
617
668
|
}
|
|
618
669
|
|
|
670
|
+
export function sanitizeProposedChange(value: string): string {
|
|
671
|
+
return value
|
|
672
|
+
.replace(SENSITIVE_TEXT_REPLACE_PATTERN, "[redacted]")
|
|
673
|
+
.replaceAll("\r\n", "\n")
|
|
674
|
+
.replaceAll("\r", "\n")
|
|
675
|
+
.trim()
|
|
676
|
+
.slice(0, MAX_PROPOSED_CHANGE_LENGTH);
|
|
677
|
+
}
|
|
678
|
+
|
|
619
679
|
export function sanitizeId(value: string): string {
|
|
620
680
|
return (
|
|
621
681
|
value
|