synergyspec-selfevolving 2.1.5 → 2.1.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/commands/learn.js +80 -24
- package/dist/commands/self-evolution-dream.d.ts +15 -1
- package/dist/commands/self-evolution-dream.js +111 -6
- package/dist/commands/self-evolution-episode.d.ts +3 -0
- package/dist/commands/self-evolution-episode.js +157 -108
- package/dist/commands/workflow/status.js +4 -0
- package/dist/core/archive.js +17 -9
- package/dist/core/change-readiness.d.ts +16 -1
- package/dist/core/change-readiness.js +441 -15
- package/dist/core/fitness/loss.d.ts +3 -5
- package/dist/core/fitness/loss.js +2 -2
- package/dist/core/fitness/test-metrics.d.ts +1 -0
- package/dist/core/fitness/test-metrics.js +49 -0
- package/dist/core/learn.js +129 -11
- package/dist/core/migration.d.ts +6 -14
- package/dist/core/migration.js +63 -21
- package/dist/core/runner-evidence.d.ts +53 -0
- package/dist/core/runner-evidence.js +613 -0
- package/dist/core/self-evolution/candidates.js +0 -2
- package/dist/core/self-evolution/dream.d.ts +57 -3
- package/dist/core/self-evolution/dream.js +480 -9
- package/dist/core/self-evolution/episode-orchestrator.d.ts +2 -0
- package/dist/core/self-evolution/episode-orchestrator.js +17 -5
- package/dist/core/self-evolution/episode-store.d.ts +5 -0
- package/dist/core/self-evolution/episode-store.js +6 -2
- package/dist/core/self-evolution/evolving-agent.js +8 -0
- package/dist/core/self-evolution/host-harness.d.ts +35 -12
- package/dist/core/self-evolution/host-harness.js +188 -49
- package/dist/core/self-evolution/reward-aggregator.js +2 -2
- package/dist/core/templates/workflows/archive-change.js +18 -18
- package/dist/core/templates/workflows/dream.js +57 -47
- package/dist/core/templates/workflows/learn.js +7 -5
- package/dist/core/templates/workflows/run-tests.js +48 -29
- package/dist/core/templates/workflows/self-evolving.js +11 -8
- package/dist/core/trajectory/facts.d.ts +1 -1
- package/dist/core/trajectory/registry.js +39 -8
- package/package.json +1 -1
|
@@ -41,6 +41,32 @@ function allPassedReportSummary(line) {
|
|
|
41
41
|
return null;
|
|
42
42
|
return Number(passed[1]);
|
|
43
43
|
}
|
|
44
|
+
function aggregateMetrics(items) {
|
|
45
|
+
const totals = items.reduce((acc, item) => ({
|
|
46
|
+
total: acc.total + item.total,
|
|
47
|
+
passed: acc.passed + item.passed,
|
|
48
|
+
failed: acc.failed + item.failed,
|
|
49
|
+
}), { total: 0, passed: 0, failed: 0 });
|
|
50
|
+
return {
|
|
51
|
+
...totals,
|
|
52
|
+
passRate: rate(totals.passed, totals.total),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function synergySpecReportSummary(line) {
|
|
56
|
+
const summary = /^Summary:\s*\d+\s+collected,\s*(\d+)\s+passed,\s*(\d+)\s+failed(?:,\s*\d+\s+skipped)?(?:,\s*(\d+)\s+collection\s+errors?)?\.?$/i.exec(line);
|
|
57
|
+
if (!summary)
|
|
58
|
+
return null;
|
|
59
|
+
const passed = Number(summary[1]);
|
|
60
|
+
const failed = Number(summary[2]);
|
|
61
|
+
const collectionErrors = summary[3] !== undefined ? Number(summary[3]) : 0;
|
|
62
|
+
const total = passed + failed + collectionErrors;
|
|
63
|
+
return {
|
|
64
|
+
total,
|
|
65
|
+
passed,
|
|
66
|
+
failed: failed + collectionErrors,
|
|
67
|
+
passRate: rate(passed, total),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
44
70
|
/**
|
|
45
71
|
* Extract pass/fail counts from test-runner summary text. Recognizes the
|
|
46
72
|
* canonical runner summary shapes and takes the LAST one (runners print the
|
|
@@ -50,6 +76,7 @@ function allPassedReportSummary(line) {
|
|
|
50
76
|
* "1 failed, 9 passed in 0.4s" (order-independent)
|
|
51
77
|
* - SynergySpec reports: table/status prose like
|
|
52
78
|
* "Passed; 85 tests collected and passed"
|
|
79
|
+
* "Summary: 29 collected, 29 passed, 0 failed, 0 skipped, 0 collection errors."
|
|
53
80
|
* A bare "N passed" line that is not a recognized summary (e.g. prose or a
|
|
54
81
|
* per-suite tally) is ignored. Pytest "errors" count toward `failed`.
|
|
55
82
|
* Returns null when no recognized summary is found.
|
|
@@ -59,8 +86,18 @@ export function parseTestMetrics(reportText) {
|
|
|
59
86
|
return null;
|
|
60
87
|
const text = reportText.replace(ANSI_SGR, '');
|
|
61
88
|
let result = null;
|
|
89
|
+
let authoritativeSummary = null;
|
|
90
|
+
const pytestSummaries = [];
|
|
91
|
+
let nonPytestSummarySeen = false;
|
|
62
92
|
for (const raw of text.split(/\r?\n/)) {
|
|
63
93
|
const line = raw.trim();
|
|
94
|
+
const synergySpecSummary = synergySpecReportSummary(line);
|
|
95
|
+
if (synergySpecSummary !== null) {
|
|
96
|
+
result = synergySpecSummary;
|
|
97
|
+
authoritativeSummary = synergySpecSummary;
|
|
98
|
+
nonPytestSummarySeen = true;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
64
101
|
const passed = count(line, /(\d+)\s+passed\b/i);
|
|
65
102
|
if (passed === null) {
|
|
66
103
|
const reportPassed = allPassedReportSummary(line);
|
|
@@ -71,6 +108,7 @@ export function parseTestMetrics(reportText) {
|
|
|
71
108
|
failed: 0,
|
|
72
109
|
passRate: rate(reportPassed, reportPassed),
|
|
73
110
|
};
|
|
111
|
+
nonPytestSummarySeen = true;
|
|
74
112
|
}
|
|
75
113
|
continue;
|
|
76
114
|
}
|
|
@@ -86,6 +124,17 @@ export function parseTestMetrics(reportText) {
|
|
|
86
124
|
continue;
|
|
87
125
|
const total = passed + failed + errored;
|
|
88
126
|
result = { total, passed, failed: failed + errored, passRate: rate(passed, total) };
|
|
127
|
+
if (/\bin\s+[\d.]+\s*m?s\b/i.test(line)) {
|
|
128
|
+
pytestSummaries.push(result);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
nonPytestSummarySeen = true;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (authoritativeSummary)
|
|
135
|
+
return authoritativeSummary;
|
|
136
|
+
if (pytestSummaries.length > 1 && !nonPytestSummarySeen) {
|
|
137
|
+
return aggregateMetrics(pytestSummaries);
|
|
89
138
|
}
|
|
90
139
|
return result;
|
|
91
140
|
}
|
package/dist/core/learn.js
CHANGED
|
@@ -8,10 +8,11 @@ import { parseTasksMarkdown } from './self-evolution/task-quality.js';
|
|
|
8
8
|
import { buildLLMSummaryCandidates, } from './learn/llm-summary.js';
|
|
9
9
|
import { parseTestMetrics, computePerChangeLoss, measureHealthReport, resolveMetricSource, } from './fitness/index.js';
|
|
10
10
|
import { readProjectConfig } from './project-config.js';
|
|
11
|
-
import {
|
|
12
|
-
import { toTrajectoryFacts, extractExpectedTestPaths } from './trajectory/facts.js';
|
|
11
|
+
import { getTrajectoryResultForChange } from './trajectory/registry.js';
|
|
12
|
+
import { toTrajectoryFacts, extractExpectedTestPaths, } from './trajectory/facts.js';
|
|
13
13
|
import { toActionSkeleton, renderActionSkeleton } from './trajectory/skeleton.js';
|
|
14
14
|
import { walkCreditPath } from './learn/credit-path.js';
|
|
15
|
+
import { readRunnerEvidenceResultFromTestReport, runnerEvidenceToTrajectoryFacts, } from './runner-evidence.js';
|
|
15
16
|
const PRIMARY_ARTIFACTS = [
|
|
16
17
|
['proposal', 'proposal.md'],
|
|
17
18
|
['usecases', 'usecases.md'],
|
|
@@ -51,7 +52,19 @@ export async function generateLearnReport(args = {}) {
|
|
|
51
52
|
// that is absent or `stub` the source returns no signal, `measureHealthPenalty`
|
|
52
53
|
// yields null, and the loss is byte-identical to the functional-only baseline.
|
|
53
54
|
const testReport = artifacts.evidence.find((f) => /(?:^|[\\/])(?:test-report|run-tests?-report|ci-report)\.md$/i.test(f.relativePath));
|
|
54
|
-
const
|
|
55
|
+
const specTestsForScope = artifacts.evidence.find((f) => /(?:^|[\\/])spec-tests\.md$/i.test(f.relativePath));
|
|
56
|
+
const expectedTestPaths = extractExpectedTestPaths(specTestsForScope?.content);
|
|
57
|
+
const runnerEvidenceResult = testReport
|
|
58
|
+
? await readRunnerEvidenceResultFromTestReport({
|
|
59
|
+
projectRoot,
|
|
60
|
+
changeName: resolved.changeName,
|
|
61
|
+
changeDir: resolved.changeDir,
|
|
62
|
+
testReportContent: testReport.content,
|
|
63
|
+
})
|
|
64
|
+
: { evidence: null };
|
|
65
|
+
const runnerEvidence = runnerEvidenceResult.evidence;
|
|
66
|
+
const reportTestMetrics = testReport ? parseTestMetrics(testReport.content) : null;
|
|
67
|
+
const testMetrics = runnerEvidence?.testMetrics ?? reportTestMetrics;
|
|
55
68
|
let healthPenalty;
|
|
56
69
|
let healthSourceName;
|
|
57
70
|
let healthContributors = [];
|
|
@@ -71,23 +84,71 @@ export async function generateLearnReport(args = {}) {
|
|
|
71
84
|
// than trusting the hand-authored test-report. `getTrajectoryForChange`
|
|
72
85
|
// returns null when nothing is discoverable, so on the artifact-only path
|
|
73
86
|
// `trajectoryFacts` is null and the fitness output is byte-identical baseline.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
87
|
+
let trajectoryMiss = null;
|
|
88
|
+
let trajectory;
|
|
89
|
+
if (args.trajectorySource) {
|
|
90
|
+
try {
|
|
91
|
+
if (args.trajectorySource.getTrajectoryResult) {
|
|
92
|
+
const trajectoryResult = await args.trajectorySource.getTrajectoryResult(resolved.changeName);
|
|
93
|
+
trajectory = trajectoryResult.trajectory;
|
|
94
|
+
if (!trajectory) {
|
|
95
|
+
trajectoryMiss = {
|
|
96
|
+
reason: trajectoryResult.reason ?? 'adapter-returned-null',
|
|
97
|
+
sourceHarness: args.trajectorySource.harness ?? null,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
trajectory = await args.trajectorySource.getTrajectory(resolved.changeName);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
trajectory = null;
|
|
107
|
+
trajectoryMiss = {
|
|
108
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
109
|
+
sourceHarness: args.trajectorySource.harness ?? null,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
const trajectoryResult = await getTrajectoryResultForChange(projectRoot, resolved.changeName);
|
|
115
|
+
trajectory = trajectoryResult.trajectory;
|
|
116
|
+
if (!trajectory) {
|
|
117
|
+
trajectoryMiss = {
|
|
118
|
+
reason: trajectoryResult.reason ?? 'adapter-returned-null',
|
|
119
|
+
sourceHarness: trajectoryResult.sourceHarness,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
77
123
|
// Change-scope guard input: the change's own expected test paths (from its
|
|
78
124
|
// spec-tests.md mapping). Lets toTrajectoryFacts DEMOTE a green-but-irrelevant
|
|
79
125
|
// run (a default `pytest` that collected ZERO of the change's tests) to
|
|
80
126
|
// unverified so the 奖励智能体 REWARD AGENT abstains instead of certifying a
|
|
81
127
|
// false-GREEN. Absent/empty ⇒ no gate (byte-identical baseline).
|
|
82
|
-
const
|
|
83
|
-
const expectedTestPaths = extractExpectedTestPaths(specTestsForScope?.content);
|
|
84
|
-
const trajectoryFacts = toTrajectoryFacts(trajectory, resolved.changeName, {
|
|
128
|
+
const rawTrajectoryFacts = toTrajectoryFacts(trajectory, resolved.changeName, {
|
|
85
129
|
expectedTestPaths,
|
|
86
130
|
});
|
|
131
|
+
const runnerEvidenceFacts = runnerEvidence
|
|
132
|
+
? runnerEvidenceToTrajectoryFacts({
|
|
133
|
+
evidence: runnerEvidence,
|
|
134
|
+
changeName: resolved.changeName,
|
|
135
|
+
expectedTestPaths,
|
|
136
|
+
})
|
|
137
|
+
: null;
|
|
138
|
+
const runnerTrajectoryConflict = runnerEvidenceFacts?.verified === true &&
|
|
139
|
+
rawTrajectoryFacts?.verified === true &&
|
|
140
|
+
factsDisagree(runnerEvidenceFacts, rawTrajectoryFacts);
|
|
141
|
+
const trajectoryFacts = runnerEvidenceFacts?.verified === true
|
|
142
|
+
? runnerEvidenceFacts
|
|
143
|
+
: (!rawTrajectoryFacts ||
|
|
144
|
+
!rawTrajectoryFacts.testRunObserved ||
|
|
145
|
+
(rawTrajectoryFacts.observedStatus === null && rawTrajectoryFacts.observedPassRate === null)) &&
|
|
146
|
+
runnerEvidenceFacts
|
|
147
|
+
? runnerEvidenceFacts
|
|
148
|
+
: rawTrajectoryFacts;
|
|
87
149
|
// "Trust the trajectory": when a real runner was observed, its pass rate wins
|
|
88
150
|
// over the authored test-report; otherwise the report stands but is flagged
|
|
89
|
-
// unverified
|
|
90
|
-
// the loss is unchanged until a maintainer raises it).
|
|
151
|
+
// unverified and receives the default soft penalty.
|
|
91
152
|
const observedPassRate = trajectoryFacts?.testRunObserved && trajectoryFacts.observedPassRate !== null
|
|
92
153
|
? trajectoryFacts.observedPassRate
|
|
93
154
|
: null;
|
|
@@ -217,6 +278,54 @@ export async function generateLearnReport(args = {}) {
|
|
|
217
278
|
});
|
|
218
279
|
}
|
|
219
280
|
}
|
|
281
|
+
if (runnerTrajectoryConflict && runnerEvidenceFacts && rawTrajectoryFacts) {
|
|
282
|
+
observations.push({
|
|
283
|
+
code: 'runner-trajectory-conflict',
|
|
284
|
+
summary: 'Durable runner-exit evidence disagrees with the host trajectory; runner-exit.json is the graded functional source.',
|
|
285
|
+
evidence: [
|
|
286
|
+
...runnerEvidenceFacts.sourcePaths.slice(0, 2).map((file) => ({
|
|
287
|
+
file,
|
|
288
|
+
detail: `runner evidence status=${runnerEvidenceFacts.observedStatus ?? 'unknown'} passRate=${runnerEvidenceFacts.observedPassRate ?? 'n/a'}`,
|
|
289
|
+
})),
|
|
290
|
+
...rawTrajectoryFacts.sourcePaths.slice(0, 2).map((file) => ({
|
|
291
|
+
file,
|
|
292
|
+
detail: `trajectory status=${rawTrajectoryFacts.observedStatus ?? 'unknown'} passRate=${rawTrajectoryFacts.observedPassRate ?? 'n/a'}`,
|
|
293
|
+
})),
|
|
294
|
+
],
|
|
295
|
+
tags: ['runner-evidence', 'trajectory', 'conflict', 'fitness'],
|
|
296
|
+
severity: 'action',
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
if (!rawTrajectoryFacts && trajectoryMiss !== null) {
|
|
300
|
+
observations.push({
|
|
301
|
+
code: 'trajectory-source-miss',
|
|
302
|
+
summary: `No observed host trajectory was available (${trajectoryMiss.reason}).`,
|
|
303
|
+
evidence: [
|
|
304
|
+
{
|
|
305
|
+
file: relativePath(projectRoot, resolved.changeDir),
|
|
306
|
+
detail: `sourceHarness=${trajectoryMiss.sourceHarness ?? 'unresolved'}; reason=${trajectoryMiss.reason}`,
|
|
307
|
+
},
|
|
308
|
+
],
|
|
309
|
+
tags: ['trajectory', 'unverified', 'fitness'],
|
|
310
|
+
severity: runnerEvidenceFacts?.verified === true ? 'info' : 'action',
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
if (runnerEvidenceResult.reason &&
|
|
314
|
+
runnerEvidenceResult.exitJsonPath &&
|
|
315
|
+
runnerEvidenceFacts?.verified !== true) {
|
|
316
|
+
observations.push({
|
|
317
|
+
code: 'runner-evidence-miss',
|
|
318
|
+
summary: `Runner evidence was present but could not be used (${runnerEvidenceResult.reason}).`,
|
|
319
|
+
evidence: [
|
|
320
|
+
{
|
|
321
|
+
file: relativePath(projectRoot, runnerEvidenceResult.exitJsonPath),
|
|
322
|
+
detail: runnerEvidenceResult.reason,
|
|
323
|
+
},
|
|
324
|
+
],
|
|
325
|
+
tags: ['runner-evidence', 'unverified', 'fitness'],
|
|
326
|
+
severity: trajectoryFacts?.verified === true ? 'info' : 'action',
|
|
327
|
+
});
|
|
328
|
+
}
|
|
220
329
|
// Health head is CONFIGURED (a non-stub source was selected) but produced NO
|
|
221
330
|
// signal: surface it loudly rather than letting the health half of the loss
|
|
222
331
|
// silently default to 0. Default-on health must never fail invisibly — this
|
|
@@ -335,6 +444,15 @@ function buildLearnObservations(report) {
|
|
|
335
444
|
}
|
|
336
445
|
return observations;
|
|
337
446
|
}
|
|
447
|
+
function factsDisagree(left, right) {
|
|
448
|
+
if (left.observedStatus !== right.observedStatus)
|
|
449
|
+
return true;
|
|
450
|
+
const leftRate = left.observedPassRate;
|
|
451
|
+
const rightRate = right.observedPassRate;
|
|
452
|
+
if (leftRate === null || rightRate === null)
|
|
453
|
+
return leftRate !== rightRate;
|
|
454
|
+
return Math.abs(leftRate - rightRate) > 0.000001;
|
|
455
|
+
}
|
|
338
456
|
export async function applyLearnCandidates(args) {
|
|
339
457
|
return applyLearnMemoryCandidates({
|
|
340
458
|
projectRoot: args.projectRoot,
|
package/dist/core/migration.d.ts
CHANGED
|
@@ -11,22 +11,14 @@ import type { AIToolOption } from './config.js';
|
|
|
11
11
|
*/
|
|
12
12
|
export declare function scanInstalledWorkflows(projectPath: string, tools: AIToolOption[]): string[];
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
14
|
+
* One-time compatibility upgrade for v2.1.5.
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* who wants every workflow picks `profile: 'all'`; a user on `profile:
|
|
20
|
-
* 'custom'` is intentionally limiting their setup, and auto-appending
|
|
21
|
-
* silently invalidates that choice.
|
|
16
|
+
* Most custom profiles are deliberate and must stay untouched. The exception
|
|
17
|
+
* is an exact snapshot of every built-in workflow from before Dream existed:
|
|
18
|
+
* that snapshot behaves like "all workflows" and should receive Dream once.
|
|
22
19
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* `fix-custom-profile-filtering` (2026-05-17).
|
|
26
|
-
*
|
|
27
|
-
* The function is kept (rather than deleted) so existing callers in
|
|
28
|
-
* `init.ts` and `update.ts` continue to compile without coordinated
|
|
29
|
-
* edits. A future cleanup change can remove all three at once.
|
|
20
|
+
* A migration marker prevents re-adding Dream if the user removes it after this
|
|
21
|
+
* backfill has already run.
|
|
30
22
|
*/
|
|
31
23
|
export declare function upgradeCustomWorkflows(): void;
|
|
32
24
|
/**
|
package/dist/core/migration.js
CHANGED
|
@@ -10,6 +10,26 @@ import { WORKFLOW_TO_SKILL_DIR } from './profile-sync-drift.js';
|
|
|
10
10
|
import { ALL_WORKFLOWS } from './profiles.js';
|
|
11
11
|
import path from 'path';
|
|
12
12
|
import * as fs from 'fs';
|
|
13
|
+
const DREAM_WORKFLOW_BACKFILL_MARKER = 'dream-workflow-v2.1.5';
|
|
14
|
+
const LEGACY_ALL_WORKFLOWS_BEFORE_DREAM = [
|
|
15
|
+
'propose',
|
|
16
|
+
'explore',
|
|
17
|
+
'new',
|
|
18
|
+
'continue',
|
|
19
|
+
'apply',
|
|
20
|
+
'tdd',
|
|
21
|
+
'ff',
|
|
22
|
+
'sync',
|
|
23
|
+
'archive',
|
|
24
|
+
'bulk-archive',
|
|
25
|
+
'verify',
|
|
26
|
+
'learn',
|
|
27
|
+
'verify-spec',
|
|
28
|
+
'onboard',
|
|
29
|
+
'gen-tests',
|
|
30
|
+
'run-tests',
|
|
31
|
+
'ci',
|
|
32
|
+
];
|
|
13
33
|
function scanInstalledWorkflowArtifacts(projectPath, tools) {
|
|
14
34
|
const installed = new Set();
|
|
15
35
|
let hasSkills = false;
|
|
@@ -62,26 +82,51 @@ function inferDelivery(artifacts) {
|
|
|
62
82
|
}
|
|
63
83
|
return 'skills';
|
|
64
84
|
}
|
|
85
|
+
function asStringArray(value) {
|
|
86
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === 'string') : [];
|
|
87
|
+
}
|
|
88
|
+
function hasSameWorkflowSet(actual, expected) {
|
|
89
|
+
if (actual.length !== expected.length)
|
|
90
|
+
return false;
|
|
91
|
+
const actualSet = new Set(actual);
|
|
92
|
+
return expected.every((workflow) => actualSet.has(workflow));
|
|
93
|
+
}
|
|
94
|
+
function hasMigrationMarker(config, marker) {
|
|
95
|
+
return asStringArray(config.migrations).includes(marker);
|
|
96
|
+
}
|
|
97
|
+
function appendMigrationMarker(config, marker) {
|
|
98
|
+
const migrations = asStringArray(config.migrations);
|
|
99
|
+
if (!migrations.includes(marker)) {
|
|
100
|
+
config.migrations = [...migrations, marker];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
65
103
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* Previously this function appended every entry from ALL_WORKFLOWS to the
|
|
69
|
-
* user's custom workflow list so that newly-shipped workflows would
|
|
70
|
-
* auto-enable. That defeats the contract — "custom means custom". A user
|
|
71
|
-
* who wants every workflow picks `profile: 'all'`; a user on `profile:
|
|
72
|
-
* 'custom'` is intentionally limiting their setup, and auto-appending
|
|
73
|
-
* silently invalidates that choice.
|
|
104
|
+
* One-time compatibility upgrade for v2.1.5.
|
|
74
105
|
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
106
|
+
* Most custom profiles are deliberate and must stay untouched. The exception
|
|
107
|
+
* is an exact snapshot of every built-in workflow from before Dream existed:
|
|
108
|
+
* that snapshot behaves like "all workflows" and should receive Dream once.
|
|
78
109
|
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* edits. A future cleanup change can remove all three at once.
|
|
110
|
+
* A migration marker prevents re-adding Dream if the user removes it after this
|
|
111
|
+
* backfill has already run.
|
|
82
112
|
*/
|
|
83
113
|
export function upgradeCustomWorkflows() {
|
|
84
|
-
|
|
114
|
+
const config = getGlobalConfig();
|
|
115
|
+
if (config.profile !== 'custom')
|
|
116
|
+
return;
|
|
117
|
+
if (hasMigrationMarker(config, DREAM_WORKFLOW_BACKFILL_MARKER))
|
|
118
|
+
return;
|
|
119
|
+
const workflows = asStringArray(config.workflows);
|
|
120
|
+
if (workflows.includes('dream')) {
|
|
121
|
+
appendMigrationMarker(config, DREAM_WORKFLOW_BACKFILL_MARKER);
|
|
122
|
+
saveGlobalConfig(config);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (!hasSameWorkflowSet(workflows, LEGACY_ALL_WORKFLOWS_BEFORE_DREAM))
|
|
126
|
+
return;
|
|
127
|
+
config.workflows = [...LEGACY_ALL_WORKFLOWS_BEFORE_DREAM, 'dream'];
|
|
128
|
+
appendMigrationMarker(config, DREAM_WORKFLOW_BACKFILL_MARKER);
|
|
129
|
+
saveGlobalConfig(config);
|
|
85
130
|
}
|
|
86
131
|
/**
|
|
87
132
|
* Performs one-time migration if the global config does not yet have a profile field.
|
|
@@ -94,7 +139,7 @@ export function upgradeCustomWorkflows() {
|
|
|
94
139
|
*/
|
|
95
140
|
export function migrateIfNeeded(projectPath, tools) {
|
|
96
141
|
const config = getGlobalConfig();
|
|
97
|
-
// Check raw config file for profile field presence
|
|
142
|
+
// Check raw config file for profile field presence.
|
|
98
143
|
const configPath = getGlobalConfigPath();
|
|
99
144
|
let rawConfig = {};
|
|
100
145
|
try {
|
|
@@ -103,20 +148,17 @@ export function migrateIfNeeded(projectPath, tools) {
|
|
|
103
148
|
}
|
|
104
149
|
}
|
|
105
150
|
catch {
|
|
106
|
-
return;
|
|
151
|
+
return;
|
|
107
152
|
}
|
|
108
|
-
// If profile is already explicitly set, no migration needed
|
|
153
|
+
// If profile is already explicitly set, no migration needed.
|
|
109
154
|
if (rawConfig.profile !== undefined) {
|
|
110
155
|
return;
|
|
111
156
|
}
|
|
112
|
-
// Scan for installed workflows
|
|
113
157
|
const artifacts = scanInstalledWorkflowArtifacts(projectPath, tools);
|
|
114
158
|
const installedWorkflows = artifacts.workflows;
|
|
115
159
|
if (installedWorkflows.length === 0) {
|
|
116
|
-
// No workflows installed, new user — defaults will apply
|
|
117
160
|
return;
|
|
118
161
|
}
|
|
119
|
-
// Migrate: set profile to custom with detected workflows
|
|
120
162
|
config.profile = 'custom';
|
|
121
163
|
config.workflows = installedWorkflows;
|
|
122
164
|
if (rawConfig.delivery === undefined) {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type TestMetrics } from './fitness/test-metrics.js';
|
|
2
|
+
import type { TrajectoryFacts } from './trajectory/facts.js';
|
|
3
|
+
export interface WorkspaceIdentityFileSnapshot {
|
|
4
|
+
path?: string;
|
|
5
|
+
name?: string;
|
|
6
|
+
sha256?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface WorkspaceIdentitySnapshot {
|
|
9
|
+
cwd?: string;
|
|
10
|
+
changeName?: string;
|
|
11
|
+
taskId?: string;
|
|
12
|
+
pyproject?: WorkspaceIdentityFileSnapshot;
|
|
13
|
+
packageJson?: WorkspaceIdentityFileSnapshot;
|
|
14
|
+
}
|
|
15
|
+
export interface RunnerEvidence {
|
|
16
|
+
exitJsonPath: string;
|
|
17
|
+
command?: string;
|
|
18
|
+
stdoutLogPath?: string;
|
|
19
|
+
stderrLogPath?: string;
|
|
20
|
+
exitCode: number | null;
|
|
21
|
+
testMetrics: TestMetrics | null;
|
|
22
|
+
outputText: string;
|
|
23
|
+
sourcePaths: string[];
|
|
24
|
+
workspaceIdentity: {
|
|
25
|
+
verified: boolean;
|
|
26
|
+
reason?: string;
|
|
27
|
+
recorded?: WorkspaceIdentitySnapshot;
|
|
28
|
+
current: WorkspaceIdentitySnapshot;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export interface RunnerEvidenceLookupResult {
|
|
32
|
+
evidence: RunnerEvidence | null;
|
|
33
|
+
reason?: string;
|
|
34
|
+
exitJsonPath?: string;
|
|
35
|
+
}
|
|
36
|
+
export declare function readRunnerEvidenceFromTestReport(args: {
|
|
37
|
+
projectRoot: string;
|
|
38
|
+
changeName: string;
|
|
39
|
+
changeDir?: string;
|
|
40
|
+
testReportContent: string;
|
|
41
|
+
}): Promise<RunnerEvidence | null>;
|
|
42
|
+
export declare function readRunnerEvidenceResultFromTestReport(args: {
|
|
43
|
+
projectRoot: string;
|
|
44
|
+
changeName: string;
|
|
45
|
+
changeDir?: string;
|
|
46
|
+
testReportContent: string;
|
|
47
|
+
}): Promise<RunnerEvidenceLookupResult>;
|
|
48
|
+
export declare function runnerEvidenceToTrajectoryFacts(args: {
|
|
49
|
+
evidence: RunnerEvidence;
|
|
50
|
+
changeName: string;
|
|
51
|
+
expectedTestPaths?: string[];
|
|
52
|
+
}): TrajectoryFacts | null;
|
|
53
|
+
//# sourceMappingURL=runner-evidence.d.ts.map
|