synergyspec-selfevolving 1.3.0 → 1.4.0
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/README.md +19 -1
- package/dist/commands/learn.js +228 -26
- package/dist/commands/self-evolution.js +171 -26
- package/dist/commands/workflow/status.js +3 -1
- package/dist/core/config-prompts.js +4 -0
- package/dist/core/fitness/health/health-metrics.d.ts +26 -56
- package/dist/core/fitness/health/health-metrics.js +19 -58
- package/dist/core/fitness/health/index.d.ts +15 -2
- package/dist/core/fitness/health/index.js +25 -1
- package/dist/core/fitness/health/local-source.d.ts +43 -4
- package/dist/core/fitness/health/local-source.js +181 -25
- package/dist/core/fitness/health/metric-source.d.ts +48 -19
- package/dist/core/fitness/health/metric-source.js +8 -18
- package/dist/core/fitness/health/resolve-source.js +4 -1
- package/dist/core/fitness/loss.d.ts +2 -2
- package/dist/core/fitness/loss.js +2 -2
- package/dist/core/fitness/sample.d.ts +10 -0
- package/dist/core/fitness/test-failures.d.ts +30 -0
- package/dist/core/fitness/test-failures.js +123 -0
- package/dist/core/learn/credit-path.d.ts +36 -0
- package/dist/core/learn/credit-path.js +198 -0
- package/dist/core/learn/trajectory-discovery.d.ts +39 -0
- package/dist/core/learn/trajectory-discovery.js +140 -0
- package/dist/core/learn.d.ts +39 -5
- package/dist/core/learn.js +131 -14
- package/dist/core/project-config.d.ts +2 -0
- package/dist/core/project-config.js +24 -1
- package/dist/core/self-evolution/canonical-targets.d.ts +8 -4
- package/dist/core/self-evolution/canonical-targets.js +8 -4
- package/dist/core/self-evolution/health-baseline.d.ts +25 -6
- package/dist/core/self-evolution/health-baseline.js +30 -6
- package/dist/core/self-evolution/index.d.ts +1 -0
- package/dist/core/self-evolution/index.js +1 -0
- package/dist/core/self-evolution/learn-hints.d.ts +31 -0
- package/dist/core/self-evolution/learn-hints.js +16 -0
- package/dist/core/self-evolution/learn-observation-adapter.d.ts +35 -0
- package/dist/core/self-evolution/learn-observation-adapter.js +285 -10
- package/dist/core/self-evolution/proposer-agent.d.ts +41 -0
- package/dist/core/self-evolution/proposer-agent.js +94 -13
- package/dist/core/self-evolution/proposer-slice.d.ts +26 -0
- package/dist/core/self-evolution/proposer-slice.js +54 -0
- package/dist/core/self-evolution/success-channel.d.ts +79 -0
- package/dist/core/self-evolution/success-channel.js +361 -0
- package/dist/core/self-evolution/target-evolution.d.ts +11 -0
- package/dist/core/self-evolution/target-evolution.js +2 -0
- package/dist/core/templates/skill-templates.d.ts +1 -0
- package/dist/core/templates/skill-templates.js +1 -0
- package/dist/core/templates/workflow-manifest.js +2 -0
- package/dist/core/templates/workflows/learn.d.ts +3 -2
- package/dist/core/templates/workflows/learn.js +24 -167
- package/dist/core/templates/workflows/self-evolving.d.ts +11 -0
- package/dist/core/templates/workflows/self-evolving.js +237 -0
- package/dist/core/trajectory/facts.d.ts +16 -0
- package/dist/core/trajectory/facts.js +12 -4
- package/dist/core/trajectory/skeleton.d.ts +43 -0
- package/dist/core/trajectory/skeleton.js +239 -0
- package/package.json +3 -1
- package/scripts/code-health.py +1066 -638
- package/scripts/slop_rules.yaml +2151 -0
package/README.md
CHANGED
|
@@ -211,10 +211,28 @@ What actually works today:
|
|
|
211
211
|
`MetricSource` selected via `health:` in `synergyspec-selfevolving/config.yaml`.
|
|
212
212
|
New projects scaffold `source: local` (default-on): a dependency-free,
|
|
213
213
|
multi-language analyzer (`scripts/code-health.py`, Python 3 stdlib only) that
|
|
214
|
-
scores Python, Rust, C, and C++
|
|
214
|
+
scores Python, Rust, C, and C++ by computing the SlopCodeBench
|
|
215
|
+
`structural_erosion` and `verbosity` scores (for Python, the slop rules are
|
|
216
|
+
the actual SlopCodeBench v0.3 ast-grep rule set, bundled) — no server, no
|
|
217
|
+
network. Set `source: stub` to
|
|
215
218
|
make the loss functional-only; `sonarqube` is also supported; `local-python` is
|
|
216
219
|
a back-compat alias for `local`. See
|
|
217
220
|
[docs/customization.md](docs/customization.md#code-health-metrics-self-evolution).
|
|
221
|
+
- **Rollout/critic separation** (`learn` → `synergyspec-selfevolving-self-evolving`):
|
|
222
|
+
the end-of-cycle critique runs in a fresh-context critic subagent — an
|
|
223
|
+
always-installed utility skill that reads only the on-disk record
|
|
224
|
+
(transcripts, hints, evolution result, learn report) and returns a
|
|
225
|
+
`## Critic Verdict` block that the thin `learn` relays. The session that did
|
|
226
|
+
the work never grades its own rollout; hosts without subagents run the critic
|
|
227
|
+
inline, marked as degraded isolation. Headless and fresh-context invokers can
|
|
228
|
+
pass explicit trajectory handles (`--transcript` / `--session-id` on `learn`,
|
|
229
|
+
`learn handoff`, `learn debug-trajectory`, and `evolve-from-edits`, or the
|
|
230
|
+
`SYNERGYSPEC_SELFEVOLVING_TRANSCRIPT` / `SYNERGYSPEC_SELFEVOLVING_SESSION_ID`
|
|
231
|
+
env vars; flags beat env vars, transcript beats session-id). An explicit
|
|
232
|
+
handle beats change-window discovery and never silently grades another
|
|
233
|
+
session: an unresolvable flag is an up-front error (exit non-zero), while a
|
|
234
|
+
missing env handle fails closed — no trajectory, and the observed-verified
|
|
235
|
+
gate refuses to promote.
|
|
218
236
|
- **Code-health gate** (auto-evolve / `evolve-from-edits`): a measured code-health
|
|
219
237
|
regression vs the last accepted state blocks auto-promotion (and surfaces a
|
|
220
238
|
loud `health-signal-unavailable` observation if a configured analyzer can't
|
package/dist/commands/learn.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { applyLearnCandidates, applyLearnMemoryCandidates, generateLearnReport, renderLearnReport, } from '../core/learn.js';
|
|
3
|
-
import { detectUnbindableHintObservations, generateEvolutionHints, isCanonicalTargetEvolvable, listCanonicalTargets, lookupCanonicalTarget, persistLearnHints, resolveTargetEvolutionPolicy, resolveTargetLocalFilesReadonly, } from '../core/self-evolution/index.js';
|
|
3
|
+
import { detectFrozenHealthRoutingObservations, detectUnbindableHintObservations, generateEvolutionHints, isCanonicalTargetEvolvable, listCanonicalTargets, lookupCanonicalTarget, persistLearnHints, resolveTargetEvolutionPolicy, resolveTargetLocalFilesReadonly, } from '../core/self-evolution/index.js';
|
|
4
4
|
import { readProjectConfig } from '../core/project-config.js';
|
|
5
5
|
import { assembleTrajectoryContext, } from '../core/learn/trajectory-assembler.js';
|
|
6
|
-
import { findTranscriptsForChange, resolveChangeDir, } from '../core/learn/trajectory-discovery.js';
|
|
6
|
+
import { findTranscriptsForChange, resolveChangeDir, validateExplicitTrajectoryHandle, } from '../core/learn/trajectory-discovery.js';
|
|
7
7
|
import { getTrajectoryForChange } from '../core/trajectory/registry.js';
|
|
8
8
|
import { toTrajectoryFacts, describeRunnerResults } from '../core/trajectory/facts.js';
|
|
9
|
+
import { toActionSkeleton } from '../core/trajectory/skeleton.js';
|
|
9
10
|
import { resolveHostHarness } from '../core/self-evolution/host-harness.js';
|
|
11
|
+
import { mineSuccessSignals } from '../core/self-evolution/success-channel.js';
|
|
10
12
|
import { buildLLMSummaryCandidates, ingestLearnHandoff, } from '../core/learn/llm-summary.js';
|
|
11
13
|
function collect(value, previous) {
|
|
12
14
|
previous.push(value);
|
|
@@ -21,15 +23,62 @@ export function registerLearnCommand(program) {
|
|
|
21
23
|
.option('--only <candidate-id>', 'When applying, write only this keep candidate id (repeatable)', collect, [])
|
|
22
24
|
.option('--exclude <candidate-id>', 'When applying, skip this candidate id (repeatable)', collect, [])
|
|
23
25
|
.option('-y, --yes', 'Confirm --apply and skip confirmation prompts')
|
|
24
|
-
.option('--persist-hints', 'Persist derived evolution hints to a learn-handoffs/ file
|
|
26
|
+
.option('--persist-hints', 'Persist derived evolution hints to a learn-handoffs/ file consumed by self-evolution evolve-from-edits --from-learn (writes no canonical file; implied by --apply)')
|
|
25
27
|
.option('--evolve-target <ids>', 'comma-separated canonical target ids whose hints learn may emit (supports all/none); overrides config selfEvolution for this run')
|
|
26
28
|
.option('--freeze-target <ids>', 'comma-separated canonical target ids whose hints learn must NOT emit (supports all/none; beats --evolve-target)')
|
|
29
|
+
.option('--no-focus', 'Disable the evolution-focus switch for this run: when the policy is frozen-by-default, frozen-kind signals are dropped silently instead of being re-aimed at the explicitly evolvable target(s) as a policy-focus hint (config: selfEvolution.focus)')
|
|
30
|
+
.option('--transcript <path>', 'Explicit transcript .jsonl to grade (bypasses change-window discovery; Claude transcript store only)')
|
|
31
|
+
.option('--session-id <id>', 'Explicit Claude session id to grade (bypasses change-window discovery; Claude transcript store only)')
|
|
27
32
|
.option('--json', 'Output as JSON')
|
|
28
33
|
.option('--no-interactive', 'Disable interactive prompts (learn is non-interactive by default)')
|
|
29
34
|
.action(async (change, options) => {
|
|
30
35
|
try {
|
|
31
36
|
const projectRoot = process.cwd();
|
|
32
|
-
|
|
37
|
+
// USER-TYPED handle flags are validated up front and fail LOUD
|
|
38
|
+
// (exit 1) on a miss — unlike the env-var channel, which keeps the
|
|
39
|
+
// fail-closed refusal semantics inside discovery (empty result, the
|
|
40
|
+
// gate refuses), and unlike debug-trajectory, which is the diagnostic
|
|
41
|
+
// that SHOWS the miss. Validated BEFORE the env is mutated below so a
|
|
42
|
+
// bad flag never leaks into the environment.
|
|
43
|
+
const handleError = await validateExplicitTrajectoryHandle({
|
|
44
|
+
projectRoot,
|
|
45
|
+
transcriptPath: options.transcript,
|
|
46
|
+
sessionId: options.sessionId,
|
|
47
|
+
});
|
|
48
|
+
if (handleError)
|
|
49
|
+
throw new Error(handleError);
|
|
50
|
+
// Explicit trajectory handle: surfaced to the discovery layer via env
|
|
51
|
+
// (same set/restore shape as debug-trajectory's --harness) so it
|
|
52
|
+
// reaches the registry adapter inside generateLearnReport without
|
|
53
|
+
// changing any core signature. Restored as soon as the report exists.
|
|
54
|
+
const prevTranscriptEnv = process.env.SYNERGYSPEC_SELFEVOLVING_TRANSCRIPT;
|
|
55
|
+
const prevSessionEnv = process.env.SYNERGYSPEC_SELFEVOLVING_SESSION_ID;
|
|
56
|
+
if (options.transcript)
|
|
57
|
+
process.env.SYNERGYSPEC_SELFEVOLVING_TRANSCRIPT = options.transcript;
|
|
58
|
+
if (options.sessionId)
|
|
59
|
+
process.env.SYNERGYSPEC_SELFEVOLVING_SESSION_ID = options.sessionId;
|
|
60
|
+
let report;
|
|
61
|
+
try {
|
|
62
|
+
report = await generateLearnReport({ projectRoot, changeName: change });
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
if (options.transcript) {
|
|
66
|
+
if (prevTranscriptEnv === undefined) {
|
|
67
|
+
delete process.env.SYNERGYSPEC_SELFEVOLVING_TRANSCRIPT;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
process.env.SYNERGYSPEC_SELFEVOLVING_TRANSCRIPT = prevTranscriptEnv;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (options.sessionId) {
|
|
74
|
+
if (prevSessionEnv === undefined) {
|
|
75
|
+
delete process.env.SYNERGYSPEC_SELFEVOLVING_SESSION_ID;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
process.env.SYNERGYSPEC_SELFEVOLVING_SESSION_ID = prevSessionEnv;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
33
82
|
if (options.apply === true && options.yes !== true) {
|
|
34
83
|
throw new Error('learn --apply requires --yes to confirm memory writes');
|
|
35
84
|
}
|
|
@@ -40,6 +89,7 @@ export function registerLearnCommand(program) {
|
|
|
40
89
|
config: readProjectConfig(projectRoot),
|
|
41
90
|
evolveTarget: options.evolveTarget,
|
|
42
91
|
freezeTarget: options.freezeTarget,
|
|
92
|
+
focus: options.focus,
|
|
43
93
|
});
|
|
44
94
|
const evolutionHints = generateEvolutionHints(report, targetPolicy);
|
|
45
95
|
const evolutionPreview = await buildEvolutionPreview(evolutionHints, targetPolicy, projectRoot);
|
|
@@ -49,10 +99,11 @@ export function registerLearnCommand(program) {
|
|
|
49
99
|
// is actually trying to evolve (--apply / --persist-hints / a named
|
|
50
100
|
// --evolve-target). On a plain preview run the kind-only ambiguity is the
|
|
51
101
|
// designed state, not a defect, so a bare `learn <change>` stays byte-identical.
|
|
52
|
-
if (options
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
102
|
+
if (isEvolvingRun(options)) {
|
|
103
|
+
report.observations.push(...detectUnbindableHintObservations(evolutionHints, targetPolicy),
|
|
104
|
+
// Health offenders whose producing target is frozen are a routing
|
|
105
|
+
// dead-end the operator must see (ACTION), never a silent drop.
|
|
106
|
+
...detectFrozenHealthRoutingObservations(report, targetPolicy));
|
|
56
107
|
}
|
|
57
108
|
const applied = options.apply === true
|
|
58
109
|
? await applyLearnCandidates({
|
|
@@ -77,6 +128,7 @@ export function registerLearnCommand(program) {
|
|
|
77
128
|
config: readProjectConfig(projectRoot),
|
|
78
129
|
evolveTarget: options.evolveTarget,
|
|
79
130
|
freezeTarget: options.freezeTarget,
|
|
131
|
+
focus: options.focus,
|
|
80
132
|
});
|
|
81
133
|
const hints = generateEvolutionHints(report, targetPolicy);
|
|
82
134
|
if (hints.length > 0) {
|
|
@@ -87,6 +139,24 @@ export function registerLearnCommand(program) {
|
|
|
87
139
|
});
|
|
88
140
|
}
|
|
89
141
|
}
|
|
142
|
+
// SUCCESS CHANNEL (R4): on an opt-in evolving run (--apply /
|
|
143
|
+
// --persist-hints) a verified-GREEN report mines load-bearing
|
|
144
|
+
// protections + exemplars — side-writes only, never a candidate, so
|
|
145
|
+
// abstain-on-success is untouched. A bare preview `learn` run never
|
|
146
|
+
// reaches this (no files created), and the mining is best-effort so a
|
|
147
|
+
// side-write failure never fails the learn run.
|
|
148
|
+
let successSummary;
|
|
149
|
+
if (options.apply === true || options.persistHints === true) {
|
|
150
|
+
try {
|
|
151
|
+
const mined = await mineSuccessSignals({ projectRoot, report });
|
|
152
|
+
if (mined.protectionsWritten > 0) {
|
|
153
|
+
successSummary = `Success channel: recorded ${mined.protectionsWritten} protection(s) for ${mined.protectedTargets.join(', ')}`;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// side-write only; never fail learn over it.
|
|
158
|
+
}
|
|
159
|
+
}
|
|
90
160
|
if (options.json) {
|
|
91
161
|
printJson(report, applied, evolutionPreview, hintsPath);
|
|
92
162
|
return;
|
|
@@ -94,6 +164,10 @@ export function registerLearnCommand(program) {
|
|
|
94
164
|
console.log(renderLearnReport(report, applied));
|
|
95
165
|
console.log('');
|
|
96
166
|
console.log(renderLearnTransparency(report, applied, evolutionPreview, hintsPath, options));
|
|
167
|
+
if (successSummary) {
|
|
168
|
+
console.log('');
|
|
169
|
+
console.log(successSummary);
|
|
170
|
+
}
|
|
97
171
|
}
|
|
98
172
|
catch (error) {
|
|
99
173
|
if (options.json) {
|
|
@@ -110,10 +184,20 @@ export function registerLearnCommand(program) {
|
|
|
110
184
|
.description('Print the assembled TrajectoryContext for a change as JSON. Read-only; runs no LLM handoff and writes nothing.')
|
|
111
185
|
.option('--preview', 'Truncate the trajectory text field to 4000 chars in the output')
|
|
112
186
|
.option('--harness <name>', 'Force the observed-run trajectory adapter (claude|codex|opencode); defaults to the resolved host harness')
|
|
113
|
-
.
|
|
187
|
+
.option('--transcript <path>', 'Explicit transcript .jsonl to grade (bypasses change-window discovery; Claude transcript store only)')
|
|
188
|
+
.option('--session-id <id>', 'Explicit Claude session id to grade (bypasses change-window discovery; Claude transcript store only)')
|
|
189
|
+
.action(async (change, _opts, command) => {
|
|
114
190
|
const projectRoot = process.cwd();
|
|
191
|
+
// --transcript/--session-id are also declared on the parent `learn`
|
|
192
|
+
// command, which consumes them before this subcommand sees them.
|
|
193
|
+
// optsWithGlobals() merges ancestor + local options so the explicit
|
|
194
|
+
// handle is honored either way.
|
|
195
|
+
const opts = command.optsWithGlobals();
|
|
115
196
|
try {
|
|
116
|
-
const discovered = await findTranscriptsForChange(change, projectRoot
|
|
197
|
+
const discovered = await findTranscriptsForChange(change, projectRoot, {
|
|
198
|
+
transcriptPath: opts.transcript,
|
|
199
|
+
sessionId: opts.sessionId,
|
|
200
|
+
});
|
|
117
201
|
const assembled = await assembleTrajectoryContext({
|
|
118
202
|
changeName: change,
|
|
119
203
|
projectRoot,
|
|
@@ -137,8 +221,17 @@ export function registerLearnCommand(program) {
|
|
|
137
221
|
// its facts + per-runner-result breakdown so a misgrade is visible in one
|
|
138
222
|
// command. `--harness` forces a specific adapter for cross-host debugging.
|
|
139
223
|
const prevHarnessEnv = process.env.SYNERGYSPEC_SELFEVOLVING_HOST_HARNESS;
|
|
224
|
+
const prevTranscriptEnv = process.env.SYNERGYSPEC_SELFEVOLVING_TRANSCRIPT;
|
|
225
|
+
const prevSessionEnv = process.env.SYNERGYSPEC_SELFEVOLVING_SESSION_ID;
|
|
140
226
|
if (opts.harness)
|
|
141
227
|
process.env.SYNERGYSPEC_SELFEVOLVING_HOST_HARNESS = opts.harness;
|
|
228
|
+
// The explicit trajectory handle must reach the adapter's own discovery
|
|
229
|
+
// call too (env is the only channel through the registry), so the
|
|
230
|
+
// introspected payload reflects the same override as `discovery` above.
|
|
231
|
+
if (opts.transcript)
|
|
232
|
+
process.env.SYNERGYSPEC_SELFEVOLVING_TRANSCRIPT = opts.transcript;
|
|
233
|
+
if (opts.sessionId)
|
|
234
|
+
process.env.SYNERGYSPEC_SELFEVOLVING_SESSION_ID = opts.sessionId;
|
|
142
235
|
try {
|
|
143
236
|
const adapterTrajectory = await getTrajectoryForChange(projectRoot, change);
|
|
144
237
|
payload.adapter = {
|
|
@@ -148,6 +241,9 @@ export function registerLearnCommand(program) {
|
|
|
148
241
|
sourcePaths: adapterTrajectory ? [...new Set(adapterTrajectory.sourcePaths)] : [],
|
|
149
242
|
facts: toTrajectoryFacts(adapterTrajectory, change),
|
|
150
243
|
runnerResults: describeRunnerResults(adapterTrajectory),
|
|
244
|
+
// Bounded play-by-play projection (file edits / test runs /
|
|
245
|
+
// commands) so a wrong skeleton is visible in one command.
|
|
246
|
+
steps: toActionSkeleton(adapterTrajectory),
|
|
151
247
|
};
|
|
152
248
|
}
|
|
153
249
|
finally {
|
|
@@ -159,6 +255,22 @@ export function registerLearnCommand(program) {
|
|
|
159
255
|
process.env.SYNERGYSPEC_SELFEVOLVING_HOST_HARNESS = prevHarnessEnv;
|
|
160
256
|
}
|
|
161
257
|
}
|
|
258
|
+
if (opts.transcript) {
|
|
259
|
+
if (prevTranscriptEnv === undefined) {
|
|
260
|
+
delete process.env.SYNERGYSPEC_SELFEVOLVING_TRANSCRIPT;
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
process.env.SYNERGYSPEC_SELFEVOLVING_TRANSCRIPT = prevTranscriptEnv;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (opts.sessionId) {
|
|
267
|
+
if (prevSessionEnv === undefined) {
|
|
268
|
+
delete process.env.SYNERGYSPEC_SELFEVOLVING_SESSION_ID;
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
process.env.SYNERGYSPEC_SELFEVOLVING_SESSION_ID = prevSessionEnv;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
162
274
|
}
|
|
163
275
|
if (assembled.kind === 'ok') {
|
|
164
276
|
const t = assembled.trajectory;
|
|
@@ -194,25 +306,69 @@ export function registerLearnCommand(program) {
|
|
|
194
306
|
learnCmd
|
|
195
307
|
.command('handoff <change>')
|
|
196
308
|
.description('Write an LLM trajectory-extraction brief for a change and return immediately (no polling). Fulfill it in-session by writing memory-items.md, then run `learn ingest-handoff`.')
|
|
309
|
+
.option('--transcript <path>', 'Explicit transcript .jsonl to grade (bypasses change-window discovery; Claude transcript store only)')
|
|
310
|
+
.option('--session-id <id>', 'Explicit Claude session id to grade (bypasses change-window discovery; Claude transcript store only)')
|
|
197
311
|
.option('--json', 'Output as JSON')
|
|
198
312
|
.action(async (change, _opts, command) => {
|
|
199
313
|
const projectRoot = process.cwd();
|
|
200
|
-
// The parent `learn [change]` command also declares --json,
|
|
201
|
-
// consumes the
|
|
202
|
-
// merges ancestor + local options so
|
|
314
|
+
// The parent `learn [change]` command also declares --json, --transcript
|
|
315
|
+
// and --session-id, so it consumes the flags before the subcommand sees
|
|
316
|
+
// them. optsWithGlobals() merges ancestor + local options so they are
|
|
317
|
+
// honored either way.
|
|
203
318
|
const opts = command.optsWithGlobals();
|
|
204
319
|
try {
|
|
205
|
-
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
|
|
209
|
-
const result = await buildLLMSummaryCandidates({
|
|
320
|
+
// USER-TYPED handle flags fail LOUD up front (exit 1) — without this
|
|
321
|
+
// a bad --session-id would silently fall back to window discovery,
|
|
322
|
+
// the exact failure mode the explicit handles exist to kill.
|
|
323
|
+
const handleError = await validateExplicitTrajectoryHandle({
|
|
210
324
|
projectRoot,
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
pollForResponse: false,
|
|
214
|
-
forceEnable: true,
|
|
325
|
+
transcriptPath: opts.transcript,
|
|
326
|
+
sessionId: opts.sessionId,
|
|
215
327
|
});
|
|
328
|
+
if (handleError)
|
|
329
|
+
throw new Error(handleError);
|
|
330
|
+
const changeDir = await resolveChangeDir(projectRoot, change);
|
|
331
|
+
// The explicit trajectory handle must reach the discovery call inside
|
|
332
|
+
// buildLLMSummaryCandidates (env is the only channel through it), so
|
|
333
|
+
// set/restore the same way the main learn action does. Restored as
|
|
334
|
+
// soon as the trajectory assembly returns.
|
|
335
|
+
const prevTranscriptEnv = process.env.SYNERGYSPEC_SELFEVOLVING_TRANSCRIPT;
|
|
336
|
+
const prevSessionEnv = process.env.SYNERGYSPEC_SELFEVOLVING_SESSION_ID;
|
|
337
|
+
if (opts.transcript)
|
|
338
|
+
process.env.SYNERGYSPEC_SELFEVOLVING_TRANSCRIPT = opts.transcript;
|
|
339
|
+
if (opts.sessionId)
|
|
340
|
+
process.env.SYNERGYSPEC_SELFEVOLVING_SESSION_ID = opts.sessionId;
|
|
341
|
+
let result;
|
|
342
|
+
try {
|
|
343
|
+
// forceEnable: invoking this command IS the opt-in, so it does not
|
|
344
|
+
// depend on SYNERGYSPEC_SELFEVOLVING_LEARN_LLM. pollForResponse:false
|
|
345
|
+
// is what avoids the single-actor poll deadlock.
|
|
346
|
+
result = await buildLLMSummaryCandidates({
|
|
347
|
+
projectRoot,
|
|
348
|
+
changeName: change,
|
|
349
|
+
changeDir,
|
|
350
|
+
pollForResponse: false,
|
|
351
|
+
forceEnable: true,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
finally {
|
|
355
|
+
if (opts.transcript) {
|
|
356
|
+
if (prevTranscriptEnv === undefined) {
|
|
357
|
+
delete process.env.SYNERGYSPEC_SELFEVOLVING_TRANSCRIPT;
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
process.env.SYNERGYSPEC_SELFEVOLVING_TRANSCRIPT = prevTranscriptEnv;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (opts.sessionId) {
|
|
364
|
+
if (prevSessionEnv === undefined) {
|
|
365
|
+
delete process.env.SYNERGYSPEC_SELFEVOLVING_SESSION_ID;
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
process.env.SYNERGYSPEC_SELFEVOLVING_SESSION_ID = prevSessionEnv;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
216
372
|
if (!result) {
|
|
217
373
|
throw new Error('handoff stage returned no result');
|
|
218
374
|
}
|
|
@@ -458,6 +614,11 @@ async function buildEvolutionPreview(hints, targetPolicy, projectRoot) {
|
|
|
458
614
|
target.localFiles = [];
|
|
459
615
|
}
|
|
460
616
|
}
|
|
617
|
+
// Surface the evolution-focus re-aim explicitly: a synthesized
|
|
618
|
+
// `origin: 'policy-focus'` hint means every ordinary signal of this change
|
|
619
|
+
// bound to a frozen kind and its evidence was re-aimed at the focused
|
|
620
|
+
// target(s). Agents read this field instead of inferring from hint ids.
|
|
621
|
+
const focusHints = hints.filter((hint) => hint.origin === 'policy-focus');
|
|
461
622
|
return {
|
|
462
623
|
hintCount: hints.length,
|
|
463
624
|
targetPolicy: {
|
|
@@ -468,6 +629,16 @@ async function buildEvolutionPreview(hints, targetPolicy, projectRoot) {
|
|
|
468
629
|
...(targetPolicy.source.cliEvolve ? { cliEvolve: targetPolicy.source.cliEvolve } : {}),
|
|
469
630
|
...(targetPolicy.source.cliFreeze ? { cliFreeze: targetPolicy.source.cliFreeze } : {}),
|
|
470
631
|
},
|
|
632
|
+
...(focusHints.length > 0
|
|
633
|
+
? {
|
|
634
|
+
focus: {
|
|
635
|
+
targetIds: [
|
|
636
|
+
...new Set(focusHints.flatMap((hint) => hint.affectedTargetId ? [hint.affectedTargetId] : [])),
|
|
637
|
+
].sort((left, right) => left.localeCompare(right)),
|
|
638
|
+
hintIds: focusHints.map((hint) => hint.id),
|
|
639
|
+
},
|
|
640
|
+
}
|
|
641
|
+
: {}),
|
|
471
642
|
targets: [...byTarget.values()].sort((left, right) => (left.targetId ?? `~${left.targetKind}`).localeCompare(right.targetId ?? `~${right.targetKind}`)),
|
|
472
643
|
};
|
|
473
644
|
}
|
|
@@ -507,9 +678,15 @@ function renderLearnTransparency(report, applied, evolutionPreview, hintsPath, o
|
|
|
507
678
|
lines.push('');
|
|
508
679
|
lines.push('### Skill/Template Optimization Preview');
|
|
509
680
|
lines.push(`- Evolution policy: default=${evolutionPreview.targetPolicy.default}${renderExplicitPolicy(evolutionPreview.targetPolicy.explicit)}`);
|
|
681
|
+
if (evolutionPreview.focus) {
|
|
682
|
+
lines.push(`- Evolution focus: ${evolutionPreview.focus.targetIds.join(', ')} — every signal of this change bound to a frozen kind, so the dropped evidence was re-aimed at the focused target as ${evolutionPreview.focus.hintIds.join(', ')} (origin: policy-focus). Disable with --no-focus or selfEvolution.focus: false.`);
|
|
683
|
+
}
|
|
510
684
|
if (evolutionPreview.targets.length === 0) {
|
|
511
685
|
lines.push('- Target: none under the current evidence and evolve/freeze policy.');
|
|
512
686
|
lines.push('- How: no prompt/template/skill optimization will be proposed from this learn report unless more evidence is added or the target policy is widened.');
|
|
687
|
+
if (options.focus === false && evolutionPreview.targetPolicy.default === 'frozen') {
|
|
688
|
+
lines.push('- Note: the evolution-focus switch is disabled for this run (--no-focus); frozen-kind signals, if any, were dropped without a focus re-aim.');
|
|
689
|
+
}
|
|
513
690
|
}
|
|
514
691
|
else {
|
|
515
692
|
for (const target of evolutionPreview.targets) {
|
|
@@ -566,12 +743,37 @@ function renderLearnTransparency(report, applied, evolutionPreview, hintsPath, o
|
|
|
566
743
|
else if (evolutionPreview.targets.length > 0) {
|
|
567
744
|
lines.push(`- Persist the optimization evidence: synergyspec-selfevolving learn "${report.changeName}" --persist-hints${renderTargetArgs(options)}`);
|
|
568
745
|
}
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
746
|
+
else if (isEvolvingRun(options)) {
|
|
747
|
+
// Empty-target evolving run: say so explicitly instead of leaving the
|
|
748
|
+
// section blank and falling through to the headless fallback (ses_148b:
|
|
749
|
+
// that fallthrough made the skill-banned `--agent` steering the only
|
|
750
|
+
// actionable next-step a host agent saw).
|
|
751
|
+
lines.push('- No evolvable optimization target bound under the current evidence and evolve/freeze policy — a safe no-op (Outcome: not-run).');
|
|
752
|
+
}
|
|
753
|
+
if (isEvolvingRun(options)) {
|
|
754
|
+
lines.push('- After reviewing or evolving, run /synspec:archive to close the change.');
|
|
755
|
+
}
|
|
756
|
+
else {
|
|
757
|
+
lines.push('');
|
|
758
|
+
lines.push('headless fallback (no host agent):');
|
|
759
|
+
lines.push(`- One-button local evolve: synergyspec-selfevolving self-evolution auto-evolve --change "${report.changeName}"${renderTargetArgs(options)}`);
|
|
760
|
+
lines.push('- After reviewing or evolving, run /synspec:archive to close the change.');
|
|
761
|
+
}
|
|
573
762
|
return lines.join('\n');
|
|
574
763
|
}
|
|
764
|
+
/**
|
|
765
|
+
* An "evolving run" is one where the operator opted into evolution
|
|
766
|
+
* (`--apply` / `--persist-hints` / a named `--evolve-target`) — per the skill,
|
|
767
|
+
* the bare CLI previews and only the skill/agent flow passes these flags, so
|
|
768
|
+
* this is the agent-in-the-loop proxy (the same signal that gates the
|
|
769
|
+
* unbindable-hint observations in the learn action). The headless
|
|
770
|
+
* `auto-evolve` fallback (it spawns its proposer internally) is for runs with
|
|
771
|
+
* NO agent in the loop; the skill forbids the headless proposer when an agent
|
|
772
|
+
* IS the proposer, so the fallback is suppressed on evolving runs.
|
|
773
|
+
*/
|
|
774
|
+
function isEvolvingRun(options) {
|
|
775
|
+
return (options.apply === true || options.persistHints === true || options.evolveTarget !== undefined);
|
|
776
|
+
}
|
|
575
777
|
function renderExplicitPolicy(explicit) {
|
|
576
778
|
if (explicit.length === 0)
|
|
577
779
|
return '';
|