@turmind/halo 2.1.6 → 2.2.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/admin-out/404.html +1 -1
- package/admin-out/_next/static/chunks/1607-bf227fc5e4db45b7.js +1 -0
- package/admin-out/_next/static/chunks/1989-dab5d5c24b8f4c9f.js +1 -0
- package/admin-out/_next/static/chunks/5287.7ea08918388fa804.js +1 -0
- package/admin-out/_next/static/chunks/app/page-cca81a2ed69100f5.js +1 -0
- package/admin-out/_next/static/chunks/{webpack-7f8156dbbdb54868.js → webpack-9532ec7f1fa9a757.js} +1 -1
- package/admin-out/_next/static/css/8fc43fce83e5ce5d.css +1 -0
- package/admin-out/index.html +1 -1
- package/admin-out/index.txt +5 -5
- package/bundled-docs/design/a2a.md +3 -3
- package/bundled-docs/design/agentcore.md +1 -1
- package/bundled-docs/design/architecture.md +3 -3
- package/bundled-docs/design/background-dispatch.md +1 -1
- package/bundled-docs/design/canvas-extensions.md +2 -2
- package/bundled-docs/design/cron.md +77 -53
- package/bundled-docs/design/evolution.md +111 -39
- package/bundled-docs/design/node.md +5 -12
- package/bundled-docs/design/observability.md +11 -11
- package/bundled-docs/design/prompt-system.md +1 -1
- package/bundled-docs/design/relay.md +2 -2
- package/bundled-docs/design/session.md +9 -7
- package/bundled-docs/design/slack.md +1 -1
- package/bundled-docs/design/storage.md +14 -11
- package/bundled-docs/design/telegram.md +1 -1
- package/bundled-docs/design/ws.md +3 -3
- package/bundled-docs/dev/api.md +16 -14
- package/bundled-docs/dev/env.md +2 -2
- package/bundled-docs/dev/logging.md +1 -1
- package/bundled-docs/dev/tools.md +2 -2
- package/bundled-docs/guide/cli.md +3 -3
- package/bundled-docs/guide/delegation-and-access.md +1 -1
- package/bundled-docs/guide/workspace.md +1 -1
- package/bundled-docs/requirements/command.md +1 -1
- package/bundled-docs/requirements/settings.md +2 -2
- package/bundled-docs/requirements/workspace.md +1 -1
- package/dist/evo-wrapper.js +913 -201
- package/dist/index.js +2626 -1762
- package/package.json +1 -1
- package/templates/agents/__apply_agent__/AGENT.md +10 -6
- package/templates/agents/__evo_agent__/AGENT.md +15 -4
- package/templates/agents/__score__/AGENT.md +4 -0
- package/templates/skills/cron/SKILL.md +37 -10
- package/templates/skills/cron/manage-cron.py +90 -15
- package/admin-out/_next/static/chunks/1101-d2f818895a3aa32e.js +0 -1
- package/admin-out/_next/static/chunks/1989-c24411a5e971b8b9.js +0 -1
- package/admin-out/_next/static/chunks/5058.8514aa6d6a947cce.js +0 -1
- package/admin-out/_next/static/chunks/app/page-84ab8d21c70823c0.js +0 -1
- package/admin-out/_next/static/css/6da86a077e119a6c.css +0 -1
- /package/admin-out/_next/static/{xg-EvYXc8NEUDwTlmbWvy → Rs9tIYSSITPLxv274DU9f}/_buildManifest.js +0 -0
- /package/admin-out/_next/static/{xg-EvYXc8NEUDwTlmbWvy → Rs9tIYSSITPLxv274DU9f}/_ssgManifest.js +0 -0
package/dist/evo-wrapper.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// ../server/dist/evolution/evo-wrapper.js
|
|
2
|
-
import
|
|
3
|
-
import
|
|
2
|
+
import fs11 from "node:fs";
|
|
3
|
+
import path13 from "node:path";
|
|
4
4
|
import { spawn as spawn2 } from "node:child_process";
|
|
5
5
|
import { homedir as homedir6 } from "node:os";
|
|
6
6
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
7
7
|
import { parseArgs } from "node:util";
|
|
8
|
-
import { eq, and } from "drizzle-orm";
|
|
9
|
-
import
|
|
8
|
+
import { eq as eq3, and as and3, inArray as inArray2 } from "drizzle-orm";
|
|
9
|
+
import YAML7 from "yaml";
|
|
10
10
|
|
|
11
11
|
// ../server/dist/db/evo-db.js
|
|
12
12
|
import Database from "better-sqlite3";
|
|
@@ -29,6 +29,12 @@ function runMigrations(sqlite, migrations) {
|
|
|
29
29
|
})();
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
|
+
function addColumnIfMissing(sqlite, table, column, decl) {
|
|
33
|
+
const cols = sqlite.prepare(`PRAGMA table_info(${table})`).all();
|
|
34
|
+
if (cols.some((c) => c.name === column))
|
|
35
|
+
return;
|
|
36
|
+
sqlite.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${decl}`);
|
|
37
|
+
}
|
|
32
38
|
|
|
33
39
|
// ../server/dist/db/evo-db.js
|
|
34
40
|
var evolutionRuns = sqliteTable("evolution_runs", {
|
|
@@ -48,10 +54,24 @@ var evolutionRuns = sqliteTable("evolution_runs", {
|
|
|
48
54
|
* Bumped on each `pending → running` claim. When it crosses the configured
|
|
49
55
|
* retry budget, the ticker stops re-claiming after timeout/failure. */
|
|
50
56
|
attempts: integer("attempts").notNull().default(0),
|
|
57
|
+
/** `attempts` when the latest reviewer retry reset the row. The retry
|
|
58
|
+
* budget is `attempts - attempts_base`; attempts itself never goes back
|
|
59
|
+
* down, so a stale wrapper's claim (ownsRun) can't match a later one. */
|
|
60
|
+
attemptsBase: integer("attempts_base").notNull().default(0),
|
|
51
61
|
/** When archive job zipped the artifacts and deleted the run dir. Null
|
|
52
62
|
* while the run is active. Rows with archived_at >= 30 days ago are
|
|
53
63
|
* candidates for full deletion (zip + db row). */
|
|
54
|
-
archivedAt: integer("archived_at")
|
|
64
|
+
archivedAt: integer("archived_at"),
|
|
65
|
+
/** The draft carries a `## Larger observation` section (patch.md body or
|
|
66
|
+
* .skip.md) — set by the wrapper after phase A so the admin list can flag
|
|
67
|
+
* and filter it without reading every run dir. */
|
|
68
|
+
hasObservation: integer("has_observation", { mode: "boolean" }).notNull().default(false),
|
|
69
|
+
/** Access level of the source session at enqueue ('full' | 'workspace' |
|
|
70
|
+
* 'readonly'), or 'webhook' for a session that took webhook events. Only
|
|
71
|
+
* a full source may be auto-applied (L2/L3): a lesser session's (or an
|
|
72
|
+
* outside payload's) conversation must not rewrite prompts without a
|
|
73
|
+
* reviewer. Pre-existing rows read null → never auto-applied. */
|
|
74
|
+
sourceAccess: text("source_access")
|
|
55
75
|
});
|
|
56
76
|
var evolutionApplies = sqliteTable("evolution_applies", {
|
|
57
77
|
id: text("id").primaryKey(),
|
|
@@ -66,7 +86,13 @@ var evolutionApplies = sqliteTable("evolution_applies", {
|
|
|
66
86
|
completedAt: integer("completed_at"),
|
|
67
87
|
failureReason: text("failure_reason"),
|
|
68
88
|
attempts: integer("attempts").notNull().default(0),
|
|
69
|
-
archivedAt: integer("archived_at")
|
|
89
|
+
archivedAt: integer("archived_at"),
|
|
90
|
+
/** 'reviewer' (Approve button) | 'auto' (L2/L3 auto-apply, evolution/auto-apply.ts). */
|
|
91
|
+
approvedBy: text("approved_by").notNull().default("reviewer"),
|
|
92
|
+
/** When the source session got the one-line "auto-applied" notice (ticker
|
|
93
|
+
* notifyAutoApplied). Null = not yet sent; only auto applies are notified. */
|
|
94
|
+
notifiedAt: integer("notified_at"),
|
|
95
|
+
rolledBackAt: integer("rolled_back_at")
|
|
70
96
|
});
|
|
71
97
|
var CREATE_SQL = `
|
|
72
98
|
CREATE TABLE IF NOT EXISTS evolution_runs (
|
|
@@ -83,7 +109,10 @@ CREATE TABLE IF NOT EXISTS evolution_runs (
|
|
|
83
109
|
applied_at INTEGER,
|
|
84
110
|
failure_reason TEXT,
|
|
85
111
|
attempts INTEGER NOT NULL DEFAULT 0,
|
|
86
|
-
archived_at INTEGER
|
|
112
|
+
archived_at INTEGER,
|
|
113
|
+
has_observation INTEGER NOT NULL DEFAULT 0,
|
|
114
|
+
source_access TEXT,
|
|
115
|
+
attempts_base INTEGER NOT NULL DEFAULT 0
|
|
87
116
|
);
|
|
88
117
|
CREATE INDEX IF NOT EXISTS idx_evo_runs_status ON evolution_runs(status, created_at);
|
|
89
118
|
CREATE INDEX IF NOT EXISTS idx_evo_runs_ws ON evolution_runs(workspace_path);
|
|
@@ -101,19 +130,39 @@ CREATE TABLE IF NOT EXISTS evolution_applies (
|
|
|
101
130
|
completed_at INTEGER,
|
|
102
131
|
failure_reason TEXT,
|
|
103
132
|
attempts INTEGER NOT NULL DEFAULT 0,
|
|
104
|
-
archived_at INTEGER
|
|
133
|
+
archived_at INTEGER,
|
|
134
|
+
approved_by TEXT NOT NULL DEFAULT 'reviewer',
|
|
135
|
+
notified_at INTEGER,
|
|
136
|
+
rolled_back_at INTEGER
|
|
105
137
|
);
|
|
106
138
|
CREATE INDEX IF NOT EXISTS idx_evo_applies_status ON evolution_applies(status, created_at);
|
|
107
139
|
CREATE INDEX IF NOT EXISTS idx_evo_applies_ws ON evolution_applies(workspace_path);
|
|
108
140
|
CREATE INDEX IF NOT EXISTS idx_evo_applies_archived ON evolution_applies(archived_at);
|
|
109
141
|
`;
|
|
142
|
+
var addV1Columns = (s) => {
|
|
143
|
+
addColumnIfMissing(s, "evolution_applies", "approved_by", "TEXT NOT NULL DEFAULT 'reviewer'");
|
|
144
|
+
addColumnIfMissing(s, "evolution_applies", "notified_at", "INTEGER");
|
|
145
|
+
addColumnIfMissing(s, "evolution_applies", "rolled_back_at", "INTEGER");
|
|
146
|
+
addColumnIfMissing(s, "evolution_runs", "has_observation", "INTEGER NOT NULL DEFAULT 0");
|
|
147
|
+
addColumnIfMissing(s, "evolution_runs", "source_access", "TEXT");
|
|
148
|
+
};
|
|
149
|
+
var EVO_MIGRATIONS = [
|
|
150
|
+
addV1Columns,
|
|
151
|
+
// v2: retry keeps attempts monotonic and records where the round started.
|
|
152
|
+
// Re-runs v1's adds (idempotent): dev builds stamped user_version 1 before
|
|
153
|
+
// v1 had all its columns (source_access).
|
|
154
|
+
(s) => {
|
|
155
|
+
addV1Columns(s);
|
|
156
|
+
addColumnIfMissing(s, "evolution_runs", "attempts_base", "INTEGER NOT NULL DEFAULT 0");
|
|
157
|
+
}
|
|
158
|
+
];
|
|
110
159
|
function createEvoDb(globalDir2) {
|
|
111
160
|
fs.mkdirSync(globalDir2, { recursive: true });
|
|
112
161
|
const dbPath = path.join(globalDir2, "evo.db");
|
|
113
162
|
const sqlite = new Database(dbPath);
|
|
114
163
|
sqlite.pragma("journal_mode = WAL");
|
|
115
164
|
sqlite.exec(CREATE_SQL);
|
|
116
|
-
runMigrations(sqlite,
|
|
165
|
+
runMigrations(sqlite, EVO_MIGRATIONS);
|
|
117
166
|
return drizzle(sqlite, { schema: { evolutionRuns, evolutionApplies } });
|
|
118
167
|
}
|
|
119
168
|
var _evoDb = null;
|
|
@@ -211,8 +260,6 @@ function cleanChildEnv() {
|
|
|
211
260
|
const env = { ...process.env };
|
|
212
261
|
delete env.HALO_PASSWORD;
|
|
213
262
|
delete env.HALO_JWT_SECRET;
|
|
214
|
-
delete env.HALO_NODE_RELAY_URL;
|
|
215
|
-
delete env.HALO_NODE_RELAY_TOKEN;
|
|
216
263
|
delete env.HALO_NODE_TOKEN;
|
|
217
264
|
for (const key of settingsOtelEnvKeys)
|
|
218
265
|
delete env[key];
|
|
@@ -856,11 +903,12 @@ function generalSection() {
|
|
|
856
903
|
// observability (OpenTelemetry). Read once at boot by observability/otel.ts
|
|
857
904
|
// and mapped onto the standard OTEL_* env vars — hence "takes effect on
|
|
858
905
|
// restart" everywhere below. Children don't inherit those settings-derived
|
|
859
|
-
// vars (child-env.ts), so each
|
|
860
|
-
|
|
861
|
-
{ key: "observability.
|
|
862
|
-
{ key: "observability.
|
|
863
|
-
{ key: "observability.
|
|
906
|
+
// vars (child-env.ts), so each evo cli run re-reads settings; cron runs in
|
|
907
|
+
// the server process and follows its boot-time values.
|
|
908
|
+
{ key: "observability.endpoint", type: "string", globalOnly: true, restartRequired: true, description: "OTLP base URL of an OpenTelemetry collector (e.g. http://localhost:4318). Traces, metrics and logs are all exported there over OTLP http/protobuf \u2014 http:// or https://, on the collector's OTLP/HTTP port (4318 by default); gRPC (4317) is not supported. Empty = observability off. Takes effect on server restart (cron runs included \u2014 they run in the server); evolution runs use the new value on their next run.", description_zh: "OpenTelemetry collector \u7684 OTLP \u57FA\u7840\u5730\u5740\uFF08\u5982 http://localhost:4318\uFF09\uFF0Ctrace / metrics / \u65E5\u5FD7\u4E09\u7C7B\u4FE1\u53F7\u90FD\u4EE5 OTLP http/protobuf \u53D1\u5230\u8FD9\u91CC\u3002http:// \u6216 https:// \u5747\u53EF\uFF0C\u7AEF\u53E3\u987B\u662F collector \u7684 OTLP/HTTP \u7AEF\u53E3\uFF08\u9ED8\u8BA4 4318\uFF09\uFF0C\u4E0D\u652F\u6301 gRPC\uFF084317\uFF09\u3002\u7559\u7A7A = \u5173\u95ED\u3002\u91CD\u542F\u670D\u52A1\u540E\u751F\u6548\uFF08cron \u4EFB\u52A1\u5728\u670D\u52A1\u8FDB\u7A0B\u5185\u8FD0\u884C\uFF0C\u540C\u6837\u9700\u91CD\u542F\uFF09\uFF1Bevolution \u4EFB\u52A1\u4E0B\u6B21\u8FD0\u884C\u5373\u7528\u65B0\u503C\u3002", default: "" },
|
|
909
|
+
{ key: "observability.service_name", type: "string", globalOnly: true, restartRequired: true, description: "OTel resource service.name reported for this server. Takes effect on server restart (cron runs included \u2014 they run in the server); evolution runs use the new value on their next run.", description_zh: "\u672C\u670D\u52A1\u5728 OTel \u4E2D\u4E0A\u62A5\u7684 service.name\u3002\u91CD\u542F\u670D\u52A1\u540E\u751F\u6548\uFF08cron \u4EFB\u52A1\u5728\u670D\u52A1\u8FDB\u7A0B\u5185\u8FD0\u884C\uFF0C\u540C\u6837\u9700\u91CD\u542F\uFF09\uFF1Bevolution \u4EFB\u52A1\u4E0B\u6B21\u8FD0\u884C\u5373\u7528\u65B0\u503C\u3002", default: "halo" },
|
|
910
|
+
{ key: "observability.headers", type: "string", secret: true, globalOnly: true, restartRequired: true, description: "Extra headers for every OTLP request, comma-separated k=v (e.g. authorization=Bearer \u2026). Takes effect on server restart (cron runs included \u2014 they run in the server); evolution runs use the new value on their next run.", description_zh: "\u9644\u52A0\u5230\u6BCF\u4E2A OTLP \u8BF7\u6C42\u7684 header\uFF0C\u9017\u53F7\u5206\u9694\u7684 k=v\uFF08\u5982 authorization=Bearer \u2026\uFF09\u3002\u91CD\u542F\u670D\u52A1\u540E\u751F\u6548\uFF08cron \u4EFB\u52A1\u5728\u670D\u52A1\u8FDB\u7A0B\u5185\u8FD0\u884C\uFF0C\u540C\u6837\u9700\u91CD\u542F\uFF09\uFF1Bevolution \u4EFB\u52A1\u4E0B\u6B21\u8FD0\u884C\u5373\u7528\u65B0\u503C\u3002", default: "" },
|
|
911
|
+
{ key: "observability.capture_content", type: "boolean", globalOnly: true, restartRequired: true, description: "Put prompt / completion / tool argument and result text on spans (gen_ai.*.messages etc.). Off = only metadata (model, tokens, latency, tool names). Env OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT (true / false) overrides. Takes effect on server restart (cron runs included \u2014 they run in the server); evolution runs use the new value on their next run.", description_zh: "\u662F\u5426\u628A\u63D0\u793A\u8BCD / \u6A21\u578B\u8F93\u51FA / \u5DE5\u5177\u53C2\u6570\u4E0E\u7ED3\u679C\u6B63\u6587\u5199\u5230 span \u4E0A\uFF08gen_ai.*.messages \u7B49\uFF09\u3002\u5173 = \u53EA\u4E0A\u62A5\u5143\u6570\u636E\uFF08\u6A21\u578B\u3001token\u3001\u8017\u65F6\u3001\u5DE5\u5177\u540D\uFF09\u3002\u73AF\u5883\u53D8\u91CF OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT\uFF08true / false\uFF09\u4F18\u5148\u3002\u91CD\u542F\u670D\u52A1\u540E\u751F\u6548\uFF08cron \u4EFB\u52A1\u5728\u670D\u52A1\u8FDB\u7A0B\u5185\u8FD0\u884C\uFF0C\u540C\u6837\u9700\u91CD\u542F\uFF09\uFF1Bevolution \u4EFB\u52A1\u4E0B\u6B21\u8FD0\u884C\u5373\u7528\u65B0\u503C\u3002", default: "false" },
|
|
864
912
|
// Notebook kernels (jupyter/manager.ts). Global-only: kernels run
|
|
865
913
|
// unsandboxed as the service user, like the terminal. Read per spawn.
|
|
866
914
|
{ key: "jupyter.enabled", type: "boolean", globalOnly: true, description: "Allow notebook execution: the ipynb viewer can start kernels through a local Jupyter server (runs as the Halo service user, unsandboxed, like the terminal).", description_zh: "\u5141\u8BB8\u8FD0\u884C notebook\uFF1Aipynb \u67E5\u770B\u5668\u53EF\u901A\u8FC7\u672C\u673A Jupyter server \u542F\u52A8\u5185\u6838\uFF08\u4EE5 Halo \u670D\u52A1\u7528\u6237\u8EAB\u4EFD\u8FD0\u884C\u3001\u4E0D\u8FDB\u6C99\u7BB1\uFF0C\u4E0E\u7EC8\u7AEF\u76F8\u540C\uFF09\u3002", default: "true" },
|
|
@@ -872,7 +920,8 @@ function generalSection() {
|
|
|
872
920
|
// self-evolution (see plans/self-evolution.md). All evo settings are
|
|
873
921
|
// global-only — the worker / ticker live in the server process and
|
|
874
922
|
// would have to reload mid-flight if a workspace could override them.
|
|
875
|
-
{ key: "evolution.level", type: "enum", options: ["L0", "L1"], globalOnly: true, description: "Self-evolution capability ladder. L0: manual only \u2014 drafts via /evo, manual approve/reject. L1: L0 plus automatic drafting on pre-compact. (
|
|
923
|
+
{ key: "evolution.level", type: "enum", options: ["L0", "L1", "L2", "L3"], globalOnly: true, description: "Self-evolution capability ladder. L0: manual only \u2014 drafts via /evo, manual approve/reject. L1: L0 plus automatic drafting on pre-compact. L2: L1 plus auto-applying prompt-only changes that score above evolution.auto_apply_threshold. L3: L2 plus auto-applying capability changes (agent.yaml tools / model, skill invocation). Auto-applied changes still pass the regression check and can be rolled back from the Evolution tab. Queued runs always execute regardless of level.", description_zh: "\u81EA\u6211\u8FDB\u5316\u7B49\u7EA7\u3002L0\uFF1A\u4EC5\u624B\u52A8\u2014\u2014\u901A\u8FC7 /evo \u8D77\u8349\uFF0C\u624B\u52A8 approve/reject\u3002L1\uFF1A\u5728 L0 \u57FA\u7840\u4E0A\uFF0C\u538B\u7F29\u524D\uFF08pre-compact\uFF09\u81EA\u52A8\u8D77\u8349\u3002L2\uFF1A\u5728 L1 \u57FA\u7840\u4E0A\uFF0C\u5BF9\u8BC4\u5206\u8D85\u8FC7 evolution.auto_apply_threshold \u7684\u7EAF\u63D0\u793A\u8BCD\u6539\u52A8\u81EA\u52A8\u5E94\u7528\u3002L3\uFF1A\u5728 L2 \u57FA\u7840\u4E0A\uFF0C\u5BF9\u80FD\u529B\u7C7B\u6539\u52A8\uFF08agent.yaml \u7684\u5DE5\u5177/\u6A21\u578B\u3001skill \u8C03\u7528\u65B9\u5F0F\uFF09\u4E5F\u81EA\u52A8\u5E94\u7528\u3002\u81EA\u52A8\u5E94\u7528\u7167\u6837\u8FC7\u56DE\u5F52\u68C0\u67E5\uFF0C\u53EF\u5728 Evolution \u9875\u56DE\u6EDA\u3002\u5DF2\u6392\u961F\u7684 run \u4E0D\u53D7\u7B49\u7EA7\u5F71\u54CD\uFF0C\u59CB\u7EC8\u6267\u884C\u3002", default: "L0" },
|
|
924
|
+
{ key: "evolution.auto_apply_threshold", type: "int", min: 0, max: 100, globalOnly: true, description: "Minimum average score (0\u2013100) for auto-apply at L2/L3. Also required: confidence high, behavior \u2265 70, lint 100, all sandbox checks passed. Ignored at L0/L1.", description_zh: "L2/L3 \u81EA\u52A8\u5E94\u7528\u8981\u6C42\u7684\u6700\u4F4E\u5E73\u5747\u5206\uFF080\u2013100\uFF09\u3002\u53E6\u9700\u540C\u65F6\u6EE1\u8DB3\uFF1Aconfidence \u4E3A high\u3001behavior \u2265 70\u3001lint \u4E3A 100\u3001\u6C99\u7BB1\u68C0\u67E5\u5168\u90E8\u901A\u8FC7\u3002L0/L1 \u4E0B\u5FFD\u7565\u3002", default: "80" },
|
|
876
925
|
{ key: "evolution.max_concurrent_run", type: "int", globalOnly: true, description: "Max concurrent evo run wrappers (drafting patches).", description_zh: "\u540C\u65F6\u8FD0\u884C\u7684 evo run wrapper \u4E0A\u9650\uFF08\u8D77\u8349 patch\uFF09\u3002", default: "1" },
|
|
877
926
|
{ key: "evolution.max_concurrent_apply", type: "int", globalOnly: true, description: "Max concurrent evo apply wrappers (merging patches).", description_zh: "\u540C\u65F6\u8FD0\u884C\u7684 evo apply wrapper \u4E0A\u9650\uFF08\u5408\u5E76 patch\uFF09\u3002", default: "1" },
|
|
878
927
|
{ key: "evolution.run_timeout_minutes", type: "int", globalOnly: true, description: "Heartbeat timeout for run wrappers. The wrapper writes a heartbeat every 60s for as long as it lives, so exceeding this means the wrapper process died \u2014 the row is requeued (or marked timeout past max_attempts). This is dead-wrapper detection latency, NOT a budget for slow models; the per-phase model budget is the wrapper-internal cap (draft 60min, other phases 45min).", description_zh: "run wrapper \u5FC3\u8DF3\u8D85\u65F6\uFF08\u5206\u949F\uFF09\u3002wrapper \u5B58\u6D3B\u671F\u95F4\u6BCF 60 \u79D2\u5199\u4E00\u6B21\u5FC3\u8DF3\uFF0C\u8D85\u65F6\u5373\u89C6\u4E3A wrapper \u8FDB\u7A0B\u5DF2\u6B7B\uFF0C\u4EFB\u52A1\u91CD\u6392\uFF08\u8D85\u8FC7 max_attempts \u5219\u6807 timeout\uFF09\u3002\u8FD9\u662F\u6B7B wrapper \u7684\u68C0\u6D4B\u5EF6\u8FDF\uFF0C\u4E0D\u662F\u6162\u6A21\u578B\u7684\u8FD0\u884C\u9884\u7B97\u2014\u2014\u5355\u9636\u6BB5\u6A21\u578B\u9884\u7B97\u662F wrapper \u5185\u90E8\u7684\u4E0A\u9650\uFF08\u8D77\u8349 60 \u5206\u949F\uFF0C\u5176\u4F59\u9636\u6BB5 45 \u5206\u949F\uFF09\u3002", default: "12" },
|
|
@@ -1254,6 +1303,13 @@ function systemString(envKey, configPath, fallback) {
|
|
|
1254
1303
|
return cv;
|
|
1255
1304
|
return fallback;
|
|
1256
1305
|
}
|
|
1306
|
+
var EVO_LEVELS = ["L0", "L1", "L2", "L3"];
|
|
1307
|
+
function parseEvoLevel(raw) {
|
|
1308
|
+
return EVO_LEVELS.includes(raw) ? raw : "L0";
|
|
1309
|
+
}
|
|
1310
|
+
function evoLevelRank(level) {
|
|
1311
|
+
return EVO_LEVELS.indexOf(level);
|
|
1312
|
+
}
|
|
1257
1313
|
var config = {
|
|
1258
1314
|
// Goal entry points are retired, not the runtime. Internal/global-only, no UI:
|
|
1259
1315
|
// set general.goal_mode_enabled in ~/.halo/secrets/settings.yaml, then restart.
|
|
@@ -1445,8 +1501,12 @@ var config = {
|
|
|
1445
1501
|
// in settings.yaml take effect on the next ticker run without restart.
|
|
1446
1502
|
evolution: {
|
|
1447
1503
|
get level() {
|
|
1448
|
-
|
|
1449
|
-
|
|
1504
|
+
return parseEvoLevel(settingsStr("general.evolution.level", "L0"));
|
|
1505
|
+
},
|
|
1506
|
+
// Minimum average score for L2/L3 auto-apply (see evolution/auto-apply.ts
|
|
1507
|
+
// for the extra fixed floors).
|
|
1508
|
+
get autoApplyThreshold() {
|
|
1509
|
+
return settingsInt("general.evolution.auto_apply_threshold", 80);
|
|
1450
1510
|
},
|
|
1451
1511
|
get maxConcurrentRun() {
|
|
1452
1512
|
return settingsInt("general.evolution.max_concurrent_run", 1);
|
|
@@ -1477,6 +1537,477 @@ var config = {
|
|
|
1477
1537
|
}
|
|
1478
1538
|
};
|
|
1479
1539
|
|
|
1540
|
+
// ../server/dist/evolution/auto-apply.js
|
|
1541
|
+
import fs9 from "node:fs";
|
|
1542
|
+
import path12 from "node:path";
|
|
1543
|
+
import YAML6 from "yaml";
|
|
1544
|
+
var AUTO_APPLY_FILE = "auto-apply.json";
|
|
1545
|
+
var OVERRIDE_DIRS = /* @__PURE__ */ new Set(["agents", "skills", "prompts"]);
|
|
1546
|
+
function isDir(p) {
|
|
1547
|
+
try {
|
|
1548
|
+
return fs9.statSync(p).isDirectory();
|
|
1549
|
+
} catch {
|
|
1550
|
+
return false;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
function readOrNull(p) {
|
|
1554
|
+
try {
|
|
1555
|
+
return fs9.readFileSync(p);
|
|
1556
|
+
} catch {
|
|
1557
|
+
return null;
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
function walkFiles(root, rel, out = []) {
|
|
1561
|
+
let st;
|
|
1562
|
+
try {
|
|
1563
|
+
st = fs9.statSync(path12.join(root, rel));
|
|
1564
|
+
} catch {
|
|
1565
|
+
return out;
|
|
1566
|
+
}
|
|
1567
|
+
if (st.isFile())
|
|
1568
|
+
out.push(rel);
|
|
1569
|
+
else if (st.isDirectory()) {
|
|
1570
|
+
for (const e of fs9.readdirSync(path12.join(root, rel)))
|
|
1571
|
+
walkFiles(root, path12.join(rel, e), out);
|
|
1572
|
+
}
|
|
1573
|
+
return out;
|
|
1574
|
+
}
|
|
1575
|
+
var toPosix = (rel) => rel.split(path12.sep).join("/");
|
|
1576
|
+
function isInternalAgentDir(dir) {
|
|
1577
|
+
const raw = readOrNull(path12.join(dir, "agent.yaml"));
|
|
1578
|
+
return raw !== null && parseYamlLoose(raw.toString("utf-8"))?.internal === true;
|
|
1579
|
+
}
|
|
1580
|
+
function liveBaseline(dirs, rel) {
|
|
1581
|
+
const parts = rel.split(path12.sep);
|
|
1582
|
+
if (!OVERRIDE_DIRS.has(parts[0]) || parts.length < 3)
|
|
1583
|
+
return { file: path12.join(dirs.mainHalo, rel), newDir: false };
|
|
1584
|
+
const dirRel = path12.join(parts[0], parts[1]);
|
|
1585
|
+
const internal = (root) => parts[0] === "agents" && isInternalAgentDir(path12.join(root, dirRel));
|
|
1586
|
+
if (isDir(path12.join(dirs.mainHalo, dirRel)))
|
|
1587
|
+
return { file: path12.join(dirs.mainHalo, rel), newDir: false, internalAgent: internal(dirs.mainHalo) };
|
|
1588
|
+
if (isDir(path12.join(dirs.globalHalo, dirRel)))
|
|
1589
|
+
return { file: path12.join(dirs.globalHalo, rel), newDir: false, globalDir: dirRel, internalAgent: internal(dirs.globalHalo) };
|
|
1590
|
+
return { file: null, newDir: true };
|
|
1591
|
+
}
|
|
1592
|
+
function fileChange(rel, base, after) {
|
|
1593
|
+
const before = base.file ? readOrNull(base.file) : null;
|
|
1594
|
+
if (before && before.equals(after))
|
|
1595
|
+
return null;
|
|
1596
|
+
return {
|
|
1597
|
+
rel: toPosix(rel),
|
|
1598
|
+
before: before?.toString("utf-8") ?? null,
|
|
1599
|
+
after: after.toString("utf-8"),
|
|
1600
|
+
newDir: base.newDir,
|
|
1601
|
+
...base.internalAgent ? { internalAgent: true } : {}
|
|
1602
|
+
};
|
|
1603
|
+
}
|
|
1604
|
+
function publishChanges(dirs, files) {
|
|
1605
|
+
const out = [];
|
|
1606
|
+
const globalBacked = /* @__PURE__ */ new Set();
|
|
1607
|
+
for (const { rel, full } of files) {
|
|
1608
|
+
const base = liveBaseline(dirs, rel);
|
|
1609
|
+
if (base.globalDir)
|
|
1610
|
+
globalBacked.add(base.globalDir);
|
|
1611
|
+
const c = fileChange(rel, base, fs9.readFileSync(full));
|
|
1612
|
+
if (c)
|
|
1613
|
+
out.push(c);
|
|
1614
|
+
}
|
|
1615
|
+
for (const dirRel of globalBacked) {
|
|
1616
|
+
for (const rel of walkFiles(dirs.globalHalo, dirRel)) {
|
|
1617
|
+
if (fs9.existsSync(path12.join(dirs.sandboxHalo, rel)))
|
|
1618
|
+
continue;
|
|
1619
|
+
out.push({ rel: toPosix(rel), before: fs9.readFileSync(path12.join(dirs.globalHalo, rel), "utf-8"), after: null, newDir: false });
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
return out;
|
|
1623
|
+
}
|
|
1624
|
+
function collectChanges(dirs) {
|
|
1625
|
+
const out = [];
|
|
1626
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1627
|
+
const globalBacked = /* @__PURE__ */ new Set();
|
|
1628
|
+
for (const top of dirs.entries) {
|
|
1629
|
+
for (const rel of walkFiles(dirs.sandboxHalo, top)) {
|
|
1630
|
+
seen.add(rel);
|
|
1631
|
+
const base = liveBaseline(dirs, rel);
|
|
1632
|
+
if (base.globalDir)
|
|
1633
|
+
globalBacked.add(base.globalDir);
|
|
1634
|
+
const c = fileChange(rel, base, fs9.readFileSync(path12.join(dirs.sandboxHalo, rel)));
|
|
1635
|
+
if (c)
|
|
1636
|
+
out.push(c);
|
|
1637
|
+
}
|
|
1638
|
+
for (const rel of walkFiles(dirs.mainHalo, top)) {
|
|
1639
|
+
if (seen.has(rel))
|
|
1640
|
+
continue;
|
|
1641
|
+
out.push({ rel: toPosix(rel), before: fs9.readFileSync(path12.join(dirs.mainHalo, rel), "utf-8"), after: null, newDir: false });
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
for (const dirRel of globalBacked) {
|
|
1645
|
+
for (const rel of walkFiles(dirs.globalHalo, dirRel)) {
|
|
1646
|
+
if (seen.has(rel))
|
|
1647
|
+
continue;
|
|
1648
|
+
out.push({ rel: toPosix(rel), before: fs9.readFileSync(path12.join(dirs.globalHalo, rel), "utf-8"), after: null, newDir: false });
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
return out;
|
|
1652
|
+
}
|
|
1653
|
+
var PROMPT_SCOPES = /* @__PURE__ */ new Set(["bootstrap", "all", "root"]);
|
|
1654
|
+
var EGRESS_TOOLS = /* @__PURE__ */ new Set(["shell_exec", "web_fetch", "relay_send", "a2a_send"]);
|
|
1655
|
+
var isEgressTool = (t) => EGRESS_TOOLS.has(t) || t.startsWith("node_");
|
|
1656
|
+
var AGENT_CAPABILITY_KEYS = /* @__PURE__ */ new Set(["description", "system_prompt", "context"]);
|
|
1657
|
+
var MODEL_CAPABILITY_KEYS = /* @__PURE__ */ new Set(["id", "maxTokens", "promptCaching", "thinking", "verbosity"]);
|
|
1658
|
+
var SKILL_PROMPT_KEYS = /* @__PURE__ */ new Set(["name", "description"]);
|
|
1659
|
+
var SKILL_CAPABILITY_KEYS = /* @__PURE__ */ new Set(["disable-model-invocation", "disableModelInvocation", "user-invocable", "userInvocable"]);
|
|
1660
|
+
var manual = (rel, reason) => ({ rel, category: "manual", reason });
|
|
1661
|
+
function same(a, b) {
|
|
1662
|
+
const canon = (v) => {
|
|
1663
|
+
if (Array.isArray(v))
|
|
1664
|
+
return v.map(canon);
|
|
1665
|
+
if (v && typeof v === "object") {
|
|
1666
|
+
return Object.fromEntries(Object.keys(v).sort().map((k) => [k, canon(v[k])]));
|
|
1667
|
+
}
|
|
1668
|
+
return v;
|
|
1669
|
+
};
|
|
1670
|
+
return JSON.stringify(canon(a)) === JSON.stringify(canon(b));
|
|
1671
|
+
}
|
|
1672
|
+
function asRecord(v) {
|
|
1673
|
+
if (v === void 0 || v === null)
|
|
1674
|
+
return {};
|
|
1675
|
+
return typeof v === "object" && !Array.isArray(v) ? v : null;
|
|
1676
|
+
}
|
|
1677
|
+
function parseYamlRecord(text2) {
|
|
1678
|
+
try {
|
|
1679
|
+
return asRecord(YAML6.parse(text2));
|
|
1680
|
+
} catch {
|
|
1681
|
+
return null;
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
function classifyAgentYaml(rel, before, after) {
|
|
1685
|
+
const a = parseYamlRecord(before);
|
|
1686
|
+
const b = parseYamlRecord(after);
|
|
1687
|
+
if (!a || !b)
|
|
1688
|
+
return manual(rel, "agent_yaml_unparsable");
|
|
1689
|
+
for (const key of /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])) {
|
|
1690
|
+
if (same(a[key], b[key]) || AGENT_CAPABILITY_KEYS.has(key))
|
|
1691
|
+
continue;
|
|
1692
|
+
if (key === "tools") {
|
|
1693
|
+
if (b.tools !== void 0 && !Array.isArray(b.tools) || a.tools !== void 0 && !Array.isArray(a.tools)) {
|
|
1694
|
+
return manual(rel, "agent_yaml:tools");
|
|
1695
|
+
}
|
|
1696
|
+
const had = new Set((a.tools ?? []).map(String));
|
|
1697
|
+
const egress = (b.tools ?? []).map(String).find((t) => !had.has(t) && isEgressTool(t));
|
|
1698
|
+
if (egress)
|
|
1699
|
+
return manual(rel, `agent_yaml:tools+${egress}`);
|
|
1700
|
+
continue;
|
|
1701
|
+
}
|
|
1702
|
+
if (key === "skills") {
|
|
1703
|
+
if (b.skills !== void 0 && !Array.isArray(b.skills) || a.skills !== void 0 && !Array.isArray(a.skills)) {
|
|
1704
|
+
return manual(rel, "agent_yaml:skills");
|
|
1705
|
+
}
|
|
1706
|
+
const had = new Set((a.skills ?? []).map(String));
|
|
1707
|
+
const added = (b.skills ?? []).map(String).find((s) => !had.has(s));
|
|
1708
|
+
if (added)
|
|
1709
|
+
return manual(rel, `agent_yaml:skills+${added}`);
|
|
1710
|
+
continue;
|
|
1711
|
+
}
|
|
1712
|
+
if (key === "model") {
|
|
1713
|
+
const ma = asRecord(a.model);
|
|
1714
|
+
const mb = asRecord(b.model);
|
|
1715
|
+
if (!ma || !mb)
|
|
1716
|
+
return manual(rel, "agent_yaml:model");
|
|
1717
|
+
for (const mk of /* @__PURE__ */ new Set([...Object.keys(ma), ...Object.keys(mb)])) {
|
|
1718
|
+
if (!same(ma[mk], mb[mk]) && !MODEL_CAPABILITY_KEYS.has(mk))
|
|
1719
|
+
return manual(rel, `agent_yaml:model.${mk}`);
|
|
1720
|
+
}
|
|
1721
|
+
continue;
|
|
1722
|
+
}
|
|
1723
|
+
return manual(rel, `agent_yaml:${key}`);
|
|
1724
|
+
}
|
|
1725
|
+
return { rel, category: "capability" };
|
|
1726
|
+
}
|
|
1727
|
+
function parseYamlLoose(text2) {
|
|
1728
|
+
let v;
|
|
1729
|
+
try {
|
|
1730
|
+
v = YAML6.parse(text2);
|
|
1731
|
+
} catch {
|
|
1732
|
+
return void 0;
|
|
1733
|
+
}
|
|
1734
|
+
return v && typeof v === "object" && !Array.isArray(v) ? v : {};
|
|
1735
|
+
}
|
|
1736
|
+
function splitSkillMd(raw) {
|
|
1737
|
+
const m = raw.match(/^---\s*\n([\s\S]*?)\n---\s*\n?/);
|
|
1738
|
+
if (!m)
|
|
1739
|
+
return { fm: {}, body: raw.trim() };
|
|
1740
|
+
const block = m[1];
|
|
1741
|
+
const fm = parseYamlLoose(block) ?? parseYamlLoose(block.replace(/^(name|description|command):.*$/gm, ""));
|
|
1742
|
+
if (!fm)
|
|
1743
|
+
return null;
|
|
1744
|
+
for (const key of ["name", "description", "command"]) {
|
|
1745
|
+
const lm = block.match(new RegExp(`^${key}:\\s*(.+)$`, "m"));
|
|
1746
|
+
if (lm)
|
|
1747
|
+
fm[key] = lm[1].trim();
|
|
1748
|
+
else
|
|
1749
|
+
delete fm[key];
|
|
1750
|
+
}
|
|
1751
|
+
return { fm, body: raw.slice(m[0].length).trim() };
|
|
1752
|
+
}
|
|
1753
|
+
var normAccess = (v) => {
|
|
1754
|
+
const t = typeof v === "string" ? v.trim() : "";
|
|
1755
|
+
return t === "full" || t === "workspace" || t === "readonly" ? t : null;
|
|
1756
|
+
};
|
|
1757
|
+
function verbAccess(fm) {
|
|
1758
|
+
const objectAccess = normAccess(fm.requiresAccess);
|
|
1759
|
+
const out = {};
|
|
1760
|
+
for (const v of Array.isArray(fm.verbs) ? fm.verbs : []) {
|
|
1761
|
+
const o = asRecord(v);
|
|
1762
|
+
if (!o || typeof o.name !== "string")
|
|
1763
|
+
continue;
|
|
1764
|
+
out[o.name.trim()] = normAccess(o.requiresAccess) ?? objectAccess;
|
|
1765
|
+
}
|
|
1766
|
+
return out;
|
|
1767
|
+
}
|
|
1768
|
+
function classifySkillMd(rel, before, after) {
|
|
1769
|
+
const a = splitSkillMd(before);
|
|
1770
|
+
const b = splitSkillMd(after);
|
|
1771
|
+
if (!a || !b)
|
|
1772
|
+
return manual(rel, "skill_frontmatter_unparsable");
|
|
1773
|
+
let category = "prompt";
|
|
1774
|
+
for (const key of /* @__PURE__ */ new Set([...Object.keys(a.fm), ...Object.keys(b.fm)])) {
|
|
1775
|
+
if (same(a.fm[key], b.fm[key]) || SKILL_PROMPT_KEYS.has(key))
|
|
1776
|
+
continue;
|
|
1777
|
+
if (SKILL_CAPABILITY_KEYS.has(key)) {
|
|
1778
|
+
category = "capability";
|
|
1779
|
+
continue;
|
|
1780
|
+
}
|
|
1781
|
+
if (key === "verbs") {
|
|
1782
|
+
if (!same(verbAccess(a.fm), verbAccess(b.fm)))
|
|
1783
|
+
return manual(rel, "skill_fm:verbs.requiresAccess");
|
|
1784
|
+
category = "capability";
|
|
1785
|
+
continue;
|
|
1786
|
+
}
|
|
1787
|
+
return manual(rel, `skill_fm:${key}`);
|
|
1788
|
+
}
|
|
1789
|
+
return { rel, category };
|
|
1790
|
+
}
|
|
1791
|
+
function classifyChange(c) {
|
|
1792
|
+
const parts = c.rel.split("/");
|
|
1793
|
+
const [top, id] = parts;
|
|
1794
|
+
if (c.after === null)
|
|
1795
|
+
return manual(c.rel, "deleted");
|
|
1796
|
+
if (parts.length === 1) {
|
|
1797
|
+
if (top === "INSTRUCTIONS.md" || top === "INDEX.md")
|
|
1798
|
+
return { rel: c.rel, category: "prompt" };
|
|
1799
|
+
if (top === "USER.md")
|
|
1800
|
+
return manual(c.rel, "user_md");
|
|
1801
|
+
return manual(c.rel, "unrecognized");
|
|
1802
|
+
}
|
|
1803
|
+
if (top === "agents" && parts.length >= 3) {
|
|
1804
|
+
if (c.internalAgent || id.startsWith("__"))
|
|
1805
|
+
return manual(c.rel, "internal_agent");
|
|
1806
|
+
if (c.newDir)
|
|
1807
|
+
return manual(c.rel, "new_agent");
|
|
1808
|
+
if (parts.length === 3 && parts[2] === "AGENT.md")
|
|
1809
|
+
return { rel: c.rel, category: "prompt" };
|
|
1810
|
+
if (parts.length === 3 && parts[2] === "agent.yaml") {
|
|
1811
|
+
return c.before === null ? manual(c.rel, "new_file") : classifyAgentYaml(c.rel, c.before, c.after);
|
|
1812
|
+
}
|
|
1813
|
+
return manual(c.rel, "agent_extra_file");
|
|
1814
|
+
}
|
|
1815
|
+
if (top === "skills" && parts.length >= 3) {
|
|
1816
|
+
if (c.newDir)
|
|
1817
|
+
return manual(c.rel, "new_skill");
|
|
1818
|
+
if (parts.length === 3 && parts[2] === "SKILL.md") {
|
|
1819
|
+
return c.before === null ? manual(c.rel, "new_file") : classifySkillMd(c.rel, c.before, c.after);
|
|
1820
|
+
}
|
|
1821
|
+
return manual(c.rel, "skill_extra_file");
|
|
1822
|
+
}
|
|
1823
|
+
if (top === "prompts" && parts.length === 3 && PROMPT_SCOPES.has(id) && parts[2].endsWith(".md")) {
|
|
1824
|
+
return { rel: c.rel, category: "prompt" };
|
|
1825
|
+
}
|
|
1826
|
+
return manual(c.rel, "unrecognized");
|
|
1827
|
+
}
|
|
1828
|
+
var CATEGORY_RANK = { prompt: 0, capability: 1, manual: 2 };
|
|
1829
|
+
function classifyChanges(changes) {
|
|
1830
|
+
const files = changes.map(classifyChange);
|
|
1831
|
+
let category = "none";
|
|
1832
|
+
for (const f of files) {
|
|
1833
|
+
if (category === "none" || CATEGORY_RANK[f.category] > CATEGORY_RANK[category])
|
|
1834
|
+
category = f.category;
|
|
1835
|
+
}
|
|
1836
|
+
return { category, files };
|
|
1837
|
+
}
|
|
1838
|
+
var AUTO_APPLY_BEHAVIOR_FLOOR = 70;
|
|
1839
|
+
var AUTO_APPLY_LINT_REQUIRED = 100;
|
|
1840
|
+
var REQUIRED_CHECKS = {
|
|
1841
|
+
patchInSandbox: "pass",
|
|
1842
|
+
usedInDryRun: "yes",
|
|
1843
|
+
probe: "valid",
|
|
1844
|
+
probeLanguage: "same"
|
|
1845
|
+
};
|
|
1846
|
+
function parseScore(raw) {
|
|
1847
|
+
const r = asRecord(raw);
|
|
1848
|
+
if (!r)
|
|
1849
|
+
return null;
|
|
1850
|
+
const { lint, behavior, scope, confidence } = r;
|
|
1851
|
+
if (![lint, behavior, scope].every((n) => typeof n === "number" && Number.isFinite(n)))
|
|
1852
|
+
return null;
|
|
1853
|
+
if (typeof confidence !== "string")
|
|
1854
|
+
return null;
|
|
1855
|
+
const [l, b, s] = [lint, behavior, scope];
|
|
1856
|
+
return { lint: l, behavior: b, scope: s, avg: Math.round((l + b + s) / 3), confidence, checks: asRecord(r.checks) ?? {} };
|
|
1857
|
+
}
|
|
1858
|
+
function readScore(file) {
|
|
1859
|
+
try {
|
|
1860
|
+
return parseScore(JSON.parse(fs9.readFileSync(file, "utf-8")));
|
|
1861
|
+
} catch {
|
|
1862
|
+
return null;
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
function categoryGate(level, cls) {
|
|
1866
|
+
const rank = evoLevelRank(level);
|
|
1867
|
+
if (rank < 2)
|
|
1868
|
+
return "level";
|
|
1869
|
+
if (cls.category === "none")
|
|
1870
|
+
return "no_effective_change";
|
|
1871
|
+
if (cls.category === "manual") {
|
|
1872
|
+
const f = cls.files.find((x) => x.category === "manual");
|
|
1873
|
+
return `category_manual:${f.rel}${f.reason ? `(${f.reason})` : ""}`;
|
|
1874
|
+
}
|
|
1875
|
+
if (cls.category === "capability" && rank < 3)
|
|
1876
|
+
return "level";
|
|
1877
|
+
return null;
|
|
1878
|
+
}
|
|
1879
|
+
function sourceGate(sourceAccess) {
|
|
1880
|
+
if (sourceAccess === SOURCE_WEBHOOK)
|
|
1881
|
+
return "source_webhook";
|
|
1882
|
+
return sourceAccess === "full" ? null : "source_access";
|
|
1883
|
+
}
|
|
1884
|
+
var SOURCE_WEBHOOK = "webhook";
|
|
1885
|
+
function scoreGate(score, threshold) {
|
|
1886
|
+
if (!score)
|
|
1887
|
+
return "score_missing";
|
|
1888
|
+
if (score.avg < threshold)
|
|
1889
|
+
return "below_threshold";
|
|
1890
|
+
if (score.confidence !== "high")
|
|
1891
|
+
return "confidence";
|
|
1892
|
+
if (score.behavior < AUTO_APPLY_BEHAVIOR_FLOOR)
|
|
1893
|
+
return "behavior_floor";
|
|
1894
|
+
if (score.lint !== AUTO_APPLY_LINT_REQUIRED)
|
|
1895
|
+
return "lint_floor";
|
|
1896
|
+
for (const [name, want] of Object.entries(REQUIRED_CHECKS)) {
|
|
1897
|
+
if (score.checks[name] !== want)
|
|
1898
|
+
return `checks:${name}`;
|
|
1899
|
+
}
|
|
1900
|
+
return null;
|
|
1901
|
+
}
|
|
1902
|
+
function decideAutoApply(input) {
|
|
1903
|
+
const reason = (evoLevelRank(input.level) >= 2 ? sourceGate(input.sourceAccess) : null) ?? categoryGate(input.level, input.classification) ?? scoreGate(input.score, input.threshold);
|
|
1904
|
+
return { auto: reason === null, reason };
|
|
1905
|
+
}
|
|
1906
|
+
function evaluateSandbox(input) {
|
|
1907
|
+
const classification = classifyChanges(collectChanges(input));
|
|
1908
|
+
const { auto, reason } = decideAutoApply({ level: input.level, threshold: input.threshold, sourceAccess: input.sourceAccess, classification, score: input.score });
|
|
1909
|
+
const sc = input.score;
|
|
1910
|
+
return {
|
|
1911
|
+
decidedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1912
|
+
level: input.level,
|
|
1913
|
+
threshold: input.threshold,
|
|
1914
|
+
sourceAccess: input.sourceAccess,
|
|
1915
|
+
category: classification.category,
|
|
1916
|
+
files: classification.files,
|
|
1917
|
+
auto,
|
|
1918
|
+
reason,
|
|
1919
|
+
score: sc ? { lint: sc.lint, behavior: sc.behavior, scope: sc.scope, avg: sc.avg, confidence: sc.confidence } : null,
|
|
1920
|
+
observation: input.observation
|
|
1921
|
+
};
|
|
1922
|
+
}
|
|
1923
|
+
var OBSERVATION_MAX_CHARS = 2e3;
|
|
1924
|
+
function extractLargerObservation(md) {
|
|
1925
|
+
const lines = md.split(/\r?\n/);
|
|
1926
|
+
const start = lines.findIndex((l) => /^#{2,3}\s+Larger observation\s*:?\s*$/i.test(l));
|
|
1927
|
+
if (start < 0)
|
|
1928
|
+
return null;
|
|
1929
|
+
const level = lines[start].match(/^#+/)[0].length;
|
|
1930
|
+
const stop = new RegExp(`^#{1,${level}}\\s`);
|
|
1931
|
+
const body = [];
|
|
1932
|
+
for (const l of lines.slice(start + 1)) {
|
|
1933
|
+
if (stop.test(l))
|
|
1934
|
+
break;
|
|
1935
|
+
body.push(l);
|
|
1936
|
+
}
|
|
1937
|
+
const text2 = body.join("\n").trim();
|
|
1938
|
+
return text2 ? text2.slice(0, OBSERVATION_MAX_CHARS) : null;
|
|
1939
|
+
}
|
|
1940
|
+
function readObservation(file) {
|
|
1941
|
+
try {
|
|
1942
|
+
return extractLargerObservation(fs9.readFileSync(file, "utf-8"));
|
|
1943
|
+
} catch {
|
|
1944
|
+
return null;
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
// ../server/dist/evolution/queue-apply.js
|
|
1949
|
+
import { and, eq, sql } from "drizzle-orm";
|
|
1950
|
+
var BLOCKING_APPLY_STATUSES = ["pending", "running", "syncing", "applied"];
|
|
1951
|
+
function queueApply(db, input) {
|
|
1952
|
+
const applyId = `apply-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1953
|
+
class Blocked extends Error {
|
|
1954
|
+
reason;
|
|
1955
|
+
constructor(reason) {
|
|
1956
|
+
super(reason);
|
|
1957
|
+
this.reason = reason;
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
try {
|
|
1961
|
+
return db.transaction((tx) => {
|
|
1962
|
+
const fromState = input.mode === "reviewer" ? eq(evolutionRuns.status, "awaiting_review") : and(eq(evolutionRuns.status, "running"), eq(evolutionRuns.attempts, input.attempts));
|
|
1963
|
+
const flipped = tx.update(evolutionRuns).set(input.mode === "auto" ? { status: "approved", completedAt: Date.now(), failureReason: null } : { status: "approved" }).where(and(eq(evolutionRuns.id, input.runId), fromState)).returning({ workspacePath: evolutionRuns.workspacePath }).all();
|
|
1964
|
+
if (flipped.length === 0)
|
|
1965
|
+
throw new Blocked("state");
|
|
1966
|
+
const existing = tx.select({ id: evolutionApplies.id }).from(evolutionApplies).where(and(sql`EXISTS (SELECT 1 FROM json_each(${evolutionApplies.sourceRunIds}) WHERE value = ${input.runId})`, sql`${evolutionApplies.status} IN (${sql.join(BLOCKING_APPLY_STATUSES.map((s) => sql`${s}`), sql`, `)})`)).get();
|
|
1967
|
+
if (existing)
|
|
1968
|
+
throw new Blocked("apply_exists");
|
|
1969
|
+
tx.insert(evolutionApplies).values({
|
|
1970
|
+
id: applyId,
|
|
1971
|
+
workspacePath: flipped[0].workspacePath,
|
|
1972
|
+
status: "pending",
|
|
1973
|
+
sourceRunIds: JSON.stringify([input.runId]),
|
|
1974
|
+
reviewerHint: input.mode === "reviewer" ? input.reviewerHint ?? null : null,
|
|
1975
|
+
createdAt: Date.now(),
|
|
1976
|
+
approvedBy: input.mode
|
|
1977
|
+
}).run();
|
|
1978
|
+
return { ok: true, applyId, workspacePath: flipped[0].workspacePath };
|
|
1979
|
+
});
|
|
1980
|
+
} catch (err) {
|
|
1981
|
+
if (err instanceof Blocked)
|
|
1982
|
+
return { ok: false, reason: err.reason };
|
|
1983
|
+
throw err;
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
function failApply(db, applyId, input) {
|
|
1987
|
+
return db.transaction((tx) => {
|
|
1988
|
+
const rows = tx.update(evolutionApplies).set({ status: input.status, failureReason: input.failureReason, completedAt: Date.now() }).where(and(eq(evolutionApplies.id, applyId), input.onlyIfStatus !== void 0 ? eq(evolutionApplies.status, input.onlyIfStatus) : void 0, input.onlyIfAttempts !== void 0 ? eq(evolutionApplies.attempts, input.onlyIfAttempts) : void 0)).returning({ approvedBy: evolutionApplies.approvedBy, sourceRunIds: evolutionApplies.sourceRunIds }).all();
|
|
1989
|
+
if (rows.length === 0)
|
|
1990
|
+
return { failed: false, revertedRunIds: [] };
|
|
1991
|
+
const revertedRunIds = [];
|
|
1992
|
+
if (rows[0].approvedBy === "auto") {
|
|
1993
|
+
for (const runId of JSON.parse(rows[0].sourceRunIds)) {
|
|
1994
|
+
const r = tx.update(evolutionRuns).set({ status: "awaiting_review" }).where(and(eq(evolutionRuns.id, runId), eq(evolutionRuns.status, "approved"))).run();
|
|
1995
|
+
if (r.changes > 0)
|
|
1996
|
+
revertedRunIds.push(runId);
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
return { failed: true, revertedRunIds };
|
|
2000
|
+
});
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
// ../server/dist/evolution/rollback.js
|
|
2004
|
+
import fs10 from "node:fs";
|
|
2005
|
+
import { createHash } from "node:crypto";
|
|
2006
|
+
import { and as and2, eq as eq2, inArray } from "drizzle-orm";
|
|
2007
|
+
function sha256File(file) {
|
|
2008
|
+
return createHash("sha256").update(fs10.readFileSync(file)).digest("hex");
|
|
2009
|
+
}
|
|
2010
|
+
|
|
1480
2011
|
// ../server/dist/evolution/evo-wrapper.js
|
|
1481
2012
|
var FIX_BUDGET = 1;
|
|
1482
2013
|
var DRY_RUN_TIMEOUT_SEC = 2700;
|
|
@@ -1507,8 +2038,10 @@ function loadRunRow(id) {
|
|
|
1507
2038
|
workspacePath: evolutionRuns.workspacePath,
|
|
1508
2039
|
triggerKind: evolutionRuns.triggerKind,
|
|
1509
2040
|
sourceSession: evolutionRuns.sourceSession,
|
|
1510
|
-
userHint: evolutionRuns.userHint
|
|
1511
|
-
|
|
2041
|
+
userHint: evolutionRuns.userHint,
|
|
2042
|
+
attempts: evolutionRuns.attempts,
|
|
2043
|
+
sourceAccess: evolutionRuns.sourceAccess
|
|
2044
|
+
}).from(evolutionRuns).where(eq3(evolutionRuns.id, id)).get();
|
|
1512
2045
|
if (!row)
|
|
1513
2046
|
throw new Error(`evolution_runs row not found: ${id}`);
|
|
1514
2047
|
return row;
|
|
@@ -1518,11 +2051,23 @@ function resolveHaloCli() {
|
|
|
1518
2051
|
return process.env.HALO_CLI;
|
|
1519
2052
|
return process.platform === "win32" ? "halo.cmd" : "halo";
|
|
1520
2053
|
}
|
|
2054
|
+
var liveProcs = /* @__PURE__ */ new Set();
|
|
2055
|
+
var KILL_GRACE_MS = 1e4;
|
|
2056
|
+
var stopping = false;
|
|
2057
|
+
function resetStoppingForTests() {
|
|
2058
|
+
stopping = false;
|
|
2059
|
+
}
|
|
1521
2060
|
function spawnProc(bin, args, logFd, teePath, stdinInput, timeoutSec) {
|
|
1522
2061
|
const loggedArgs = stdinInput === void 0 ? args.slice(0, -1) : args;
|
|
2062
|
+
if (stopping) {
|
|
2063
|
+
writeLog(logFd, `[wrapper] stopping (claim lost) \u2014 not spawning ${bin}
|
|
2064
|
+
`);
|
|
2065
|
+
return new Promise(() => {
|
|
2066
|
+
});
|
|
2067
|
+
}
|
|
1523
2068
|
writeLog(logFd, `[wrapper] $ ${bin} ${loggedArgs.join(" ")}${stdinInput === void 0 ? " <last-arg-omitted>" : " <prompt-on-stdin>"}${timeoutSec ? ` (timeout ${timeoutSec}s)` : ""}
|
|
1524
2069
|
`);
|
|
1525
|
-
const tee = teePath ?
|
|
2070
|
+
const tee = teePath ? fs11.createWriteStream(teePath, { flags: "a" }) : null;
|
|
1526
2071
|
const [spawnBin, spawnArgs] = process.platform === "win32" && bin.endsWith(".cmd") ? ["cmd.exe", ["/c", bin, ...args]] : [bin, args];
|
|
1527
2072
|
return new Promise((resolve) => {
|
|
1528
2073
|
const child = spawn2(spawnBin, spawnArgs, {
|
|
@@ -1563,16 +2108,17 @@ function spawnProc(bin, args, logFd, teePath, stdinInput, timeoutSec) {
|
|
|
1563
2108
|
} catch {
|
|
1564
2109
|
}
|
|
1565
2110
|
};
|
|
2111
|
+
liveProcs.add(killTree);
|
|
1566
2112
|
const killTimer = timeoutSec ? setTimeout(() => {
|
|
1567
2113
|
timedOut = true;
|
|
1568
2114
|
writeLog(logFd, `[wrapper] timeout ${timeoutSec}s \u2014 killing process group (SIGTERM)
|
|
1569
2115
|
`);
|
|
1570
2116
|
killTree("SIGTERM");
|
|
1571
2117
|
sigkillTimer = setTimeout(() => {
|
|
1572
|
-
writeLog(logFd, `[wrapper] still alive
|
|
2118
|
+
writeLog(logFd, `[wrapper] still alive ${KILL_GRACE_MS / 1e3}s after SIGTERM \u2014 SIGKILL
|
|
1573
2119
|
`);
|
|
1574
2120
|
killTree("SIGKILL");
|
|
1575
|
-
},
|
|
2121
|
+
}, KILL_GRACE_MS);
|
|
1576
2122
|
sigkillTimer.unref?.();
|
|
1577
2123
|
}, timeoutSec * 1e3) : null;
|
|
1578
2124
|
child.stdout?.on("data", (chunk) => {
|
|
@@ -1584,6 +2130,7 @@ function spawnProc(bin, args, logFd, teePath, stdinInput, timeoutSec) {
|
|
|
1584
2130
|
tee?.write(chunk);
|
|
1585
2131
|
});
|
|
1586
2132
|
child.on("exit", (code, signal) => {
|
|
2133
|
+
liveProcs.delete(killTree);
|
|
1587
2134
|
if (killTimer)
|
|
1588
2135
|
clearTimeout(killTimer);
|
|
1589
2136
|
if (sigkillTimer)
|
|
@@ -1592,9 +2139,12 @@ function spawnProc(bin, args, logFd, teePath, stdinInput, timeoutSec) {
|
|
|
1592
2139
|
writeLog(logFd, `[wrapper] proc exit code=${code} signal=${signal}${timedOut ? " (timed out \u2192 124)" : ""} stdout=${stdout.length}B stderr=${stderr.length}B
|
|
1593
2140
|
`);
|
|
1594
2141
|
tee?.end();
|
|
2142
|
+
if (stopping)
|
|
2143
|
+
return;
|
|
1595
2144
|
resolve({ exitCode, stdout, stderr });
|
|
1596
2145
|
});
|
|
1597
2146
|
child.on("error", (err) => {
|
|
2147
|
+
liveProcs.delete(killTree);
|
|
1598
2148
|
if (killTimer)
|
|
1599
2149
|
clearTimeout(killTimer);
|
|
1600
2150
|
if (sigkillTimer)
|
|
@@ -1605,17 +2155,19 @@ function spawnProc(bin, args, logFd, teePath, stdinInput, timeoutSec) {
|
|
|
1605
2155
|
[spawn error] ${err.message}
|
|
1606
2156
|
`);
|
|
1607
2157
|
tee?.end();
|
|
2158
|
+
if (stopping)
|
|
2159
|
+
return;
|
|
1608
2160
|
resolve({ exitCode: 1, stdout, stderr: stderr + `
|
|
1609
2161
|
[spawn error] ${err.message}` });
|
|
1610
2162
|
});
|
|
1611
2163
|
});
|
|
1612
2164
|
}
|
|
1613
2165
|
function subCliLogPath(runDir) {
|
|
1614
|
-
return
|
|
2166
|
+
return path13.join(runDir, "sub-cli.log");
|
|
1615
2167
|
}
|
|
1616
2168
|
function appendSubCliHeader(runDir, header) {
|
|
1617
2169
|
try {
|
|
1618
|
-
|
|
2170
|
+
fs11.appendFileSync(subCliLogPath(runDir), `
|
|
1619
2171
|
=== ${(/* @__PURE__ */ new Date()).toISOString()} ${header} ===
|
|
1620
2172
|
`);
|
|
1621
2173
|
} catch {
|
|
@@ -1649,10 +2201,10 @@ function buildLanguageClause(langHint, scope) {
|
|
|
1649
2201
|
];
|
|
1650
2202
|
}
|
|
1651
2203
|
function readPatchFrontmatter(runDir) {
|
|
1652
|
-
const patchPath =
|
|
2204
|
+
const patchPath = path13.join(runDir, "patch.md");
|
|
1653
2205
|
let raw;
|
|
1654
2206
|
try {
|
|
1655
|
-
raw =
|
|
2207
|
+
raw = fs11.readFileSync(patchPath, "utf-8");
|
|
1656
2208
|
} catch {
|
|
1657
2209
|
return null;
|
|
1658
2210
|
}
|
|
@@ -1660,7 +2212,7 @@ function readPatchFrontmatter(runDir) {
|
|
|
1660
2212
|
if (!m)
|
|
1661
2213
|
return null;
|
|
1662
2214
|
try {
|
|
1663
|
-
const parsed =
|
|
2215
|
+
const parsed = YAML7.parse(m[1]);
|
|
1664
2216
|
return parsed ?? null;
|
|
1665
2217
|
} catch {
|
|
1666
2218
|
return null;
|
|
@@ -1682,14 +2234,14 @@ function extractTestScenario(fm) {
|
|
|
1682
2234
|
return { agentId: o.agentId, testMessage, originalMessage };
|
|
1683
2235
|
}
|
|
1684
2236
|
function readEvoContext(runDir, logFd) {
|
|
1685
|
-
const p =
|
|
1686
|
-
if (!
|
|
2237
|
+
const p = path13.join(runDir, "evo-context.json");
|
|
2238
|
+
if (!fs11.existsSync(p)) {
|
|
1687
2239
|
writeLog(logFd, `[brief] evo-context.json missing at ${p}
|
|
1688
2240
|
`);
|
|
1689
2241
|
return null;
|
|
1690
2242
|
}
|
|
1691
2243
|
try {
|
|
1692
|
-
return JSON.parse(
|
|
2244
|
+
return JSON.parse(fs11.readFileSync(p, "utf-8"));
|
|
1693
2245
|
} catch (err) {
|
|
1694
2246
|
writeLog(logFd, `[brief] evo-context.json parse failed: ${err instanceof Error ? err.message : String(err)}
|
|
1695
2247
|
`);
|
|
@@ -1697,12 +2249,12 @@ function readEvoContext(runDir, logFd) {
|
|
|
1697
2249
|
}
|
|
1698
2250
|
}
|
|
1699
2251
|
function dumpSnapshotImages(runDir, logFd) {
|
|
1700
|
-
const snapshotPath =
|
|
1701
|
-
if (!
|
|
2252
|
+
const snapshotPath = path13.join(runDir, "source-snapshot.json");
|
|
2253
|
+
if (!fs11.existsSync(snapshotPath))
|
|
1702
2254
|
return [];
|
|
1703
2255
|
let parsed;
|
|
1704
2256
|
try {
|
|
1705
|
-
parsed = JSON.parse(
|
|
2257
|
+
parsed = JSON.parse(fs11.readFileSync(snapshotPath, "utf-8"));
|
|
1706
2258
|
} catch {
|
|
1707
2259
|
writeLog(logFd, `[dumpSnapshotImages] snapshot parse failed
|
|
1708
2260
|
`);
|
|
@@ -1711,8 +2263,8 @@ function dumpSnapshotImages(runDir, logFd) {
|
|
|
1711
2263
|
const messages = Array.isArray(parsed.rawMessages) ? parsed.rawMessages : [];
|
|
1712
2264
|
if (messages.length === 0)
|
|
1713
2265
|
return [];
|
|
1714
|
-
const imagesDir =
|
|
1715
|
-
|
|
2266
|
+
const imagesDir = path13.join(runDir, "images");
|
|
2267
|
+
fs11.mkdirSync(imagesDir, { recursive: true });
|
|
1716
2268
|
const out = [];
|
|
1717
2269
|
const extByMime = {
|
|
1718
2270
|
"image/png": "png",
|
|
@@ -1751,10 +2303,10 @@ function dumpSnapshotImages(runDir, logFd) {
|
|
|
1751
2303
|
continue;
|
|
1752
2304
|
const ext = extByMime[mediaType] ?? "bin";
|
|
1753
2305
|
const fileName = c.subBlockIdx == null ? `${m}-${b}.${ext}` : `${m}-${b}-${c.subBlockIdx}.${ext}`;
|
|
1754
|
-
const fullPath =
|
|
2306
|
+
const fullPath = path13.join(imagesDir, fileName);
|
|
1755
2307
|
try {
|
|
1756
2308
|
const buf = Buffer.from(data, "base64");
|
|
1757
|
-
|
|
2309
|
+
fs11.writeFileSync(fullPath, buf);
|
|
1758
2310
|
out.push({
|
|
1759
2311
|
relPath: `images/${fileName}`,
|
|
1760
2312
|
msgIdx: m,
|
|
@@ -1841,15 +2393,15 @@ function renderEvoContextSections(ctx) {
|
|
|
1841
2393
|
lines.push("");
|
|
1842
2394
|
}
|
|
1843
2395
|
if (ctx.skills.length > 0) {
|
|
1844
|
-
const globalSkillsDir =
|
|
2396
|
+
const globalSkillsDir = path13.join(homedir6(), ".halo", "global", "skills");
|
|
1845
2397
|
lines.push("=== Skills in this workspace ===");
|
|
1846
2398
|
for (const s of ctx.skills)
|
|
1847
2399
|
lines.push(` ${s.id} [${s.scope}]`);
|
|
1848
2400
|
lines.push("");
|
|
1849
2401
|
lines.push("To read a skill's content, file_read its SKILL.md (you have full");
|
|
1850
2402
|
lines.push("file access \u2014 absolute paths resolve):");
|
|
1851
|
-
lines.push(` [workspace] \u2192 ${
|
|
1852
|
-
lines.push(` [global] / [builtin] \u2192 ${
|
|
2403
|
+
lines.push(` [workspace] \u2192 ${path13.join("<workspace>", ".halo", "skills", "<id>", "SKILL.md")}`);
|
|
2404
|
+
lines.push(` [global] / [builtin] \u2192 ${path13.join(globalSkillsDir, "<id>", "SKILL.md")}`);
|
|
1853
2405
|
lines.push("A [global]/[builtin] skill is NOT inside your sandbox \u2014 read it");
|
|
1854
2406
|
lines.push(`from ${globalSkillsDir} directly; don't glob the sandbox for it.`);
|
|
1855
2407
|
lines.push("Sibling resource files live next to SKILL.md in that same dir.");
|
|
@@ -1872,13 +2424,13 @@ function buildDraftBrief(args) {
|
|
|
1872
2424
|
];
|
|
1873
2425
|
if (args.userHint)
|
|
1874
2426
|
lines.push(`Reviewer hint: ${args.userHint}`);
|
|
1875
|
-
lines.push("", "The triggering agent's full message log lives on disk:", ` ${
|
|
2427
|
+
lines.push("", "The triggering agent's full message log lives on disk:", ` ${path13.join(args.runDir, "tool-flow.md")} \u2014 clipped, fast skim`, ` ${path13.join(args.runDir, "source-snapshot.json")} \u2014 full rawMessages`, "", "Read tool-flow.md first to understand what the user wanted, what the", "agent did, and where it diverged. Each tool_result is clipped to a", "~200-char peek (with the `is_error` flag when the tool failed) \u2014", "enough to see the *shape* of the flow without re-tokenizing large", "grep / shell outputs. Fall back to source-snapshot.json only for", "messages whose full tool result actually matters to your patch.", "", ...renderEvoContextSections(args.evoContext), ...renderImageManifest(args.images), "=== Non-text media note ===", "", "Inline images in source-snapshot.json are base64 \u2014 you can't read them", "directly. The `images/` dump", "(see manifest above) decodes them to disk so you can `view_image` if", "a specific image is load-bearing. Video, audio, and binary documents", "(PDF, etc.) are even more limited: at trigger time the source agent", "worked with them via shell tooling (ffmpeg, pdftotext, etc.), and your", "view is limited to the textual results those tools produced (visible", "inside source-snapshot.json as tool_result text).", "", "When the patch you have in mind requires understanding what's INSIDE", "the media file (a video's visual content, an audio's acoustic", "content, a PDF's layout), the dry-run test scenario can't reliably", "verify it \u2014 skipping the run is the right call. When the patch is", "about the shell-tooling pattern itself (how to call ffmpeg, how to", "grep its output, how to handle credential errors), it can be", "verified at the textual level and the run proceeds normally.", "", "=== Task ===", "", "Decide what change in the prompt surface above would help the next time", "a similar conversation comes up. Three shapes, in order of preference:", "rewrite an existing rule; tighten / reorganize what's there; or, only", "when neither applies, add a new rule.", "", `Sandbox path: ${evoSandboxDir(args.runDir)}`, "", `When you have a change worth making, write ${path13.join(args.runDir, "patch.md")}`, "(frontmatter + body) and one new file at", `${evoSandboxHaloDir(args.runDir)}/<target> with the`, "full new contents. AGENT.md describes the patch.md schema.", "", "When the conversation has no signal worth a patch (rule already covers,", "too short to learn from, agent did fine, depends on media content", `you can't see), write ${path13.join(args.runDir, ".skip.md")} with a`, "one or two sentence reason instead, and skip the rest.", "", ...buildLanguageClause(args.langHint, "all natural-language content (patch.md body, prose drafted into sandbox files, .skip.md body)"));
|
|
1876
2428
|
return lines.join("\n");
|
|
1877
2429
|
}
|
|
1878
2430
|
function buildFixBrief(args) {
|
|
1879
2431
|
let failLogText = "";
|
|
1880
2432
|
try {
|
|
1881
|
-
failLogText =
|
|
2433
|
+
failLogText = fs11.readFileSync(args.failLogPath, "utf-8");
|
|
1882
2434
|
} catch {
|
|
1883
2435
|
failLogText = `(failed to read ${args.failLogPath})`;
|
|
1884
2436
|
}
|
|
@@ -1893,7 +2445,7 @@ function buildFixBrief(args) {
|
|
|
1893
2445
|
`Fix attempt: ${args.attempt}/${args.maxAttempts}`,
|
|
1894
2446
|
"",
|
|
1895
2447
|
`An earlier draft pass produced patch.md + a sandbox at`,
|
|
1896
|
-
`${
|
|
2448
|
+
`${path13.join(args.runDir, "patch.md")} and`,
|
|
1897
2449
|
`${evoSandboxHaloDir(args.runDir)} respectively.`,
|
|
1898
2450
|
"The dry-run failed; the wrapper's failure log:",
|
|
1899
2451
|
"",
|
|
@@ -1914,16 +2466,16 @@ function buildFixBrief(args) {
|
|
|
1914
2466
|
function buildScoreBrief(args) {
|
|
1915
2467
|
function readOrEmpty(p, label) {
|
|
1916
2468
|
try {
|
|
1917
|
-
return
|
|
2469
|
+
return fs11.readFileSync(p, "utf-8");
|
|
1918
2470
|
} catch (err) {
|
|
1919
2471
|
writeLog(args.logFd, `[buildScoreBrief] ${label} read failed: ${err instanceof Error ? err.message : String(err)}
|
|
1920
2472
|
`);
|
|
1921
2473
|
return `(${label} unavailable)`;
|
|
1922
2474
|
}
|
|
1923
2475
|
}
|
|
1924
|
-
const patchMd = readOrEmpty(
|
|
1925
|
-
const dryRun = readOrEmpty(
|
|
1926
|
-
const meta = readOrEmpty(
|
|
2476
|
+
const patchMd = readOrEmpty(path13.join(args.runDir, "patch.md"), "patch.md");
|
|
2477
|
+
const dryRun = readOrEmpty(path13.join(args.runDir, "dry-run-output.txt"), "dry-run-output.txt");
|
|
2478
|
+
const meta = readOrEmpty(path13.join(args.runDir, "meta.json"), "meta.json");
|
|
1927
2479
|
return [
|
|
1928
2480
|
"You are running as the Evolution scorer.",
|
|
1929
2481
|
"",
|
|
@@ -1934,8 +2486,8 @@ function buildScoreBrief(args) {
|
|
|
1934
2486
|
"write score.json with the absolute path given below)",
|
|
1935
2487
|
"",
|
|
1936
2488
|
`The original conversation lives on disk:`,
|
|
1937
|
-
` ${
|
|
1938
|
-
` ${
|
|
2489
|
+
` ${path13.join(args.runDir, "tool-flow.md")} \u2014 clipped, fast skim`,
|
|
2490
|
+
` ${path13.join(args.runDir, "source-snapshot.json")} \u2014 full rawMessages, when needed`,
|
|
1939
2491
|
"Read tool-flow.md first to find the user turn matching",
|
|
1940
2492
|
"`testScenario.originalMessage` and the assistant reply that followed",
|
|
1941
2493
|
"it. Fall back to source-snapshot.json only when the full tool result",
|
|
@@ -1985,7 +2537,7 @@ function buildScoreBrief(args) {
|
|
|
1985
2537
|
"find the assistant turn that followed it in tool-flow.md (or",
|
|
1986
2538
|
"source-snapshot.json) \u2014 that's your baseline. Compare baseline to",
|
|
1987
2539
|
"dry-run-output.txt, rate lint / behavior / scope, write",
|
|
1988
|
-
`${
|
|
2540
|
+
`${path13.join(args.runDir, "score.json")} with the shape AGENT.md describes.`,
|
|
1989
2541
|
"",
|
|
1990
2542
|
...buildLanguageClause(args.langHint, 'the score.json "notes" field'),
|
|
1991
2543
|
'The numeric fields and "confidence" enum stay in their canonical form.'
|
|
@@ -2028,14 +2580,14 @@ async function phaseDraft(args) {
|
|
|
2028
2580
|
return outcome;
|
|
2029
2581
|
}
|
|
2030
2582
|
function judgeDraft(runDir, workspacePath, exitCode) {
|
|
2031
|
-
if (
|
|
2583
|
+
if (fs11.existsSync(path13.join(runDir, ".skip.md"))) {
|
|
2032
2584
|
return { outcome: "skipped", why: ".skip.md present \u2014 evo declared skip" };
|
|
2033
2585
|
}
|
|
2034
2586
|
if (exitCode !== 0) {
|
|
2035
2587
|
const timeout = exitCode === 124 ? " (timeout)" : "";
|
|
2036
2588
|
return { outcome: "failed", why: `drafter exited ${exitCode}${timeout} \u2014 draft interrupted, not trusted` };
|
|
2037
2589
|
}
|
|
2038
|
-
if (!
|
|
2590
|
+
if (!fs11.existsSync(path13.join(runDir, "patch.md"))) {
|
|
2039
2591
|
return { outcome: "failed", why: "patch.md missing" };
|
|
2040
2592
|
}
|
|
2041
2593
|
const target = readPatchFrontmatter(runDir)?.target;
|
|
@@ -2043,16 +2595,16 @@ function judgeDraft(runDir, workspacePath, exitCode) {
|
|
|
2043
2595
|
return { outcome: "drafted", why: "patch.md present, no target in frontmatter \u2014 target file not checked" };
|
|
2044
2596
|
}
|
|
2045
2597
|
const sandboxDir = evoSandboxDir(runDir);
|
|
2046
|
-
const rel =
|
|
2047
|
-
if (!rel.startsWith(".halo" +
|
|
2598
|
+
const rel = path13.relative(sandboxDir, path13.resolve(sandboxDir, target));
|
|
2599
|
+
if (!rel.startsWith(".halo" + path13.sep)) {
|
|
2048
2600
|
return { outcome: "failed", why: `target ${JSON.stringify(target)} does not resolve under sandbox/.halo/` };
|
|
2049
2601
|
}
|
|
2050
|
-
const sandboxFile =
|
|
2051
|
-
if (!
|
|
2602
|
+
const sandboxFile = path13.join(sandboxDir, rel);
|
|
2603
|
+
if (!fs11.statSync(sandboxFile, { throwIfNoEntry: false })?.isFile()) {
|
|
2052
2604
|
return { outcome: "failed", why: `target not written to sandbox: ${target}` };
|
|
2053
2605
|
}
|
|
2054
|
-
const wsFile =
|
|
2055
|
-
if (
|
|
2606
|
+
const wsFile = path13.join(workspacePath, rel);
|
|
2607
|
+
if (fs11.statSync(wsFile, { throwIfNoEntry: false })?.isFile() && fs11.readFileSync(wsFile).equals(fs11.readFileSync(sandboxFile))) {
|
|
2056
2608
|
return { outcome: "failed", why: `target unchanged from workspace: ${target}` };
|
|
2057
2609
|
}
|
|
2058
2610
|
return { outcome: "drafted", why: `patch.md + target ${target} present` };
|
|
@@ -2079,15 +2631,15 @@ async function phaseDryRun(args) {
|
|
|
2079
2631
|
}
|
|
2080
2632
|
async function tryDryRun(args, attemptIndex) {
|
|
2081
2633
|
const sandboxDir = evoSandboxDir(args.runDir);
|
|
2082
|
-
const outputPath =
|
|
2083
|
-
const failLogPath =
|
|
2634
|
+
const outputPath = path13.join(args.runDir, "dry-run-output.txt");
|
|
2635
|
+
const failLogPath = path13.join(args.runDir, `dry-run-fail-${attemptIndex}.log`);
|
|
2084
2636
|
const cli = resolveHaloCli();
|
|
2085
2637
|
const fm = readPatchFrontmatter(args.runDir);
|
|
2086
2638
|
const scenario = extractTestScenario(fm);
|
|
2087
2639
|
if (!scenario) {
|
|
2088
2640
|
writeLog(args.logFd, `[tryDryRun:${attemptIndex}] patch.md frontmatter missing testScenario.{agentId, testMessage}
|
|
2089
2641
|
`);
|
|
2090
|
-
|
|
2642
|
+
fs11.writeFileSync(failLogPath, "patch.md frontmatter missing testScenario.{agentId, testMessage} \u2014 wrapper cannot run dry-run without it.\n", "utf-8");
|
|
2091
2643
|
return { kind: "no-scenario", failLogPath };
|
|
2092
2644
|
}
|
|
2093
2645
|
writeLog(args.logFd, `[tryDryRun:${attemptIndex}] agent=${scenario.agentId} testMessage=${JSON.stringify(scenario.testMessage).slice(0, 200)}
|
|
@@ -2105,14 +2657,14 @@ async function tryDryRun(args, attemptIndex) {
|
|
|
2105
2657
|
const timedOut = result.exitCode === 124;
|
|
2106
2658
|
const ok = result.exitCode === 0 && result.stdout.trim().length > 0;
|
|
2107
2659
|
if (ok) {
|
|
2108
|
-
|
|
2660
|
+
fs11.writeFileSync(outputPath, result.stdout, "utf-8");
|
|
2109
2661
|
writeLog(args.logFd, `[tryDryRun:${attemptIndex}] success: wrote ${outputPath} (${result.stdout.length}B)
|
|
2110
2662
|
`);
|
|
2111
2663
|
return { kind: "ok" };
|
|
2112
2664
|
}
|
|
2113
2665
|
const stdoutTail = result.stdout.length > 4e3 ? "\u2026" + result.stdout.slice(-4e3) : result.stdout;
|
|
2114
2666
|
const stderrTail = result.stderr.length > 4e3 ? "\u2026" + result.stderr.slice(-4e3) : result.stderr;
|
|
2115
|
-
|
|
2667
|
+
fs11.writeFileSync(failLogPath, [
|
|
2116
2668
|
`Wrapper command: halo cli -a ${scenario.agentId} -n -w ${sandboxDir} <testMessage>`,
|
|
2117
2669
|
`Test message: ${scenario.testMessage}`,
|
|
2118
2670
|
`Exit code: ${result.exitCode}${timedOut ? " (timeout)" : ""}`,
|
|
@@ -2168,6 +2720,7 @@ async function phaseScore(args) {
|
|
|
2168
2720
|
const sandboxWs = evoSandboxDir(args.runDir);
|
|
2169
2721
|
buildEvoSandbox(args.workspacePath, args.runDir, args.logFd);
|
|
2170
2722
|
const cliArgs = ["cli", "-a", "__score__", "-n", "-w", sandboxWs];
|
|
2723
|
+
fs11.rmSync(path13.join(args.runDir, "score.json"), { force: true });
|
|
2171
2724
|
appendSubCliHeader(args.runDir, "Phase C: __score__");
|
|
2172
2725
|
const result = await spawnProc(
|
|
2173
2726
|
resolveHaloCli(),
|
|
@@ -2188,7 +2741,7 @@ async function phaseScore(args) {
|
|
|
2188
2741
|
writeLog(args.logFd, `[phaseScore] stdout (last 1KB): ${result.stdout.slice(-1e3)}
|
|
2189
2742
|
`);
|
|
2190
2743
|
}
|
|
2191
|
-
const hasScore =
|
|
2744
|
+
const hasScore = fs11.existsSync(path13.join(args.runDir, "score.json"));
|
|
2192
2745
|
writeLog(args.logFd, `[phaseScore] score.json=${hasScore}
|
|
2193
2746
|
`);
|
|
2194
2747
|
return hasScore;
|
|
@@ -2202,7 +2755,7 @@ var RUN_INPUT_FILES = /* @__PURE__ */ new Set([
|
|
|
2202
2755
|
function resetRunArtifacts(runDir, logFd) {
|
|
2203
2756
|
let entries;
|
|
2204
2757
|
try {
|
|
2205
|
-
entries =
|
|
2758
|
+
entries = fs11.readdirSync(runDir);
|
|
2206
2759
|
} catch {
|
|
2207
2760
|
return;
|
|
2208
2761
|
}
|
|
@@ -2210,26 +2763,19 @@ function resetRunArtifacts(runDir, logFd) {
|
|
|
2210
2763
|
if (RUN_INPUT_FILES.has(entry))
|
|
2211
2764
|
continue;
|
|
2212
2765
|
try {
|
|
2213
|
-
|
|
2766
|
+
fs11.rmSync(path13.join(runDir, entry), { recursive: true, force: true });
|
|
2214
2767
|
} catch (err) {
|
|
2215
2768
|
writeLog(logFd, `[resetRunArtifacts] failed to remove ${entry}: ${err instanceof Error ? err.message : String(err)}
|
|
2216
2769
|
`);
|
|
2217
2770
|
}
|
|
2218
2771
|
}
|
|
2219
2772
|
}
|
|
2220
|
-
async function runMode(
|
|
2221
|
-
const
|
|
2773
|
+
async function runMode(row, logFd) {
|
|
2774
|
+
const { id } = row;
|
|
2222
2775
|
const runDir = wsEvoRunDir(row.workspacePath, id);
|
|
2223
|
-
|
|
2776
|
+
fs11.mkdirSync(runDir, { recursive: true });
|
|
2224
2777
|
resetRunArtifacts(runDir, logFd);
|
|
2225
|
-
const hb = setInterval(() =>
|
|
2226
|
-
try {
|
|
2227
|
-
getEvoDb().update(evolutionRuns).set({ heartbeatAt: Date.now() }).where(eq(evolutionRuns.id, id)).run();
|
|
2228
|
-
} catch (err) {
|
|
2229
|
-
writeLog(logFd, `[wrapper] heartbeat failed: ${err instanceof Error ? err.message : String(err)}
|
|
2230
|
-
`);
|
|
2231
|
-
}
|
|
2232
|
-
}, 6e4);
|
|
2778
|
+
const hb = setInterval(() => heartbeat("run", id, row.attempts, logFd), 6e4);
|
|
2233
2779
|
const langHint = resolveLangHint();
|
|
2234
2780
|
writeLog(logFd, `[wrapper] resolved langHint=${langHint}
|
|
2235
2781
|
`);
|
|
@@ -2244,35 +2790,118 @@ async function runMode(id, logFd) {
|
|
|
2244
2790
|
};
|
|
2245
2791
|
try {
|
|
2246
2792
|
const outcome = await phaseDraft(ctx);
|
|
2793
|
+
markObservation(id, row.attempts, runDir, outcome, logFd);
|
|
2247
2794
|
if (outcome === "failed") {
|
|
2248
|
-
finalize(id, "failed", "phase A: draft incomplete (drafter exited non-zero, or patch.md / target file missing or unchanged) \u2014 see run log", logFd);
|
|
2795
|
+
finalize(id, row.attempts, "failed", "phase A: draft incomplete (drafter exited non-zero, or patch.md / target file missing or unchanged) \u2014 see run log", logFd);
|
|
2249
2796
|
return;
|
|
2250
2797
|
}
|
|
2251
2798
|
if (outcome === "skipped") {
|
|
2252
|
-
finalize(id, "skipped", null, logFd);
|
|
2799
|
+
finalize(id, row.attempts, "skipped", null, logFd);
|
|
2253
2800
|
return;
|
|
2254
2801
|
}
|
|
2255
2802
|
const dryRunOk = await phaseDryRun(ctx);
|
|
2256
2803
|
if (!dryRunOk) {
|
|
2257
|
-
finalize(id, "failed", "phase B: dry-run failed (fix budget exhausted)", logFd);
|
|
2804
|
+
finalize(id, row.attempts, "failed", "phase B: dry-run failed (fix budget exhausted)", logFd);
|
|
2258
2805
|
return;
|
|
2259
2806
|
}
|
|
2260
2807
|
const scored = await phaseScore(ctx);
|
|
2261
2808
|
if (!scored) {
|
|
2262
|
-
finalize(id, "failed", "phase C: scorer did not produce score.json", logFd);
|
|
2809
|
+
finalize(id, row.attempts, "failed", "phase C: scorer did not produce score.json", logFd);
|
|
2263
2810
|
return;
|
|
2264
2811
|
}
|
|
2265
|
-
|
|
2812
|
+
if (maybeAutoApply({ runId: id, workspacePath: row.workspacePath, runDir, attempts: row.attempts, sourceAccess: row.sourceAccess, logFd }))
|
|
2813
|
+
return;
|
|
2814
|
+
finalize(id, row.attempts, "awaiting_review", null, logFd);
|
|
2266
2815
|
} finally {
|
|
2267
2816
|
clearInterval(hb);
|
|
2268
2817
|
}
|
|
2269
2818
|
}
|
|
2270
|
-
function
|
|
2819
|
+
function markObservation(id, attempts, runDir, outcome, logFd) {
|
|
2820
|
+
const has = outcome !== "failed" && readObservation(path13.join(runDir, outcome === "skipped" ? ".skip.md" : "patch.md")) !== null;
|
|
2821
|
+
getEvoDb().update(evolutionRuns).set({ hasObservation: has }).where(ownsRun(id, attempts)).run();
|
|
2822
|
+
if (has)
|
|
2823
|
+
writeLog(logFd, `[wrapper] draft carries a Larger observation
|
|
2824
|
+
`);
|
|
2825
|
+
}
|
|
2826
|
+
function maybeAutoApply(args) {
|
|
2827
|
+
const level = config.evolution.level;
|
|
2828
|
+
if (evoLevelRank(level) < 2)
|
|
2829
|
+
return false;
|
|
2830
|
+
let record;
|
|
2831
|
+
try {
|
|
2832
|
+
record = evaluateSandbox({
|
|
2833
|
+
mainHalo: wsHaloDir(args.workspacePath),
|
|
2834
|
+
globalHalo: globalDir(),
|
|
2835
|
+
sandboxHalo: evoSandboxHaloDir(args.runDir),
|
|
2836
|
+
entries: PUBLISH_WHITELIST,
|
|
2837
|
+
level,
|
|
2838
|
+
threshold: config.evolution.autoApplyThreshold,
|
|
2839
|
+
sourceAccess: args.sourceAccess,
|
|
2840
|
+
score: readScore(path13.join(args.runDir, "score.json")),
|
|
2841
|
+
observation: readObservation(path13.join(args.runDir, "patch.md"))
|
|
2842
|
+
});
|
|
2843
|
+
fs11.writeFileSync(path13.join(args.runDir, AUTO_APPLY_FILE), JSON.stringify(record, null, 2), "utf-8");
|
|
2844
|
+
} catch (err) {
|
|
2845
|
+
writeLog(args.logFd, `[autoApply] evaluation failed, leaving the run for review: ${err instanceof Error ? err.message : String(err)}
|
|
2846
|
+
`);
|
|
2847
|
+
return false;
|
|
2848
|
+
}
|
|
2849
|
+
writeLog(args.logFd, `[autoApply] level=${level} category=${record.category} auto=${record.auto}${record.reason ? ` reason=${record.reason}` : ""} files=${record.files.map((f) => `${f.rel}:${f.category}`).join(",")}
|
|
2850
|
+
`);
|
|
2851
|
+
if (!record.auto)
|
|
2852
|
+
return false;
|
|
2853
|
+
const queued = queueApply(getEvoDb(), { mode: "auto", runId: args.runId, attempts: args.attempts });
|
|
2854
|
+
if (queued.ok) {
|
|
2855
|
+
writeLog(args.logFd, `[autoApply] queued ${queued.applyId} (approved_by=auto)
|
|
2856
|
+
`);
|
|
2857
|
+
return true;
|
|
2858
|
+
}
|
|
2859
|
+
writeLog(args.logFd, `[autoApply] not queued: ${queued.reason}
|
|
2860
|
+
`);
|
|
2861
|
+
return queued.reason === "state";
|
|
2862
|
+
}
|
|
2863
|
+
function finalize(id, attempts, status, failureReason, logFd) {
|
|
2271
2864
|
const now = Date.now();
|
|
2272
|
-
getEvoDb().update(evolutionRuns).set({ status, failureReason, completedAt: now }).where(
|
|
2865
|
+
const r = getEvoDb().update(evolutionRuns).set({ status, failureReason, completedAt: now }).where(ownsRun(id, attempts)).run();
|
|
2866
|
+
if (r.changes === 0) {
|
|
2867
|
+
writeLog(logFd, `[wrapper] finalize skipped: run is no longer this wrapper's claim (re-claimed or moved on) \u2014 wanted status=${status}
|
|
2868
|
+
`);
|
|
2869
|
+
return;
|
|
2870
|
+
}
|
|
2273
2871
|
writeLog(logFd, `[wrapper] finalize: status=${status}${failureReason ? ` reason=${failureReason}` : ""}
|
|
2274
2872
|
`);
|
|
2275
2873
|
}
|
|
2874
|
+
function ownsRun(id, attempts) {
|
|
2875
|
+
return and3(eq3(evolutionRuns.id, id), eq3(evolutionRuns.status, "running"), eq3(evolutionRuns.attempts, attempts));
|
|
2876
|
+
}
|
|
2877
|
+
function heartbeat(mode, id, attempts, logFd) {
|
|
2878
|
+
if (stopping)
|
|
2879
|
+
return;
|
|
2880
|
+
let owned;
|
|
2881
|
+
try {
|
|
2882
|
+
owned = mode === "run" ? getEvoDb().update(evolutionRuns).set({ heartbeatAt: Date.now() }).where(ownsRun(id, attempts)).run().changes > 0 : touchApply(id, attempts, ["running", "syncing"]);
|
|
2883
|
+
} catch (err) {
|
|
2884
|
+
writeLog(logFd, `[wrapper] heartbeat failed: ${err instanceof Error ? err.message : String(err)}
|
|
2885
|
+
`);
|
|
2886
|
+
return;
|
|
2887
|
+
}
|
|
2888
|
+
if (owned)
|
|
2889
|
+
return;
|
|
2890
|
+
stopping = true;
|
|
2891
|
+
writeLog(logFd, `[wrapper] heartbeat refused: ${mode} ${id} is no longer this wrapper's claim (attempt ${attempts}) \u2014 stopping ${liveProcs.size} sub-cli and exiting
|
|
2892
|
+
`);
|
|
2893
|
+
if (liveProcs.size === 0) {
|
|
2894
|
+
process.exit(1);
|
|
2895
|
+
return;
|
|
2896
|
+
}
|
|
2897
|
+
for (const kill of liveProcs)
|
|
2898
|
+
kill("SIGTERM");
|
|
2899
|
+
setTimeout(() => {
|
|
2900
|
+
for (const kill of liveProcs)
|
|
2901
|
+
kill("SIGKILL");
|
|
2902
|
+
process.exit(1);
|
|
2903
|
+
}, KILL_GRACE_MS);
|
|
2904
|
+
}
|
|
2276
2905
|
var APPLY_REGRESSION_FLOOR = 50;
|
|
2277
2906
|
function loadApplyRow(id) {
|
|
2278
2907
|
const db = getEvoDb();
|
|
@@ -2281,8 +2910,10 @@ function loadApplyRow(id) {
|
|
|
2281
2910
|
workspacePath: evolutionApplies.workspacePath,
|
|
2282
2911
|
sourceRunIds: evolutionApplies.sourceRunIds,
|
|
2283
2912
|
reviewerHint: evolutionApplies.reviewerHint,
|
|
2284
|
-
status: evolutionApplies.status
|
|
2285
|
-
|
|
2913
|
+
status: evolutionApplies.status,
|
|
2914
|
+
approvedBy: evolutionApplies.approvedBy,
|
|
2915
|
+
attempts: evolutionApplies.attempts
|
|
2916
|
+
}).from(evolutionApplies).where(eq3(evolutionApplies.id, id)).get();
|
|
2286
2917
|
if (!row)
|
|
2287
2918
|
throw new Error(`evolution_applies row not found: ${id}`);
|
|
2288
2919
|
let sourceRunIds;
|
|
@@ -2299,20 +2930,22 @@ function loadApplyRow(id) {
|
|
|
2299
2930
|
workspacePath: row.workspacePath,
|
|
2300
2931
|
sourceRunIds,
|
|
2301
2932
|
reviewerHint: row.reviewerHint,
|
|
2302
|
-
status: row.status
|
|
2933
|
+
status: row.status,
|
|
2934
|
+
approvedBy: row.approvedBy,
|
|
2935
|
+
attempts: row.attempts
|
|
2303
2936
|
};
|
|
2304
2937
|
}
|
|
2305
2938
|
function buildEvoSandbox(workspacePath, sandboxParent, logFd) {
|
|
2306
2939
|
const srcWs = wsHaloDir(workspacePath);
|
|
2307
2940
|
const dstWs = evoSandboxHaloDir(sandboxParent);
|
|
2308
|
-
|
|
2941
|
+
fs11.mkdirSync(dstWs, { recursive: true });
|
|
2309
2942
|
const whitelist = SANDBOX_WHITELIST;
|
|
2310
2943
|
for (const entry of whitelist) {
|
|
2311
|
-
const srcEntry =
|
|
2312
|
-
if (!
|
|
2944
|
+
const srcEntry = path13.join(srcWs, entry);
|
|
2945
|
+
if (!fs11.existsSync(srcEntry))
|
|
2313
2946
|
continue;
|
|
2314
|
-
const dstEntry =
|
|
2315
|
-
if (
|
|
2947
|
+
const dstEntry = path13.join(dstWs, entry);
|
|
2948
|
+
if (fs11.existsSync(dstEntry))
|
|
2316
2949
|
continue;
|
|
2317
2950
|
copyDereferenced(srcEntry, dstEntry);
|
|
2318
2951
|
}
|
|
@@ -2320,13 +2953,13 @@ function buildEvoSandbox(workspacePath, sandboxParent, logFd) {
|
|
|
2320
2953
|
`);
|
|
2321
2954
|
}
|
|
2322
2955
|
function copyDereferenced(src, dst) {
|
|
2323
|
-
if (
|
|
2324
|
-
|
|
2325
|
-
for (const entry of
|
|
2326
|
-
copyDereferenced(
|
|
2956
|
+
if (fs11.statSync(src).isDirectory()) {
|
|
2957
|
+
fs11.mkdirSync(dst, { recursive: true });
|
|
2958
|
+
for (const entry of fs11.readdirSync(src))
|
|
2959
|
+
copyDereferenced(path13.join(src, entry), path13.join(dst, entry));
|
|
2327
2960
|
return;
|
|
2328
2961
|
}
|
|
2329
|
-
|
|
2962
|
+
fs11.copyFileSync(src, dst);
|
|
2330
2963
|
}
|
|
2331
2964
|
function buildApplySandbox(workspacePath, applyDir, logFd) {
|
|
2332
2965
|
buildEvoSandbox(workspacePath, applyDir, logFd);
|
|
@@ -2354,11 +2987,11 @@ function buildApplyMergeBrief(args) {
|
|
|
2354
2987
|
"",
|
|
2355
2988
|
"Hard rules: only edit files under sandbox/.halo/ \u2014 main workspace",
|
|
2356
2989
|
"and ~/.halo/global/ are off-limits. Do NOT spawn halo cli. Do NOT",
|
|
2357
|
-
`write score.json. Write ${
|
|
2990
|
+
`write score.json. Write ${path13.join(args.applyDir, "apply.log")}`,
|
|
2358
2991
|
"with a summary of what you did.",
|
|
2359
2992
|
"",
|
|
2360
2993
|
"If the merge cannot proceed (irreconcilable patch conflicts, malformed",
|
|
2361
|
-
`input, missing sandbox), write ${
|
|
2994
|
+
`input, missing sandbox), write ${path13.join(args.applyDir, "ABORT.md")}`,
|
|
2362
2995
|
'with the diagnosis \u2014 the wrapper treats its presence as "abort this',
|
|
2363
2996
|
'apply" and fails the apply instead of publishing. You cannot signal',
|
|
2364
2997
|
"abort via exit code; ABORT.md is the only abort channel.",
|
|
@@ -2379,19 +3012,19 @@ function buildApplyScoreBrief(args) {
|
|
|
2379
3012
|
"The apply agent has merged this patch (and possibly others) into",
|
|
2380
3013
|
"an apply sandbox. The wrapper just ran the patch's testScenario",
|
|
2381
3014
|
"against the merged sandbox; stdout is in",
|
|
2382
|
-
`${
|
|
3015
|
+
`${path13.join(args.regressDir, "dry-run-output.txt")}.`,
|
|
2383
3016
|
"An empty dry-run-output.txt means the dry-run against the merged",
|
|
2384
3017
|
"sandbox failed \u2014 score lint 0 (that is the regression signal the",
|
|
2385
3018
|
`wrapper acts on); the failure detail is in`,
|
|
2386
|
-
`${
|
|
3019
|
+
`${path13.join(args.regressDir, "dry-run-fail.log")} when present.`,
|
|
2387
3020
|
"",
|
|
2388
3021
|
"Follow your AGENT.md procedure. Read:",
|
|
2389
3022
|
` patch.md \u2192 ${args.patchMdPath}`,
|
|
2390
|
-
` tool-flow.md \u2192 ${
|
|
3023
|
+
` tool-flow.md \u2192 ${path13.join(path13.dirname(args.patchMdPath), "tool-flow.md")} (baseline conversation)`,
|
|
2391
3024
|
` source-snapshot.json \u2192 ${args.snapshotPath}`,
|
|
2392
|
-
` dry-run output \u2192 ${
|
|
3025
|
+
` dry-run output \u2192 ${path13.join(args.regressDir, "dry-run-output.txt")}`,
|
|
2393
3026
|
"",
|
|
2394
|
-
`Write ${
|
|
3027
|
+
`Write ${path13.join(args.regressDir, "score.json")} \u2014 NOT the source`,
|
|
2395
3028
|
"run's score.json. Same format as run mode.",
|
|
2396
3029
|
"Do not modify any other file.",
|
|
2397
3030
|
...buildLanguageClause(args.langHint, 'the score.json "notes" field'),
|
|
@@ -2403,9 +3036,9 @@ async function phaseApplyMerge(ctx) {
|
|
|
2403
3036
|
=== Phase A': merge ===
|
|
2404
3037
|
`);
|
|
2405
3038
|
buildApplySandbox(ctx.workspacePath, ctx.applyDir, ctx.logFd);
|
|
2406
|
-
const abortPath =
|
|
3039
|
+
const abortPath = path13.join(ctx.applyDir, "ABORT.md");
|
|
2407
3040
|
try {
|
|
2408
|
-
|
|
3041
|
+
fs11.rmSync(abortPath, { force: true });
|
|
2409
3042
|
} catch {
|
|
2410
3043
|
}
|
|
2411
3044
|
const brief = buildApplyMergeBrief(ctx);
|
|
@@ -2424,10 +3057,10 @@ async function phaseApplyMerge(ctx) {
|
|
|
2424
3057
|
`);
|
|
2425
3058
|
return { ok: false, reason: `apply cli exited ${result.exitCode}` };
|
|
2426
3059
|
}
|
|
2427
|
-
if (
|
|
3060
|
+
if (fs11.existsSync(abortPath)) {
|
|
2428
3061
|
let abortText = "";
|
|
2429
3062
|
try {
|
|
2430
|
-
abortText =
|
|
3063
|
+
abortText = fs11.readFileSync(abortPath, "utf-8").trim();
|
|
2431
3064
|
} catch {
|
|
2432
3065
|
}
|
|
2433
3066
|
writeLog(ctx.logFd, `[phaseApplyMerge] ABORT.md present \u2014 agent aborted the merge:
|
|
@@ -2436,7 +3069,7 @@ ${abortText}
|
|
|
2436
3069
|
const firstLine = abortText.split("\n")[0]?.slice(0, 300) ?? "";
|
|
2437
3070
|
return { ok: false, reason: `apply agent aborted: ${firstLine || "see ABORT.md in apply dir"}` };
|
|
2438
3071
|
}
|
|
2439
|
-
const hasLog =
|
|
3072
|
+
const hasLog = fs11.existsSync(path13.join(ctx.applyDir, "apply.log"));
|
|
2440
3073
|
writeLog(ctx.logFd, `[phaseApplyMerge] apply.log=${hasLog}
|
|
2441
3074
|
`);
|
|
2442
3075
|
if (!hasLog)
|
|
@@ -2448,17 +3081,17 @@ async function phaseApplyRegress(ctx) {
|
|
|
2448
3081
|
=== Phase B': regress ===
|
|
2449
3082
|
`);
|
|
2450
3083
|
const sandboxDir = evoSandboxDir(ctx.applyDir);
|
|
2451
|
-
|
|
3084
|
+
fs11.mkdirSync(evoRegressDir(ctx.applyDir), { recursive: true });
|
|
2452
3085
|
const outcomes = [];
|
|
2453
3086
|
for (const runId of ctx.sourceRunIds) {
|
|
2454
3087
|
writeLog(ctx.logFd, `
|
|
2455
3088
|
--- regress for source run ${runId} ---
|
|
2456
3089
|
`);
|
|
2457
3090
|
const regressDir = evoRegressRunDir(ctx.applyDir, runId);
|
|
2458
|
-
|
|
3091
|
+
fs11.mkdirSync(regressDir, { recursive: true });
|
|
2459
3092
|
const runDir = wsEvoRunDir(ctx.workspacePath, runId);
|
|
2460
|
-
const patchMdPath =
|
|
2461
|
-
const snapshotPath =
|
|
3093
|
+
const patchMdPath = path13.join(runDir, "patch.md");
|
|
3094
|
+
const snapshotPath = path13.join(runDir, "source-snapshot.json");
|
|
2462
3095
|
const fm = readPatchFrontmatter(runDir);
|
|
2463
3096
|
const scenario = extractTestScenario(fm);
|
|
2464
3097
|
if (!scenario) {
|
|
@@ -2485,14 +3118,14 @@ async function phaseApplyRegress(ctx) {
|
|
|
2485
3118
|
// stdin, not argv
|
|
2486
3119
|
DRY_RUN_TIMEOUT_SEC
|
|
2487
3120
|
);
|
|
2488
|
-
const dryRunOut =
|
|
3121
|
+
const dryRunOut = path13.join(regressDir, "dry-run-output.txt");
|
|
2489
3122
|
if (dryRun.exitCode === 0 && dryRun.stdout.trim().length > 0) {
|
|
2490
|
-
|
|
3123
|
+
fs11.writeFileSync(dryRunOut, dryRun.stdout, "utf-8");
|
|
2491
3124
|
} else {
|
|
2492
3125
|
const tail = dryRun.stdout.length > 4e3 ? "\u2026" + dryRun.stdout.slice(-4e3) : dryRun.stdout;
|
|
2493
3126
|
const errTail = dryRun.stderr.length > 4e3 ? "\u2026" + dryRun.stderr.slice(-4e3) : dryRun.stderr;
|
|
2494
|
-
|
|
2495
|
-
|
|
3127
|
+
fs11.writeFileSync(dryRunOut, "", "utf-8");
|
|
3128
|
+
fs11.writeFileSync(path13.join(regressDir, "dry-run-fail.log"), [
|
|
2496
3129
|
`Wrapper command: halo cli -a ${scenario.agentId} -n -w ${sandboxDir} <testMessage>`,
|
|
2497
3130
|
`Test message: ${scenario.testMessage}`,
|
|
2498
3131
|
`Exit code: ${dryRun.exitCode}${dryRun.exitCode === 124 ? " (timeout)" : ""}`,
|
|
@@ -2513,6 +3146,7 @@ async function phaseApplyRegress(ctx) {
|
|
|
2513
3146
|
langHint: ctx.langHint
|
|
2514
3147
|
});
|
|
2515
3148
|
const scoreSandboxWs = evoSandboxDir(ctx.applyDir);
|
|
3149
|
+
fs11.rmSync(path13.join(regressDir, "score.json"), { force: true });
|
|
2516
3150
|
const scoreRun = await spawnProc(
|
|
2517
3151
|
resolveHaloCli(),
|
|
2518
3152
|
["cli", "-a", "__score__", "-n", "-w", scoreSandboxWs],
|
|
@@ -2526,10 +3160,10 @@ async function phaseApplyRegress(ctx) {
|
|
|
2526
3160
|
writeLog(ctx.logFd, `[phaseApplyRegress:${runId}] scorer cli exited ${scoreRun.exitCode}
|
|
2527
3161
|
`);
|
|
2528
3162
|
}
|
|
2529
|
-
const scorePath =
|
|
3163
|
+
const scorePath = path13.join(regressDir, "score.json");
|
|
2530
3164
|
let scoreJson = null;
|
|
2531
3165
|
try {
|
|
2532
|
-
scoreJson = JSON.parse(
|
|
3166
|
+
scoreJson = JSON.parse(fs11.readFileSync(scorePath, "utf-8"));
|
|
2533
3167
|
} catch {
|
|
2534
3168
|
}
|
|
2535
3169
|
if (!scoreJson) {
|
|
@@ -2567,31 +3201,36 @@ async function phaseApplyRegress(ctx) {
|
|
|
2567
3201
|
`);
|
|
2568
3202
|
return { allOk, outcomes };
|
|
2569
3203
|
}
|
|
2570
|
-
function finalizeApply(applyId,
|
|
2571
|
-
const
|
|
2572
|
-
|
|
2573
|
-
|
|
3204
|
+
function finalizeApply(applyId, attempts, failureReason, logFd) {
|
|
3205
|
+
const { failed, revertedRunIds } = failApply(getEvoDb(), applyId, { status: "failed", failureReason, onlyIfStatus: "running", onlyIfAttempts: attempts });
|
|
3206
|
+
if (!failed) {
|
|
3207
|
+
writeLog(logFd, `[wrapper] finalize apply skipped: row is no longer running at attempt ${attempts} (re-claimed, or syncing \u2014 left to the ticker) \u2014 wanted failed: ${failureReason}
|
|
3208
|
+
`);
|
|
3209
|
+
return;
|
|
3210
|
+
}
|
|
3211
|
+
writeLog(logFd, `[wrapper] finalize apply: status=failed reason=${failureReason}${revertedRunIds.length ? ` (auto: ${revertedRunIds.join(", ")} back to awaiting_review)` : ""}
|
|
2574
3212
|
`);
|
|
2575
3213
|
}
|
|
3214
|
+
function autoApplyRecheck(workspacePath, applyDir, changed) {
|
|
3215
|
+
const cls = classifyChanges(publishChanges({
|
|
3216
|
+
mainHalo: wsHaloDir(workspacePath),
|
|
3217
|
+
globalHalo: globalDir(),
|
|
3218
|
+
sandboxHalo: evoSandboxHaloDir(applyDir)
|
|
3219
|
+
}, changed));
|
|
3220
|
+
return categoryGate(config.evolution.level, cls);
|
|
3221
|
+
}
|
|
2576
3222
|
async function applyMode(id, logFd) {
|
|
2577
3223
|
const row = loadApplyRow(id);
|
|
2578
3224
|
const applyDir = wsEvoApplyDir(row.workspacePath, id);
|
|
2579
|
-
|
|
2580
|
-
|
|
3225
|
+
fs11.mkdirSync(applyDir, { recursive: true });
|
|
3226
|
+
fs11.writeFileSync(path13.join(applyDir, "meta.json"), JSON.stringify({
|
|
2581
3227
|
id,
|
|
2582
3228
|
sourceRunIds: row.sourceRunIds,
|
|
2583
3229
|
reviewerHint: row.reviewerHint,
|
|
2584
3230
|
workspacePath: row.workspacePath,
|
|
2585
3231
|
createdAt: Date.now()
|
|
2586
3232
|
}, null, 2), "utf-8");
|
|
2587
|
-
const hb = setInterval(() =>
|
|
2588
|
-
try {
|
|
2589
|
-
getEvoDb().update(evolutionApplies).set({ heartbeatAt: Date.now() }).where(eq(evolutionApplies.id, id)).run();
|
|
2590
|
-
} catch (err) {
|
|
2591
|
-
writeLog(logFd, `[wrapper] heartbeat (apply) failed: ${err instanceof Error ? err.message : String(err)}
|
|
2592
|
-
`);
|
|
2593
|
-
}
|
|
2594
|
-
}, 6e4);
|
|
3233
|
+
const hb = setInterval(() => heartbeat("apply", id, row.attempts, logFd), 6e4);
|
|
2595
3234
|
const ctx = {
|
|
2596
3235
|
applyId: id,
|
|
2597
3236
|
workspacePath: row.workspacePath,
|
|
@@ -2610,75 +3249,93 @@ async function applyMode(id, logFd) {
|
|
|
2610
3249
|
if (!isResume) {
|
|
2611
3250
|
const merged = await phaseApplyMerge(ctx);
|
|
2612
3251
|
if (!merged.ok) {
|
|
2613
|
-
finalizeApply(id,
|
|
3252
|
+
finalizeApply(id, row.attempts, `phase A': ${merged.reason}`, logFd);
|
|
2614
3253
|
return;
|
|
2615
3254
|
}
|
|
2616
3255
|
const { allOk, outcomes } = await phaseApplyRegress(ctx);
|
|
2617
3256
|
if (!allOk) {
|
|
2618
3257
|
const regressedRuns = outcomes.filter((o) => o.regressed).map((o) => `${o.runId}(${o.reason ?? "?"})`).join("; ");
|
|
2619
|
-
finalizeApply(id,
|
|
3258
|
+
finalizeApply(id, row.attempts, `phase B': regression in ${regressedRuns}`, logFd);
|
|
2620
3259
|
return;
|
|
2621
3260
|
}
|
|
2622
3261
|
}
|
|
2623
3262
|
try {
|
|
2624
|
-
|
|
2625
|
-
if (!preflight.ok) {
|
|
2626
|
-
finalizeApply(id, "failed", `phase 12 preflight: ${preflight.reason}`, logFd);
|
|
2627
|
-
return;
|
|
2628
|
-
}
|
|
2629
|
-
if (!isResume)
|
|
2630
|
-
markSyncing(id, logFd);
|
|
2631
|
-
await phaseApplyPublish(ctx, preflight.result);
|
|
2632
|
-
for (const runId of ctx.sourceRunIds) {
|
|
2633
|
-
getEvoDb().update(evolutionRuns).set({ status: "applied", appliedAt: Date.now() }).where(eq(evolutionRuns.id, runId)).run();
|
|
2634
|
-
}
|
|
2635
|
-
finalizeApplied(id, logFd);
|
|
2636
|
-
writeLog(logFd, `[wrapper] phase 12: published ${preflight.result.fileCount} files to main; history snapshot at ${preflight.result.historyDir}
|
|
2637
|
-
`);
|
|
2638
|
-
writeLog(logFd, `[wrapper] note: no explicit session-release step needed. Halo's SessionManager evicts every session from the in-memory cache as soon as its current message turn finishes (runSession's finally block). The next message \u2014 channel reply, web reload, /new \u2014 runs ensureSession \u2192 buildAgentInstance, which re-reads agent.yaml + INSTRUCTIONS.md + USER.md from disk. So the new prompts kick in on the next turn for free, and currently-running turns finish on the old prompts (which is the right behavior \u2014 we don't want to abort a user mid-conversation).
|
|
2639
|
-
`);
|
|
3263
|
+
await phase12Sync(ctx, row, isResume);
|
|
2640
3264
|
} catch (err) {
|
|
2641
|
-
const currentStatus = getEvoDb().select({ status: evolutionApplies.status }).from(evolutionApplies).where(
|
|
3265
|
+
const currentStatus = getEvoDb().select({ status: evolutionApplies.status }).from(evolutionApplies).where(eq3(evolutionApplies.id, id)).get()?.status;
|
|
2642
3266
|
if (currentStatus === "syncing") {
|
|
2643
|
-
|
|
2644
|
-
`);
|
|
2645
|
-
getEvoDb().update(evolutionApplies).set({ startedAt: null, heartbeatAt: null }).where(and(eq(evolutionApplies.id, id), eq(evolutionApplies.status, "syncing"))).run();
|
|
3267
|
+
releaseSyncing(id, row.attempts, `phase 12 publish: ${err instanceof Error ? err.message : String(err)}`, logFd);
|
|
2646
3268
|
} else {
|
|
2647
|
-
finalizeApply(id,
|
|
3269
|
+
finalizeApply(id, row.attempts, `phase 12: ${err instanceof Error ? err.message : String(err)}`, logFd);
|
|
2648
3270
|
}
|
|
2649
3271
|
}
|
|
2650
3272
|
} finally {
|
|
2651
3273
|
clearInterval(hb);
|
|
2652
3274
|
}
|
|
2653
3275
|
}
|
|
2654
|
-
async function
|
|
3276
|
+
async function phase12Sync(ctx, row, isResume) {
|
|
3277
|
+
const { applyId: id, logFd } = ctx;
|
|
3278
|
+
if (!isResume && !touchApply(id, row.attempts, ["running"])) {
|
|
3279
|
+
writeLog(logFd, `[wrapper] phase 12: apply is no longer this wrapper's claim (attempt ${row.attempts}) \u2014 not writing history
|
|
3280
|
+
`);
|
|
3281
|
+
return;
|
|
3282
|
+
}
|
|
3283
|
+
const preflight = await phaseApplyPreflight(ctx, isResume);
|
|
3284
|
+
if (!preflight.ok) {
|
|
3285
|
+
const reason = `phase 12 preflight: ${preflight.reason}`;
|
|
3286
|
+
if (isResume)
|
|
3287
|
+
releaseSyncing(id, row.attempts, reason, logFd);
|
|
3288
|
+
else
|
|
3289
|
+
finalizeApply(id, row.attempts, reason, logFd);
|
|
3290
|
+
return;
|
|
3291
|
+
}
|
|
3292
|
+
if (!isResume && row.approvedBy === "auto") {
|
|
3293
|
+
const blocked = autoApplyRecheck(ctx.workspacePath, ctx.applyDir, preflight.result.changed);
|
|
3294
|
+
if (blocked) {
|
|
3295
|
+
finalizeApply(id, row.attempts, `auto_recheck:${blocked}`, logFd);
|
|
3296
|
+
return;
|
|
3297
|
+
}
|
|
3298
|
+
writeLog(logFd, `[wrapper] auto-apply recheck passed
|
|
3299
|
+
`);
|
|
3300
|
+
}
|
|
3301
|
+
if (!markSyncing(id, isResume ? "syncing" : "running", row.attempts, logFd))
|
|
3302
|
+
return;
|
|
3303
|
+
await phaseApplyPublish(ctx, preflight.result);
|
|
3304
|
+
if (!finalizeApplied(id, row.attempts, ctx.sourceRunIds, logFd))
|
|
3305
|
+
return;
|
|
3306
|
+
writeLog(logFd, `[wrapper] phase 12: published ${preflight.result.fileCount} files to main; history snapshot at ${preflight.result.historyDir}
|
|
3307
|
+
`);
|
|
3308
|
+
writeLog(logFd, `[wrapper] note: no explicit session-release step needed. Halo's SessionManager evicts every session from the in-memory cache as soon as its current message turn finishes (runSession's finally block). The next message \u2014 channel reply, web reload, /new \u2014 runs ensureSession \u2192 buildAgentInstance, which re-reads agent.yaml + INSTRUCTIONS.md + USER.md from disk. So the new prompts kick in on the next turn for free, and currently-running turns finish on the old prompts (which is the right behavior \u2014 we don't want to abort a user mid-conversation).
|
|
3309
|
+
`);
|
|
3310
|
+
}
|
|
3311
|
+
async function phaseApplyPreflight(ctx, isResume) {
|
|
2655
3312
|
writeLog(ctx.logFd, `
|
|
2656
3313
|
=== Phase 12 preflight: diff + history backup ===
|
|
2657
3314
|
`);
|
|
2658
3315
|
const sandboxHalo = evoSandboxHaloDir(ctx.applyDir);
|
|
2659
3316
|
const mainHalo = wsHaloDir(ctx.workspacePath);
|
|
2660
|
-
if (!
|
|
3317
|
+
if (!fs11.existsSync(sandboxHalo))
|
|
2661
3318
|
return { ok: false, reason: "sandbox missing \u2014 phase 11 must run first" };
|
|
2662
3319
|
const whitelistTop = new Set(PUBLISH_WHITELIST);
|
|
2663
3320
|
const filesToConsider = [];
|
|
2664
|
-
for (const top of
|
|
3321
|
+
for (const top of fs11.readdirSync(sandboxHalo)) {
|
|
2665
3322
|
if (!whitelistTop.has(top))
|
|
2666
3323
|
continue;
|
|
2667
|
-
walkInto(
|
|
3324
|
+
walkInto(path13.join(sandboxHalo, top), top, filesToConsider);
|
|
2668
3325
|
}
|
|
2669
3326
|
const historyDir = wsEvoHistoryDir(ctx.workspacePath, ctx.applyId);
|
|
2670
3327
|
const changed = [];
|
|
2671
3328
|
for (const { rel, full } of filesToConsider) {
|
|
2672
|
-
const mainPath =
|
|
3329
|
+
const mainPath = path13.join(mainHalo, rel);
|
|
2673
3330
|
let existsInMain = false;
|
|
2674
3331
|
try {
|
|
2675
|
-
|
|
3332
|
+
fs11.accessSync(mainPath);
|
|
2676
3333
|
existsInMain = true;
|
|
2677
3334
|
} catch {
|
|
2678
3335
|
}
|
|
2679
3336
|
if (existsInMain) {
|
|
2680
|
-
const sandboxBuf =
|
|
2681
|
-
const mainBuf =
|
|
3337
|
+
const sandboxBuf = fs11.readFileSync(full);
|
|
3338
|
+
const mainBuf = fs11.readFileSync(mainPath);
|
|
2682
3339
|
if (sandboxBuf.equals(mainBuf))
|
|
2683
3340
|
continue;
|
|
2684
3341
|
}
|
|
@@ -2686,29 +3343,39 @@ async function phaseApplyPreflight(ctx) {
|
|
|
2686
3343
|
}
|
|
2687
3344
|
writeLog(ctx.logFd, `[preflight] ${changed.length} of ${filesToConsider.length} whitelisted files differ from main
|
|
2688
3345
|
`);
|
|
3346
|
+
if (!isResume)
|
|
3347
|
+
fs11.rmSync(historyDir, { recursive: true, force: true });
|
|
2689
3348
|
if (changed.length === 0) {
|
|
2690
3349
|
return { ok: true, result: { changed, historyDir, fileCount: 0 } };
|
|
2691
3350
|
}
|
|
2692
|
-
|
|
2693
|
-
const manifestPath =
|
|
2694
|
-
|
|
2695
|
-
if (isFirstPreflight) {
|
|
3351
|
+
fs11.mkdirSync(historyDir, { recursive: true });
|
|
3352
|
+
const manifestPath = path13.join(historyDir, "MANIFEST.json");
|
|
3353
|
+
if (!isResume) {
|
|
2696
3354
|
for (const { rel, existsInMain } of changed) {
|
|
2697
3355
|
if (!existsInMain)
|
|
2698
3356
|
continue;
|
|
2699
|
-
const mainPath =
|
|
2700
|
-
const histPath =
|
|
2701
|
-
|
|
2702
|
-
|
|
3357
|
+
const mainPath = path13.join(mainHalo, rel);
|
|
3358
|
+
const histPath = path13.join(historyDir, rel);
|
|
3359
|
+
fs11.mkdirSync(path13.dirname(histPath), { recursive: true });
|
|
3360
|
+
fs11.copyFileSync(mainPath, histPath);
|
|
2703
3361
|
}
|
|
2704
|
-
|
|
3362
|
+
const effective = new Set(publishChanges({ mainHalo, globalHalo: globalDir(), sandboxHalo }, changed).map((c) => c.rel));
|
|
3363
|
+
const manifest = {
|
|
2705
3364
|
applyId: ctx.applyId,
|
|
2706
3365
|
sourceRunIds: ctx.sourceRunIds,
|
|
2707
3366
|
capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2708
|
-
files: changed.map((c) => ({
|
|
2709
|
-
|
|
3367
|
+
files: changed.map((c) => ({
|
|
3368
|
+
rel: c.rel,
|
|
3369
|
+
kind: c.existsInMain ? "overwrite" : "new",
|
|
3370
|
+
before: c.existsInMain ? sha256File(path13.join(mainHalo, c.rel)) : null,
|
|
3371
|
+
after: sha256File(c.full),
|
|
3372
|
+
...effective.has(c.rel.split(path13.sep).join("/")) ? {} : { unchanged: true }
|
|
3373
|
+
})),
|
|
3374
|
+
createdDirs: dirsCreatedBy(mainHalo, changed.filter((c) => !c.existsInMain).map((c) => c.rel))
|
|
3375
|
+
};
|
|
3376
|
+
fs11.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
|
|
2710
3377
|
} else {
|
|
2711
|
-
writeLog(ctx.logFd, `[preflight]
|
|
3378
|
+
writeLog(ctx.logFd, `[preflight] resume path, skipping history backup (the pre-apply state is already preserved from the fresh claim's preflight)
|
|
2712
3379
|
`);
|
|
2713
3380
|
}
|
|
2714
3381
|
writeLog(ctx.logFd, `[preflight] history snapshot: ${historyDir} (${changed.filter((c) => c.existsInMain).length} backed up, ${changed.filter((c) => !c.existsInMain).length} new)
|
|
@@ -2721,59 +3388,96 @@ async function phaseApplyPublish(ctx, preflight) {
|
|
|
2721
3388
|
`);
|
|
2722
3389
|
const mainHalo = wsHaloDir(ctx.workspacePath);
|
|
2723
3390
|
for (const { rel, full } of preflight.changed) {
|
|
2724
|
-
const mainPath =
|
|
2725
|
-
|
|
2726
|
-
|
|
3391
|
+
const mainPath = path13.join(mainHalo, rel);
|
|
3392
|
+
fs11.mkdirSync(path13.dirname(mainPath), { recursive: true });
|
|
3393
|
+
fs11.copyFileSync(full, mainPath);
|
|
2727
3394
|
}
|
|
2728
3395
|
writeLog(ctx.logFd, `[publish] copied ${preflight.changed.length} files to ${mainHalo}
|
|
2729
3396
|
`);
|
|
2730
3397
|
}
|
|
2731
|
-
function
|
|
2732
|
-
|
|
3398
|
+
function dirsCreatedBy(mainHalo, newFiles) {
|
|
3399
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
3400
|
+
for (const rel of newFiles) {
|
|
3401
|
+
for (let d = path13.dirname(rel); d !== "." && !fs11.existsSync(path13.join(mainHalo, d)); d = path13.dirname(d))
|
|
3402
|
+
dirs.add(d);
|
|
3403
|
+
}
|
|
3404
|
+
return [...dirs].sort((a, b) => b.split(path13.sep).length - a.split(path13.sep).length || (a < b ? -1 : 1));
|
|
3405
|
+
}
|
|
3406
|
+
function ownsApply(id, status, attempts) {
|
|
3407
|
+
return and3(eq3(evolutionApplies.id, id), eq3(evolutionApplies.status, status), eq3(evolutionApplies.attempts, attempts));
|
|
3408
|
+
}
|
|
3409
|
+
function touchApply(id, attempts, statuses) {
|
|
3410
|
+
return getEvoDb().update(evolutionApplies).set({ heartbeatAt: Date.now() }).where(and3(eq3(evolutionApplies.id, id), inArray2(evolutionApplies.status, statuses), eq3(evolutionApplies.attempts, attempts))).run().changes > 0;
|
|
3411
|
+
}
|
|
3412
|
+
function markSyncing(applyId, from, attempts, logFd) {
|
|
3413
|
+
const r = getEvoDb().update(evolutionApplies).set(from === "running" ? { status: "syncing", heartbeatAt: Date.now() } : { heartbeatAt: Date.now() }).where(ownsApply(applyId, from, attempts)).run();
|
|
3414
|
+
if (r.changes === 0) {
|
|
3415
|
+
writeLog(logFd, `[wrapper] checkpoint refused: apply is no longer ${from} at attempt ${attempts} (re-claimed) \u2014 not publishing
|
|
3416
|
+
`);
|
|
3417
|
+
return false;
|
|
3418
|
+
}
|
|
2733
3419
|
writeLog(logFd, `[wrapper] checkpoint: status=syncing (publishing main)
|
|
3420
|
+
`);
|
|
3421
|
+
return true;
|
|
3422
|
+
}
|
|
3423
|
+
function releaseSyncing(applyId, attempts, reason, logFd) {
|
|
3424
|
+
const r = getEvoDb().update(evolutionApplies).set({ startedAt: null, heartbeatAt: null, failureReason: reason }).where(ownsApply(applyId, "syncing", attempts)).run();
|
|
3425
|
+
writeLog(logFd, r.changes === 0 ? `[wrapper] ${reason}; row is no longer this wrapper's syncing claim \u2014 left alone
|
|
3426
|
+
` : `[wrapper] ${reason}; leaving status='syncing' for the ticker to resume \u2014 main may be half-applied
|
|
2734
3427
|
`);
|
|
2735
3428
|
}
|
|
2736
3429
|
function walkInto(absPath, relPath, out) {
|
|
2737
|
-
const stat =
|
|
3430
|
+
const stat = fs11.statSync(absPath);
|
|
2738
3431
|
if (stat.isFile()) {
|
|
2739
3432
|
out.push({ rel: relPath, full: absPath });
|
|
2740
3433
|
return;
|
|
2741
3434
|
}
|
|
2742
3435
|
if (!stat.isDirectory())
|
|
2743
3436
|
return;
|
|
2744
|
-
for (const entry of
|
|
2745
|
-
walkInto(
|
|
3437
|
+
for (const entry of fs11.readdirSync(absPath)) {
|
|
3438
|
+
walkInto(path13.join(absPath, entry), path13.join(relPath, entry), out);
|
|
2746
3439
|
}
|
|
2747
3440
|
}
|
|
2748
|
-
function finalizeApplied(applyId, logFd) {
|
|
3441
|
+
function finalizeApplied(applyId, attempts, sourceRunIds, logFd) {
|
|
2749
3442
|
const now = Date.now();
|
|
2750
|
-
getEvoDb().
|
|
2751
|
-
|
|
3443
|
+
const ok = getEvoDb().transaction((tx) => {
|
|
3444
|
+
const r = tx.update(evolutionApplies).set({ status: "applied", completedAt: now, failureReason: null }).where(ownsApply(applyId, "syncing", attempts)).run();
|
|
3445
|
+
if (r.changes === 0)
|
|
3446
|
+
return false;
|
|
3447
|
+
tx.update(evolutionRuns).set({ status: "applied", appliedAt: now }).where(and3(inArray2(evolutionRuns.id, sourceRunIds), eq3(evolutionRuns.status, "approved"))).run();
|
|
3448
|
+
return true;
|
|
3449
|
+
});
|
|
3450
|
+
writeLog(logFd, ok ? `[wrapper] finalize apply: status=applied
|
|
3451
|
+
` : `[wrapper] finalize apply skipped: row is no longer syncing at attempt ${attempts} (re-claimed)
|
|
2752
3452
|
`);
|
|
3453
|
+
return ok;
|
|
2753
3454
|
}
|
|
2754
3455
|
function writeLog(fd, msg) {
|
|
2755
3456
|
try {
|
|
2756
|
-
|
|
3457
|
+
fs11.writeSync(fd, msg);
|
|
2757
3458
|
} catch {
|
|
2758
3459
|
}
|
|
2759
3460
|
}
|
|
2760
3461
|
function openLogFor(mode, id) {
|
|
2761
|
-
|
|
3462
|
+
fs11.mkdirSync(evoLogsDir(), { recursive: true });
|
|
2762
3463
|
const logPath = mode === "run" ? evoWrapperLogFile(id) : evoApplyLogFile(id);
|
|
2763
|
-
const fd =
|
|
3464
|
+
const fd = fs11.openSync(logPath, "a");
|
|
2764
3465
|
return { fd, logPath };
|
|
2765
3466
|
}
|
|
2766
3467
|
async function main() {
|
|
2767
3468
|
const { mode, id } = parseCli(process.argv.slice(2));
|
|
2768
|
-
setEvoDb(createEvoDb(
|
|
3469
|
+
setEvoDb(createEvoDb(path13.join(homedir6(), ".halo", "global")));
|
|
2769
3470
|
const { fd, logPath } = openLogFor(mode, id);
|
|
2770
3471
|
writeLog(fd, `
|
|
2771
3472
|
=== ${(/* @__PURE__ */ new Date()).toISOString()} wrapper start mode=${mode} id=${id} pid=${process.pid} ===
|
|
2772
3473
|
`);
|
|
3474
|
+
let runAttempts = null;
|
|
2773
3475
|
try {
|
|
2774
|
-
if (mode === "run")
|
|
2775
|
-
|
|
2776
|
-
|
|
3476
|
+
if (mode === "run") {
|
|
3477
|
+
const row = loadRunRow(id);
|
|
3478
|
+
runAttempts = row.attempts;
|
|
3479
|
+
await runMode(row, fd);
|
|
3480
|
+
} else
|
|
2777
3481
|
await applyMode(id, fd);
|
|
2778
3482
|
writeLog(fd, `[wrapper] done; log: ${logPath}
|
|
2779
3483
|
`);
|
|
@@ -2783,16 +3487,14 @@ async function main() {
|
|
|
2783
3487
|
writeLog(fd, `[wrapper] fatal: ${msg}
|
|
2784
3488
|
`);
|
|
2785
3489
|
try {
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
getEvoDb().update(evolutionRuns).set({ status: "failed", failureReason: `wrapper crash: ${msg}`, completedAt: now }).where(eq(evolutionRuns.id, id)).run();
|
|
2789
|
-
}
|
|
3490
|
+
if (mode === "run" && runAttempts !== null)
|
|
3491
|
+
finalize(id, runAttempts, "failed", `wrapper crash: ${msg}`, fd);
|
|
2790
3492
|
} catch {
|
|
2791
3493
|
}
|
|
2792
3494
|
process.exit(1);
|
|
2793
3495
|
}
|
|
2794
3496
|
}
|
|
2795
|
-
if (process.argv[1] &&
|
|
3497
|
+
if (process.argv[1] && path13.resolve(process.argv[1]) === fileURLToPath2(import.meta.url)) {
|
|
2796
3498
|
main().catch((err) => {
|
|
2797
3499
|
process.stderr.write(`[wrapper] startup error: ${err instanceof Error ? err.message : String(err)}
|
|
2798
3500
|
`);
|
|
@@ -2802,11 +3504,21 @@ if (process.argv[1] && path12.resolve(process.argv[1]) === fileURLToPath2(import
|
|
|
2802
3504
|
export {
|
|
2803
3505
|
PUBLISH_WHITELIST,
|
|
2804
3506
|
SANDBOX_WHITELIST,
|
|
3507
|
+
autoApplyRecheck,
|
|
2805
3508
|
buildEvoSandbox,
|
|
2806
3509
|
extractTestScenario,
|
|
3510
|
+
finalize,
|
|
3511
|
+
finalizeApplied,
|
|
3512
|
+
finalizeApply,
|
|
3513
|
+
heartbeat,
|
|
2807
3514
|
judgeDraft,
|
|
3515
|
+
markObservation,
|
|
3516
|
+
markSyncing,
|
|
3517
|
+
maybeAutoApply,
|
|
3518
|
+
phase12Sync,
|
|
2808
3519
|
phaseApplyPreflight,
|
|
2809
3520
|
phaseApplyPublish,
|
|
2810
3521
|
readPatchFrontmatter,
|
|
3522
|
+
resetStoppingForTests,
|
|
2811
3523
|
spawnProc
|
|
2812
3524
|
};
|