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
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { HarnessName, NormalizedTrajectory } from './model.js';
|
|
2
|
+
export interface SkeletonEvent {
|
|
3
|
+
kind: 'file-edit' | 'test-run' | 'command';
|
|
4
|
+
/** 0-based position in the (post-rollup, pre-truncation) event sequence. */
|
|
5
|
+
ordinal: number;
|
|
6
|
+
/** Tool name as the harness reports it. */
|
|
7
|
+
tool: string;
|
|
8
|
+
/** POSIX-normalized file path (file-edit only). */
|
|
9
|
+
file?: string;
|
|
10
|
+
/** How many consecutive edits to `file` this event rolls up (file-edit only). */
|
|
11
|
+
editCount?: number;
|
|
12
|
+
/** Executed command line, capped (test-run / command only). */
|
|
13
|
+
command?: string;
|
|
14
|
+
/** Exit code of the paired result, when structured (test-run / command). */
|
|
15
|
+
exitCode?: number | null;
|
|
16
|
+
/** Pass rate parsed from the paired runner output (test-run only). */
|
|
17
|
+
passRate?: number | null;
|
|
18
|
+
/** Failed-test count parsed from the paired runner output (test-run only). */
|
|
19
|
+
failedCount?: number | null;
|
|
20
|
+
/** Session that produced the event (main or subagent). */
|
|
21
|
+
sessionId?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface ActionSkeleton {
|
|
24
|
+
harness: HarnessName;
|
|
25
|
+
/** Ordered, rolled-up, bounded events (≤ {@link MAX_SKELETON_EVENTS}). */
|
|
26
|
+
events: SkeletonEvent[];
|
|
27
|
+
/** Total tool_call parts seen (pre-projection activity signal). */
|
|
28
|
+
totalToolCalls: number;
|
|
29
|
+
/** True when events were middle-out truncated to the cap. */
|
|
30
|
+
truncated: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Project the bounded action skeleton from a normalized trajectory. One walk,
|
|
34
|
+
* the same call→result pairing approach as facts.ts's collectRunnerResults
|
|
35
|
+
* (callId map with a positional fallback).
|
|
36
|
+
*/
|
|
37
|
+
export declare function toActionSkeleton(trajectory: NormalizedTrajectory | null): ActionSkeleton | null;
|
|
38
|
+
/**
|
|
39
|
+
* Human-readable one-line(ish) play-by-play. Deterministic; events are dropped
|
|
40
|
+
* middle-out (with an elision marker) until the string fits `maxChars`.
|
|
41
|
+
*/
|
|
42
|
+
export declare function renderActionSkeleton(skeleton: ActionSkeleton, maxChars?: number): string;
|
|
43
|
+
//# sourceMappingURL=skeleton.d.ts.map
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Action-skeleton projection: a BOUNDED, ordered play-by-play of what the agent
|
|
3
|
+
* actually did in the observed run — `wrote design.md → ran tests: 3 failed →
|
|
4
|
+
* edited normalize.py (x4) → ran tests: 0 failed`.
|
|
5
|
+
*
|
|
6
|
+
* This is the trajectory the critic reads. It is deliberately a PROJECTION of
|
|
7
|
+
* the same window-scoped {@link NormalizedTrajectory} the observed-verified
|
|
8
|
+
* gate grades (one shared implementation over the normalized shape — never
|
|
9
|
+
* per-adapter, so the three harnesses cannot drift), and deliberately lossy:
|
|
10
|
+
* only file edits, test runs, and other shell commands survive; reads, prose,
|
|
11
|
+
* and reasoning are dropped. Ordering is the stitched turn order (main session
|
|
12
|
+
* first, then subagents) — the exact order `collectRunnerResults` grades in.
|
|
13
|
+
*
|
|
14
|
+
* Pure + no throw. `null` in → `null` out.
|
|
15
|
+
*/
|
|
16
|
+
import { parseTestMetrics } from '../fitness/test-metrics.js';
|
|
17
|
+
import { commandText, inputLooksLikeRunner, isExecTool } from './facts.js';
|
|
18
|
+
const MAX_SKELETON_EVENTS = 40;
|
|
19
|
+
const MAX_COMMAND_CHARS = 120;
|
|
20
|
+
/**
|
|
21
|
+
* Matches the NAME of a file-mutating tool across harnesses — Claude
|
|
22
|
+
* `Write`/`Edit`/`MultiEdit`/`NotebookEdit`; opencode `write`/`edit`/`patch`;
|
|
23
|
+
* Codex `apply_patch`; generic `str_replace`/`create_file` variants. Deny by
|
|
24
|
+
* default (same philosophy as facts.ts's EXEC_TOOL_RE): an unrecognized tool
|
|
25
|
+
* name degrades to "event skipped", never to a phantom edit. Word-boundaried
|
|
26
|
+
* on `._-`; `multiedit`/`notebookedit` are single lowercase tokens after
|
|
27
|
+
* `.toLowerCase()`, so they are listed explicitly.
|
|
28
|
+
*/
|
|
29
|
+
const EDIT_TOOL_RE = /(?:^|[._-])(?:write|edit|multiedit|notebookedit|patch|apply_patch|str_replace|replace|create|update)(?:[._-]|$)/i;
|
|
30
|
+
/** Input fields that carry the edited file path, in preference order. */
|
|
31
|
+
const FILE_FIELDS = ['file_path', 'filePath', 'path', 'notebook_path', 'notebookPath'];
|
|
32
|
+
/** `*** (Add|Update|Delete) File: <path>` lines inside an apply_patch payload. */
|
|
33
|
+
const PATCH_FILE_RE = /^\*{3}\s+(?:Add|Update|Delete)\s+File:\s+(.+)$/gm;
|
|
34
|
+
function toPosix(p) {
|
|
35
|
+
return p.replace(/\\/g, '/').trim();
|
|
36
|
+
}
|
|
37
|
+
function isEditTool(tool) {
|
|
38
|
+
return tool !== undefined && EDIT_TOOL_RE.test(tool);
|
|
39
|
+
}
|
|
40
|
+
/** `*** … File:` paths inside an apply_patch-style payload (markers only). */
|
|
41
|
+
function patchFilesFromPayload(input) {
|
|
42
|
+
if (!input)
|
|
43
|
+
return [];
|
|
44
|
+
const files = [];
|
|
45
|
+
for (const value of Object.values(input)) {
|
|
46
|
+
if (typeof value !== 'string')
|
|
47
|
+
continue;
|
|
48
|
+
for (const m of value.matchAll(PATCH_FILE_RE))
|
|
49
|
+
files.push(toPosix(m[1]));
|
|
50
|
+
}
|
|
51
|
+
return files;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Extract the edited file path(s) from a FILE-tool call's input: a path field
|
|
55
|
+
* first, else patch-body markers. Never used for exec tools — their inputs
|
|
56
|
+
* legitimately carry `path`-like fields (cwd) that are not edits.
|
|
57
|
+
*/
|
|
58
|
+
function editedFiles(input) {
|
|
59
|
+
if (!input)
|
|
60
|
+
return [];
|
|
61
|
+
for (const f of FILE_FIELDS) {
|
|
62
|
+
const v = input[f];
|
|
63
|
+
if (typeof v === 'string' && v.trim().length > 0)
|
|
64
|
+
return [toPosix(v)];
|
|
65
|
+
}
|
|
66
|
+
return patchFilesFromPayload(input);
|
|
67
|
+
}
|
|
68
|
+
function capCommand(command) {
|
|
69
|
+
if (!command)
|
|
70
|
+
return undefined;
|
|
71
|
+
const c = command.trim().replace(/\s+/g, ' ');
|
|
72
|
+
return c.length > MAX_COMMAND_CHARS ? `${c.slice(0, MAX_COMMAND_CHARS - 1)}…` : c;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Project the bounded action skeleton from a normalized trajectory. One walk,
|
|
76
|
+
* the same call→result pairing approach as facts.ts's collectRunnerResults
|
|
77
|
+
* (callId map with a positional fallback).
|
|
78
|
+
*/
|
|
79
|
+
export function toActionSkeleton(trajectory) {
|
|
80
|
+
if (!trajectory)
|
|
81
|
+
return null;
|
|
82
|
+
const pendingByCallId = new Map();
|
|
83
|
+
let lastPending = null;
|
|
84
|
+
let totalToolCalls = 0;
|
|
85
|
+
const events = [];
|
|
86
|
+
const append = (event) => {
|
|
87
|
+
// Per-file rollup: consecutive edits to the same file collapse.
|
|
88
|
+
const prev = events[events.length - 1];
|
|
89
|
+
if (event.kind === 'file-edit' &&
|
|
90
|
+
prev?.kind === 'file-edit' &&
|
|
91
|
+
prev.file === event.file &&
|
|
92
|
+
prev.sessionId === event.sessionId) {
|
|
93
|
+
prev.editCount = (prev.editCount ?? 1) + 1;
|
|
94
|
+
return prev;
|
|
95
|
+
}
|
|
96
|
+
events.push(event);
|
|
97
|
+
return event;
|
|
98
|
+
};
|
|
99
|
+
for (const turn of trajectory.turns) {
|
|
100
|
+
for (const part of turn.parts) {
|
|
101
|
+
if (part.kind === 'tool_call') {
|
|
102
|
+
totalToolCalls++;
|
|
103
|
+
const exec = isExecTool(part.tool);
|
|
104
|
+
if (exec) {
|
|
105
|
+
const command = capCommand(commandText(part.input));
|
|
106
|
+
// A shell-driven apply_patch (codex heredoc style) carries
|
|
107
|
+
// `*** Update File: <path>` lines in its payload — that's a file
|
|
108
|
+
// edit, not a command. Markers only: an exec input's `path`-like
|
|
109
|
+
// fields (cwd) must never read as edits.
|
|
110
|
+
const patchedFiles = patchFilesFromPayload(part.input);
|
|
111
|
+
if (patchedFiles.length > 0) {
|
|
112
|
+
for (const file of patchedFiles) {
|
|
113
|
+
append({
|
|
114
|
+
kind: 'file-edit',
|
|
115
|
+
ordinal: 0,
|
|
116
|
+
tool: part.tool,
|
|
117
|
+
file,
|
|
118
|
+
editCount: 1,
|
|
119
|
+
...(turn.sessionId ? { sessionId: turn.sessionId } : {}),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
lastPending = null;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const event = append({
|
|
126
|
+
kind: inputLooksLikeRunner(part.input) ? 'test-run' : 'command',
|
|
127
|
+
ordinal: 0,
|
|
128
|
+
tool: part.tool,
|
|
129
|
+
...(command ? { command } : {}),
|
|
130
|
+
...(turn.sessionId ? { sessionId: turn.sessionId } : {}),
|
|
131
|
+
});
|
|
132
|
+
const pending = { event };
|
|
133
|
+
lastPending = pending;
|
|
134
|
+
if (part.callId)
|
|
135
|
+
pendingByCallId.set(part.callId, pending);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (isEditTool(part.tool)) {
|
|
139
|
+
for (const file of editedFiles(part.input)) {
|
|
140
|
+
append({
|
|
141
|
+
kind: 'file-edit',
|
|
142
|
+
ordinal: 0,
|
|
143
|
+
tool: part.tool,
|
|
144
|
+
file,
|
|
145
|
+
editCount: 1,
|
|
146
|
+
...(turn.sessionId ? { sessionId: turn.sessionId } : {}),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
lastPending = null;
|
|
151
|
+
}
|
|
152
|
+
else if (part.kind === 'tool_result') {
|
|
153
|
+
const pending = part.callId && pendingByCallId.has(part.callId)
|
|
154
|
+
? pendingByCallId.get(part.callId)
|
|
155
|
+
: !part.callId && lastPending
|
|
156
|
+
? lastPending
|
|
157
|
+
: undefined;
|
|
158
|
+
if (pending) {
|
|
159
|
+
const e = pending.event;
|
|
160
|
+
if (typeof part.exitCode === 'number')
|
|
161
|
+
e.exitCode = part.exitCode;
|
|
162
|
+
if (e.kind === 'test-run' && typeof part.output === 'string') {
|
|
163
|
+
const metrics = parseTestMetrics(part.output);
|
|
164
|
+
if (metrics) {
|
|
165
|
+
e.passRate = metrics.passRate;
|
|
166
|
+
e.failedCount = metrics.failed;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
lastPending = null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// Stamp ordinals on the full (rolled-up) sequence, then middle-out truncate.
|
|
175
|
+
events.forEach((e, i) => {
|
|
176
|
+
e.ordinal = i;
|
|
177
|
+
});
|
|
178
|
+
let bounded = events;
|
|
179
|
+
let truncated = false;
|
|
180
|
+
if (events.length > MAX_SKELETON_EVENTS) {
|
|
181
|
+
const head = Math.ceil(MAX_SKELETON_EVENTS / 2);
|
|
182
|
+
const tail = MAX_SKELETON_EVENTS - head;
|
|
183
|
+
bounded = [...events.slice(0, head), ...events.slice(events.length - tail)];
|
|
184
|
+
truncated = true;
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
harness: trajectory.harness,
|
|
188
|
+
events: bounded,
|
|
189
|
+
totalToolCalls,
|
|
190
|
+
truncated,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function basename(p) {
|
|
194
|
+
const parts = p.split('/');
|
|
195
|
+
return parts[parts.length - 1] || p;
|
|
196
|
+
}
|
|
197
|
+
function renderEvent(e) {
|
|
198
|
+
if (e.kind === 'file-edit') {
|
|
199
|
+
const verb = /(?:^|[._-])(?:write|create)(?:[._-]|$)/i.test(e.tool) ? 'wrote' : 'edited';
|
|
200
|
+
const times = (e.editCount ?? 1) > 1 ? ` (x${e.editCount})` : '';
|
|
201
|
+
return `${verb} ${basename(e.file ?? '?')}${times}`;
|
|
202
|
+
}
|
|
203
|
+
if (e.kind === 'test-run') {
|
|
204
|
+
if (typeof e.failedCount === 'number') {
|
|
205
|
+
return `ran tests: ${e.failedCount} failed`;
|
|
206
|
+
}
|
|
207
|
+
if (typeof e.exitCode === 'number') {
|
|
208
|
+
return `ran tests (exit ${e.exitCode})`;
|
|
209
|
+
}
|
|
210
|
+
return 'ran tests';
|
|
211
|
+
}
|
|
212
|
+
return e.command ? `ran \`${e.command}\`` : `ran ${e.tool}`;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Human-readable one-line(ish) play-by-play. Deterministic; events are dropped
|
|
216
|
+
* middle-out (with an elision marker) until the string fits `maxChars`.
|
|
217
|
+
*/
|
|
218
|
+
export function renderActionSkeleton(skeleton, maxChars = 1600) {
|
|
219
|
+
if (skeleton.events.length === 0)
|
|
220
|
+
return '(no file edits or shell commands observed)';
|
|
221
|
+
let omitted = skeleton.truncated ? 1 : 0; // marker for projection-time truncation
|
|
222
|
+
let items = skeleton.events.map(renderEvent);
|
|
223
|
+
const join = (parts, elided) => {
|
|
224
|
+
if (elided <= 0)
|
|
225
|
+
return parts.join(' → ');
|
|
226
|
+
const head = parts.slice(0, Math.ceil(parts.length / 2));
|
|
227
|
+
const tail = parts.slice(Math.ceil(parts.length / 2));
|
|
228
|
+
return `${head.join(' → ')} → … → ${tail.join(' → ')}`;
|
|
229
|
+
};
|
|
230
|
+
let text = join(items, omitted);
|
|
231
|
+
while (text.length > maxChars && items.length > 2) {
|
|
232
|
+
// Drop the middle event and re-join with an elision marker.
|
|
233
|
+
items = [...items.slice(0, Math.floor(items.length / 2)), ...items.slice(Math.floor(items.length / 2) + 1)];
|
|
234
|
+
omitted++;
|
|
235
|
+
text = join(items, omitted);
|
|
236
|
+
}
|
|
237
|
+
return text.length > maxChars ? `${text.slice(0, maxChars - 1)}…` : text;
|
|
238
|
+
}
|
|
239
|
+
//# sourceMappingURL=skeleton.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "synergyspec-selfevolving",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "AI-native system for spec-driven development",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"synergyspec-selfevolving",
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"scripts/postinstall.js",
|
|
39
39
|
"scripts/nl2repo_synergyspec-selfevolving_wrapper.py",
|
|
40
40
|
"scripts/code-health.py",
|
|
41
|
+
"scripts/slop_rules.yaml",
|
|
41
42
|
"!dist/**/*.test.js",
|
|
42
43
|
"!dist/**/__tests__",
|
|
43
44
|
"!dist/**/*.map"
|
|
@@ -79,6 +80,7 @@
|
|
|
79
80
|
"vitest": "^3.2.4"
|
|
80
81
|
},
|
|
81
82
|
"dependencies": {
|
|
83
|
+
"@ast-grep/cli": "0.43.0",
|
|
82
84
|
"@inquirer/core": "^10.2.2",
|
|
83
85
|
"@inquirer/prompts": "^7.8.0",
|
|
84
86
|
"ansi-regex": "^5.0.1",
|