@f5xc-salesdemos/xcsh 18.56.0 → 18.57.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/package.json +7 -7
- package/src/autoresearch/contract.ts +33 -63
- package/src/autoresearch/dashboard.ts +30 -66
- package/src/autoresearch/git.ts +18 -54
- package/src/autoresearch/helpers.ts +68 -130
- package/src/autoresearch/index.ts +73 -124
- package/src/autoresearch/state.ts +27 -77
- package/src/autoresearch/types.ts +5 -34
- package/src/internal-urls/build-info.generated.ts +8 -8
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5xc-salesdemos/xcsh",
|
|
4
|
-
"version": "18.
|
|
4
|
+
"version": "18.57.0",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/f5xc-salesdemos/xcsh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -48,12 +48,12 @@
|
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@agentclientprotocol/sdk": "0.16.1",
|
|
50
50
|
"@mozilla/readability": "^0.6",
|
|
51
|
-
"@f5xc-salesdemos/xcsh-stats": "18.
|
|
52
|
-
"@f5xc-salesdemos/pi-agent-core": "18.
|
|
53
|
-
"@f5xc-salesdemos/pi-ai": "18.
|
|
54
|
-
"@f5xc-salesdemos/pi-natives": "18.
|
|
55
|
-
"@f5xc-salesdemos/pi-tui": "18.
|
|
56
|
-
"@f5xc-salesdemos/pi-utils": "18.
|
|
51
|
+
"@f5xc-salesdemos/xcsh-stats": "18.57.0",
|
|
52
|
+
"@f5xc-salesdemos/pi-agent-core": "18.57.0",
|
|
53
|
+
"@f5xc-salesdemos/pi-ai": "18.57.0",
|
|
54
|
+
"@f5xc-salesdemos/pi-natives": "18.57.0",
|
|
55
|
+
"@f5xc-salesdemos/pi-tui": "18.57.0",
|
|
56
|
+
"@f5xc-salesdemos/pi-utils": "18.57.0",
|
|
57
57
|
"@sinclair/typebox": "^0.34",
|
|
58
58
|
"@xterm/headless": "^6.0",
|
|
59
59
|
"ajv": "^8.18",
|
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { inferMetricUnitFromName } from "./helpers";
|
|
3
|
+
import { inferMetricUnitFromName, normalizeAutoresearchPath } from "./helpers";
|
|
4
4
|
import type { AutoresearchContract, ExperimentState, MetricDirection } from "./types";
|
|
5
5
|
|
|
6
6
|
const HEADING_REGEX = /^##\s+(.+?)\s*$/;
|
|
7
7
|
const LIST_ITEM_REGEX = /^\s*[-*]\s+(.*)$/;
|
|
8
8
|
const KEY_VALUE_REGEX = /^\s*[-*]\s+([^:]+):\s*(.*)$/;
|
|
9
|
-
|
|
10
|
-
export function readAutoresearchContract(workDir: string) {
|
|
11
|
-
const contractPath = path.join(workDir, "autoresearch.md");
|
|
12
|
-
let content = "";
|
|
9
|
+
function tryReadFile(filePath: string): string | null {
|
|
13
10
|
try {
|
|
14
|
-
|
|
11
|
+
return fs.readFileSync(filePath, "utf8");
|
|
15
12
|
} catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function readAutoresearchContract(workDir: string) {
|
|
17
|
+
const contractPath = path.join(workDir, "autoresearch.md");
|
|
18
|
+
const content = tryReadFile(contractPath);
|
|
19
|
+
if (content === null)
|
|
16
20
|
return {
|
|
17
21
|
contract: {
|
|
18
22
|
benchmark: { command: null, primaryMetric: null, metricUnit: "", direction: null, secondaryMetrics: [] },
|
|
@@ -23,13 +27,9 @@ export function readAutoresearchContract(workDir: string) {
|
|
|
23
27
|
errors: [`${contractPath} does not exist. Create it before initializing autoresearch.`],
|
|
24
28
|
path: contractPath,
|
|
25
29
|
};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
30
|
const contract = parseAutoresearchContract(content);
|
|
29
|
-
|
|
30
|
-
return { contract, errors, path: contractPath };
|
|
31
|
+
return { contract, errors: validateAutoresearchContract(contract), path: contractPath };
|
|
31
32
|
}
|
|
32
|
-
|
|
33
33
|
export function parseAutoresearchContract(markdown: string): AutoresearchContract {
|
|
34
34
|
const sections = extractSections(markdown);
|
|
35
35
|
return {
|
|
@@ -39,7 +39,6 @@ export function parseAutoresearchContract(markdown: string): AutoresearchContrac
|
|
|
39
39
|
constraints: parseListSection(sections.get("constraints") ?? ""),
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
|
-
|
|
43
42
|
function validateAutoresearchContract(contract: AutoresearchContract): string[] {
|
|
44
43
|
const errors: string[] = [];
|
|
45
44
|
if (!contract.benchmark.command) errors.push("Benchmark.command is required in autoresearch.md.");
|
|
@@ -48,77 +47,54 @@ function validateAutoresearchContract(contract: AutoresearchContract): string[]
|
|
|
48
47
|
errors.push("Benchmark.direction must be `lower` or `higher` in autoresearch.md.");
|
|
49
48
|
if (contract.scopePaths.length === 0)
|
|
50
49
|
errors.push("Files in Scope must contain at least one path in autoresearch.md.");
|
|
51
|
-
for (const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
50
|
+
for (const [label, paths] of [
|
|
51
|
+
["Files in Scope", contract.scopePaths],
|
|
52
|
+
["Off Limits", contract.offLimits],
|
|
53
|
+
] as const)
|
|
54
|
+
for (const p of paths)
|
|
55
|
+
if (path.posix.isAbsolute(p) || p === ".." || p.startsWith("../"))
|
|
56
|
+
errors.push(`${label} contains an invalid path: ${p}`);
|
|
57
57
|
return errors;
|
|
58
58
|
}
|
|
59
|
-
|
|
60
59
|
export function loadAutoresearchScriptSnapshot(workDir: string) {
|
|
61
60
|
const benchmarkScriptPath = path.join(workDir, "autoresearch.sh");
|
|
62
61
|
const checksScriptPath = path.join(workDir, "autoresearch.checks.sh");
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
let benchmarkScript = "";
|
|
66
|
-
try {
|
|
67
|
-
benchmarkScript = fs.readFileSync(benchmarkScriptPath, "utf8");
|
|
68
|
-
} catch {
|
|
69
|
-
errors.push(`${benchmarkScriptPath} does not exist. Create it before initializing autoresearch.`);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
let checksScript: string | null = null;
|
|
73
|
-
try {
|
|
74
|
-
checksScript = fs.readFileSync(checksScriptPath, "utf8");
|
|
75
|
-
} catch {
|
|
76
|
-
checksScript = null;
|
|
77
|
-
}
|
|
78
|
-
|
|
62
|
+
const benchmarkScript = tryReadFile(benchmarkScriptPath);
|
|
79
63
|
return {
|
|
80
|
-
benchmarkScript,
|
|
64
|
+
benchmarkScript: benchmarkScript ?? "",
|
|
81
65
|
benchmarkScriptPath,
|
|
82
|
-
checksScript,
|
|
66
|
+
checksScript: tryReadFile(checksScriptPath),
|
|
83
67
|
checksScriptPath,
|
|
84
|
-
errors
|
|
68
|
+
errors:
|
|
69
|
+
benchmarkScript === null
|
|
70
|
+
? [`${benchmarkScriptPath} does not exist. Create it before initializing autoresearch.`]
|
|
71
|
+
: [],
|
|
85
72
|
};
|
|
86
73
|
}
|
|
87
|
-
|
|
88
74
|
export function normalizeAutoresearchList(values: readonly string[]): string[] {
|
|
89
75
|
return [...new Set(values.map(v => v.trim()).filter(Boolean))];
|
|
90
76
|
}
|
|
91
|
-
|
|
92
77
|
export function normalizeContractPathSpec(value: string): string {
|
|
93
|
-
|
|
94
|
-
if (normalized === "." || normalized === "./") return ".";
|
|
95
|
-
return normalized.replace(/^\.\/+/, "").replace(/\/+$/, "");
|
|
78
|
+
return normalizeAutoresearchPath(path.posix.normalize(value.trim().replaceAll("\\", "/")));
|
|
96
79
|
}
|
|
97
|
-
|
|
98
80
|
export function pathMatchesContractPath(pathValue: string, specValue: string): boolean {
|
|
99
81
|
const normalizedPath = normalizeContractPathSpec(pathValue);
|
|
100
82
|
const normalizedSpec = normalizeContractPathSpec(specValue);
|
|
101
83
|
if (normalizedSpec === ".") return true;
|
|
102
84
|
return normalizedPath === normalizedSpec || normalizedPath.startsWith(`${normalizedSpec}/`);
|
|
103
85
|
}
|
|
104
|
-
|
|
105
86
|
export function contractListsEqual(left: readonly string[], right: readonly string[]): boolean {
|
|
106
|
-
|
|
87
|
+
const a = normalizeAutoresearchList(left);
|
|
88
|
+
const b = normalizeAutoresearchList(right);
|
|
89
|
+
return a.length === b.length && a.every((v, i) => v === b[i]);
|
|
107
90
|
}
|
|
108
|
-
|
|
109
91
|
export function contractPathListsEqual(left: readonly string[], right: readonly string[]): boolean {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
92
|
+
const norm = (values: readonly string[]) =>
|
|
93
|
+
normalizeAutoresearchList(values.map(normalizeContractPathSpec)).sort((l, r) => l.localeCompare(r));
|
|
94
|
+
const a = norm(left);
|
|
95
|
+
const b = norm(right);
|
|
114
96
|
return a.length === b.length && a.every((v, i) => v === b[i]);
|
|
115
97
|
}
|
|
116
|
-
function normalizeContractPathList(values: readonly string[]): string[] {
|
|
117
|
-
return normalizeAutoresearchList(values.map(normalizeContractPathSpec)).sort((left, right) =>
|
|
118
|
-
left.localeCompare(right),
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
98
|
function extractSections(markdown: string): Map<string, string> {
|
|
123
99
|
const sections = new Map<string, string>();
|
|
124
100
|
let heading: string | null = null;
|
|
@@ -136,7 +112,6 @@ function extractSections(markdown: string): Map<string, string> {
|
|
|
136
112
|
if (heading) sections.set(heading, content.join("\n").trim());
|
|
137
113
|
return sections;
|
|
138
114
|
}
|
|
139
|
-
|
|
140
115
|
function parseBenchmarkSection(section: string): AutoresearchContract["benchmark"] {
|
|
141
116
|
const entries = new Map<string, string>();
|
|
142
117
|
const lines = section.split("\n");
|
|
@@ -181,7 +156,6 @@ function parseBenchmarkSection(section: string): AutoresearchContract["benchmark
|
|
|
181
156
|
: [],
|
|
182
157
|
};
|
|
183
158
|
}
|
|
184
|
-
|
|
185
159
|
function parseListSection(section: string, normalizeItem?: (value: string) => string): string[] {
|
|
186
160
|
const items: string[] = [];
|
|
187
161
|
let activeItem: string | null = null;
|
|
@@ -208,10 +182,6 @@ function parseListSection(section: string, normalizeItem?: (value: string) => st
|
|
|
208
182
|
const normalizedItems = normalizeAutoresearchList(items);
|
|
209
183
|
return normalizeItem ? normalizedItems.map(normalizeItem) : normalizedItems;
|
|
210
184
|
}
|
|
211
|
-
function isUnsafeContractPathSpec(value: string): boolean {
|
|
212
|
-
return path.posix.isAbsolute(value) || value === ".." || value.startsWith("../");
|
|
213
|
-
}
|
|
214
|
-
|
|
215
185
|
/**
|
|
216
186
|
* Updates session fields from a validated `autoresearch.md` parse (same fields as `init_experiment`).
|
|
217
187
|
* Does not touch `name`, `currentSegment`, `results`, `bestMetric`, `confidence`, or `maxExperiments`.
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { matchesKey, replaceTabs, Text, truncateToWidth, visibleWidth } from "@f5xc-salesdemos/pi-tui";
|
|
2
2
|
import type { Theme } from "../modes/theme/theme";
|
|
3
|
-
import { formatElapsed, formatNum
|
|
4
|
-
import {
|
|
3
|
+
import { formatElapsed, formatNum } from "./helpers";
|
|
4
|
+
import {
|
|
5
|
+
currentResults,
|
|
6
|
+
findBaselineMetric,
|
|
7
|
+
findBaselineRunNumber,
|
|
8
|
+
findBaselineSecondary,
|
|
9
|
+
findBestKeptResult,
|
|
10
|
+
} from "./state";
|
|
5
11
|
import type { AutoresearchRuntime, DashboardController, ExperimentResult, ExperimentState } from "./types";
|
|
6
12
|
|
|
7
13
|
export function createDashboardController(): DashboardController {
|
|
@@ -15,10 +21,8 @@ export function createDashboardController(): DashboardController {
|
|
|
15
21
|
|
|
16
22
|
const clear = (): void => {
|
|
17
23
|
overlayTui = null;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
spinnerTimer = undefined;
|
|
21
|
-
}
|
|
24
|
+
clearInterval(spinnerTimer);
|
|
25
|
+
spinnerTimer = undefined;
|
|
22
26
|
};
|
|
23
27
|
|
|
24
28
|
return {
|
|
@@ -117,14 +121,12 @@ export function createDashboardController(): DashboardController {
|
|
|
117
121
|
},
|
|
118
122
|
};
|
|
119
123
|
}
|
|
120
|
-
|
|
121
124
|
function renderRunningOnly(runtime: AutoresearchRuntime, state: ExperimentState, theme: Theme): string {
|
|
122
125
|
const parts = [theme.fg("contentAccent", "autoresearch"), theme.fg("warning", " running...")];
|
|
123
126
|
if (state.name) parts.push(theme.fg("dim", ` | ${replaceTabs(state.name)}`));
|
|
124
127
|
if (runtime.runningExperiment) parts.push(theme.fg("dim", ` | ${replaceTabs(runtime.runningExperiment.command)}`));
|
|
125
128
|
return parts.join("");
|
|
126
129
|
}
|
|
127
|
-
|
|
128
130
|
function shouldShowDashboard(runtime: AutoresearchRuntime, state: ExperimentState): boolean {
|
|
129
131
|
return (
|
|
130
132
|
runtime.autoresearchMode ||
|
|
@@ -133,7 +135,6 @@ function shouldShowDashboard(runtime: AutoresearchRuntime, state: ExperimentStat
|
|
|
133
135
|
runtime.lastRunSummary !== null
|
|
134
136
|
);
|
|
135
137
|
}
|
|
136
|
-
|
|
137
138
|
function renderExpandedHeader(runtime: AutoresearchRuntime, width: number, theme: Theme): string {
|
|
138
139
|
const state = runtime.state;
|
|
139
140
|
const status = renderModeStatus(runtime, state);
|
|
@@ -145,7 +146,6 @@ function renderExpandedHeader(runtime: AutoresearchRuntime, width: number, theme
|
|
|
145
146
|
width,
|
|
146
147
|
);
|
|
147
148
|
}
|
|
148
|
-
|
|
149
149
|
function renderCollapsedLine(runtime: AutoresearchRuntime, state: ExperimentState, theme: Theme): string {
|
|
150
150
|
if (runtime.lastRunSummary) {
|
|
151
151
|
const parts = [
|
|
@@ -153,14 +153,13 @@ function renderCollapsedLine(runtime: AutoresearchRuntime, state: ExperimentStat
|
|
|
153
153
|
theme.fg("warning", ` pending run #${runtime.lastRunSummary.runNumber}`),
|
|
154
154
|
theme.fg("dim", runtime.lastRunSummary.passed ? " pass" : " fail"),
|
|
155
155
|
];
|
|
156
|
-
if (runtime.lastRunSummary.parsedPrimary !== null)
|
|
156
|
+
if (runtime.lastRunSummary.parsedPrimary !== null)
|
|
157
157
|
parts.push(
|
|
158
158
|
theme.fg(
|
|
159
159
|
"muted",
|
|
160
160
|
` | ${state.metricName}=${formatNum(runtime.lastRunSummary.parsedPrimary, state.metricUnit)}`,
|
|
161
161
|
),
|
|
162
162
|
);
|
|
163
|
-
}
|
|
164
163
|
parts.push(theme.fg("warning", " | log_experiment required"));
|
|
165
164
|
if (!runtime.autoresearchMode) parts.push(theme.fg("dim", " | mode off"));
|
|
166
165
|
return parts.join("");
|
|
@@ -176,7 +175,7 @@ function renderCollapsedLine(runtime: AutoresearchRuntime, state: ExperimentStat
|
|
|
176
175
|
const kept = current.filter(result => result.status === "keep").length;
|
|
177
176
|
const crashed = current.filter(result => result.status === "crash").length;
|
|
178
177
|
const checksFailed = current.filter(result => result.status === "checks_failed").length;
|
|
179
|
-
const best =
|
|
178
|
+
const best = findBestKeptResult(state);
|
|
180
179
|
const archivedRuns = Math.max(0, state.results.length - current.length);
|
|
181
180
|
const parts = [
|
|
182
181
|
theme.fg("contentAccent", "autoresearch"),
|
|
@@ -197,8 +196,7 @@ function renderCollapsedLine(runtime: AutoresearchRuntime, state: ExperimentStat
|
|
|
197
196
|
}
|
|
198
197
|
if (state.confidence !== null) {
|
|
199
198
|
const confidenceColor = state.confidence >= 2 ? "success" : state.confidence >= 1 ? "warning" : "error";
|
|
200
|
-
parts.push(theme.fg("dim", " | "));
|
|
201
|
-
parts.push(theme.fg(confidenceColor, `conf ${state.confidence.toFixed(1)}x`));
|
|
199
|
+
parts.push(theme.fg("dim", " | "), theme.fg(confidenceColor, `conf ${state.confidence.toFixed(1)}x`));
|
|
202
200
|
}
|
|
203
201
|
if (runtime.runningExperiment) {
|
|
204
202
|
parts.push(theme.fg("dim", ` | running ${formatElapsed(Date.now() - runtime.runningExperiment.startedAt)}`));
|
|
@@ -208,7 +206,6 @@ function renderCollapsedLine(runtime: AutoresearchRuntime, state: ExperimentStat
|
|
|
208
206
|
parts.push(theme.fg("dim", " | ctrl+x expand"));
|
|
209
207
|
return parts.join("");
|
|
210
208
|
}
|
|
211
|
-
|
|
212
209
|
function renderDashboardLines(runtime: AutoresearchRuntime, width: number, theme: Theme, maxRows: number): string[] {
|
|
213
210
|
const state = runtime.state;
|
|
214
211
|
if (state.results.length === 0) {
|
|
@@ -242,7 +239,7 @@ function renderDashboardLines(runtime: AutoresearchRuntime, width: number, theme
|
|
|
242
239
|
const baseline = findBaselineMetric(state.results, state.currentSegment);
|
|
243
240
|
const baselineRunNumber = findBaselineRunNumber(state.results, state.currentSegment);
|
|
244
241
|
const baselineSecondary = findBaselineSecondary(state.results, state.currentSegment, state.secondaryMetrics);
|
|
245
|
-
const best =
|
|
242
|
+
const best = findBestKeptResult(state);
|
|
246
243
|
const lines = [
|
|
247
244
|
truncateToWidth(
|
|
248
245
|
`Current segment: ${current.length} runs ${kept} kept ${discarded} discarded ${crashed} crashed ${checksFailed} checks_failed`,
|
|
@@ -268,28 +265,31 @@ function renderDashboardLines(runtime: AutoresearchRuntime, width: number, theme
|
|
|
268
265
|
}
|
|
269
266
|
if (!runtime.autoresearchMode) lines.push(truncateToWidth(`Mode: ${renderModeStatus(runtime, state)}`, width));
|
|
270
267
|
if (best) {
|
|
271
|
-
|
|
272
|
-
let progress = `Best: ${formatNum(best.result.metric, state.metricUnit)} (#${bestRunNumber})`;
|
|
268
|
+
let progress = `Best: ${formatNum(best.result.metric, state.metricUnit)} (#${best.result.runNumber ?? best.index + 1})`;
|
|
273
269
|
if (baseline !== null && baseline !== 0 && best.result.metric !== baseline)
|
|
274
270
|
progress += ` ${formatDelta(best.result.metric, baseline)}`;
|
|
275
271
|
if (state.confidence !== null) progress += ` conf ${state.confidence.toFixed(1)}x`;
|
|
276
272
|
lines.push(truncateToWidth(progress, width));
|
|
277
273
|
if (state.secondaryMetrics.length > 0) {
|
|
278
274
|
const details = state.secondaryMetrics
|
|
279
|
-
.map(metric =>
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
),
|
|
286
|
-
)
|
|
275
|
+
.map(metric => {
|
|
276
|
+
const v = best.result.metrics[metric.name];
|
|
277
|
+
return v !== undefined
|
|
278
|
+
? `${metric.name} ${renderSecondaryCell(v, metric.unit, baselineSecondary[metric.name])}`
|
|
279
|
+
: null;
|
|
280
|
+
})
|
|
287
281
|
.filter((value): value is string => Boolean(value));
|
|
288
282
|
if (details.length > 0) lines.push(truncateToWidth(`Secondary: ${details.join(" ")}`, width));
|
|
289
283
|
}
|
|
290
284
|
}
|
|
291
285
|
lines.push("");
|
|
292
|
-
|
|
286
|
+
const secondaryHeader = state.secondaryMetrics.map(metric => truncateToWidth(metric.name, 10)).join(" ");
|
|
287
|
+
lines.push(
|
|
288
|
+
truncateToWidth(
|
|
289
|
+
`${theme.fg("muted", "#".padEnd(4))}${theme.fg("muted", "commit".padEnd(10))}${theme.fg("warning", state.metricName.padEnd(12))}${secondaryHeader ? `${theme.fg("muted", secondaryHeader)} ` : ""}${theme.fg("muted", "status".padEnd(14))}${theme.fg("muted", "description")}`,
|
|
290
|
+
width,
|
|
291
|
+
),
|
|
292
|
+
);
|
|
293
293
|
lines.push(theme.fg("borderMuted", "-".repeat(Math.max(0, width - 1))));
|
|
294
294
|
|
|
295
295
|
const visible = maxRows > 0 ? current.slice(-maxRows) : current;
|
|
@@ -300,15 +300,6 @@ function renderDashboardLines(runtime: AutoresearchRuntime, width: number, theme
|
|
|
300
300
|
}
|
|
301
301
|
return lines;
|
|
302
302
|
}
|
|
303
|
-
|
|
304
|
-
function renderTableHeader(state: ExperimentState, width: number, theme: Theme): string {
|
|
305
|
-
const secondaryHeader = state.secondaryMetrics.map(metric => truncateToWidth(metric.name, 10)).join(" ");
|
|
306
|
-
return truncateToWidth(
|
|
307
|
-
`${theme.fg("muted", "#".padEnd(4))}${theme.fg("muted", "commit".padEnd(10))}${theme.fg("warning", state.metricName.padEnd(12))}${secondaryHeader ? `${theme.fg("muted", secondaryHeader)} ` : ""}${theme.fg("muted", "status".padEnd(14))}${theme.fg("muted", "description")}`,
|
|
308
|
-
width,
|
|
309
|
-
);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
303
|
function renderResultRow(
|
|
313
304
|
result: ExperimentResult,
|
|
314
305
|
state: ExperimentState,
|
|
@@ -316,7 +307,6 @@ function renderResultRow(
|
|
|
316
307
|
width: number,
|
|
317
308
|
theme: Theme,
|
|
318
309
|
): string {
|
|
319
|
-
const runNumber = result.runNumber ?? state.results.indexOf(result) + 1;
|
|
320
310
|
const secondary = state.secondaryMetrics
|
|
321
311
|
.map(metric =>
|
|
322
312
|
truncateToWidth(
|
|
@@ -327,7 +317,7 @@ function renderResultRow(
|
|
|
327
317
|
.join("");
|
|
328
318
|
const statusColor = result.status === "keep" ? "success" : result.status === "discard" ? "warning" : "error";
|
|
329
319
|
const line =
|
|
330
|
-
theme.fg("dim", String(runNumber).padEnd(4)) +
|
|
320
|
+
theme.fg("dim", String(result.runNumber ?? state.results.indexOf(result) + 1).padEnd(4)) +
|
|
331
321
|
theme.fg("contentAccent", (result.commit || "-").padEnd(10)) +
|
|
332
322
|
theme.fg(statusColor, formatNum(result.metric, state.metricUnit).padEnd(12)) +
|
|
333
323
|
secondary +
|
|
@@ -335,47 +325,32 @@ function renderResultRow(
|
|
|
335
325
|
theme.fg("muted", replaceTabs(result.description));
|
|
336
326
|
return truncateToWidth(line, width);
|
|
337
327
|
}
|
|
338
|
-
|
|
339
328
|
function renderSecondaryCell(value: number | undefined, unit: string, baseline: number | undefined): string {
|
|
340
329
|
if (value === undefined) return "-";
|
|
341
330
|
const formatted = formatNum(value, unit);
|
|
342
331
|
if (baseline === undefined || baseline === 0 || baseline === value) return formatted;
|
|
343
332
|
return `${formatted} ${formatDelta(value, baseline)}`;
|
|
344
333
|
}
|
|
345
|
-
|
|
346
|
-
function renderSecondarySummary(
|
|
347
|
-
name: string,
|
|
348
|
-
value: number | undefined,
|
|
349
|
-
baseline: number | undefined,
|
|
350
|
-
unit: string,
|
|
351
|
-
): string | null {
|
|
352
|
-
if (value === undefined) return null;
|
|
353
|
-
return `${name} ${renderSecondaryCell(value, unit, baseline)}`;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
334
|
function formatDelta(value: number, baseline: number): string {
|
|
357
335
|
const delta = ((value - baseline) / baseline) * 100;
|
|
358
336
|
return `${delta > 0 ? "+" : ""}${delta.toFixed(1)}%`;
|
|
359
337
|
}
|
|
360
|
-
|
|
361
338
|
function renderOverlayRunningLine(
|
|
362
339
|
runtime: AutoresearchRuntime,
|
|
363
340
|
theme: Theme,
|
|
364
341
|
width: number,
|
|
365
342
|
spinnerFrame: number,
|
|
366
343
|
): string {
|
|
367
|
-
const spinner = theme.spinnerFrames[spinnerFrame % theme.spinnerFrames.length] ?? "*";
|
|
368
344
|
return truncateToWidth(
|
|
369
345
|
theme.fg(
|
|
370
346
|
"warning",
|
|
371
|
-
`${
|
|
347
|
+
`${theme.spinnerFrames[spinnerFrame % theme.spinnerFrames.length] ?? "*"} running ${formatElapsed(Date.now() - (runtime.runningExperiment?.startedAt ?? Date.now()))} ${replaceTabs(
|
|
372
348
|
runtime.runningExperiment?.command ?? "",
|
|
373
349
|
)}`,
|
|
374
350
|
),
|
|
375
351
|
width,
|
|
376
352
|
);
|
|
377
353
|
}
|
|
378
|
-
|
|
379
354
|
function renderOverlayFooter(
|
|
380
355
|
width: number,
|
|
381
356
|
scrollOffset: number,
|
|
@@ -391,20 +366,9 @@ function renderOverlayFooter(
|
|
|
391
366
|
const fill = Math.max(0, width - visibleWidth(hint));
|
|
392
367
|
return theme.fg("borderMuted", "-".repeat(fill)) + hint;
|
|
393
368
|
}
|
|
394
|
-
|
|
395
369
|
function renderModeStatus(runtime: AutoresearchRuntime, state: ExperimentState): string {
|
|
396
370
|
if (runtime.autoresearchMode) return state.results.length === 0 ? "baseline pending" : "mode on";
|
|
397
371
|
const current = currentResults(state.results, state.currentSegment);
|
|
398
372
|
if (state.maxExperiments !== null && current.length >= state.maxExperiments) return "segment complete";
|
|
399
373
|
return "mode off";
|
|
400
374
|
}
|
|
401
|
-
|
|
402
|
-
function findBestResult(state: ExperimentState): { index: number; result: ExperimentResult } | null {
|
|
403
|
-
let best: { index: number; result: ExperimentResult } | null = null;
|
|
404
|
-
for (let index = 0; index < state.results.length; index += 1) {
|
|
405
|
-
const result = state.results[index];
|
|
406
|
-
if (result.segment !== state.currentSegment || result.status !== "keep" || result.metric <= 0) continue;
|
|
407
|
-
if (!best || isBetter(result.metric, best.result.metric, state.bestDirection)) best = { index, result };
|
|
408
|
-
}
|
|
409
|
-
return best;
|
|
410
|
-
}
|
package/src/autoresearch/git.ts
CHANGED
|
@@ -4,14 +4,11 @@ import { isAutoresearchLocalStatePath, normalizeAutoresearchPath } from "./helpe
|
|
|
4
4
|
|
|
5
5
|
const AUTORESEARCH_BRANCH_PREFIX = "autoresearch/";
|
|
6
6
|
const BRANCH_NAME_MAX_LENGTH = 48;
|
|
7
|
-
|
|
8
7
|
type EnsureAutoresearchBranchResult = { error: string; ok: false } | { branchName: string; created: boolean; ok: true };
|
|
9
|
-
|
|
10
8
|
export async function getCurrentAutoresearchBranch(_api: ExtensionAPI, workDir: string): Promise<string | null> {
|
|
11
9
|
const currentBranch = (await git.branch.current(workDir)) ?? "";
|
|
12
10
|
return currentBranch.startsWith(AUTORESEARCH_BRANCH_PREFIX) ? currentBranch : null;
|
|
13
11
|
}
|
|
14
|
-
|
|
15
12
|
export async function ensureAutoresearchBranch(
|
|
16
13
|
api: ExtensionAPI,
|
|
17
14
|
workDir: string,
|
|
@@ -68,17 +65,9 @@ export async function ensureAutoresearchBranch(
|
|
|
68
65
|
ok: true,
|
|
69
66
|
};
|
|
70
67
|
}
|
|
71
|
-
|
|
72
68
|
export function parseWorkDirDirtyPaths(statusOutput: string, workDirPrefix: string): string[] {
|
|
73
|
-
|
|
74
|
-
for (const dirtyPath of parseDirtyPaths(statusOutput)) {
|
|
75
|
-
const relativePath = relativizeGitPathToWorkDir(dirtyPath, workDirPrefix);
|
|
76
|
-
if (relativePath === null) continue;
|
|
77
|
-
relativePaths.push(relativePath);
|
|
78
|
-
}
|
|
79
|
-
return relativePaths;
|
|
69
|
+
return parseWorkDirDirtyPathsWithStatus(statusOutput, workDirPrefix).map(e => e.path);
|
|
80
70
|
}
|
|
81
|
-
|
|
82
71
|
function relativizeGitPathToWorkDir(repoRelativePath: string, workDirPrefix: string): string | null {
|
|
83
72
|
const normalizedPath = normalizeStatusPath(repoRelativePath);
|
|
84
73
|
const normalizedPrefix = normalizeAutoresearchPath(workDirPrefix);
|
|
@@ -87,16 +76,11 @@ function relativizeGitPathToWorkDir(repoRelativePath: string, workDirPrefix: str
|
|
|
87
76
|
if (!normalizedPath.startsWith(`${normalizedPrefix}/`)) return null;
|
|
88
77
|
return normalizeAutoresearchPath(normalizedPath.slice(normalizedPrefix.length + 1));
|
|
89
78
|
}
|
|
90
|
-
function parseDirtyPaths(statusOutput: string): string[] {
|
|
91
|
-
return parseDirtyPathsWithStatus(statusOutput).map(entry => entry.path);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
79
|
function normalizeStatusPath(path: string): string {
|
|
95
80
|
let normalized = path.trim();
|
|
96
81
|
if (normalized.startsWith('"') && normalized.endsWith('"')) normalized = normalized.slice(1, -1);
|
|
97
82
|
return normalizeAutoresearchPath(normalized);
|
|
98
83
|
}
|
|
99
|
-
|
|
100
84
|
async function allocateBranchName(workDir: string, goal: string | null): Promise<string> {
|
|
101
85
|
const baseName = `${AUTORESEARCH_BRANCH_PREFIX}${slugifyGoal(goal)}-${new Date().toISOString().slice(0, 10).replaceAll("-", "")}`;
|
|
102
86
|
let candidate = baseName;
|
|
@@ -108,49 +92,34 @@ async function allocateBranchName(workDir: string, goal: string | null): Promise
|
|
|
108
92
|
return candidate;
|
|
109
93
|
}
|
|
110
94
|
function slugifyGoal(goal: string | null): string {
|
|
111
|
-
const
|
|
95
|
+
const slug = (goal ?? "")
|
|
112
96
|
.toLowerCase()
|
|
113
97
|
.replace(/[^a-z0-9]+/g, "-")
|
|
114
98
|
.replace(/^-+|-+$/g, "");
|
|
115
|
-
|
|
116
|
-
return trimmed || "session";
|
|
99
|
+
return slug.slice(0, BRANCH_NAME_MAX_LENGTH).replace(/-+$/g, "") || "session";
|
|
117
100
|
}
|
|
118
101
|
function buildUnsafeDirtyPathsFailure(unsafeDirtyPaths: string[]): EnsureAutoresearchBranchResult {
|
|
119
|
-
const preview = unsafeDirtyPaths.slice(0, 5).join(", ");
|
|
120
|
-
const suffix = unsafeDirtyPaths.length > 5 ? ` (+${unsafeDirtyPaths.length - 5} more)` : "";
|
|
121
102
|
return {
|
|
122
103
|
error:
|
|
123
104
|
"Autoresearch needs a clean git worktree before it can create or reuse an isolated branch. " +
|
|
124
|
-
`Commit or stash these paths first: ${
|
|
105
|
+
`Commit or stash these paths first: ${unsafeDirtyPaths.slice(0, 5).join(", ")}${unsafeDirtyPaths.length > 5 ? ` (+${unsafeDirtyPaths.length - 5} more)` : ""}`,
|
|
125
106
|
ok: false,
|
|
126
107
|
};
|
|
127
108
|
}
|
|
128
|
-
|
|
129
|
-
function isRenameOrCopy(statusToken: string): boolean {
|
|
130
|
-
const trimmed = statusToken.trim();
|
|
131
|
-
return trimmed.startsWith("R") || trimmed.startsWith("C");
|
|
132
|
-
}
|
|
133
|
-
|
|
134
109
|
function collectUnsafeDirtyPaths(statusOutput: string, workDirPrefix: string): string[] {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
unsafeDirtyPaths.push(relativePath ?? normalizeStatusPath(dirtyPath));
|
|
140
|
-
}
|
|
141
|
-
return unsafeDirtyPaths;
|
|
110
|
+
return parseDirtyPathsWithStatus(statusOutput)
|
|
111
|
+
.map(entry => ({ rel: relativizeGitPathToWorkDir(entry.path, workDirPrefix), raw: entry.path }))
|
|
112
|
+
.filter(({ rel }) => !(rel && isAutoresearchLocalStatePath(rel)))
|
|
113
|
+
.map(({ rel, raw }) => rel ?? normalizeStatusPath(raw));
|
|
142
114
|
}
|
|
143
|
-
|
|
144
115
|
interface DirtyPathEntry {
|
|
145
116
|
path: string;
|
|
146
117
|
untracked: boolean;
|
|
147
118
|
}
|
|
148
|
-
|
|
149
119
|
function parseDirtyPathsWithStatus(statusOutput: string): DirtyPathEntry[] {
|
|
150
120
|
if (statusOutput.includes("\0")) return parseDirtyPathsNulWithStatus(statusOutput);
|
|
151
121
|
return parseDirtyPathsLinesWithStatus(statusOutput);
|
|
152
122
|
}
|
|
153
|
-
|
|
154
123
|
function parseDirtyPathsNulWithStatus(statusOutput: string): DirtyPathEntry[] {
|
|
155
124
|
const seen = new Set<string>();
|
|
156
125
|
const results: DirtyPathEntry[] = [];
|
|
@@ -162,9 +131,9 @@ function parseDirtyPathsNulWithStatus(statusOutput: string): DirtyPathEntry[] {
|
|
|
162
131
|
if (pathEnd < 0) break;
|
|
163
132
|
const firstPath = statusOutput.slice(index, pathEnd);
|
|
164
133
|
index = pathEnd + 1;
|
|
165
|
-
const
|
|
166
|
-
addDirtyPathEntry(seen, results, firstPath,
|
|
167
|
-
if (
|
|
134
|
+
const statusCode = statusToken.trim();
|
|
135
|
+
addDirtyPathEntry(seen, results, firstPath, statusCode.startsWith("??"));
|
|
136
|
+
if (statusCode.startsWith("R") || statusCode.startsWith("C")) {
|
|
168
137
|
const secondPathEnd = statusOutput.indexOf("\0", index);
|
|
169
138
|
if (secondPathEnd < 0) break;
|
|
170
139
|
const secondPath = statusOutput.slice(index, secondPathEnd);
|
|
@@ -174,7 +143,6 @@ function parseDirtyPathsNulWithStatus(statusOutput: string): DirtyPathEntry[] {
|
|
|
174
143
|
}
|
|
175
144
|
return results;
|
|
176
145
|
}
|
|
177
|
-
|
|
178
146
|
function parseDirtyPathsLinesWithStatus(statusOutput: string): DirtyPathEntry[] {
|
|
179
147
|
const seen = new Set<string>();
|
|
180
148
|
const results: DirtyPathEntry[] = [];
|
|
@@ -192,14 +160,12 @@ function parseDirtyPathsLinesWithStatus(statusOutput: string): DirtyPathEntry[]
|
|
|
192
160
|
}
|
|
193
161
|
return results;
|
|
194
162
|
}
|
|
195
|
-
|
|
196
163
|
function addDirtyPathEntry(seen: Set<string>, results: DirtyPathEntry[], rawPath: string, untracked: boolean): void {
|
|
197
164
|
const normalizedPath = normalizeStatusPath(rawPath);
|
|
198
165
|
if (normalizedPath.length === 0 || seen.has(normalizedPath)) return;
|
|
199
166
|
seen.add(normalizedPath);
|
|
200
167
|
results.push({ path: normalizedPath, untracked });
|
|
201
168
|
}
|
|
202
|
-
|
|
203
169
|
export function parseWorkDirDirtyPathsWithStatus(statusOutput: string, workDirPrefix: string): DirtyPathEntry[] {
|
|
204
170
|
const results: DirtyPathEntry[] = [];
|
|
205
171
|
for (const entry of parseDirtyPathsWithStatus(statusOutput)) {
|
|
@@ -209,19 +175,17 @@ export function parseWorkDirDirtyPathsWithStatus(statusOutput: string, workDirPr
|
|
|
209
175
|
}
|
|
210
176
|
return results;
|
|
211
177
|
}
|
|
212
|
-
|
|
213
178
|
export function computeRunModifiedPaths(
|
|
214
179
|
preRunDirtyPaths: string[],
|
|
215
180
|
currentStatusOutput: string,
|
|
216
181
|
workDirPrefix: string,
|
|
217
182
|
): { tracked: string[]; untracked: string[] } {
|
|
218
183
|
const preRunSet = new Set(preRunDirtyPaths);
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
}
|
|
226
|
-
return { tracked, untracked };
|
|
184
|
+
const entries = parseWorkDirDirtyPathsWithStatus(currentStatusOutput, workDirPrefix).filter(
|
|
185
|
+
e => !preRunSet.has(e.path) && !isAutoresearchLocalStatePath(e.path),
|
|
186
|
+
);
|
|
187
|
+
return {
|
|
188
|
+
tracked: entries.filter(e => !e.untracked).map(e => e.path),
|
|
189
|
+
untracked: entries.filter(e => e.untracked).map(e => e.path),
|
|
190
|
+
};
|
|
227
191
|
}
|