@osfactory/har 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/index.js +1817 -651
- package/dist/prompts/system-authoring.md +27 -2
- package/dist/templates/adaptation-prompt-init.md +60 -5
- package/dist/templates/adaptation-prompt-maintain.md +7 -4
- package/dist/templates/har-boilerplate/CLAUDE.agent.md +5 -3
- package/dist/templates/har-boilerplate/README.md +51 -9
- package/dist/templates/har-boilerplate/agent-cli.sh +72 -26
- package/dist/templates/har-boilerplate/agent-slot.sh +351 -0
- package/dist/templates/har-boilerplate/attach.sh +7 -1
- package/dist/templates/har-boilerplate/ecosystem.agent.template.cjs +3 -1
- package/dist/templates/har-boilerplate/env.template +4 -1
- package/dist/templates/har-boilerplate/harness.env +32 -0
- package/dist/templates/har-boilerplate/launch.sh +167 -120
- package/dist/templates/har-boilerplate/preflight.sh +41 -0
- package/dist/templates/har-boilerplate/provision-toolchain.sh +285 -0
- package/dist/templates/har-boilerplate/setup-infra.sh +87 -11
- package/dist/templates/har-boilerplate/teardown.sh +2 -1
- package/dist/templates/har-boilerplate/verify.sh +91 -11
- package/dist/templates/har-boilerplate-cli/CLAUDE.agent.md +5 -2
- package/dist/templates/har-boilerplate-cli/README.md +40 -6
- package/dist/templates/har-boilerplate-cli/agent-cli.sh +1 -1
- package/dist/templates/har-boilerplate-cli/agent-slot.sh +331 -0
- package/dist/templates/har-boilerplate-cli/harness.env +13 -0
- package/dist/templates/har-boilerplate-cli/launch.sh +140 -95
- package/dist/templates/har-boilerplate-cli/preflight.sh +37 -0
- package/dist/templates/har-boilerplate-cli/provision-toolchain.sh +285 -0
- package/dist/templates/har-boilerplate-cli/setup-infra.sh +87 -11
- package/dist/templates/har-boilerplate-cli/verify.sh +93 -12
- package/dist/templates/har-boilerplate-ios/README.md +29 -2
- package/dist/templates/har-boilerplate-ios/harness.env +6 -1
- package/dist/templates/har-boilerplate-ios/launch.sh +8 -13
- package/dist/templates/har-boilerplate-ios/provision-toolchain.sh +285 -0
- package/dist/templates/har-boilerplate-ios/verify.sh +20 -20
- package/package.json +1 -1
- package/dist/templates/har-boilerplate-ios/ADAPT-PROMPT.md +0 -57
package/dist/index.js
CHANGED
|
@@ -765,14 +765,14 @@ var require_url_state_machine = __commonJS({
|
|
|
765
765
|
return url.replace(/\u0009|\u000A|\u000D/g, "");
|
|
766
766
|
}
|
|
767
767
|
function shortenPath(url) {
|
|
768
|
-
const
|
|
769
|
-
if (
|
|
768
|
+
const path37 = url.path;
|
|
769
|
+
if (path37.length === 0) {
|
|
770
770
|
return;
|
|
771
771
|
}
|
|
772
|
-
if (url.scheme === "file" &&
|
|
772
|
+
if (url.scheme === "file" && path37.length === 1 && isNormalizedWindowsDriveLetter(path37[0])) {
|
|
773
773
|
return;
|
|
774
774
|
}
|
|
775
|
-
|
|
775
|
+
path37.pop();
|
|
776
776
|
}
|
|
777
777
|
function includesCredentials(url) {
|
|
778
778
|
return url.username !== "" || url.password !== "";
|
|
@@ -2092,7 +2092,7 @@ var require_lib2 = __commonJS({
|
|
|
2092
2092
|
let accum = [];
|
|
2093
2093
|
let accumBytes = 0;
|
|
2094
2094
|
let abort = false;
|
|
2095
|
-
return new Body.Promise(function(
|
|
2095
|
+
return new Body.Promise(function(resolve26, reject) {
|
|
2096
2096
|
let resTimeout;
|
|
2097
2097
|
if (_this4.timeout) {
|
|
2098
2098
|
resTimeout = setTimeout(function() {
|
|
@@ -2126,7 +2126,7 @@ var require_lib2 = __commonJS({
|
|
|
2126
2126
|
}
|
|
2127
2127
|
clearTimeout(resTimeout);
|
|
2128
2128
|
try {
|
|
2129
|
-
|
|
2129
|
+
resolve26(Buffer.concat(accum, accumBytes));
|
|
2130
2130
|
} catch (err) {
|
|
2131
2131
|
reject(new FetchError(`Could not create Buffer from response body for ${_this4.url}: ${err.message}`, "system", err));
|
|
2132
2132
|
}
|
|
@@ -2801,7 +2801,7 @@ var require_lib2 = __commonJS({
|
|
|
2801
2801
|
throw new Error("native promise missing, set fetch.Promise to your favorite alternative");
|
|
2802
2802
|
}
|
|
2803
2803
|
Body.Promise = fetch3.Promise;
|
|
2804
|
-
return new fetch3.Promise(function(
|
|
2804
|
+
return new fetch3.Promise(function(resolve26, reject) {
|
|
2805
2805
|
const request = new Request3(url, opts);
|
|
2806
2806
|
const options = getNodeRequestOptions(request);
|
|
2807
2807
|
const send = (options.protocol === "https:" ? https : http).request;
|
|
@@ -2934,7 +2934,7 @@ var require_lib2 = __commonJS({
|
|
|
2934
2934
|
requestOpts.body = void 0;
|
|
2935
2935
|
requestOpts.headers.delete("content-length");
|
|
2936
2936
|
}
|
|
2937
|
-
|
|
2937
|
+
resolve26(fetch3(new Request3(locationURL, requestOpts)));
|
|
2938
2938
|
finalize();
|
|
2939
2939
|
return;
|
|
2940
2940
|
}
|
|
@@ -2955,7 +2955,7 @@ var require_lib2 = __commonJS({
|
|
|
2955
2955
|
const codings = headers.get("Content-Encoding");
|
|
2956
2956
|
if (!request.compress || request.method === "HEAD" || codings === null || res.statusCode === 204 || res.statusCode === 304) {
|
|
2957
2957
|
response = new Response3(body, response_options);
|
|
2958
|
-
|
|
2958
|
+
resolve26(response);
|
|
2959
2959
|
return;
|
|
2960
2960
|
}
|
|
2961
2961
|
const zlibOptions = {
|
|
@@ -2965,7 +2965,7 @@ var require_lib2 = __commonJS({
|
|
|
2965
2965
|
if (codings == "gzip" || codings == "x-gzip") {
|
|
2966
2966
|
body = body.pipe(zlib.createGunzip(zlibOptions));
|
|
2967
2967
|
response = new Response3(body, response_options);
|
|
2968
|
-
|
|
2968
|
+
resolve26(response);
|
|
2969
2969
|
return;
|
|
2970
2970
|
}
|
|
2971
2971
|
if (codings == "deflate" || codings == "x-deflate") {
|
|
@@ -2977,12 +2977,12 @@ var require_lib2 = __commonJS({
|
|
|
2977
2977
|
body = body.pipe(zlib.createInflateRaw());
|
|
2978
2978
|
}
|
|
2979
2979
|
response = new Response3(body, response_options);
|
|
2980
|
-
|
|
2980
|
+
resolve26(response);
|
|
2981
2981
|
});
|
|
2982
2982
|
raw.on("end", function() {
|
|
2983
2983
|
if (!response) {
|
|
2984
2984
|
response = new Response3(body, response_options);
|
|
2985
|
-
|
|
2985
|
+
resolve26(response);
|
|
2986
2986
|
}
|
|
2987
2987
|
});
|
|
2988
2988
|
return;
|
|
@@ -2990,11 +2990,11 @@ var require_lib2 = __commonJS({
|
|
|
2990
2990
|
if (codings == "br" && typeof zlib.createBrotliDecompress === "function") {
|
|
2991
2991
|
body = body.pipe(zlib.createBrotliDecompress());
|
|
2992
2992
|
response = new Response3(body, response_options);
|
|
2993
|
-
|
|
2993
|
+
resolve26(response);
|
|
2994
2994
|
return;
|
|
2995
2995
|
}
|
|
2996
2996
|
response = new Response3(body, response_options);
|
|
2997
|
-
|
|
2997
|
+
resolve26(response);
|
|
2998
2998
|
});
|
|
2999
2999
|
writeToStream(req, request);
|
|
3000
3000
|
});
|
|
@@ -6351,14 +6351,14 @@ __export(fileFromPath_exports, {
|
|
|
6351
6351
|
fileFromPathSync: () => fileFromPathSync,
|
|
6352
6352
|
isFile: () => isFile
|
|
6353
6353
|
});
|
|
6354
|
-
function createFileFromPath(
|
|
6354
|
+
function createFileFromPath(path37, { mtimeMs, size }, filenameOrOptions, options = {}) {
|
|
6355
6355
|
let filename;
|
|
6356
6356
|
if (isPlainObject_default2(filenameOrOptions)) {
|
|
6357
6357
|
[options, filename] = [filenameOrOptions, void 0];
|
|
6358
6358
|
} else {
|
|
6359
6359
|
filename = filenameOrOptions;
|
|
6360
6360
|
}
|
|
6361
|
-
const file = new FileFromPath({ path:
|
|
6361
|
+
const file = new FileFromPath({ path: path37, size, lastModified: mtimeMs });
|
|
6362
6362
|
if (!filename) {
|
|
6363
6363
|
filename = file.name;
|
|
6364
6364
|
}
|
|
@@ -6367,13 +6367,13 @@ function createFileFromPath(path32, { mtimeMs, size }, filenameOrOptions, option
|
|
|
6367
6367
|
lastModified: file.lastModified
|
|
6368
6368
|
});
|
|
6369
6369
|
}
|
|
6370
|
-
function fileFromPathSync(
|
|
6371
|
-
const stats = (0, import_fs5.statSync)(
|
|
6372
|
-
return createFileFromPath(
|
|
6370
|
+
function fileFromPathSync(path37, filenameOrOptions, options = {}) {
|
|
6371
|
+
const stats = (0, import_fs5.statSync)(path37);
|
|
6372
|
+
return createFileFromPath(path37, stats, filenameOrOptions, options);
|
|
6373
6373
|
}
|
|
6374
|
-
async function fileFromPath2(
|
|
6375
|
-
const stats = await import_fs5.promises.stat(
|
|
6376
|
-
return createFileFromPath(
|
|
6374
|
+
async function fileFromPath2(path37, filenameOrOptions, options) {
|
|
6375
|
+
const stats = await import_fs5.promises.stat(path37);
|
|
6376
|
+
return createFileFromPath(path37, stats, filenameOrOptions, options);
|
|
6377
6377
|
}
|
|
6378
6378
|
var import_fs5, import_path5, import_node_domexception, __classPrivateFieldSet5, __classPrivateFieldGet6, _FileFromPath_path, _FileFromPath_start, MESSAGE, FileFromPath;
|
|
6379
6379
|
var init_fileFromPath = __esm({
|
|
@@ -9384,7 +9384,7 @@ var require_compile = __commonJS({
|
|
|
9384
9384
|
const schOrFunc = root.refs[ref];
|
|
9385
9385
|
if (schOrFunc)
|
|
9386
9386
|
return schOrFunc;
|
|
9387
|
-
let _sch =
|
|
9387
|
+
let _sch = resolve26.call(this, root, ref);
|
|
9388
9388
|
if (_sch === void 0) {
|
|
9389
9389
|
const schema = (_a3 = root.localRefs) === null || _a3 === void 0 ? void 0 : _a3[ref];
|
|
9390
9390
|
const { schemaId } = this.opts;
|
|
@@ -9411,7 +9411,7 @@ var require_compile = __commonJS({
|
|
|
9411
9411
|
function sameSchemaEnv(s1, s2) {
|
|
9412
9412
|
return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
|
|
9413
9413
|
}
|
|
9414
|
-
function
|
|
9414
|
+
function resolve26(root, ref) {
|
|
9415
9415
|
let sch;
|
|
9416
9416
|
while (typeof (sch = this.refs[ref]) == "string")
|
|
9417
9417
|
ref = sch;
|
|
@@ -9629,8 +9629,8 @@ var require_utils2 = __commonJS({
|
|
|
9629
9629
|
}
|
|
9630
9630
|
return ind;
|
|
9631
9631
|
}
|
|
9632
|
-
function removeDotSegments(
|
|
9633
|
-
let input =
|
|
9632
|
+
function removeDotSegments(path37) {
|
|
9633
|
+
let input = path37;
|
|
9634
9634
|
const output = [];
|
|
9635
9635
|
let nextSlash = -1;
|
|
9636
9636
|
let len = 0;
|
|
@@ -9882,8 +9882,8 @@ var require_schemes = __commonJS({
|
|
|
9882
9882
|
wsComponent.secure = void 0;
|
|
9883
9883
|
}
|
|
9884
9884
|
if (wsComponent.resourceName) {
|
|
9885
|
-
const [
|
|
9886
|
-
wsComponent.path =
|
|
9885
|
+
const [path37, query] = wsComponent.resourceName.split("?");
|
|
9886
|
+
wsComponent.path = path37 && path37 !== "/" ? path37 : void 0;
|
|
9887
9887
|
wsComponent.query = query;
|
|
9888
9888
|
wsComponent.resourceName = void 0;
|
|
9889
9889
|
}
|
|
@@ -10042,7 +10042,7 @@ var require_fast_uri = __commonJS({
|
|
|
10042
10042
|
}
|
|
10043
10043
|
return uri;
|
|
10044
10044
|
}
|
|
10045
|
-
function
|
|
10045
|
+
function resolve26(baseURI, relativeURI, options) {
|
|
10046
10046
|
const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
|
|
10047
10047
|
const resolved = resolveComponent(parse4(baseURI, schemelessOptions), parse4(relativeURI, schemelessOptions), schemelessOptions, true);
|
|
10048
10048
|
schemelessOptions.skipEscape = true;
|
|
@@ -10300,7 +10300,7 @@ var require_fast_uri = __commonJS({
|
|
|
10300
10300
|
var fastUri = {
|
|
10301
10301
|
SCHEMES,
|
|
10302
10302
|
normalize: normalize3,
|
|
10303
|
-
resolve:
|
|
10303
|
+
resolve: resolve26,
|
|
10304
10304
|
resolveComponent,
|
|
10305
10305
|
equal,
|
|
10306
10306
|
serialize,
|
|
@@ -13276,12 +13276,12 @@ var require_dist = __commonJS({
|
|
|
13276
13276
|
throw new Error(`Unknown format "${name}"`);
|
|
13277
13277
|
return f2;
|
|
13278
13278
|
};
|
|
13279
|
-
function addFormats(ajv, list,
|
|
13279
|
+
function addFormats(ajv, list, fs32, exportName) {
|
|
13280
13280
|
var _a3;
|
|
13281
13281
|
var _b2;
|
|
13282
13282
|
(_a3 = (_b2 = ajv.opts.code).formats) !== null && _a3 !== void 0 ? _a3 : _b2.formats = (0, codegen_1._)`require("ajv-formats/dist/formats").${exportName}`;
|
|
13283
13283
|
for (const f2 of list)
|
|
13284
|
-
ajv.addFormat(f2,
|
|
13284
|
+
ajv.addFormat(f2, fs32[f2]);
|
|
13285
13285
|
}
|
|
13286
13286
|
module2.exports = exports2 = formatsPlugin;
|
|
13287
13287
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
@@ -14565,11 +14565,11 @@ var parser = new YargsParser({
|
|
|
14565
14565
|
resolve: import_path2.resolve,
|
|
14566
14566
|
// TODO: figure out a way to combine ESM and CJS coverage, such that
|
|
14567
14567
|
// we can exercise all the lines below:
|
|
14568
|
-
require: (
|
|
14568
|
+
require: (path37) => {
|
|
14569
14569
|
if (typeof require !== "undefined") {
|
|
14570
|
-
return require(
|
|
14571
|
-
} else if (
|
|
14572
|
-
return JSON.parse((0, import_fs2.readFileSync)(
|
|
14570
|
+
return require(path37);
|
|
14571
|
+
} else if (path37.match(/\.json$/)) {
|
|
14572
|
+
return JSON.parse((0, import_fs2.readFileSync)(path37, "utf8"));
|
|
14573
14573
|
} else {
|
|
14574
14574
|
throw Error("only .json config files are supported in ESM");
|
|
14575
14575
|
}
|
|
@@ -17095,12 +17095,12 @@ var YargsInstance = class {
|
|
|
17095
17095
|
async getCompletion(args, done) {
|
|
17096
17096
|
argsert("<array> [function]", [args, done], arguments.length);
|
|
17097
17097
|
if (!done) {
|
|
17098
|
-
return new Promise((
|
|
17098
|
+
return new Promise((resolve26, reject) => {
|
|
17099
17099
|
__classPrivateFieldGet(this, _YargsInstance_completion, "f").getCompletion(args, (err, completions) => {
|
|
17100
17100
|
if (err)
|
|
17101
17101
|
reject(err);
|
|
17102
17102
|
else
|
|
17103
|
-
|
|
17103
|
+
resolve26(completions);
|
|
17104
17104
|
});
|
|
17105
17105
|
});
|
|
17106
17106
|
} else {
|
|
@@ -18139,26 +18139,45 @@ function isYargsInstance(y2) {
|
|
|
18139
18139
|
var Yargs = YargsFactory(esm_default);
|
|
18140
18140
|
var yargs_default = Yargs;
|
|
18141
18141
|
|
|
18142
|
+
// src/core/package-version.ts
|
|
18143
|
+
var fs = __toESM(require("fs"));
|
|
18144
|
+
var path = __toESM(require("path"));
|
|
18145
|
+
function getHarPackageVersion() {
|
|
18146
|
+
if (process.env.HAR_PACKAGE_VERSION) {
|
|
18147
|
+
return process.env.HAR_PACKAGE_VERSION;
|
|
18148
|
+
}
|
|
18149
|
+
const candidates = [
|
|
18150
|
+
path.resolve(__dirname, "..", "package.json"),
|
|
18151
|
+
path.resolve(__dirname, "..", "..", "package.json")
|
|
18152
|
+
];
|
|
18153
|
+
for (const packageJsonPath of candidates) {
|
|
18154
|
+
if (!fs.existsSync(packageJsonPath)) continue;
|
|
18155
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
18156
|
+
return pkg.version;
|
|
18157
|
+
}
|
|
18158
|
+
throw new Error("Could not resolve @osfactory/har package.json for version lookup");
|
|
18159
|
+
}
|
|
18160
|
+
|
|
18142
18161
|
// src/cli/commands/env.ts
|
|
18143
|
-
var
|
|
18162
|
+
var path31 = __toESM(require("path"));
|
|
18144
18163
|
|
|
18145
18164
|
// src/core/harness.ts
|
|
18146
|
-
var
|
|
18147
|
-
var
|
|
18165
|
+
var fs18 = __toESM(require("fs"));
|
|
18166
|
+
var path16 = __toESM(require("path"));
|
|
18148
18167
|
|
|
18149
18168
|
// src/harness/generator.ts
|
|
18150
|
-
var
|
|
18151
|
-
var
|
|
18169
|
+
var fs6 = __toESM(require("fs"));
|
|
18170
|
+
var path6 = __toESM(require("path"));
|
|
18152
18171
|
|
|
18153
18172
|
// src/utils/file-ops.ts
|
|
18154
|
-
var
|
|
18155
|
-
var
|
|
18173
|
+
var fs2 = __toESM(require("fs"));
|
|
18174
|
+
var path2 = __toESM(require("path"));
|
|
18156
18175
|
function readFile(filePath, maxChars = 8e3) {
|
|
18157
|
-
if (!
|
|
18158
|
-
if (
|
|
18176
|
+
if (!fs2.existsSync(filePath)) return `[file not found: ${filePath}]`;
|
|
18177
|
+
if (fs2.statSync(filePath).isDirectory()) {
|
|
18159
18178
|
return `[path is a directory, not a file: ${filePath}. Use listDir instead.]`;
|
|
18160
18179
|
}
|
|
18161
|
-
const content =
|
|
18180
|
+
const content = fs2.readFileSync(filePath, "utf8");
|
|
18162
18181
|
if (content.length > maxChars) {
|
|
18163
18182
|
return content.slice(0, maxChars) + `
|
|
18164
18183
|
... [truncated at ${maxChars} chars]`;
|
|
@@ -18166,8 +18185,8 @@ function readFile(filePath, maxChars = 8e3) {
|
|
|
18166
18185
|
return content;
|
|
18167
18186
|
}
|
|
18168
18187
|
function listDir(dirPath, maxFiles = 60) {
|
|
18169
|
-
if (!
|
|
18170
|
-
const entries =
|
|
18188
|
+
if (!fs2.existsSync(dirPath)) return [];
|
|
18189
|
+
const entries = fs2.readdirSync(dirPath, { withFileTypes: true });
|
|
18171
18190
|
const result = [];
|
|
18172
18191
|
for (const entry of entries.slice(0, maxFiles)) {
|
|
18173
18192
|
const name = entry.isDirectory() ? entry.name + "/" : entry.name;
|
|
@@ -18176,30 +18195,30 @@ function listDir(dirPath, maxFiles = 60) {
|
|
|
18176
18195
|
return result;
|
|
18177
18196
|
}
|
|
18178
18197
|
function writeFileSafe(filePath, content) {
|
|
18179
|
-
|
|
18180
|
-
|
|
18198
|
+
fs2.mkdirSync(path2.dirname(filePath), { recursive: true });
|
|
18199
|
+
fs2.writeFileSync(filePath, content);
|
|
18181
18200
|
}
|
|
18182
18201
|
function copyDirRecursive(srcDir, destDir) {
|
|
18183
|
-
|
|
18184
|
-
for (const entry of
|
|
18185
|
-
const srcPath =
|
|
18186
|
-
const destPath =
|
|
18202
|
+
fs2.mkdirSync(destDir, { recursive: true });
|
|
18203
|
+
for (const entry of fs2.readdirSync(srcDir, { withFileTypes: true })) {
|
|
18204
|
+
const srcPath = path2.join(srcDir, entry.name);
|
|
18205
|
+
const destPath = path2.join(destDir, entry.name);
|
|
18187
18206
|
if (entry.isDirectory()) {
|
|
18188
18207
|
copyDirRecursive(srcPath, destPath);
|
|
18189
18208
|
} else {
|
|
18190
|
-
|
|
18191
|
-
|
|
18209
|
+
fs2.mkdirSync(path2.dirname(destPath), { recursive: true });
|
|
18210
|
+
fs2.copyFileSync(srcPath, destPath);
|
|
18192
18211
|
if (entry.name.endsWith(".sh")) {
|
|
18193
|
-
|
|
18212
|
+
fs2.chmodSync(destPath, 493);
|
|
18194
18213
|
}
|
|
18195
18214
|
}
|
|
18196
18215
|
}
|
|
18197
18216
|
}
|
|
18198
18217
|
function resolveSafePath(baseDir, relativePath) {
|
|
18199
|
-
const normalized =
|
|
18200
|
-
const resolved =
|
|
18201
|
-
const base =
|
|
18202
|
-
if (!resolved.startsWith(base +
|
|
18218
|
+
const normalized = path2.normalize(relativePath).replace(/^(\.\.(\/|\\|$))+/, "");
|
|
18219
|
+
const resolved = path2.resolve(baseDir, normalized);
|
|
18220
|
+
const base = path2.resolve(baseDir);
|
|
18221
|
+
if (!resolved.startsWith(base + path2.sep) && resolved !== base) {
|
|
18203
18222
|
throw new Error(`Path escapes harness directory: ${relativePath}`);
|
|
18204
18223
|
}
|
|
18205
18224
|
return resolved;
|
|
@@ -18721,45 +18740,45 @@ function divider() {
|
|
|
18721
18740
|
}
|
|
18722
18741
|
|
|
18723
18742
|
// src/utils/paths.ts
|
|
18724
|
-
var
|
|
18725
|
-
var
|
|
18743
|
+
var fs3 = __toESM(require("fs"));
|
|
18744
|
+
var path3 = __toESM(require("path"));
|
|
18726
18745
|
function resolveTemplatesDir() {
|
|
18727
18746
|
const candidates = [
|
|
18728
|
-
|
|
18729
|
-
|
|
18730
|
-
|
|
18747
|
+
path3.join(__dirname, "templates"),
|
|
18748
|
+
path3.join(__dirname, "..", "templates"),
|
|
18749
|
+
path3.join(__dirname, "..", "harness", "templates")
|
|
18731
18750
|
];
|
|
18732
18751
|
for (const dir of candidates) {
|
|
18733
|
-
if (
|
|
18752
|
+
if (fs3.existsSync(path3.join(dir, "har-boilerplate"))) return dir;
|
|
18734
18753
|
}
|
|
18735
18754
|
throw new Error("Templates directory not found. Run npm run build.");
|
|
18736
18755
|
}
|
|
18737
18756
|
function resolvePromptPath(name) {
|
|
18738
18757
|
const candidates = [
|
|
18739
|
-
|
|
18740
|
-
|
|
18758
|
+
path3.join(__dirname, "prompts", name),
|
|
18759
|
+
path3.join(__dirname, "..", "llm", "prompts", name)
|
|
18741
18760
|
];
|
|
18742
18761
|
for (const p2 of candidates) {
|
|
18743
|
-
if (
|
|
18762
|
+
if (fs3.existsSync(p2)) return p2;
|
|
18744
18763
|
}
|
|
18745
18764
|
return candidates[0];
|
|
18746
18765
|
}
|
|
18747
18766
|
function resolveTemplateFile(name) {
|
|
18748
18767
|
const dir = resolveTemplatesDir();
|
|
18749
|
-
const filePath =
|
|
18750
|
-
return
|
|
18768
|
+
const filePath = path3.join(dir, name);
|
|
18769
|
+
return fs3.existsSync(filePath) ? filePath : null;
|
|
18751
18770
|
}
|
|
18752
18771
|
|
|
18753
18772
|
// src/core/gitignore.ts
|
|
18754
|
-
var
|
|
18755
|
-
var
|
|
18773
|
+
var fs4 = __toESM(require("fs"));
|
|
18774
|
+
var path4 = __toESM(require("path"));
|
|
18756
18775
|
var HARNESS_ROOT_GITIGNORE_PATTERNS = [
|
|
18757
18776
|
".env.agent.*",
|
|
18758
18777
|
"ecosystem.agent.*.config.cjs"
|
|
18759
18778
|
];
|
|
18760
18779
|
function ensureRootGitignorePatterns(checkoutDir) {
|
|
18761
|
-
const gitignorePath =
|
|
18762
|
-
const content =
|
|
18780
|
+
const gitignorePath = path4.join(checkoutDir, ".gitignore");
|
|
18781
|
+
const content = fs4.existsSync(gitignorePath) ? fs4.readFileSync(gitignorePath, "utf8") : "";
|
|
18763
18782
|
const lines = content.split("\n").map((line) => line.trim());
|
|
18764
18783
|
const missing = HARNESS_ROOT_GITIGNORE_PATTERNS.filter((pattern) => !lines.includes(pattern));
|
|
18765
18784
|
if (missing.length === 0) return;
|
|
@@ -18768,13 +18787,13 @@ function ensureRootGitignorePatterns(checkoutDir) {
|
|
|
18768
18787
|
const block = lines.some((line) => line.includes("HAR agent slot artifacts")) || header2.length === 0 ? `${missing.join("\n")}
|
|
18769
18788
|
` : `${header2}${missing.join("\n")}
|
|
18770
18789
|
`;
|
|
18771
|
-
|
|
18790
|
+
fs4.writeFileSync(gitignorePath, `${content}${suffix}${block}`);
|
|
18772
18791
|
}
|
|
18773
18792
|
|
|
18774
18793
|
// src/harness/manifest.ts
|
|
18775
18794
|
var crypto = __toESM(require("crypto"));
|
|
18776
|
-
var
|
|
18777
|
-
var
|
|
18795
|
+
var fs5 = __toESM(require("fs"));
|
|
18796
|
+
var path5 = __toESM(require("path"));
|
|
18778
18797
|
|
|
18779
18798
|
// node_modules/zod/v3/external.js
|
|
18780
18799
|
var external_exports = {};
|
|
@@ -19254,8 +19273,8 @@ function getErrorMap() {
|
|
|
19254
19273
|
|
|
19255
19274
|
// node_modules/zod/v3/helpers/parseUtil.js
|
|
19256
19275
|
var makeIssue = (params) => {
|
|
19257
|
-
const { data, path:
|
|
19258
|
-
const fullPath = [...
|
|
19276
|
+
const { data, path: path37, errorMaps, issueData } = params;
|
|
19277
|
+
const fullPath = [...path37, ...issueData.path || []];
|
|
19259
19278
|
const fullIssue = {
|
|
19260
19279
|
...issueData,
|
|
19261
19280
|
path: fullPath
|
|
@@ -19371,11 +19390,11 @@ var errorUtil;
|
|
|
19371
19390
|
|
|
19372
19391
|
// node_modules/zod/v3/types.js
|
|
19373
19392
|
var ParseInputLazyPath = class {
|
|
19374
|
-
constructor(parent, value,
|
|
19393
|
+
constructor(parent, value, path37, key) {
|
|
19375
19394
|
this._cachedPath = [];
|
|
19376
19395
|
this.parent = parent;
|
|
19377
19396
|
this.data = value;
|
|
19378
|
-
this._path =
|
|
19397
|
+
this._path = path37;
|
|
19379
19398
|
this._key = key;
|
|
19380
19399
|
}
|
|
19381
19400
|
get path() {
|
|
@@ -22999,6 +23018,22 @@ var AgentSlotHarnessUsageSchema = external_exports.enum([
|
|
|
22999
23018
|
"none",
|
|
23000
23019
|
"bypass_warning"
|
|
23001
23020
|
]);
|
|
23021
|
+
var PreflightBlockerSchema = external_exports.object({
|
|
23022
|
+
code: external_exports.string(),
|
|
23023
|
+
message: external_exports.string(),
|
|
23024
|
+
remediation: external_exports.string().optional(),
|
|
23025
|
+
details: external_exports.record(external_exports.unknown()).optional()
|
|
23026
|
+
});
|
|
23027
|
+
var SlotReadinessSchema = external_exports.object({
|
|
23028
|
+
canLaunch: external_exports.boolean(),
|
|
23029
|
+
verdict: external_exports.enum(["ready", "blocked"]),
|
|
23030
|
+
blockers: external_exports.array(PreflightBlockerSchema),
|
|
23031
|
+
remediations: external_exports.array(external_exports.string()),
|
|
23032
|
+
ports: external_exports.record(external_exports.number()).optional(),
|
|
23033
|
+
allocatedPorts: external_exports.boolean().optional(),
|
|
23034
|
+
/** Non-blocking notices (e.g. har control up holds the default port but an alternate was picked). */
|
|
23035
|
+
warnings: external_exports.array(external_exports.string()).optional()
|
|
23036
|
+
});
|
|
23002
23037
|
var AgentSlotStatusSchema = external_exports.object({
|
|
23003
23038
|
agentId: external_exports.number().int().min(HAR_AGENT_SLOT_MIN),
|
|
23004
23039
|
active: external_exports.boolean(),
|
|
@@ -23019,6 +23054,8 @@ var AgentSlotStatusSchema = external_exports.object({
|
|
|
23019
23054
|
purpose: external_exports.string().optional(),
|
|
23020
23055
|
sessionStatus: external_exports.enum(["starting", "active", "failed", "completed"]).optional(),
|
|
23021
23056
|
lastError: external_exports.string().optional(),
|
|
23057
|
+
/** Set when a failed/starting session can be resumed without --replace. */
|
|
23058
|
+
resumeHint: external_exports.string().optional(),
|
|
23022
23059
|
/** Worktree checked out to no branch (legacy failure mode; should not happen with sessions). */
|
|
23023
23060
|
detachedHead: external_exports.boolean().optional(),
|
|
23024
23061
|
/** Worktree has uncommitted changes. */
|
|
@@ -23027,7 +23064,13 @@ var AgentSlotStatusSchema = external_exports.object({
|
|
|
23027
23064
|
ahead: external_exports.number().int().optional(),
|
|
23028
23065
|
/** Main checkout has commits the session base doesn't (worktree serves stale code). */
|
|
23029
23066
|
behind: external_exports.number().int().optional(),
|
|
23030
|
-
stale: external_exports.boolean().optional()
|
|
23067
|
+
stale: external_exports.boolean().optional(),
|
|
23068
|
+
/** Persisted host ports from the slot registry (when dynamically allocated). */
|
|
23069
|
+
ports: external_exports.record(external_exports.number()).optional(),
|
|
23070
|
+
/** PM2 namespace mismatch — foreign or legacy processes for this slot id. */
|
|
23071
|
+
pm2Issue: external_exports.enum(["foreign_pm2", "registry_missing", "project_mismatch"]).optional(),
|
|
23072
|
+
/** Launch readiness slice — same core as `har env preflight`. */
|
|
23073
|
+
readiness: SlotReadinessSchema.optional()
|
|
23031
23074
|
});
|
|
23032
23075
|
var EnvironmentStatusSchema = external_exports.object({
|
|
23033
23076
|
repoPath: external_exports.string(),
|
|
@@ -23081,17 +23124,22 @@ var SyncValidationsInputSchema = external_exports.object({
|
|
|
23081
23124
|
var GENERATOR_VERSION = "0.4.0";
|
|
23082
23125
|
var MANIFEST_VERSION = "1";
|
|
23083
23126
|
var DEFAULT_HAR_DIR = ".har";
|
|
23084
|
-
var CHECKSUM_SKIP = /* @__PURE__ */ new Set([
|
|
23127
|
+
var CHECKSUM_SKIP = /* @__PURE__ */ new Set([
|
|
23128
|
+
"manifest.json",
|
|
23129
|
+
"AGENT.md.proposed",
|
|
23130
|
+
"AGENT.md.proposed.meta.json",
|
|
23131
|
+
"ADAPT-PROMPT.md"
|
|
23132
|
+
]);
|
|
23085
23133
|
function getHarnessDir(repoPath) {
|
|
23086
|
-
return
|
|
23134
|
+
return path5.join(repoPath, DEFAULT_HAR_DIR);
|
|
23087
23135
|
}
|
|
23088
23136
|
function getManifestPath(repoPath) {
|
|
23089
|
-
return
|
|
23137
|
+
return path5.join(getHarnessDir(repoPath), "manifest.json");
|
|
23090
23138
|
}
|
|
23091
23139
|
function readManifest(repoPath) {
|
|
23092
23140
|
const manifestPath = getManifestPath(repoPath);
|
|
23093
|
-
if (!
|
|
23094
|
-
const raw = JSON.parse(
|
|
23141
|
+
if (!fs5.existsSync(manifestPath)) return null;
|
|
23142
|
+
const raw = JSON.parse(fs5.readFileSync(manifestPath, "utf8"));
|
|
23095
23143
|
const result = HarnessManifestSchema.safeParse(raw);
|
|
23096
23144
|
return result.success ? result.data : null;
|
|
23097
23145
|
}
|
|
@@ -23104,11 +23152,11 @@ function computeFileChecksum(content) {
|
|
|
23104
23152
|
}
|
|
23105
23153
|
function computeHarnessChecksums(harnessDir) {
|
|
23106
23154
|
const checksums = {};
|
|
23107
|
-
if (!
|
|
23108
|
-
for (const entry of
|
|
23155
|
+
if (!fs5.existsSync(harnessDir)) return checksums;
|
|
23156
|
+
for (const entry of fs5.readdirSync(harnessDir, { withFileTypes: true })) {
|
|
23109
23157
|
if (!entry.isFile() || CHECKSUM_SKIP.has(entry.name)) continue;
|
|
23110
|
-
const full =
|
|
23111
|
-
checksums[entry.name] = computeFileChecksum(
|
|
23158
|
+
const full = path5.join(harnessDir, entry.name);
|
|
23159
|
+
checksums[entry.name] = computeFileChecksum(fs5.readFileSync(full, "utf8"));
|
|
23112
23160
|
}
|
|
23113
23161
|
return checksums;
|
|
23114
23162
|
}
|
|
@@ -23128,17 +23176,17 @@ function createManifest(repoPath, adaptationSummary, stack, profile) {
|
|
|
23128
23176
|
};
|
|
23129
23177
|
}
|
|
23130
23178
|
function resolveHarnessRoot(inputPath) {
|
|
23131
|
-
let current =
|
|
23132
|
-
const { root } =
|
|
23179
|
+
let current = path5.resolve(inputPath);
|
|
23180
|
+
const { root } = path5.parse(current);
|
|
23133
23181
|
for (; ; ) {
|
|
23134
|
-
const manifestPath =
|
|
23135
|
-
if (
|
|
23182
|
+
const manifestPath = path5.join(current, DEFAULT_HAR_DIR, "manifest.json");
|
|
23183
|
+
if (fs5.existsSync(manifestPath)) {
|
|
23136
23184
|
return current;
|
|
23137
23185
|
}
|
|
23138
23186
|
if (current === root) break;
|
|
23139
|
-
current =
|
|
23187
|
+
current = path5.dirname(current);
|
|
23140
23188
|
}
|
|
23141
|
-
return
|
|
23189
|
+
return path5.resolve(inputPath);
|
|
23142
23190
|
}
|
|
23143
23191
|
|
|
23144
23192
|
// src/harness/generator.ts
|
|
@@ -23154,46 +23202,46 @@ var CLI_PRUNE_FILES = [
|
|
|
23154
23202
|
];
|
|
23155
23203
|
function pruneCliProfile(harnessDir) {
|
|
23156
23204
|
for (const file of CLI_PRUNE_FILES) {
|
|
23157
|
-
const filePath =
|
|
23158
|
-
if (
|
|
23159
|
-
|
|
23205
|
+
const filePath = path6.join(harnessDir, file);
|
|
23206
|
+
if (fs6.existsSync(filePath)) {
|
|
23207
|
+
fs6.unlinkSync(filePath);
|
|
23160
23208
|
}
|
|
23161
23209
|
}
|
|
23162
23210
|
}
|
|
23163
23211
|
function scaffoldClaudeMd(repoPath, projectName, force) {
|
|
23164
23212
|
const templatePath = resolveTemplateFile("CLAUDE.md.template");
|
|
23165
23213
|
if (!templatePath) return;
|
|
23166
|
-
const dest =
|
|
23167
|
-
if (
|
|
23214
|
+
const dest = path6.join(repoPath, "CLAUDE.md");
|
|
23215
|
+
if (fs6.existsSync(dest) && !force) return;
|
|
23168
23216
|
const displayName = projectName.replace(/_/g, " ");
|
|
23169
|
-
const content =
|
|
23170
|
-
|
|
23217
|
+
const content = fs6.readFileSync(templatePath, "utf8").replace(/__PROJECT_DISPLAY_NAME__/g, displayName);
|
|
23218
|
+
fs6.writeFileSync(dest, content);
|
|
23171
23219
|
}
|
|
23172
23220
|
function scaffoldHarnessBoilerplate(repoPath, options = {}) {
|
|
23173
|
-
const harnessDir =
|
|
23174
|
-
const projectName =
|
|
23221
|
+
const harnessDir = path6.join(repoPath, DEFAULT_HAR_DIR);
|
|
23222
|
+
const projectName = path6.basename(repoPath).toLowerCase().replace(/[^a-z0-9]/g, "_");
|
|
23175
23223
|
const profile = options.profile ?? "default";
|
|
23176
|
-
const boilerplateDir =
|
|
23177
|
-
if (
|
|
23224
|
+
const boilerplateDir = path6.join(resolveTemplatesDir(), PROFILE_DIRS[profile]);
|
|
23225
|
+
if (fs6.existsSync(harnessDir) && !options.force) {
|
|
23178
23226
|
throw new Error(
|
|
23179
23227
|
'.har/ already exists. Use --force to overwrite or run "har env maintain" to update in place.'
|
|
23180
23228
|
);
|
|
23181
23229
|
}
|
|
23182
|
-
if (!
|
|
23230
|
+
if (!fs6.existsSync(boilerplateDir)) {
|
|
23183
23231
|
throw new Error(`Boilerplate template not found at ${boilerplateDir}`);
|
|
23184
23232
|
}
|
|
23185
|
-
if (options.force &&
|
|
23186
|
-
|
|
23233
|
+
if (options.force && fs6.existsSync(harnessDir)) {
|
|
23234
|
+
fs6.rmSync(harnessDir, { recursive: true, force: true });
|
|
23187
23235
|
}
|
|
23188
23236
|
copyDirRecursive(boilerplateDir, harnessDir);
|
|
23189
23237
|
if (profile === "cli") {
|
|
23190
23238
|
pruneCliProfile(harnessDir);
|
|
23191
23239
|
}
|
|
23192
|
-
const harnessEnvPath =
|
|
23193
|
-
if (
|
|
23194
|
-
let content =
|
|
23240
|
+
const harnessEnvPath = path6.join(harnessDir, "harness.env");
|
|
23241
|
+
if (fs6.existsSync(harnessEnvPath)) {
|
|
23242
|
+
let content = fs6.readFileSync(harnessEnvPath, "utf8");
|
|
23195
23243
|
content = content.replace(/__PROJECT_NAME__/g, projectName).replace(/template___PROJECT_NAME__/g, `template_${projectName}`);
|
|
23196
|
-
|
|
23244
|
+
fs6.writeFileSync(harnessEnvPath, content);
|
|
23197
23245
|
}
|
|
23198
23246
|
const manifest = createManifest(
|
|
23199
23247
|
repoPath,
|
|
@@ -23216,8 +23264,8 @@ function finalizeHarness(repoPath, adaptationSummary, stack) {
|
|
|
23216
23264
|
}
|
|
23217
23265
|
|
|
23218
23266
|
// src/llm/authoring-agent.ts
|
|
23219
|
-
var
|
|
23220
|
-
var
|
|
23267
|
+
var fs10 = __toESM(require("fs"));
|
|
23268
|
+
var path9 = __toESM(require("path"));
|
|
23221
23269
|
|
|
23222
23270
|
// node_modules/@anthropic-ai/sdk/error.mjs
|
|
23223
23271
|
var error_exports = {};
|
|
@@ -23616,13 +23664,13 @@ var MultipartBody = class {
|
|
|
23616
23664
|
// node_modules/@anthropic-ai/sdk/_shims/node-runtime.mjs
|
|
23617
23665
|
var import_web = require("node:stream/web");
|
|
23618
23666
|
var fileFromPathWarned = false;
|
|
23619
|
-
async function fileFromPath3(
|
|
23667
|
+
async function fileFromPath3(path37, ...args) {
|
|
23620
23668
|
const { fileFromPath: _fileFromPath } = await Promise.resolve().then(() => (init_fileFromPath(), fileFromPath_exports));
|
|
23621
23669
|
if (!fileFromPathWarned) {
|
|
23622
|
-
console.warn(`fileFromPath is deprecated; use fs.createReadStream(${JSON.stringify(
|
|
23670
|
+
console.warn(`fileFromPath is deprecated; use fs.createReadStream(${JSON.stringify(path37)}) instead`);
|
|
23623
23671
|
fileFromPathWarned = true;
|
|
23624
23672
|
}
|
|
23625
|
-
return await _fileFromPath(
|
|
23673
|
+
return await _fileFromPath(path37, ...args);
|
|
23626
23674
|
}
|
|
23627
23675
|
var defaultHttpAgent = new import_agentkeepalive.default({ keepAlive: true, timeout: 5 * 60 * 1e3 });
|
|
23628
23676
|
var defaultHttpsAgent = new import_agentkeepalive.default.HttpsAgent({ keepAlive: true, timeout: 5 * 60 * 1e3 });
|
|
@@ -24118,8 +24166,8 @@ async function defaultParseResponse(props) {
|
|
|
24118
24166
|
}
|
|
24119
24167
|
var APIPromise = class _APIPromise extends Promise {
|
|
24120
24168
|
constructor(responsePromise, parseResponse = defaultParseResponse) {
|
|
24121
|
-
super((
|
|
24122
|
-
|
|
24169
|
+
super((resolve26) => {
|
|
24170
|
+
resolve26(null);
|
|
24123
24171
|
});
|
|
24124
24172
|
this.responsePromise = responsePromise;
|
|
24125
24173
|
this.parseResponse = parseResponse;
|
|
@@ -24219,29 +24267,29 @@ var APIClient = class {
|
|
|
24219
24267
|
defaultIdempotencyKey() {
|
|
24220
24268
|
return `stainless-node-retry-${uuid4()}`;
|
|
24221
24269
|
}
|
|
24222
|
-
get(
|
|
24223
|
-
return this.methodRequest("get",
|
|
24270
|
+
get(path37, opts) {
|
|
24271
|
+
return this.methodRequest("get", path37, opts);
|
|
24224
24272
|
}
|
|
24225
|
-
post(
|
|
24226
|
-
return this.methodRequest("post",
|
|
24273
|
+
post(path37, opts) {
|
|
24274
|
+
return this.methodRequest("post", path37, opts);
|
|
24227
24275
|
}
|
|
24228
|
-
patch(
|
|
24229
|
-
return this.methodRequest("patch",
|
|
24276
|
+
patch(path37, opts) {
|
|
24277
|
+
return this.methodRequest("patch", path37, opts);
|
|
24230
24278
|
}
|
|
24231
|
-
put(
|
|
24232
|
-
return this.methodRequest("put",
|
|
24279
|
+
put(path37, opts) {
|
|
24280
|
+
return this.methodRequest("put", path37, opts);
|
|
24233
24281
|
}
|
|
24234
|
-
delete(
|
|
24235
|
-
return this.methodRequest("delete",
|
|
24282
|
+
delete(path37, opts) {
|
|
24283
|
+
return this.methodRequest("delete", path37, opts);
|
|
24236
24284
|
}
|
|
24237
|
-
methodRequest(method,
|
|
24285
|
+
methodRequest(method, path37, opts) {
|
|
24238
24286
|
return this.request(Promise.resolve(opts).then(async (opts2) => {
|
|
24239
24287
|
const body = opts2 && isBlobLike(opts2?.body) ? new DataView(await opts2.body.arrayBuffer()) : opts2?.body instanceof DataView ? opts2.body : opts2?.body instanceof ArrayBuffer ? new DataView(opts2.body) : opts2 && ArrayBuffer.isView(opts2?.body) ? new DataView(opts2.body.buffer) : opts2?.body;
|
|
24240
|
-
return { method, path:
|
|
24288
|
+
return { method, path: path37, ...opts2, body };
|
|
24241
24289
|
}));
|
|
24242
24290
|
}
|
|
24243
|
-
getAPIList(
|
|
24244
|
-
return this.requestAPIList(Page, { method: "get", path:
|
|
24291
|
+
getAPIList(path37, Page, opts) {
|
|
24292
|
+
return this.requestAPIList(Page, { method: "get", path: path37, ...opts });
|
|
24245
24293
|
}
|
|
24246
24294
|
calculateContentLength(body) {
|
|
24247
24295
|
if (typeof body === "string") {
|
|
@@ -24259,10 +24307,10 @@ var APIClient = class {
|
|
|
24259
24307
|
return null;
|
|
24260
24308
|
}
|
|
24261
24309
|
buildRequest(options) {
|
|
24262
|
-
const { method, path:
|
|
24310
|
+
const { method, path: path37, query, headers = {} } = options;
|
|
24263
24311
|
const body = ArrayBuffer.isView(options.body) || options.__binaryRequest && typeof options.body === "string" ? options.body : isMultipartBody(options.body) ? options.body.body : options.body ? JSON.stringify(options.body, null, 2) : null;
|
|
24264
24312
|
const contentLength = this.calculateContentLength(body);
|
|
24265
|
-
const url = this.buildURL(
|
|
24313
|
+
const url = this.buildURL(path37, query);
|
|
24266
24314
|
if ("timeout" in options)
|
|
24267
24315
|
validatePositiveInteger("timeout", options.timeout);
|
|
24268
24316
|
const timeout = options.timeout ?? this.timeout;
|
|
@@ -24371,8 +24419,8 @@ var APIClient = class {
|
|
|
24371
24419
|
const request = this.makeRequest(options, null);
|
|
24372
24420
|
return new PagePromise(this, request, Page);
|
|
24373
24421
|
}
|
|
24374
|
-
buildURL(
|
|
24375
|
-
const url = isAbsoluteURL(
|
|
24422
|
+
buildURL(path37, query) {
|
|
24423
|
+
const url = isAbsoluteURL(path37) ? new URL(path37) : new URL(this.baseURL + (this.baseURL.endsWith("/") && path37.startsWith("/") ? path37.slice(1) : path37));
|
|
24376
24424
|
const defaultQuery = this.defaultQuery();
|
|
24377
24425
|
if (!isEmptyObj(defaultQuery)) {
|
|
24378
24426
|
query = { ...defaultQuery, ...query };
|
|
@@ -24657,7 +24705,7 @@ var startsWithSchemeRegexp = new RegExp("^(?:[a-z]+:)?//", "i");
|
|
|
24657
24705
|
var isAbsoluteURL = (url) => {
|
|
24658
24706
|
return startsWithSchemeRegexp.test(url);
|
|
24659
24707
|
};
|
|
24660
|
-
var sleep = (ms) => new Promise((
|
|
24708
|
+
var sleep = (ms) => new Promise((resolve26) => setTimeout(resolve26, ms));
|
|
24661
24709
|
var validatePositiveInteger = (name, n2) => {
|
|
24662
24710
|
if (typeof n2 !== "number" || !Number.isInteger(n2)) {
|
|
24663
24711
|
throw new AnthropicError(`${name} must be an integer`);
|
|
@@ -25148,12 +25196,12 @@ var PromptCachingBetaMessageStream = class _PromptCachingBetaMessageStream {
|
|
|
25148
25196
|
}
|
|
25149
25197
|
return this._emit("error", new AnthropicError(String(error3)));
|
|
25150
25198
|
});
|
|
25151
|
-
__classPrivateFieldSet7(this, _PromptCachingBetaMessageStream_connectedPromise, new Promise((
|
|
25152
|
-
__classPrivateFieldSet7(this, _PromptCachingBetaMessageStream_resolveConnectedPromise,
|
|
25199
|
+
__classPrivateFieldSet7(this, _PromptCachingBetaMessageStream_connectedPromise, new Promise((resolve26, reject) => {
|
|
25200
|
+
__classPrivateFieldSet7(this, _PromptCachingBetaMessageStream_resolveConnectedPromise, resolve26, "f");
|
|
25153
25201
|
__classPrivateFieldSet7(this, _PromptCachingBetaMessageStream_rejectConnectedPromise, reject, "f");
|
|
25154
25202
|
}), "f");
|
|
25155
|
-
__classPrivateFieldSet7(this, _PromptCachingBetaMessageStream_endPromise, new Promise((
|
|
25156
|
-
__classPrivateFieldSet7(this, _PromptCachingBetaMessageStream_resolveEndPromise,
|
|
25203
|
+
__classPrivateFieldSet7(this, _PromptCachingBetaMessageStream_endPromise, new Promise((resolve26, reject) => {
|
|
25204
|
+
__classPrivateFieldSet7(this, _PromptCachingBetaMessageStream_resolveEndPromise, resolve26, "f");
|
|
25157
25205
|
__classPrivateFieldSet7(this, _PromptCachingBetaMessageStream_rejectEndPromise, reject, "f");
|
|
25158
25206
|
}), "f");
|
|
25159
25207
|
__classPrivateFieldGet8(this, _PromptCachingBetaMessageStream_connectedPromise, "f").catch(() => {
|
|
@@ -25282,11 +25330,11 @@ var PromptCachingBetaMessageStream = class _PromptCachingBetaMessageStream {
|
|
|
25282
25330
|
* const message = await stream.emitted('message') // rejects if the stream errors
|
|
25283
25331
|
*/
|
|
25284
25332
|
emitted(event) {
|
|
25285
|
-
return new Promise((
|
|
25333
|
+
return new Promise((resolve26, reject) => {
|
|
25286
25334
|
__classPrivateFieldSet7(this, _PromptCachingBetaMessageStream_catchingPromiseCreated, true, "f");
|
|
25287
25335
|
if (event !== "error")
|
|
25288
25336
|
this.once("error", reject);
|
|
25289
|
-
this.once(event,
|
|
25337
|
+
this.once(event, resolve26);
|
|
25290
25338
|
});
|
|
25291
25339
|
}
|
|
25292
25340
|
async done() {
|
|
@@ -25513,7 +25561,7 @@ var PromptCachingBetaMessageStream = class _PromptCachingBetaMessageStream {
|
|
|
25513
25561
|
if (done) {
|
|
25514
25562
|
return { value: void 0, done: true };
|
|
25515
25563
|
}
|
|
25516
|
-
return new Promise((
|
|
25564
|
+
return new Promise((resolve26, reject) => readQueue.push({ resolve: resolve26, reject })).then((chunk2) => chunk2 ? { value: chunk2, done: false } : { value: void 0, done: true });
|
|
25517
25565
|
}
|
|
25518
25566
|
const chunk = pushQueue.shift();
|
|
25519
25567
|
return { value: chunk, done: false };
|
|
@@ -25661,12 +25709,12 @@ var MessageStream = class _MessageStream {
|
|
|
25661
25709
|
}
|
|
25662
25710
|
return this._emit("error", new AnthropicError(String(error3)));
|
|
25663
25711
|
});
|
|
25664
|
-
__classPrivateFieldSet8(this, _MessageStream_connectedPromise, new Promise((
|
|
25665
|
-
__classPrivateFieldSet8(this, _MessageStream_resolveConnectedPromise,
|
|
25712
|
+
__classPrivateFieldSet8(this, _MessageStream_connectedPromise, new Promise((resolve26, reject) => {
|
|
25713
|
+
__classPrivateFieldSet8(this, _MessageStream_resolveConnectedPromise, resolve26, "f");
|
|
25666
25714
|
__classPrivateFieldSet8(this, _MessageStream_rejectConnectedPromise, reject, "f");
|
|
25667
25715
|
}), "f");
|
|
25668
|
-
__classPrivateFieldSet8(this, _MessageStream_endPromise, new Promise((
|
|
25669
|
-
__classPrivateFieldSet8(this, _MessageStream_resolveEndPromise,
|
|
25716
|
+
__classPrivateFieldSet8(this, _MessageStream_endPromise, new Promise((resolve26, reject) => {
|
|
25717
|
+
__classPrivateFieldSet8(this, _MessageStream_resolveEndPromise, resolve26, "f");
|
|
25670
25718
|
__classPrivateFieldSet8(this, _MessageStream_rejectEndPromise, reject, "f");
|
|
25671
25719
|
}), "f");
|
|
25672
25720
|
__classPrivateFieldGet9(this, _MessageStream_connectedPromise, "f").catch(() => {
|
|
@@ -25795,11 +25843,11 @@ var MessageStream = class _MessageStream {
|
|
|
25795
25843
|
* const message = await stream.emitted('message') // rejects if the stream errors
|
|
25796
25844
|
*/
|
|
25797
25845
|
emitted(event) {
|
|
25798
|
-
return new Promise((
|
|
25846
|
+
return new Promise((resolve26, reject) => {
|
|
25799
25847
|
__classPrivateFieldSet8(this, _MessageStream_catchingPromiseCreated, true, "f");
|
|
25800
25848
|
if (event !== "error")
|
|
25801
25849
|
this.once("error", reject);
|
|
25802
|
-
this.once(event,
|
|
25850
|
+
this.once(event, resolve26);
|
|
25803
25851
|
});
|
|
25804
25852
|
}
|
|
25805
25853
|
async done() {
|
|
@@ -26026,7 +26074,7 @@ var MessageStream = class _MessageStream {
|
|
|
26026
26074
|
if (done) {
|
|
26027
26075
|
return { value: void 0, done: true };
|
|
26028
26076
|
}
|
|
26029
|
-
return new Promise((
|
|
26077
|
+
return new Promise((resolve26, reject) => readQueue.push({ resolve: resolve26, reject })).then((chunk2) => chunk2 ? { value: chunk2, done: false } : { value: void 0, done: true });
|
|
26030
26078
|
}
|
|
26031
26079
|
const chunk = pushQueue.shift();
|
|
26032
26080
|
return { value: chunk, done: false };
|
|
@@ -26195,34 +26243,34 @@ var { AnthropicError: AnthropicError2, APIError: APIError2, APIConnectionError:
|
|
|
26195
26243
|
var sdk_default = Anthropic;
|
|
26196
26244
|
|
|
26197
26245
|
// src/llm/tools.ts
|
|
26198
|
-
var
|
|
26199
|
-
var
|
|
26246
|
+
var fs9 = __toESM(require("fs"));
|
|
26247
|
+
var path8 = __toESM(require("path"));
|
|
26200
26248
|
var readline2 = __toESM(require("readline"));
|
|
26201
26249
|
|
|
26202
26250
|
// src/harness/agent-md.ts
|
|
26203
|
-
var
|
|
26204
|
-
var
|
|
26251
|
+
var fs8 = __toESM(require("fs"));
|
|
26252
|
+
var path7 = __toESM(require("path"));
|
|
26205
26253
|
var readline = __toESM(require("readline"));
|
|
26206
26254
|
var AGENT_MD_PROPOSAL = "AGENT.md.proposed";
|
|
26207
26255
|
var AGENT_MD_PROPOSAL_META = "AGENT.md.proposed.meta.json";
|
|
26208
26256
|
function writeAgentMdProposal(repoPath, content, rationale) {
|
|
26209
26257
|
const harnessDir = getHarnessDir(repoPath);
|
|
26210
|
-
writeFileSafe(
|
|
26258
|
+
writeFileSafe(path7.join(harnessDir, AGENT_MD_PROPOSAL), content);
|
|
26211
26259
|
writeFileSafe(
|
|
26212
|
-
|
|
26260
|
+
path7.join(harnessDir, AGENT_MD_PROPOSAL_META),
|
|
26213
26261
|
JSON.stringify({ rationale, createdAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2) + "\n"
|
|
26214
26262
|
);
|
|
26215
26263
|
}
|
|
26216
26264
|
function readAgentMdProposal(repoPath) {
|
|
26217
26265
|
const harnessDir = getHarnessDir(repoPath);
|
|
26218
|
-
const proposalPath =
|
|
26219
|
-
const metaPath =
|
|
26220
|
-
if (!
|
|
26221
|
-
const content =
|
|
26266
|
+
const proposalPath = path7.join(harnessDir, AGENT_MD_PROPOSAL);
|
|
26267
|
+
const metaPath = path7.join(harnessDir, AGENT_MD_PROPOSAL_META);
|
|
26268
|
+
if (!fs8.existsSync(proposalPath)) return null;
|
|
26269
|
+
const content = fs8.readFileSync(proposalPath, "utf8");
|
|
26222
26270
|
let rationale = "";
|
|
26223
|
-
if (
|
|
26271
|
+
if (fs8.existsSync(metaPath)) {
|
|
26224
26272
|
try {
|
|
26225
|
-
rationale = JSON.parse(
|
|
26273
|
+
rationale = JSON.parse(fs8.readFileSync(metaPath, "utf8")).rationale ?? "";
|
|
26226
26274
|
} catch {
|
|
26227
26275
|
rationale = "";
|
|
26228
26276
|
}
|
|
@@ -26230,21 +26278,21 @@ function readAgentMdProposal(repoPath) {
|
|
|
26230
26278
|
return {
|
|
26231
26279
|
content,
|
|
26232
26280
|
rationale,
|
|
26233
|
-
createdAt:
|
|
26281
|
+
createdAt: fs8.existsSync(metaPath) ? JSON.parse(fs8.readFileSync(metaPath, "utf8")).createdAt : (/* @__PURE__ */ new Date()).toISOString()
|
|
26234
26282
|
};
|
|
26235
26283
|
}
|
|
26236
26284
|
function clearAgentMdProposal(repoPath) {
|
|
26237
26285
|
const harnessDir = getHarnessDir(repoPath);
|
|
26238
26286
|
for (const file of [AGENT_MD_PROPOSAL, AGENT_MD_PROPOSAL_META]) {
|
|
26239
|
-
const p2 =
|
|
26240
|
-
if (
|
|
26287
|
+
const p2 = path7.join(harnessDir, file);
|
|
26288
|
+
if (fs8.existsSync(p2)) fs8.unlinkSync(p2);
|
|
26241
26289
|
}
|
|
26242
26290
|
}
|
|
26243
26291
|
async function promptApplyAgentMdProposal(repoPath) {
|
|
26244
26292
|
const proposal = readAgentMdProposal(repoPath);
|
|
26245
26293
|
if (!proposal) return false;
|
|
26246
|
-
const agentMdPath =
|
|
26247
|
-
const exists =
|
|
26294
|
+
const agentMdPath = path7.join(repoPath, "AGENT.md");
|
|
26295
|
+
const exists = fs8.existsSync(agentMdPath);
|
|
26248
26296
|
process.stderr.write("\n");
|
|
26249
26297
|
process.stderr.write("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
|
|
26250
26298
|
process.stderr.write("Proposed AGENT.md (repo root)\n");
|
|
@@ -26286,11 +26334,11 @@ async function promptApplyAgentMdProposal(repoPath) {
|
|
|
26286
26334
|
}
|
|
26287
26335
|
async function askYesNo(question) {
|
|
26288
26336
|
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
26289
|
-
return new Promise((
|
|
26337
|
+
return new Promise((resolve26) => {
|
|
26290
26338
|
process.stderr.write(`${question} `);
|
|
26291
26339
|
rl.once("line", (answer) => {
|
|
26292
26340
|
rl.close();
|
|
26293
|
-
|
|
26341
|
+
resolve26(/^y(es)?$/i.test(answer.trim()));
|
|
26294
26342
|
});
|
|
26295
26343
|
});
|
|
26296
26344
|
}
|
|
@@ -26432,13 +26480,13 @@ async function handleAuthoringToolCall(toolName, input, repoPath, harnessDir, ca
|
|
|
26432
26480
|
async function executeAuthoringToolCall(toolName, input, repoPath, harnessDir, callbacks) {
|
|
26433
26481
|
switch (toolName) {
|
|
26434
26482
|
case "readRepoFile": {
|
|
26435
|
-
const filePath =
|
|
26483
|
+
const filePath = path8.join(repoPath, input.path);
|
|
26436
26484
|
const content = readFile(filePath, input.maxChars ?? 8e3);
|
|
26437
26485
|
info(`Read repo: ${input.path}`);
|
|
26438
26486
|
return content;
|
|
26439
26487
|
}
|
|
26440
26488
|
case "listRepoDir": {
|
|
26441
|
-
const dirPath =
|
|
26489
|
+
const dirPath = path8.join(repoPath, input.path ?? ".");
|
|
26442
26490
|
const entries = listDir(dirPath, input.maxFiles ?? 60);
|
|
26443
26491
|
info(`List repo: ${input.path} \u2192 ${entries.length} entries`);
|
|
26444
26492
|
return entries.length > 0 ? entries.join("\n") : "(empty directory)";
|
|
@@ -26463,17 +26511,17 @@ async function executeAuthoringToolCall(toolName, input, repoPath, harnessDir, c
|
|
|
26463
26511
|
const filePath = resolveSafePath(harnessDir, relPath);
|
|
26464
26512
|
writeFileSafe(filePath, input.content);
|
|
26465
26513
|
if (relPath.endsWith(".sh")) {
|
|
26466
|
-
|
|
26514
|
+
fs9.chmodSync(filePath, 493);
|
|
26467
26515
|
}
|
|
26468
26516
|
info(`Wrote harness: ${relPath}`);
|
|
26469
26517
|
return `Written ${relPath} (${input.content.length} bytes)`;
|
|
26470
26518
|
}
|
|
26471
26519
|
case "editHarnessFile": {
|
|
26472
26520
|
const filePath = resolveSafePath(harnessDir, input.path);
|
|
26473
|
-
if (!
|
|
26521
|
+
if (!fs9.existsSync(filePath)) {
|
|
26474
26522
|
return `Error: file not found: ${input.path}`;
|
|
26475
26523
|
}
|
|
26476
|
-
const content =
|
|
26524
|
+
const content = fs9.readFileSync(filePath, "utf8");
|
|
26477
26525
|
const oldStr = input.old_string;
|
|
26478
26526
|
const newStr = input.new_string;
|
|
26479
26527
|
const count = content.split(oldStr).length - 1;
|
|
@@ -26490,10 +26538,10 @@ async function executeAuthoringToolCall(toolName, input, repoPath, harnessDir, c
|
|
|
26490
26538
|
}
|
|
26491
26539
|
case "deleteHarnessFile": {
|
|
26492
26540
|
const filePath = resolveSafePath(harnessDir, input.path);
|
|
26493
|
-
if (!
|
|
26541
|
+
if (!fs9.existsSync(filePath)) {
|
|
26494
26542
|
return `File not found (already deleted): ${input.path}`;
|
|
26495
26543
|
}
|
|
26496
|
-
|
|
26544
|
+
fs9.unlinkSync(filePath);
|
|
26497
26545
|
info(`Deleted harness: ${input.path}`);
|
|
26498
26546
|
return `Deleted ${input.path}`;
|
|
26499
26547
|
}
|
|
@@ -26520,7 +26568,7 @@ async function executeAuthoringToolCall(toolName, input, repoPath, harnessDir, c
|
|
|
26520
26568
|
}
|
|
26521
26569
|
async function askUser(question, options) {
|
|
26522
26570
|
const rl = readline2.createInterface({ input: process.stdin, output: process.stderr });
|
|
26523
|
-
return new Promise((
|
|
26571
|
+
return new Promise((resolve26) => {
|
|
26524
26572
|
process.stderr.write("\n");
|
|
26525
26573
|
process.stderr.write(`\u2753 ${question}
|
|
26526
26574
|
`);
|
|
@@ -26531,7 +26579,7 @@ async function askUser(question, options) {
|
|
|
26531
26579
|
process.stderr.write("> ");
|
|
26532
26580
|
rl.once("line", (answer) => {
|
|
26533
26581
|
rl.close();
|
|
26534
|
-
|
|
26582
|
+
resolve26(answer.trim());
|
|
26535
26583
|
});
|
|
26536
26584
|
});
|
|
26537
26585
|
}
|
|
@@ -26544,29 +26592,29 @@ async function authorHarness(repoPath, apiKey, options = {}) {
|
|
|
26544
26592
|
const mode = options.mode ?? "init";
|
|
26545
26593
|
const systemPromptPath = resolvePromptPath("system-authoring.md");
|
|
26546
26594
|
const agentMdTemplatePath = resolveTemplateFile("AGENT.md.template");
|
|
26547
|
-
let systemPrompt =
|
|
26595
|
+
let systemPrompt = fs10.existsSync(systemPromptPath) ? fs10.readFileSync(systemPromptPath, "utf8") : "Adapt the .har/ boilerplate to match the repository. Edit files directly. Call finishAuthoring when done.";
|
|
26548
26596
|
if (agentMdTemplatePath) {
|
|
26549
26597
|
systemPrompt += `
|
|
26550
26598
|
|
|
26551
26599
|
## AGENT.md template (starting point for proposeAgentMd)
|
|
26552
26600
|
|
|
26553
|
-
${
|
|
26601
|
+
${fs10.readFileSync(agentMdTemplatePath, "utf8")}`;
|
|
26554
26602
|
}
|
|
26555
26603
|
const tools = buildAuthoringTools();
|
|
26556
26604
|
const messages = [];
|
|
26557
26605
|
let rootListing;
|
|
26558
26606
|
try {
|
|
26559
|
-
rootListing =
|
|
26607
|
+
rootListing = fs10.readdirSync(repoPath).slice(0, 50).join("\n");
|
|
26560
26608
|
} catch {
|
|
26561
26609
|
throw new Error(`Cannot read repo at path: ${repoPath}`);
|
|
26562
26610
|
}
|
|
26563
26611
|
let harnessListing;
|
|
26564
26612
|
try {
|
|
26565
|
-
harnessListing =
|
|
26613
|
+
harnessListing = fs10.readdirSync(harnessDir).join("\n");
|
|
26566
26614
|
} catch {
|
|
26567
26615
|
throw new Error(`Cannot read harness dir at: ${harnessDir}`);
|
|
26568
26616
|
}
|
|
26569
|
-
const existingAgentMd =
|
|
26617
|
+
const existingAgentMd = fs10.existsSync(path9.join(repoPath, "AGENT.md")) ? "AGENT.md exists at repo root \u2014 read it before proposing changes via proposeAgentMd." : "No AGENT.md at repo root yet \u2014 propose one via proposeAgentMd.";
|
|
26570
26618
|
const modeInstructions = mode === "maintain" ? `This is a MAINTENANCE run. .har/ already exists and may have been edited by humans.
|
|
26571
26619
|
Update it to reflect current repo changes. Prefer editHarnessFile over writeHarnessFile.
|
|
26572
26620
|
Keep README.md accurate \u2014 it is the index of the harness. ${existingAgentMd}` : `This is an INITIAL setup. A fresh boilerplate has been copied to .har/.
|
|
@@ -26646,8 +26694,8 @@ Try running with --verbose to see what the agent is doing.`
|
|
|
26646
26694
|
}
|
|
26647
26695
|
|
|
26648
26696
|
// src/harness/validator.ts
|
|
26649
|
-
var
|
|
26650
|
-
var
|
|
26697
|
+
var fs13 = __toESM(require("fs"));
|
|
26698
|
+
var path12 = __toESM(require("path"));
|
|
26651
26699
|
|
|
26652
26700
|
// src/utils/shell.ts
|
|
26653
26701
|
var import_child_process = require("child_process");
|
|
@@ -26675,7 +26723,7 @@ function runScript(scriptPath, args = [], options = {}) {
|
|
|
26675
26723
|
function runScriptCapture(scriptPath, args = [], options = {}) {
|
|
26676
26724
|
const stream = options.stream ?? false;
|
|
26677
26725
|
const { stream: _stream, ...spawnOptions } = options;
|
|
26678
|
-
return new Promise((
|
|
26726
|
+
return new Promise((resolve26) => {
|
|
26679
26727
|
const proc = (0, import_child_process.spawn)("bash", [scriptPath, ...args], {
|
|
26680
26728
|
...spawnOptions,
|
|
26681
26729
|
stdio: ["inherit", "pipe", "pipe"]
|
|
@@ -26690,13 +26738,13 @@ function runScriptCapture(scriptPath, args = [], options = {}) {
|
|
|
26690
26738
|
stderr += d2;
|
|
26691
26739
|
if (stream) process.stderr.write(d2);
|
|
26692
26740
|
});
|
|
26693
|
-
proc.on("close", (code) =>
|
|
26741
|
+
proc.on("close", (code) => resolve26({ stdout, stderr, code: code ?? 0 }));
|
|
26694
26742
|
});
|
|
26695
26743
|
}
|
|
26696
26744
|
function runShellCommand(command2, options = {}) {
|
|
26697
26745
|
const stream = options.stream ?? false;
|
|
26698
26746
|
const { stream: _stream, ...spawnOptions } = options;
|
|
26699
|
-
return new Promise((
|
|
26747
|
+
return new Promise((resolve26) => {
|
|
26700
26748
|
const proc = (0, import_child_process.spawn)("bash", ["-lc", command2], {
|
|
26701
26749
|
...spawnOptions,
|
|
26702
26750
|
stdio: ["inherit", "pipe", "pipe"]
|
|
@@ -26711,22 +26759,22 @@ function runShellCommand(command2, options = {}) {
|
|
|
26711
26759
|
stderr += d2;
|
|
26712
26760
|
if (stream) process.stderr.write(d2);
|
|
26713
26761
|
});
|
|
26714
|
-
proc.on("close", (code) =>
|
|
26762
|
+
proc.on("close", (code) => resolve26({ stdout, stderr, code: code ?? 0 }));
|
|
26715
26763
|
});
|
|
26716
26764
|
}
|
|
26717
26765
|
|
|
26718
26766
|
// src/harness/stages.ts
|
|
26719
|
-
var
|
|
26720
|
-
var
|
|
26767
|
+
var fs12 = __toESM(require("fs"));
|
|
26768
|
+
var path11 = __toESM(require("path"));
|
|
26721
26769
|
|
|
26722
26770
|
// src/harness/env.ts
|
|
26723
|
-
var
|
|
26724
|
-
var
|
|
26771
|
+
var fs11 = __toESM(require("fs"));
|
|
26772
|
+
var path10 = __toESM(require("path"));
|
|
26725
26773
|
function readHarnessEnv(repoPath) {
|
|
26726
|
-
const envPath =
|
|
26727
|
-
if (!
|
|
26774
|
+
const envPath = path10.join(getHarnessDir(repoPath), "harness.env");
|
|
26775
|
+
if (!fs11.existsSync(envPath)) return {};
|
|
26728
26776
|
const values = {};
|
|
26729
|
-
for (const line of
|
|
26777
|
+
for (const line of fs11.readFileSync(envPath, "utf8").split("\n")) {
|
|
26730
26778
|
const match = line.match(/^export\s+([A-Z0-9_]+)=(.*)$/);
|
|
26731
26779
|
if (!match) continue;
|
|
26732
26780
|
values[match[1]] = match[2].replace(/^"|"$/g, "");
|
|
@@ -26753,7 +26801,7 @@ var AGENT_REQUIRED_KINDS = /* @__PURE__ */ new Set([
|
|
|
26753
26801
|
"custom"
|
|
26754
26802
|
]);
|
|
26755
26803
|
function getStageRegistryPath(repoPath) {
|
|
26756
|
-
return
|
|
26804
|
+
return path11.join(getHarnessDir(repoPath), STAGE_REGISTRY_FILE);
|
|
26757
26805
|
}
|
|
26758
26806
|
function synthesizeStageRegistry(repoPath) {
|
|
26759
26807
|
const harnessDir = getHarnessDir(repoPath);
|
|
@@ -26828,13 +26876,13 @@ function readAgentSlotsFromHarnessEnv(harnessEnv) {
|
|
|
26828
26876
|
}
|
|
26829
26877
|
function stageScriptExists(harnessDir, stage) {
|
|
26830
26878
|
if (stage.script) {
|
|
26831
|
-
return
|
|
26879
|
+
return fs12.existsSync(path11.join(harnessDir, stage.script));
|
|
26832
26880
|
}
|
|
26833
26881
|
if (stage.command) {
|
|
26834
26882
|
const scriptName = stage.command.split(/\s+/)[0].replace(/^\.\/\.har\//, "").replace(/^\.\//, "");
|
|
26835
|
-
return
|
|
26883
|
+
return fs12.existsSync(path11.join(harnessDir, scriptName));
|
|
26836
26884
|
}
|
|
26837
|
-
return
|
|
26885
|
+
return fs12.existsSync(path11.join(harnessDir, "stages", `${stage.id}.sh`));
|
|
26838
26886
|
}
|
|
26839
26887
|
function addStageIfRunnable(stages, harnessDir, stage) {
|
|
26840
26888
|
const parsed = HarnessStageSchema.parse(stage);
|
|
@@ -26844,10 +26892,10 @@ function addStageIfRunnable(stages, harnessDir, stage) {
|
|
|
26844
26892
|
}
|
|
26845
26893
|
function readStageRegistry(repoPath) {
|
|
26846
26894
|
const registryPath = getStageRegistryPath(repoPath);
|
|
26847
|
-
if (!
|
|
26895
|
+
if (!fs12.existsSync(registryPath)) {
|
|
26848
26896
|
return synthesizeStageRegistry(repoPath);
|
|
26849
26897
|
}
|
|
26850
|
-
const raw = JSON.parse(
|
|
26898
|
+
const raw = JSON.parse(fs12.readFileSync(registryPath, "utf8"));
|
|
26851
26899
|
const parsed = HarnessStageRegistrySchema.safeParse(raw);
|
|
26852
26900
|
if (!parsed.success) {
|
|
26853
26901
|
throw new Error(`Invalid .har/${STAGE_REGISTRY_FILE}: ${parsed.error.message}`);
|
|
@@ -26860,7 +26908,7 @@ function writeStageRegistry(repoPath, registry2) {
|
|
|
26860
26908
|
if (!parsed.success) {
|
|
26861
26909
|
throw new Error(`Invalid stage registry: ${parsed.error.message}`);
|
|
26862
26910
|
}
|
|
26863
|
-
|
|
26911
|
+
fs12.writeFileSync(registryPath, JSON.stringify(parsed.data, null, 2) + "\n");
|
|
26864
26912
|
}
|
|
26865
26913
|
function listStages(repoPath) {
|
|
26866
26914
|
return readStageRegistry(repoPath).stages;
|
|
@@ -26921,6 +26969,7 @@ var REQUIRED_FILES_DEFAULT = [
|
|
|
26921
26969
|
"harness.env",
|
|
26922
26970
|
"setup-infra.sh",
|
|
26923
26971
|
"launch.sh",
|
|
26972
|
+
"provision-toolchain.sh",
|
|
26924
26973
|
"verify.sh",
|
|
26925
26974
|
"teardown.sh",
|
|
26926
26975
|
"agent-cli.sh",
|
|
@@ -26939,6 +26988,7 @@ var REQUIRED_FILES_IOS = [
|
|
|
26939
26988
|
"harness.env",
|
|
26940
26989
|
"setup-infra.sh",
|
|
26941
26990
|
"launch.sh",
|
|
26991
|
+
"provision-toolchain.sh",
|
|
26942
26992
|
"verify.sh",
|
|
26943
26993
|
"teardown.sh",
|
|
26944
26994
|
"agent-cli.sh",
|
|
@@ -26955,6 +27005,7 @@ function getRequiredFiles(repoPath) {
|
|
|
26955
27005
|
var SHELL_SCRIPTS = [
|
|
26956
27006
|
"setup-infra.sh",
|
|
26957
27007
|
"launch.sh",
|
|
27008
|
+
"provision-toolchain.sh",
|
|
26958
27009
|
"verify.sh",
|
|
26959
27010
|
"teardown.sh",
|
|
26960
27011
|
"agent-cli.sh",
|
|
@@ -26963,22 +27014,22 @@ var SHELL_SCRIPTS = [
|
|
|
26963
27014
|
function validateHarness(repoPath) {
|
|
26964
27015
|
const harnessDir = getHarnessDir(repoPath);
|
|
26965
27016
|
const issues = [];
|
|
26966
|
-
if (!
|
|
27017
|
+
if (!fs13.existsSync(harnessDir)) {
|
|
26967
27018
|
return {
|
|
26968
27019
|
pass: false,
|
|
26969
27020
|
issues: [{ file: ".har", message: "Harness directory not found", severity: "error" }]
|
|
26970
27021
|
};
|
|
26971
27022
|
}
|
|
26972
27023
|
for (const file of getRequiredFiles(repoPath)) {
|
|
26973
|
-
const filePath =
|
|
26974
|
-
if (!
|
|
27024
|
+
const filePath = path12.join(harnessDir, file);
|
|
27025
|
+
if (!fs13.existsSync(filePath)) {
|
|
26975
27026
|
issues.push({ file, message: "Required file missing", severity: "error" });
|
|
26976
27027
|
}
|
|
26977
27028
|
}
|
|
26978
27029
|
for (const script of SHELL_SCRIPTS) {
|
|
26979
|
-
const scriptPath =
|
|
26980
|
-
if (!
|
|
26981
|
-
const stat =
|
|
27030
|
+
const scriptPath = path12.join(harnessDir, script);
|
|
27031
|
+
if (!fs13.existsSync(scriptPath)) continue;
|
|
27032
|
+
const stat = fs13.statSync(scriptPath);
|
|
26982
27033
|
if (!(stat.mode & 73)) {
|
|
26983
27034
|
issues.push({ file: script, message: "Script is not executable", severity: "warning" });
|
|
26984
27035
|
}
|
|
@@ -26993,9 +27044,9 @@ function validateHarness(repoPath) {
|
|
|
26993
27044
|
}
|
|
26994
27045
|
const manifest = readManifest(repoPath);
|
|
26995
27046
|
const profile = manifest?.profile ?? "default";
|
|
26996
|
-
const ecosystemPath =
|
|
26997
|
-
if (profile !== "cli" &&
|
|
26998
|
-
const content =
|
|
27047
|
+
const ecosystemPath = path12.join(harnessDir, "ecosystem.agent.template.cjs");
|
|
27048
|
+
if (profile !== "cli" && fs13.existsSync(ecosystemPath)) {
|
|
27049
|
+
const content = fs13.readFileSync(ecosystemPath, "utf8");
|
|
26999
27050
|
if (!content.includes("module.exports")) {
|
|
27000
27051
|
issues.push({
|
|
27001
27052
|
file: "ecosystem.agent.template.cjs",
|
|
@@ -27004,9 +27055,9 @@ function validateHarness(repoPath) {
|
|
|
27004
27055
|
});
|
|
27005
27056
|
}
|
|
27006
27057
|
}
|
|
27007
|
-
const harnessEnvPath =
|
|
27008
|
-
if (
|
|
27009
|
-
const content =
|
|
27058
|
+
const harnessEnvPath = path12.join(harnessDir, "harness.env");
|
|
27059
|
+
if (fs13.existsSync(harnessEnvPath)) {
|
|
27060
|
+
const content = fs13.readFileSync(harnessEnvPath, "utf8");
|
|
27010
27061
|
if (content.includes("TODO: set migrate command")) {
|
|
27011
27062
|
issues.push({ file: "harness.env", message: "Migrate command still has TODO", severity: "warning" });
|
|
27012
27063
|
}
|
|
@@ -27014,17 +27065,17 @@ function validateHarness(repoPath) {
|
|
|
27014
27065
|
issues.push({ file: "harness.env", message: "Seed command still has TODO", severity: "warning" });
|
|
27015
27066
|
}
|
|
27016
27067
|
}
|
|
27017
|
-
const verifyPath =
|
|
27018
|
-
if (
|
|
27019
|
-
const content =
|
|
27068
|
+
const verifyPath = path12.join(harnessDir, "verify.sh");
|
|
27069
|
+
if (fs13.existsSync(verifyPath)) {
|
|
27070
|
+
const content = fs13.readFileSync(verifyPath, "utf8");
|
|
27020
27071
|
if (content.includes("echo 'TODO:")) {
|
|
27021
27072
|
issues.push({ file: "verify.sh", message: "Verification steps still have TODO placeholders", severity: "warning" });
|
|
27022
27073
|
}
|
|
27023
27074
|
}
|
|
27024
|
-
const stagesPath =
|
|
27025
|
-
if (
|
|
27075
|
+
const stagesPath = path12.join(harnessDir, "stages.json");
|
|
27076
|
+
if (fs13.existsSync(stagesPath)) {
|
|
27026
27077
|
try {
|
|
27027
|
-
JSON.parse(
|
|
27078
|
+
JSON.parse(fs13.readFileSync(stagesPath, "utf8"));
|
|
27028
27079
|
const registry2 = readStageRegistry(repoPath);
|
|
27029
27080
|
if (registry2.stages.length === 0) {
|
|
27030
27081
|
issues.push({ file: "stages.json", message: "No harness stages declared", severity: "warning" });
|
|
@@ -27039,9 +27090,9 @@ function validateHarness(repoPath) {
|
|
|
27039
27090
|
} else {
|
|
27040
27091
|
issues.push({ file: "stages.json", message: "Stage registry missing", severity: "warning" });
|
|
27041
27092
|
}
|
|
27042
|
-
const readmePath =
|
|
27043
|
-
if (
|
|
27044
|
-
const content =
|
|
27093
|
+
const readmePath = path12.join(harnessDir, "README.md");
|
|
27094
|
+
if (fs13.existsSync(readmePath)) {
|
|
27095
|
+
const content = fs13.readFileSync(readmePath, "utf8");
|
|
27045
27096
|
if (!content.includes(".har")) {
|
|
27046
27097
|
issues.push({ file: "README.md", message: "README should document .har/ paths", severity: "warning" });
|
|
27047
27098
|
}
|
|
@@ -27055,8 +27106,8 @@ function validateHarness(repoPath) {
|
|
|
27055
27106
|
async function smokeTestHarness(repoPath) {
|
|
27056
27107
|
const harnessDir = getHarnessDir(repoPath);
|
|
27057
27108
|
const issues = [];
|
|
27058
|
-
const setupScript =
|
|
27059
|
-
if (
|
|
27109
|
+
const setupScript = path12.join(harnessDir, "setup-infra.sh");
|
|
27110
|
+
if (fs13.existsSync(setupScript)) {
|
|
27060
27111
|
const result = run(`bash "${setupScript}"`, { cwd: repoPath });
|
|
27061
27112
|
if (result.code !== 0) {
|
|
27062
27113
|
issues.push({
|
|
@@ -27071,8 +27122,8 @@ async function smokeTestHarness(repoPath) {
|
|
|
27071
27122
|
}
|
|
27072
27123
|
|
|
27073
27124
|
// src/harness/drift.ts
|
|
27074
|
-
var
|
|
27075
|
-
var
|
|
27125
|
+
var fs14 = __toESM(require("fs"));
|
|
27126
|
+
var path13 = __toESM(require("path"));
|
|
27076
27127
|
var PROFILE_DIRS2 = {
|
|
27077
27128
|
default: "har-boilerplate",
|
|
27078
27129
|
cli: "har-boilerplate-cli",
|
|
@@ -27083,37 +27134,90 @@ var CLI_EXPECTED_ABSENT = /* @__PURE__ */ new Set([
|
|
|
27083
27134
|
"env.template",
|
|
27084
27135
|
"attach.sh"
|
|
27085
27136
|
]);
|
|
27137
|
+
var APP_PORT_VARS = [
|
|
27138
|
+
"HARNESS_FE_BASE_PORT",
|
|
27139
|
+
"HARNESS_API_BASE_PORT",
|
|
27140
|
+
"HARNESS_PORT_STEP"
|
|
27141
|
+
];
|
|
27142
|
+
var INFRA_PORT_VARS_BY_SERVICE = {
|
|
27143
|
+
db: [
|
|
27144
|
+
"HARNESS_DB_PORT_DEFAULT",
|
|
27145
|
+
"HARNESS_DB_PORT_SCAN_START",
|
|
27146
|
+
"HARNESS_DB_PORT_SCAN_END"
|
|
27147
|
+
],
|
|
27148
|
+
minio: [
|
|
27149
|
+
"HARNESS_MINIO_PORT_DEFAULT",
|
|
27150
|
+
"HARNESS_MINIO_PORT_SCAN_START",
|
|
27151
|
+
"HARNESS_MINIO_PORT_SCAN_END",
|
|
27152
|
+
"HARNESS_MINIO_CONSOLE_PORT_DEFAULT",
|
|
27153
|
+
"HARNESS_MINIO_CONSOLE_PORT_SCAN_START",
|
|
27154
|
+
"HARNESS_MINIO_CONSOLE_PORT_SCAN_END"
|
|
27155
|
+
],
|
|
27156
|
+
mailpit: [
|
|
27157
|
+
"HARNESS_MAILPIT_WEB_PORT_DEFAULT",
|
|
27158
|
+
"HARNESS_MAILPIT_WEB_PORT_SCAN_START",
|
|
27159
|
+
"HARNESS_MAILPIT_WEB_PORT_SCAN_END",
|
|
27160
|
+
"HARNESS_MAILPIT_SMTP_PORT_DEFAULT",
|
|
27161
|
+
"HARNESS_MAILPIT_SMTP_PORT_SCAN_START",
|
|
27162
|
+
"HARNESS_MAILPIT_SMTP_PORT_SCAN_END"
|
|
27163
|
+
],
|
|
27164
|
+
"headless-browser": [
|
|
27165
|
+
"HARNESS_BROWSER_PORT_DEFAULT",
|
|
27166
|
+
"HARNESS_BROWSER_PORT_SCAN_START",
|
|
27167
|
+
"HARNESS_BROWSER_PORT_SCAN_END"
|
|
27168
|
+
]
|
|
27169
|
+
};
|
|
27170
|
+
function missingPortDocumentationVars(profile, env3) {
|
|
27171
|
+
const missing = [];
|
|
27172
|
+
if (profile === "default") {
|
|
27173
|
+
for (const key of APP_PORT_VARS) {
|
|
27174
|
+
if (!(key in env3)) missing.push(key);
|
|
27175
|
+
}
|
|
27176
|
+
}
|
|
27177
|
+
if (profile === "cli" && !("HARNESS_PORT_STEP" in env3)) {
|
|
27178
|
+
missing.push("HARNESS_PORT_STEP");
|
|
27179
|
+
}
|
|
27180
|
+
const services = (env3.HARNESS_INFRA_SERVICES ?? "").trim().split(/\s+/).filter(Boolean);
|
|
27181
|
+
for (const service of services) {
|
|
27182
|
+
const vars = INFRA_PORT_VARS_BY_SERVICE[service];
|
|
27183
|
+
if (!vars) continue;
|
|
27184
|
+
for (const key of vars) {
|
|
27185
|
+
if (!(key in env3)) missing.push(key);
|
|
27186
|
+
}
|
|
27187
|
+
}
|
|
27188
|
+
return [...new Set(missing)];
|
|
27189
|
+
}
|
|
27086
27190
|
function substituteProjectName(content, projectName) {
|
|
27087
27191
|
return content.replace(/__PROJECT_NAME__/g, projectName).replace(/template___PROJECT_NAME__/g, `template_${projectName}`);
|
|
27088
27192
|
}
|
|
27089
27193
|
function listBoilerplateFiles(boilerplateDir) {
|
|
27090
|
-
if (!
|
|
27091
|
-
return
|
|
27194
|
+
if (!fs14.existsSync(boilerplateDir)) return [];
|
|
27195
|
+
return fs14.readdirSync(boilerplateDir, { withFileTypes: true }).filter((entry) => entry.isFile()).map((entry) => entry.name).sort();
|
|
27092
27196
|
}
|
|
27093
27197
|
function compareHarnessToTemplate(repoPath) {
|
|
27094
|
-
const resolved =
|
|
27198
|
+
const resolved = path13.resolve(repoPath);
|
|
27095
27199
|
const manifest = readManifest(resolved);
|
|
27096
27200
|
const profile = manifest?.profile ?? "default";
|
|
27097
27201
|
const harnessDir = getHarnessDir(resolved);
|
|
27098
|
-
const projectName =
|
|
27099
|
-
const boilerplateDir =
|
|
27202
|
+
const projectName = path13.basename(resolved).toLowerCase().replace(/[^a-z0-9]/g, "_");
|
|
27203
|
+
const boilerplateDir = path13.join(resolveTemplatesDir(), PROFILE_DIRS2[profile]);
|
|
27100
27204
|
const templateFiles = listBoilerplateFiles(boilerplateDir);
|
|
27101
27205
|
const missing = [];
|
|
27102
27206
|
const checksumMismatch = [];
|
|
27103
27207
|
const extra = [];
|
|
27104
27208
|
const unchanged = [];
|
|
27105
27209
|
for (const file of templateFiles) {
|
|
27106
|
-
const templatePath =
|
|
27107
|
-
const harnessPath =
|
|
27108
|
-
let templateContent =
|
|
27210
|
+
const templatePath = path13.join(boilerplateDir, file);
|
|
27211
|
+
const harnessPath = path13.join(harnessDir, file);
|
|
27212
|
+
let templateContent = fs14.readFileSync(templatePath, "utf8");
|
|
27109
27213
|
if (file === "harness.env") {
|
|
27110
27214
|
templateContent = substituteProjectName(templateContent, projectName);
|
|
27111
27215
|
}
|
|
27112
|
-
if (!
|
|
27216
|
+
if (!fs14.existsSync(harnessPath)) {
|
|
27113
27217
|
missing.push(file);
|
|
27114
27218
|
continue;
|
|
27115
27219
|
}
|
|
27116
|
-
const harnessChecksum = computeFileChecksum(
|
|
27220
|
+
const harnessChecksum = computeFileChecksum(fs14.readFileSync(harnessPath, "utf8"));
|
|
27117
27221
|
const templateChecksum = computeFileChecksum(templateContent);
|
|
27118
27222
|
if (harnessChecksum === templateChecksum) {
|
|
27119
27223
|
unchanged.push(file);
|
|
@@ -27121,12 +27225,11 @@ function compareHarnessToTemplate(repoPath) {
|
|
|
27121
27225
|
checksumMismatch.push(file);
|
|
27122
27226
|
}
|
|
27123
27227
|
}
|
|
27124
|
-
if (
|
|
27125
|
-
for (const file of
|
|
27126
|
-
const full =
|
|
27127
|
-
if (!
|
|
27228
|
+
if (fs14.existsSync(harnessDir)) {
|
|
27229
|
+
for (const file of fs14.readdirSync(harnessDir)) {
|
|
27230
|
+
const full = path13.join(harnessDir, file);
|
|
27231
|
+
if (!fs14.statSync(full).isFile()) continue;
|
|
27128
27232
|
if (file === "manifest.json" || file.startsWith("ADAPT-PROMPT")) continue;
|
|
27129
|
-
if (templateFiles.includes(file)) continue;
|
|
27130
27233
|
if (profile === "cli" && CLI_EXPECTED_ABSENT.has(file)) {
|
|
27131
27234
|
extra.push(file);
|
|
27132
27235
|
} else if (!templateFiles.includes(file)) {
|
|
@@ -27135,6 +27238,8 @@ function compareHarnessToTemplate(repoPath) {
|
|
|
27135
27238
|
}
|
|
27136
27239
|
}
|
|
27137
27240
|
const installed = manifest?.generatorVersion;
|
|
27241
|
+
const harnessEnv = readHarnessEnv(resolved);
|
|
27242
|
+
const missingPortVars = missingPortDocumentationVars(profile, harnessEnv);
|
|
27138
27243
|
return {
|
|
27139
27244
|
generatorVersion: {
|
|
27140
27245
|
installed,
|
|
@@ -27144,71 +27249,355 @@ function compareHarnessToTemplate(repoPath) {
|
|
|
27144
27249
|
missing,
|
|
27145
27250
|
checksumMismatch,
|
|
27146
27251
|
extra,
|
|
27147
|
-
unchanged
|
|
27252
|
+
unchanged,
|
|
27253
|
+
missingPortVars
|
|
27148
27254
|
};
|
|
27149
27255
|
}
|
|
27150
27256
|
|
|
27151
|
-
// src/harness/
|
|
27257
|
+
// src/harness/maintain-bundle.ts
|
|
27152
27258
|
var fs15 = __toESM(require("fs"));
|
|
27153
|
-
var
|
|
27259
|
+
var os2 = __toESM(require("os"));
|
|
27260
|
+
var path14 = __toESM(require("path"));
|
|
27261
|
+
var MAINTAIN_DIR = "maintain";
|
|
27262
|
+
var PROFILE_DIRS3 = {
|
|
27263
|
+
default: "har-boilerplate",
|
|
27264
|
+
cli: "har-boilerplate-cli",
|
|
27265
|
+
ios: "har-boilerplate-ios"
|
|
27266
|
+
};
|
|
27267
|
+
function substituteProjectName2(content, projectName) {
|
|
27268
|
+
return content.replace(/__PROJECT_NAME__/g, projectName).replace(/template___PROJECT_NAME__/g, `template_${projectName}`);
|
|
27269
|
+
}
|
|
27270
|
+
function projectNameFromRepo(repoPath) {
|
|
27271
|
+
return path14.basename(repoPath).toLowerCase().replace(/[^a-z0-9]/g, "_");
|
|
27272
|
+
}
|
|
27273
|
+
function boilerplateDirForProfile(profile) {
|
|
27274
|
+
return path14.join(resolveTemplatesDir(), PROFILE_DIRS3[profile]);
|
|
27275
|
+
}
|
|
27276
|
+
function readBundledTemplateContent(repoPath, profile, file) {
|
|
27277
|
+
const templatePath = path14.join(boilerplateDirForProfile(profile), file);
|
|
27278
|
+
let content = fs15.readFileSync(templatePath, "utf8");
|
|
27279
|
+
if (file === "harness.env") {
|
|
27280
|
+
content = substituteProjectName2(content, projectNameFromRepo(repoPath));
|
|
27281
|
+
}
|
|
27282
|
+
return content;
|
|
27283
|
+
}
|
|
27284
|
+
function staleFileHint(file) {
|
|
27285
|
+
if (file.endsWith(".sh")) {
|
|
27286
|
+
return "Legacy harness script \u2014 merge behavior into current templates, then delete.";
|
|
27287
|
+
}
|
|
27288
|
+
if (file.endsWith(".local") || file.includes(".env")) {
|
|
27289
|
+
return "Local override \u2014 move needed values into harness.env or delete if obsolete.";
|
|
27290
|
+
}
|
|
27291
|
+
return "Not in the current bundled template \u2014 review, merge, or delete.";
|
|
27292
|
+
}
|
|
27293
|
+
function actionHint(file, kind2) {
|
|
27294
|
+
if (kind2 === "missing") {
|
|
27295
|
+
if (file === "provision-toolchain.sh") {
|
|
27296
|
+
return "Add this file and ensure launch.sh calls it to provision the toolchain.";
|
|
27297
|
+
}
|
|
27298
|
+
return `Copy/adapt from maintain/templates/${file} into .har/${file}.`;
|
|
27299
|
+
}
|
|
27300
|
+
if (file === "verify.sh") {
|
|
27301
|
+
return "Merge template upgrades into .har/verify.sh \u2014 do not blind-overwrite repo-specific checks.";
|
|
27302
|
+
}
|
|
27303
|
+
if (file === "harness.env") {
|
|
27304
|
+
return "Merge template changes; keep repo-specific commands and add any missing port vars.";
|
|
27305
|
+
}
|
|
27306
|
+
return `Read maintain/diffs/${file}.diff and merge into .har/${file}.`;
|
|
27307
|
+
}
|
|
27308
|
+
function createUnifiedDiff(installedContent, templateContent, installedLabel, templateLabel) {
|
|
27309
|
+
const tmpDir = fs15.mkdtempSync(path14.join(os2.tmpdir(), "har-maintain-diff-"));
|
|
27310
|
+
try {
|
|
27311
|
+
const installedPath = path14.join(tmpDir, "installed");
|
|
27312
|
+
const templatePath = path14.join(tmpDir, "template");
|
|
27313
|
+
fs15.writeFileSync(installedPath, installedContent);
|
|
27314
|
+
fs15.writeFileSync(templatePath, templateContent);
|
|
27315
|
+
const result = run(`diff -u "${installedPath}" "${templatePath}"`);
|
|
27316
|
+
const body = result.stdout.trim();
|
|
27317
|
+
if (!body) return "";
|
|
27318
|
+
return `--- ${installedLabel}
|
|
27319
|
+
+++ ${templateLabel}
|
|
27320
|
+
${body.split("\n").slice(2).join("\n")}
|
|
27321
|
+
`;
|
|
27322
|
+
} finally {
|
|
27323
|
+
fs15.rmSync(tmpDir, { recursive: true, force: true });
|
|
27324
|
+
}
|
|
27325
|
+
}
|
|
27326
|
+
function buildActions(repoPath, profile, drift) {
|
|
27327
|
+
const harnessDir = getHarnessDir(repoPath);
|
|
27328
|
+
const actions = [];
|
|
27329
|
+
for (const file of drift.missing) {
|
|
27330
|
+
actions.push({
|
|
27331
|
+
file,
|
|
27332
|
+
kind: "missing",
|
|
27333
|
+
template: `maintain/templates/${file}`,
|
|
27334
|
+
hint: actionHint(file, "missing")
|
|
27335
|
+
});
|
|
27336
|
+
}
|
|
27337
|
+
for (const file of drift.checksumMismatch) {
|
|
27338
|
+
const installedPath = path14.join(harnessDir, file);
|
|
27339
|
+
const installedRel = `maintain/installed/${file}`;
|
|
27340
|
+
const templateRel = `maintain/templates/${file}`;
|
|
27341
|
+
const diffRel = `maintain/diffs/${file}.diff`;
|
|
27342
|
+
actions.push({
|
|
27343
|
+
file,
|
|
27344
|
+
kind: "drift",
|
|
27345
|
+
template: templateRel,
|
|
27346
|
+
installed: fs15.existsSync(installedPath) ? installedRel : void 0,
|
|
27347
|
+
diff: diffRel,
|
|
27348
|
+
hint: actionHint(file, "drift")
|
|
27349
|
+
});
|
|
27350
|
+
}
|
|
27351
|
+
return actions.sort((a2, b2) => a2.file.localeCompare(b2.file));
|
|
27352
|
+
}
|
|
27353
|
+
function buildStale(drift) {
|
|
27354
|
+
return drift.extra.map((file) => ({ file, hint: staleFileHint(file) })).sort((a2, b2) => a2.file.localeCompare(b2.file));
|
|
27355
|
+
}
|
|
27356
|
+
function buildReadme(report) {
|
|
27357
|
+
const lines = [
|
|
27358
|
+
"# Harness maintenance bundle",
|
|
27359
|
+
"",
|
|
27360
|
+
`Generated by \`har env maintain\` on ${report.generatedAt}.`,
|
|
27361
|
+
`Bundled generator: ${report.generatorVersion.bundled} | Installed: ${report.generatorVersion.installed ?? "(unknown)"} | Profile: ${report.profile}`,
|
|
27362
|
+
"",
|
|
27363
|
+
"Reference templates and diffs live in this directory. Edit live files in `.har/` \u2014 not `maintain/templates/` directly.",
|
|
27364
|
+
"",
|
|
27365
|
+
"## Validation (fix before `--finalize`)",
|
|
27366
|
+
""
|
|
27367
|
+
];
|
|
27368
|
+
if (report.validation.errors.length === 0 && report.validation.warnings.length === 0) {
|
|
27369
|
+
lines.push("- All structural checks passed.");
|
|
27370
|
+
} else {
|
|
27371
|
+
for (const issue2 of report.validation.errors) {
|
|
27372
|
+
lines.push(`- \u2717 **${issue2.file}**: ${issue2.message}`);
|
|
27373
|
+
}
|
|
27374
|
+
for (const issue2 of report.validation.warnings) {
|
|
27375
|
+
lines.push(`- \u26A0 **${issue2.file}**: ${issue2.message}`);
|
|
27376
|
+
}
|
|
27377
|
+
}
|
|
27378
|
+
lines.push("", "## Drift actions", "");
|
|
27379
|
+
if (report.actions.length === 0) {
|
|
27380
|
+
lines.push("No missing or drifted template files.");
|
|
27381
|
+
} else {
|
|
27382
|
+
lines.push("| File | Status | Reference |");
|
|
27383
|
+
lines.push("|------|--------|-----------|");
|
|
27384
|
+
for (const action of report.actions) {
|
|
27385
|
+
const ref = action.kind === "missing" ? `templates/${action.file}` : `diffs/${action.file}.diff`;
|
|
27386
|
+
lines.push(`| ${action.file} | ${action.kind} | ${ref} |`);
|
|
27387
|
+
}
|
|
27388
|
+
}
|
|
27389
|
+
if (report.missingPortVars.length > 0) {
|
|
27390
|
+
lines.push("", "## Missing port vars in harness.env", "");
|
|
27391
|
+
for (const v2 of report.missingPortVars) {
|
|
27392
|
+
lines.push(`- \`${v2}\` \u2014 copy from \`maintain/templates/harness.env\``);
|
|
27393
|
+
}
|
|
27394
|
+
}
|
|
27395
|
+
lines.push("", "## Stale files (review)", "");
|
|
27396
|
+
if (report.stale.length === 0) {
|
|
27397
|
+
lines.push("None detected.");
|
|
27398
|
+
} else {
|
|
27399
|
+
for (const stale of report.stale) {
|
|
27400
|
+
lines.push(`- **${stale.file}** \u2014 ${stale.hint}`);
|
|
27401
|
+
}
|
|
27402
|
+
}
|
|
27403
|
+
lines.push(
|
|
27404
|
+
"",
|
|
27405
|
+
"## Next steps",
|
|
27406
|
+
"",
|
|
27407
|
+
"1. Resolve each drift action (merge diffs; keep repo-specific customizations).",
|
|
27408
|
+
"2. Review stale files \u2014 delete or merge superseded scripts.",
|
|
27409
|
+
"3. Re-run `har env maintain` until validation passes and drift is clear.",
|
|
27410
|
+
'4. `har env maintain --finalize --summary "<what changed>"`',
|
|
27411
|
+
""
|
|
27412
|
+
);
|
|
27413
|
+
return lines.join("\n");
|
|
27414
|
+
}
|
|
27415
|
+
function writeBundleArtifacts(repoPath, profile, drift, report) {
|
|
27416
|
+
const harnessDir = getHarnessDir(repoPath);
|
|
27417
|
+
const bundleDir = path14.join(harnessDir, MAINTAIN_DIR);
|
|
27418
|
+
if (fs15.existsSync(bundleDir)) {
|
|
27419
|
+
fs15.rmSync(bundleDir, { recursive: true, force: true });
|
|
27420
|
+
}
|
|
27421
|
+
const dirs = [
|
|
27422
|
+
bundleDir,
|
|
27423
|
+
path14.join(bundleDir, "templates"),
|
|
27424
|
+
path14.join(bundleDir, "installed"),
|
|
27425
|
+
path14.join(bundleDir, "diffs"),
|
|
27426
|
+
path14.join(bundleDir, "stale")
|
|
27427
|
+
];
|
|
27428
|
+
for (const dir of dirs) {
|
|
27429
|
+
fs15.mkdirSync(dir, { recursive: true });
|
|
27430
|
+
}
|
|
27431
|
+
const affectedFiles = [...drift.missing, ...drift.checksumMismatch];
|
|
27432
|
+
for (const file of affectedFiles) {
|
|
27433
|
+
const templateContent = readBundledTemplateContent(repoPath, profile, file);
|
|
27434
|
+
writeFileSafe(path14.join(bundleDir, "templates", file), templateContent);
|
|
27435
|
+
const harnessPath = path14.join(harnessDir, file);
|
|
27436
|
+
if (fs15.existsSync(harnessPath)) {
|
|
27437
|
+
const installedContent = fs15.readFileSync(harnessPath, "utf8");
|
|
27438
|
+
writeFileSafe(path14.join(bundleDir, "installed", file), installedContent);
|
|
27439
|
+
const diff = createUnifiedDiff(
|
|
27440
|
+
installedContent,
|
|
27441
|
+
templateContent,
|
|
27442
|
+
`installed/${file}`,
|
|
27443
|
+
`templates/${file}`
|
|
27444
|
+
);
|
|
27445
|
+
if (diff) {
|
|
27446
|
+
writeFileSafe(path14.join(bundleDir, "diffs", `${file}.diff`), diff);
|
|
27447
|
+
}
|
|
27448
|
+
}
|
|
27449
|
+
}
|
|
27450
|
+
writeFileSafe(path14.join(bundleDir, "README.md"), buildReadme(report));
|
|
27451
|
+
writeFileSafe(path14.join(bundleDir, "drift-report.json"), JSON.stringify(report, null, 2) + "\n");
|
|
27452
|
+
writeFileSafe(
|
|
27453
|
+
path14.join(bundleDir, "validation.json"),
|
|
27454
|
+
JSON.stringify(report.validation, null, 2) + "\n"
|
|
27455
|
+
);
|
|
27456
|
+
if (report.stale.length > 0) {
|
|
27457
|
+
const staleLines = [
|
|
27458
|
+
"# Stale harness files",
|
|
27459
|
+
"",
|
|
27460
|
+
"These files exist in `.har/` but are not part of the current bundled template.",
|
|
27461
|
+
"",
|
|
27462
|
+
...report.stale.map((s2) => `- **${s2.file}** \u2014 ${s2.hint}`),
|
|
27463
|
+
""
|
|
27464
|
+
];
|
|
27465
|
+
writeFileSafe(path14.join(bundleDir, "stale", "MANIFEST.md"), staleLines.join("\n"));
|
|
27466
|
+
}
|
|
27467
|
+
return bundleDir;
|
|
27468
|
+
}
|
|
27469
|
+
function buildMaintainBundle(repoPath, validation2, drift) {
|
|
27470
|
+
const manifest = readManifest(repoPath);
|
|
27471
|
+
const profile = manifest?.profile ?? "default";
|
|
27472
|
+
const errors = validation2.issues.filter((i2) => i2.severity === "error");
|
|
27473
|
+
const warnings = validation2.issues.filter((i2) => i2.severity === "warning");
|
|
27474
|
+
const report = {
|
|
27475
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
27476
|
+
generatorVersion: drift.generatorVersion,
|
|
27477
|
+
profile,
|
|
27478
|
+
actions: buildActions(repoPath, profile, drift),
|
|
27479
|
+
stale: buildStale(drift),
|
|
27480
|
+
missingPortVars: drift.missingPortVars,
|
|
27481
|
+
validation: {
|
|
27482
|
+
pass: validation2.pass,
|
|
27483
|
+
errors,
|
|
27484
|
+
warnings
|
|
27485
|
+
}
|
|
27486
|
+
};
|
|
27487
|
+
const bundleDir = writeBundleArtifacts(repoPath, profile, drift, report);
|
|
27488
|
+
return { bundleDir, report };
|
|
27489
|
+
}
|
|
27490
|
+
function removeMaintainBundle(repoPath) {
|
|
27491
|
+
const bundleDir = path14.join(getHarnessDir(repoPath), MAINTAIN_DIR);
|
|
27492
|
+
if (fs15.existsSync(bundleDir)) {
|
|
27493
|
+
fs15.rmSync(bundleDir, { recursive: true, force: true });
|
|
27494
|
+
}
|
|
27495
|
+
}
|
|
27496
|
+
function formatMaintainBundlePromptSection(report) {
|
|
27497
|
+
const lines = [
|
|
27498
|
+
"## Step 0 \u2014 Read the maintenance bundle",
|
|
27499
|
+
"",
|
|
27500
|
+
"Open `.har/maintain/README.md` and `.har/maintain/drift-report.json`.",
|
|
27501
|
+
"All reference templates are under `.har/maintain/templates/`.",
|
|
27502
|
+
"Do **not** read files from the globally installed har package.",
|
|
27503
|
+
""
|
|
27504
|
+
];
|
|
27505
|
+
if (report.validation.errors.length > 0) {
|
|
27506
|
+
lines.push("### Validation blockers (fix first)", "");
|
|
27507
|
+
for (const issue2 of report.validation.errors) {
|
|
27508
|
+
lines.push(`- \u2717 **${issue2.file}**: ${issue2.message}`);
|
|
27509
|
+
}
|
|
27510
|
+
lines.push("");
|
|
27511
|
+
}
|
|
27512
|
+
if (report.actions.length > 0) {
|
|
27513
|
+
lines.push("### Drift actions", "", "| File | Status | Reference |", "|------|--------|-----------|");
|
|
27514
|
+
for (const action of report.actions) {
|
|
27515
|
+
const ref = action.kind === "missing" ? `maintain/templates/${action.file}` : `maintain/diffs/${action.file}.diff`;
|
|
27516
|
+
lines.push(`| ${action.file} | ${action.kind} | ${ref} |`);
|
|
27517
|
+
}
|
|
27518
|
+
lines.push("");
|
|
27519
|
+
}
|
|
27520
|
+
if (report.stale.length > 0) {
|
|
27521
|
+
lines.push("### Stale files", "");
|
|
27522
|
+
for (const stale of report.stale) {
|
|
27523
|
+
lines.push(`- **${stale.file}** \u2014 ${stale.hint}`);
|
|
27524
|
+
}
|
|
27525
|
+
lines.push("");
|
|
27526
|
+
}
|
|
27527
|
+
if (report.missingPortVars.length > 0) {
|
|
27528
|
+
lines.push("### Missing port vars in harness.env", "");
|
|
27529
|
+
for (const v2 of report.missingPortVars) {
|
|
27530
|
+
lines.push(`- \`${v2}\` \u2014 see \`maintain/templates/harness.env\``);
|
|
27531
|
+
}
|
|
27532
|
+
lines.push("");
|
|
27533
|
+
}
|
|
27534
|
+
if (report.actions.length === 0 && report.stale.length === 0 && report.missingPortVars.length === 0 && report.validation.errors.length === 0) {
|
|
27535
|
+
lines.push("No template drift detected. Review repo stack changes manually if needed.", "");
|
|
27536
|
+
}
|
|
27537
|
+
return lines.join("\n");
|
|
27538
|
+
}
|
|
27539
|
+
|
|
27540
|
+
// src/harness/stage-templates.ts
|
|
27541
|
+
var fs17 = __toESM(require("fs"));
|
|
27542
|
+
var path15 = __toESM(require("path"));
|
|
27154
27543
|
|
|
27155
27544
|
// src/harness/parser.ts
|
|
27156
|
-
var
|
|
27545
|
+
var fs16 = __toESM(require("fs"));
|
|
27157
27546
|
function harnessExists(repoPath) {
|
|
27158
|
-
return
|
|
27547
|
+
return fs16.existsSync(`${repoPath}/.har/setup-infra.sh`);
|
|
27159
27548
|
}
|
|
27160
27549
|
|
|
27161
27550
|
// src/harness/stage-templates.ts
|
|
27162
27551
|
function resolveTemplateDir(templateId) {
|
|
27163
|
-
const dir =
|
|
27164
|
-
if (!
|
|
27552
|
+
const dir = path15.join(resolveTemplatesDir(), "stage-templates", templateId);
|
|
27553
|
+
if (!fs17.existsSync(dir)) {
|
|
27165
27554
|
throw new Error(`Stage template not found: ${templateId}. Run npm run build.`);
|
|
27166
27555
|
}
|
|
27167
27556
|
return dir;
|
|
27168
27557
|
}
|
|
27169
27558
|
function readTemplateManifest(templateId) {
|
|
27170
|
-
const manifestPath =
|
|
27171
|
-
const raw = JSON.parse(
|
|
27559
|
+
const manifestPath = path15.join(resolveTemplateDir(templateId), "template.manifest.json");
|
|
27560
|
+
const raw = JSON.parse(fs17.readFileSync(manifestPath, "utf8"));
|
|
27172
27561
|
if (raw.id !== templateId) {
|
|
27173
27562
|
throw new Error(`Template manifest id mismatch: expected ${templateId}, got ${raw.id}`);
|
|
27174
27563
|
}
|
|
27175
27564
|
return raw;
|
|
27176
27565
|
}
|
|
27177
27566
|
function ensureParentDir(filePath) {
|
|
27178
|
-
const parent =
|
|
27179
|
-
if (!
|
|
27180
|
-
|
|
27567
|
+
const parent = path15.dirname(filePath);
|
|
27568
|
+
if (!fs17.existsSync(parent)) {
|
|
27569
|
+
fs17.mkdirSync(parent, { recursive: true });
|
|
27181
27570
|
}
|
|
27182
27571
|
}
|
|
27183
27572
|
function copyTemplateFile(templateDir, file, repoPath, force) {
|
|
27184
|
-
const srcPath =
|
|
27185
|
-
const destPath =
|
|
27186
|
-
if (!
|
|
27573
|
+
const srcPath = path15.join(templateDir, file.src);
|
|
27574
|
+
const destPath = path15.join(repoPath, file.dest);
|
|
27575
|
+
if (!fs17.existsSync(srcPath)) {
|
|
27187
27576
|
throw new Error(`Template file missing: ${file.src}`);
|
|
27188
27577
|
}
|
|
27189
|
-
if (
|
|
27578
|
+
if (fs17.existsSync(destPath) && !force) {
|
|
27190
27579
|
throw new Error(
|
|
27191
27580
|
`File already exists: ${file.dest}. Use --force to overwrite or remove it first.`
|
|
27192
27581
|
);
|
|
27193
27582
|
}
|
|
27194
27583
|
ensureParentDir(destPath);
|
|
27195
|
-
|
|
27584
|
+
fs17.copyFileSync(srcPath, destPath);
|
|
27196
27585
|
if (file.executable) {
|
|
27197
|
-
|
|
27586
|
+
fs17.chmodSync(destPath, 493);
|
|
27198
27587
|
}
|
|
27199
27588
|
return { written: true, path: file.dest };
|
|
27200
27589
|
}
|
|
27201
27590
|
function mergePackageJson(repoPath, templateDir, fragmentRelPath, warnings) {
|
|
27202
|
-
const packagePath =
|
|
27203
|
-
if (!
|
|
27591
|
+
const packagePath = path15.join(repoPath, "package.json");
|
|
27592
|
+
if (!fs17.existsSync(packagePath)) {
|
|
27204
27593
|
throw new Error("No package.json in repo root. Add one before applying the Playwright template.");
|
|
27205
27594
|
}
|
|
27206
|
-
const fragmentPath =
|
|
27207
|
-
if (!
|
|
27595
|
+
const fragmentPath = path15.join(templateDir, fragmentRelPath);
|
|
27596
|
+
if (!fs17.existsSync(fragmentPath)) {
|
|
27208
27597
|
throw new Error(`Package fragment missing: ${fragmentRelPath}`);
|
|
27209
27598
|
}
|
|
27210
|
-
const pkg = JSON.parse(
|
|
27211
|
-
const fragment = JSON.parse(
|
|
27599
|
+
const pkg = JSON.parse(fs17.readFileSync(packagePath, "utf8"));
|
|
27600
|
+
const fragment = JSON.parse(fs17.readFileSync(fragmentPath, "utf8"));
|
|
27212
27601
|
for (const section of ["scripts", "devDependencies"]) {
|
|
27213
27602
|
const existing = pkg[section] ?? {};
|
|
27214
27603
|
const incoming = fragment[section] ?? {};
|
|
@@ -27221,7 +27610,7 @@ function mergePackageJson(repoPath, templateDir, fragmentRelPath, warnings) {
|
|
|
27221
27610
|
}
|
|
27222
27611
|
pkg[section] = existing;
|
|
27223
27612
|
}
|
|
27224
|
-
|
|
27613
|
+
fs17.writeFileSync(packagePath, JSON.stringify(pkg, null, 2) + "\n");
|
|
27225
27614
|
}
|
|
27226
27615
|
function patchStageRegistry(repoPath, manifest, force) {
|
|
27227
27616
|
const registry2 = readStageRegistry(repoPath);
|
|
@@ -27262,8 +27651,8 @@ function assertHarnessPresent(repoPath) {
|
|
|
27262
27651
|
}
|
|
27263
27652
|
function assertStageNotPresent(repoPath, stageId, force) {
|
|
27264
27653
|
if (force) return;
|
|
27265
|
-
const scriptPath =
|
|
27266
|
-
if (
|
|
27654
|
+
const scriptPath = path15.join(repoPath, ".har", "stages", `${stageId}.sh`);
|
|
27655
|
+
if (fs17.existsSync(scriptPath)) {
|
|
27267
27656
|
throw new Error(
|
|
27268
27657
|
`Stage script already exists: .har/stages/${stageId}.sh. Use --force to overwrite.`
|
|
27269
27658
|
);
|
|
@@ -27276,7 +27665,7 @@ function assertStageNotPresent(repoPath, stageId, force) {
|
|
|
27276
27665
|
}
|
|
27277
27666
|
}
|
|
27278
27667
|
function applyStageTemplate(repoPath, templateId, options = {}) {
|
|
27279
|
-
const resolved =
|
|
27668
|
+
const resolved = path15.resolve(repoPath);
|
|
27280
27669
|
const force = options.force ?? false;
|
|
27281
27670
|
const warnings = [];
|
|
27282
27671
|
const filesWritten = [];
|
|
@@ -27295,7 +27684,7 @@ function applyStageTemplate(repoPath, templateId, options = {}) {
|
|
|
27295
27684
|
if (file.skipFlag === "skipCi" && options.skipCi) {
|
|
27296
27685
|
continue;
|
|
27297
27686
|
}
|
|
27298
|
-
if (
|
|
27687
|
+
if (fs17.existsSync(path15.join(resolved, file.dest)) && !force) {
|
|
27299
27688
|
warnings.push(`Skipped optional file (exists): ${file.dest}`);
|
|
27300
27689
|
continue;
|
|
27301
27690
|
}
|
|
@@ -27358,11 +27747,11 @@ function validateAgentId(id, repoPath) {
|
|
|
27358
27747
|
// src/core/harness.ts
|
|
27359
27748
|
function listHarnessScripts(repoPath) {
|
|
27360
27749
|
const harnessDir = getHarnessDir(repoPath);
|
|
27361
|
-
if (!
|
|
27362
|
-
return
|
|
27750
|
+
if (!fs18.existsSync(harnessDir)) return [];
|
|
27751
|
+
return fs18.readdirSync(harnessDir).filter((name) => name.endsWith(".sh")).sort();
|
|
27363
27752
|
}
|
|
27364
27753
|
function describeProject(repoPath) {
|
|
27365
|
-
const resolved =
|
|
27754
|
+
const resolved = path16.resolve(repoPath);
|
|
27366
27755
|
const manifest = readManifest(resolved);
|
|
27367
27756
|
const present = harnessExists(resolved);
|
|
27368
27757
|
return {
|
|
@@ -27382,8 +27771,8 @@ function describeProject(repoPath) {
|
|
|
27382
27771
|
};
|
|
27383
27772
|
}
|
|
27384
27773
|
async function initHarness(options) {
|
|
27385
|
-
const repoPath =
|
|
27386
|
-
if (!
|
|
27774
|
+
const repoPath = path16.resolve(options.repoPath);
|
|
27775
|
+
if (!fs18.existsSync(repoPath)) {
|
|
27387
27776
|
throw new Error(`Path not found: ${repoPath}`);
|
|
27388
27777
|
}
|
|
27389
27778
|
const scaffold = scaffoldHarnessBoilerplate(repoPath, {
|
|
@@ -27414,9 +27803,9 @@ async function initHarness(options) {
|
|
|
27414
27803
|
};
|
|
27415
27804
|
}
|
|
27416
27805
|
async function maintainHarness(options) {
|
|
27417
|
-
const repoPath =
|
|
27806
|
+
const repoPath = path16.resolve(options.repoPath);
|
|
27418
27807
|
const harnessDir = getHarnessDir(repoPath);
|
|
27419
|
-
if (!
|
|
27808
|
+
if (!fs18.existsSync(harnessDir)) {
|
|
27420
27809
|
throw new Error('No .har/ found. Run "har env init" first.');
|
|
27421
27810
|
}
|
|
27422
27811
|
if (options.auto) {
|
|
@@ -27427,6 +27816,7 @@ async function maintainHarness(options) {
|
|
|
27427
27816
|
mode: "maintain"
|
|
27428
27817
|
});
|
|
27429
27818
|
finalizeHarness(repoPath, authoringResult.summary, authoringResult.stack);
|
|
27819
|
+
removeMaintainBundle(repoPath);
|
|
27430
27820
|
const validation3 = validateHarness(repoPath);
|
|
27431
27821
|
return {
|
|
27432
27822
|
validation: validation3,
|
|
@@ -27434,6 +27824,7 @@ async function maintainHarness(options) {
|
|
|
27434
27824
|
drift: compareHarnessToTemplate(repoPath)
|
|
27435
27825
|
};
|
|
27436
27826
|
}
|
|
27827
|
+
const drift = compareHarnessToTemplate(repoPath);
|
|
27437
27828
|
const validation2 = validateHarness(repoPath);
|
|
27438
27829
|
if (options.finalize) {
|
|
27439
27830
|
if (!validation2.pass) {
|
|
@@ -27445,16 +27836,19 @@ async function maintainHarness(options) {
|
|
|
27445
27836
|
options.summary ?? "Manual adaptation finalized via har env maintain --finalize",
|
|
27446
27837
|
existing?.stack
|
|
27447
27838
|
);
|
|
27839
|
+
removeMaintainBundle(repoPath);
|
|
27448
27840
|
return {
|
|
27449
27841
|
validation: validation2,
|
|
27450
27842
|
adaptationSummary: options.summary,
|
|
27451
27843
|
drift: compareHarnessToTemplate(repoPath)
|
|
27452
27844
|
};
|
|
27453
27845
|
}
|
|
27846
|
+
const bundle = buildMaintainBundle(repoPath, validation2, drift);
|
|
27454
27847
|
return {
|
|
27455
27848
|
validation: validation2,
|
|
27456
27849
|
adaptationSummary: "Manual maintenance \u2014 use coding agent prompt in .har/ADAPT-PROMPT.md",
|
|
27457
|
-
drift
|
|
27850
|
+
drift,
|
|
27851
|
+
bundle
|
|
27458
27852
|
};
|
|
27459
27853
|
}
|
|
27460
27854
|
function addStageTemplate(repoPath, templateId, options = {}) {
|
|
@@ -27462,20 +27856,20 @@ function addStageTemplate(repoPath, templateId, options = {}) {
|
|
|
27462
27856
|
}
|
|
27463
27857
|
|
|
27464
27858
|
// src/harness/adaptation-prompt.ts
|
|
27465
|
-
var
|
|
27466
|
-
var
|
|
27859
|
+
var fs19 = __toESM(require("fs"));
|
|
27860
|
+
var path17 = __toESM(require("path"));
|
|
27467
27861
|
var ADAPTATION_PROMPT_FILE = "ADAPT-PROMPT.md";
|
|
27468
27862
|
var PROFILE_HINTS = {
|
|
27469
|
-
default: "Web app profile \u2014 Docker Compose for shared infra (HARNESS_INFRA_SERVICES), PM2 for the primary application only, git worktree per agent slot by default. Identify the primary app agents modify; run supporting services shared.
|
|
27470
|
-
cli: "CLI/library profile \u2014 no PM2. Optional Docker Compose via
|
|
27471
|
-
ios: "iOS mobile app profile \u2014
|
|
27863
|
+
default: "Web app profile (SaaS/full-stack) \u2014 Docker Compose for shared infra (HARNESS_INFRA_SERVICES), PM2 for the primary application only, git worktree per agent slot by default. Launch provisions toolchain via harness.env (HARNESS_ECOSYSTEM, HARNESS_INSTALL_CMD) and writes paths to .env.agent.<id>. Identify the primary app agents modify; run supporting services shared.",
|
|
27864
|
+
cli: "CLI/library profile (typical SWE-bench) \u2014 no PM2. Optional Docker Compose via HARNESS_INFRA_SERVICES. Git worktree by default. Launch provisions toolchain declaratively (HARNESS_ECOSYSTEM auto-detects common ecosystems); verify must use resolved tool paths from .env.agent.<id>, never hardcoded interpreter or package-manager paths.",
|
|
27865
|
+
ios: "iOS mobile app profile \u2014 xcodebuild + iOS Simulator in an isolated git worktree. Set HARNESS_XCODE_SCHEME, workspace/project, HARNESS_SIMULATOR_NAME, HARNESS_BUNDLE_ID. Launch writes XCODEBUILD_BIN to .env.agent.<id>; verify uses it. Install RocketSim stage (har env add-stage rocketsim) for user-flow validation."
|
|
27472
27866
|
};
|
|
27473
27867
|
function loadTemplate(name) {
|
|
27474
27868
|
const filePath = resolveTemplateFile(name);
|
|
27475
27869
|
if (!filePath) {
|
|
27476
27870
|
throw new Error(`Adaptation prompt template not found: ${name}. Run npm run build.`);
|
|
27477
27871
|
}
|
|
27478
|
-
return
|
|
27872
|
+
return fs19.readFileSync(filePath, "utf8");
|
|
27479
27873
|
}
|
|
27480
27874
|
function applyProfilePlaceholders(content, profile) {
|
|
27481
27875
|
return content.replace(/\{\{PROFILE\}\}/g, profile).replace(/\{\{PROFILE_HINT\}\}/g, PROFILE_HINTS[profile]);
|
|
@@ -27483,12 +27877,21 @@ function applyProfilePlaceholders(content, profile) {
|
|
|
27483
27877
|
function buildInitAdaptationPrompt(_repoPath, profile) {
|
|
27484
27878
|
return applyProfilePlaceholders(loadTemplate("adaptation-prompt-init.md"), profile);
|
|
27485
27879
|
}
|
|
27486
|
-
function buildMaintainAdaptationPrompt(_repoPath) {
|
|
27487
|
-
|
|
27880
|
+
function buildMaintainAdaptationPrompt(_repoPath, bundleReport) {
|
|
27881
|
+
const template = loadTemplate("adaptation-prompt-maintain.md");
|
|
27882
|
+
const section = bundleReport ? formatMaintainBundlePromptSection(bundleReport) : [
|
|
27883
|
+
"## Step 0 \u2014 Read the maintenance bundle",
|
|
27884
|
+
"",
|
|
27885
|
+
"Open `.har/maintain/README.md` and `.har/maintain/drift-report.json`.",
|
|
27886
|
+
"All reference templates are under `.har/maintain/templates/`.",
|
|
27887
|
+
"Do **not** read files from the globally installed har package.",
|
|
27888
|
+
""
|
|
27889
|
+
].join("\n");
|
|
27890
|
+
return template.replace("{{MAINTAIN_BUNDLE_SECTION}}", section);
|
|
27488
27891
|
}
|
|
27489
27892
|
function writeAdaptationPrompt(repoPath, content) {
|
|
27490
27893
|
const harnessDir = getHarnessDir(repoPath);
|
|
27491
|
-
const filePath =
|
|
27894
|
+
const filePath = path17.join(harnessDir, ADAPTATION_PROMPT_FILE);
|
|
27492
27895
|
writeFileSafe(filePath, content);
|
|
27493
27896
|
return filePath;
|
|
27494
27897
|
}
|
|
@@ -27507,25 +27910,25 @@ function printAdaptationPrompt(content) {
|
|
|
27507
27910
|
}
|
|
27508
27911
|
|
|
27509
27912
|
// src/harness/cursor-rule.ts
|
|
27510
|
-
var
|
|
27511
|
-
var
|
|
27913
|
+
var fs20 = __toESM(require("fs"));
|
|
27914
|
+
var path18 = __toESM(require("path"));
|
|
27512
27915
|
var readline3 = __toESM(require("readline"));
|
|
27513
27916
|
var CURSOR_RULE_RELATIVE_PATH = ".cursor/rules/har-workflow.mdc";
|
|
27514
27917
|
function getCursorRulePath(repoPath) {
|
|
27515
|
-
return
|
|
27918
|
+
return path18.join(repoPath, CURSOR_RULE_RELATIVE_PATH);
|
|
27516
27919
|
}
|
|
27517
27920
|
function isCursorWorkspace(repoPath) {
|
|
27518
|
-
return
|
|
27921
|
+
return fs20.existsSync(path18.join(repoPath, ".cursor"));
|
|
27519
27922
|
}
|
|
27520
27923
|
function cursorRuleExists(repoPath) {
|
|
27521
|
-
return
|
|
27924
|
+
return fs20.existsSync(getCursorRulePath(repoPath));
|
|
27522
27925
|
}
|
|
27523
27926
|
function scaffoldCursorRule(repoPath) {
|
|
27524
27927
|
const templatePath = resolveTemplateFile("cursor-rule.mdc.template");
|
|
27525
27928
|
if (!templatePath) {
|
|
27526
27929
|
throw new Error("Cursor rule template not found (cursor-rule.mdc.template). Run npm run build.");
|
|
27527
27930
|
}
|
|
27528
|
-
const content =
|
|
27931
|
+
const content = fs20.readFileSync(templatePath, "utf8");
|
|
27529
27932
|
writeFileSafe(getCursorRulePath(repoPath), content);
|
|
27530
27933
|
}
|
|
27531
27934
|
async function handleCursorRule(options) {
|
|
@@ -27563,16 +27966,16 @@ async function promptScaffoldCursorRule(exists) {
|
|
|
27563
27966
|
}
|
|
27564
27967
|
async function askYesNo2(question) {
|
|
27565
27968
|
const rl = readline3.createInterface({ input: process.stdin, output: process.stderr });
|
|
27566
|
-
return new Promise((
|
|
27969
|
+
return new Promise((resolve26) => {
|
|
27567
27970
|
process.stderr.write(`${question} `);
|
|
27568
27971
|
rl.once("line", (answer) => {
|
|
27569
27972
|
rl.close();
|
|
27570
27973
|
const trimmed = answer.trim();
|
|
27571
27974
|
if (trimmed === "") {
|
|
27572
|
-
|
|
27975
|
+
resolve26(true);
|
|
27573
27976
|
return;
|
|
27574
27977
|
}
|
|
27575
|
-
|
|
27978
|
+
resolve26(/^y(es)?$/i.test(trimmed));
|
|
27576
27979
|
});
|
|
27577
27980
|
});
|
|
27578
27981
|
}
|
|
@@ -27651,17 +28054,65 @@ function buildStageResult(input) {
|
|
|
27651
28054
|
}
|
|
27652
28055
|
|
|
27653
28056
|
// src/core/local-executor.ts
|
|
27654
|
-
var
|
|
27655
|
-
var
|
|
28057
|
+
var fs22 = __toESM(require("fs"));
|
|
28058
|
+
var path20 = __toESM(require("path"));
|
|
28059
|
+
|
|
28060
|
+
// src/core/slot-registry.ts
|
|
28061
|
+
var fs21 = __toESM(require("fs"));
|
|
28062
|
+
var path19 = __toESM(require("path"));
|
|
28063
|
+
function getSlotRegistryDir(repoPath) {
|
|
28064
|
+
return path19.join(getHarnessDir(repoPath), "slots");
|
|
28065
|
+
}
|
|
28066
|
+
function getSlotRegistryPath(repoPath, agentId) {
|
|
28067
|
+
return path19.join(getSlotRegistryDir(repoPath), `agent-${agentId}.json`);
|
|
28068
|
+
}
|
|
28069
|
+
function isSlotResumable(session) {
|
|
28070
|
+
return session?.status === "failed" || session?.status === "starting";
|
|
28071
|
+
}
|
|
28072
|
+
function readSlotRegistry(repoPath, agentId) {
|
|
28073
|
+
const file = getSlotRegistryPath(repoPath, agentId);
|
|
28074
|
+
if (!fs21.existsSync(file)) return void 0;
|
|
28075
|
+
try {
|
|
28076
|
+
const raw = JSON.parse(fs21.readFileSync(file, "utf8"));
|
|
28077
|
+
const result = SlotRegistryEntrySchema.safeParse(raw);
|
|
28078
|
+
return result.success ? result.data : void 0;
|
|
28079
|
+
} catch {
|
|
28080
|
+
return void 0;
|
|
28081
|
+
}
|
|
28082
|
+
}
|
|
28083
|
+
|
|
28084
|
+
// src/core/local-executor.ts
|
|
27656
28085
|
function resolveRepoPath(repoPath) {
|
|
27657
|
-
return
|
|
28086
|
+
return path20.resolve(repoPath);
|
|
28087
|
+
}
|
|
28088
|
+
function buildPreviewUrlsFromPorts(ports, env3) {
|
|
28089
|
+
const urls = {};
|
|
28090
|
+
if (ports.frontend) urls.frontend = `http://localhost:${ports.frontend}`;
|
|
28091
|
+
if (ports.api) {
|
|
28092
|
+
urls.api = `http://localhost:${ports.api}`;
|
|
28093
|
+
if (env3.HARNESS_HEALTH_CHECK_PATH) {
|
|
28094
|
+
urls.health = `http://localhost:${ports.api}${env3.HARNESS_HEALTH_CHECK_PATH}`;
|
|
28095
|
+
}
|
|
28096
|
+
}
|
|
28097
|
+
if (ports.browser) urls.browser = `http://localhost:${ports.browser}`;
|
|
28098
|
+
if (ports.mailpit) urls.mailpit = `http://localhost:${ports.mailpit}`;
|
|
28099
|
+
return urls;
|
|
27658
28100
|
}
|
|
27659
28101
|
function computePreviewUrls(repoPath, agentId) {
|
|
27660
|
-
const
|
|
28102
|
+
const harnessRoot = resolveHarnessRoot(repoPath);
|
|
28103
|
+
const session = readSlotRegistry(harnessRoot, agentId);
|
|
28104
|
+
if (session?.previewUrls && Object.keys(session.previewUrls).length > 0) {
|
|
28105
|
+
return session.previewUrls;
|
|
28106
|
+
}
|
|
28107
|
+
const env3 = readHarnessEnv(harnessRoot);
|
|
28108
|
+
if (session?.ports && Object.keys(session.ports).length > 0) {
|
|
28109
|
+
return buildPreviewUrlsFromPorts(session.ports, env3);
|
|
28110
|
+
}
|
|
28111
|
+
const step = Number(env3.HARNESS_PORT_STEP ?? 10);
|
|
27661
28112
|
const feBase = Number(env3.HARNESS_FE_BASE_PORT ?? 3e3);
|
|
27662
28113
|
const apiBase = Number(env3.HARNESS_API_BASE_PORT ?? 8e3);
|
|
27663
|
-
const fePort = feBase + agentId *
|
|
27664
|
-
const apiPort = apiBase + agentId *
|
|
28114
|
+
const fePort = feBase + agentId * step;
|
|
28115
|
+
const apiPort = apiBase + agentId * step;
|
|
27665
28116
|
const urls = {
|
|
27666
28117
|
frontend: `http://localhost:${fePort}`,
|
|
27667
28118
|
api: `http://localhost:${apiPort}`
|
|
@@ -27670,10 +28121,10 @@ function computePreviewUrls(repoPath, agentId) {
|
|
|
27670
28121
|
urls.health = `http://localhost:${apiPort}${env3.HARNESS_HEALTH_CHECK_PATH}`;
|
|
27671
28122
|
}
|
|
27672
28123
|
if (env3.HARNESS_INFRA_BROWSER === "true") {
|
|
27673
|
-
urls.browser =
|
|
28124
|
+
urls.browser = `http://localhost:${env3.HARNESS_BROWSER_PORT_DEFAULT ?? 13001}`;
|
|
27674
28125
|
}
|
|
27675
28126
|
if (env3.HARNESS_INFRA_MAILPIT === "true") {
|
|
27676
|
-
urls.mailpit =
|
|
28127
|
+
urls.mailpit = `http://localhost:${env3.HARNESS_MAILPIT_WEB_PORT_DEFAULT ?? 18025}`;
|
|
27677
28128
|
}
|
|
27678
28129
|
return urls;
|
|
27679
28130
|
}
|
|
@@ -27684,14 +28135,14 @@ function substituteAgentId(value, agentId) {
|
|
|
27684
28135
|
function resolveStageScriptPath(repoPath, stage) {
|
|
27685
28136
|
const harnessDir = getHarnessDir(repoPath);
|
|
27686
28137
|
if (stage.script) {
|
|
27687
|
-
return
|
|
28138
|
+
return path20.join(harnessDir, stage.script);
|
|
27688
28139
|
}
|
|
27689
28140
|
if (stage.command) {
|
|
27690
28141
|
const scriptName = stage.command.split(/\s+/)[0].replace(/^\.\/\.har\//, "").replace(/^\.\//, "");
|
|
27691
|
-
return
|
|
28142
|
+
return path20.join(harnessDir, scriptName);
|
|
27692
28143
|
}
|
|
27693
|
-
const stageScript =
|
|
27694
|
-
if (
|
|
28144
|
+
const stageScript = path20.join(harnessDir, "stages", `${stage.id}.sh`);
|
|
28145
|
+
if (fs22.existsSync(stageScript)) {
|
|
27695
28146
|
return stageScript;
|
|
27696
28147
|
}
|
|
27697
28148
|
throw new Error(
|
|
@@ -27706,7 +28157,7 @@ function buildExecutionPlan(repoPath, stage, options) {
|
|
|
27706
28157
|
...harnessEnv,
|
|
27707
28158
|
...stage.env ?? {}
|
|
27708
28159
|
};
|
|
27709
|
-
const cwd = stage.cwd ?
|
|
28160
|
+
const cwd = stage.cwd ? path20.resolve(resolvedRepo, stage.cwd) : resolvedRepo;
|
|
27710
28161
|
const extraArgs = options.args ?? [];
|
|
27711
28162
|
const launchFlagArgs = [];
|
|
27712
28163
|
if (stage.kind === "launch" && options.launchFlags) {
|
|
@@ -27714,6 +28165,7 @@ function buildExecutionPlan(repoPath, stage, options) {
|
|
|
27714
28165
|
if (options.launchFlags.claude) launchFlagArgs.push("--claude");
|
|
27715
28166
|
if (options.launchFlags.confirmReplace) launchFlagArgs.push("--replace");
|
|
27716
28167
|
if (options.launchFlags.force) launchFlagArgs.push("--force");
|
|
28168
|
+
if (options.launchFlags.resume) launchFlagArgs.push("--resume");
|
|
27717
28169
|
}
|
|
27718
28170
|
if (stage.command) {
|
|
27719
28171
|
let shellCommand = substituteAgentId(stage.command, options.agentId);
|
|
@@ -27782,19 +28234,19 @@ var LocalScriptExecutor = class {
|
|
|
27782
28234
|
const repoPath = resolveRepoPath(ctx.repoPath);
|
|
27783
28235
|
const harnessDir = getHarnessDir(repoPath);
|
|
27784
28236
|
const artifactsDirName = getArtifactsDir(repoPath);
|
|
27785
|
-
const artifactsDir =
|
|
27786
|
-
if (!
|
|
28237
|
+
const artifactsDir = path20.join(harnessDir, artifactsDirName);
|
|
28238
|
+
if (!fs22.existsSync(artifactsDir)) return [];
|
|
27787
28239
|
const entries = [];
|
|
27788
28240
|
const walk = (dir, prefix) => {
|
|
27789
|
-
for (const entry of
|
|
27790
|
-
const full =
|
|
27791
|
-
const relative2 =
|
|
28241
|
+
for (const entry of fs22.readdirSync(dir, { withFileTypes: true })) {
|
|
28242
|
+
const full = path20.join(dir, entry.name);
|
|
28243
|
+
const relative2 = path20.join(prefix, entry.name);
|
|
27792
28244
|
if (entry.isDirectory()) {
|
|
27793
28245
|
walk(full, relative2);
|
|
27794
28246
|
continue;
|
|
27795
28247
|
}
|
|
27796
28248
|
if (filter?.stageId && !relative2.includes(filter.stageId)) continue;
|
|
27797
|
-
const stat =
|
|
28249
|
+
const stat = fs22.statSync(full);
|
|
27798
28250
|
entries.push({
|
|
27799
28251
|
path: full,
|
|
27800
28252
|
relativePath: relative2,
|
|
@@ -27810,7 +28262,7 @@ var LocalScriptExecutor = class {
|
|
|
27810
28262
|
var localScriptExecutor = new LocalScriptExecutor();
|
|
27811
28263
|
|
|
27812
28264
|
// src/core/control-sync.ts
|
|
27813
|
-
var
|
|
28265
|
+
var path30 = __toESM(require("path"));
|
|
27814
28266
|
|
|
27815
28267
|
// src/core/control-config.ts
|
|
27816
28268
|
var DEFAULT_CONTROL_API_URL = "http://localhost:3847";
|
|
@@ -27822,32 +28274,32 @@ function isControlEnabled() {
|
|
|
27822
28274
|
}
|
|
27823
28275
|
|
|
27824
28276
|
// src/core/control-registry.ts
|
|
27825
|
-
var
|
|
27826
|
-
var
|
|
27827
|
-
var
|
|
28277
|
+
var fs23 = __toESM(require("fs"));
|
|
28278
|
+
var os3 = __toESM(require("os"));
|
|
28279
|
+
var path21 = __toESM(require("path"));
|
|
27828
28280
|
function getRegistryPath() {
|
|
27829
28281
|
if (process.env.HAR_CONTROL_REGISTRY_PATH) {
|
|
27830
|
-
return
|
|
28282
|
+
return path21.resolve(process.env.HAR_CONTROL_REGISTRY_PATH);
|
|
27831
28283
|
}
|
|
27832
|
-
return
|
|
28284
|
+
return path21.join(os3.homedir(), ".har", "repos.json");
|
|
27833
28285
|
}
|
|
27834
28286
|
function readRegistry() {
|
|
27835
28287
|
const registryPath = getRegistryPath();
|
|
27836
28288
|
try {
|
|
27837
|
-
if (!
|
|
27838
|
-
return JSON.parse(
|
|
28289
|
+
if (!fs23.existsSync(registryPath)) return { repos: [] };
|
|
28290
|
+
return JSON.parse(fs23.readFileSync(registryPath, "utf8"));
|
|
27839
28291
|
} catch {
|
|
27840
28292
|
return { repos: [] };
|
|
27841
28293
|
}
|
|
27842
28294
|
}
|
|
27843
28295
|
function writeRegistry(registry2) {
|
|
27844
28296
|
const registryPath = getRegistryPath();
|
|
27845
|
-
|
|
27846
|
-
|
|
28297
|
+
fs23.mkdirSync(path21.dirname(registryPath), { recursive: true });
|
|
28298
|
+
fs23.writeFileSync(registryPath, JSON.stringify(registry2, null, 2) + "\n");
|
|
27847
28299
|
}
|
|
27848
28300
|
function recordRepoForControlSync(repoPath) {
|
|
27849
28301
|
if (process.env.HAR_CONTROL_DISABLED === "true") return;
|
|
27850
|
-
const resolved =
|
|
28302
|
+
const resolved = path21.resolve(repoPath);
|
|
27851
28303
|
if (!readManifest(resolved)) return;
|
|
27852
28304
|
const registry2 = readRegistry();
|
|
27853
28305
|
if (registry2.repos.includes(resolved)) return;
|
|
@@ -27856,7 +28308,7 @@ function recordRepoForControlSync(repoPath) {
|
|
|
27856
28308
|
}
|
|
27857
28309
|
function listRegisteredRepos() {
|
|
27858
28310
|
const registry2 = readRegistry();
|
|
27859
|
-
const kept = registry2.repos.filter((repoPath) =>
|
|
28311
|
+
const kept = registry2.repos.filter((repoPath) => fs23.existsSync(repoPath) && readManifest(repoPath));
|
|
27860
28312
|
if (kept.length !== registry2.repos.length) {
|
|
27861
28313
|
writeRegistry({ repos: kept });
|
|
27862
28314
|
}
|
|
@@ -27864,40 +28316,17 @@ function listRegisteredRepos() {
|
|
|
27864
28316
|
}
|
|
27865
28317
|
|
|
27866
28318
|
// src/core/slot-status.ts
|
|
27867
|
-
var
|
|
27868
|
-
var
|
|
27869
|
-
var
|
|
27870
|
-
var
|
|
28319
|
+
var fs28 = __toESM(require("fs"));
|
|
28320
|
+
var os6 = __toESM(require("os"));
|
|
28321
|
+
var path27 = __toESM(require("path"));
|
|
28322
|
+
var import_child_process7 = require("child_process");
|
|
27871
28323
|
|
|
27872
28324
|
// src/core/runs.ts
|
|
27873
28325
|
var crypto2 = __toESM(require("crypto"));
|
|
27874
28326
|
var import_child_process2 = require("child_process");
|
|
27875
|
-
var
|
|
27876
|
-
var
|
|
27877
|
-
var
|
|
27878
|
-
|
|
27879
|
-
// src/core/slot-registry.ts
|
|
27880
|
-
var fs21 = __toESM(require("fs"));
|
|
27881
|
-
var path19 = __toESM(require("path"));
|
|
27882
|
-
function getSlotRegistryDir(repoPath) {
|
|
27883
|
-
return path19.join(getHarnessDir(repoPath), "slots");
|
|
27884
|
-
}
|
|
27885
|
-
function getSlotRegistryPath(repoPath, agentId) {
|
|
27886
|
-
return path19.join(getSlotRegistryDir(repoPath), `agent-${agentId}.json`);
|
|
27887
|
-
}
|
|
27888
|
-
function readSlotRegistry(repoPath, agentId) {
|
|
27889
|
-
const file = getSlotRegistryPath(repoPath, agentId);
|
|
27890
|
-
if (!fs21.existsSync(file)) return void 0;
|
|
27891
|
-
try {
|
|
27892
|
-
const raw = JSON.parse(fs21.readFileSync(file, "utf8"));
|
|
27893
|
-
const result = SlotRegistryEntrySchema.safeParse(raw);
|
|
27894
|
-
return result.success ? result.data : void 0;
|
|
27895
|
-
} catch {
|
|
27896
|
-
return void 0;
|
|
27897
|
-
}
|
|
27898
|
-
}
|
|
27899
|
-
|
|
27900
|
-
// src/core/runs.ts
|
|
28327
|
+
var fs24 = __toESM(require("fs"));
|
|
28328
|
+
var os4 = __toESM(require("os"));
|
|
28329
|
+
var path22 = __toESM(require("path"));
|
|
27901
28330
|
var RUNS_DIR = "runs";
|
|
27902
28331
|
function gitCommonDir(cwd) {
|
|
27903
28332
|
try {
|
|
@@ -27906,7 +28335,7 @@ function gitCommonDir(cwd) {
|
|
|
27906
28335
|
encoding: "utf8",
|
|
27907
28336
|
stdio: ["pipe", "pipe", "ignore"]
|
|
27908
28337
|
}).trim();
|
|
27909
|
-
return out ?
|
|
28338
|
+
return out ? path22.resolve(cwd, out) : void 0;
|
|
27910
28339
|
} catch {
|
|
27911
28340
|
return void 0;
|
|
27912
28341
|
}
|
|
@@ -27928,7 +28357,7 @@ function gitPrefix(cwd) {
|
|
|
27928
28357
|
}
|
|
27929
28358
|
}
|
|
27930
28359
|
function getRunsDir(harnessRoot) {
|
|
27931
|
-
return
|
|
28360
|
+
return path22.join(getHarnessDir(harnessRoot), RUNS_DIR);
|
|
27932
28361
|
}
|
|
27933
28362
|
function formatLocalDate(d2) {
|
|
27934
28363
|
const y2 = d2.getFullYear();
|
|
@@ -27948,24 +28377,24 @@ function buildRunRelativePath(stageId, agentId, startedAt, runId, runsDir) {
|
|
|
27948
28377
|
const timePart = formatLocalTime(started);
|
|
27949
28378
|
const agentPart = agentId !== void 0 ? `_agent-${agentId}` : "";
|
|
27950
28379
|
let filename = `${timePart}_${stageId}${agentPart}.json`;
|
|
27951
|
-
const fullPath =
|
|
27952
|
-
if (
|
|
28380
|
+
const fullPath = path22.join(runsDir, dateFolder, filename);
|
|
28381
|
+
if (fs24.existsSync(fullPath)) {
|
|
27953
28382
|
filename = `${timePart}_${stageId}${agentPart}-${runId.slice(0, 8)}.json`;
|
|
27954
28383
|
}
|
|
27955
|
-
return
|
|
28384
|
+
return path22.join(dateFolder, filename);
|
|
27956
28385
|
}
|
|
27957
28386
|
function resolveRunFilePath(harnessRoot, run2) {
|
|
27958
28387
|
if (run2.relativePath) {
|
|
27959
|
-
return
|
|
28388
|
+
return path22.join(getRunsDir(harnessRoot), run2.relativePath);
|
|
27960
28389
|
}
|
|
27961
|
-
return
|
|
28390
|
+
return path22.join(getRunsDir(harnessRoot), `${run2.runId}.json`);
|
|
27962
28391
|
}
|
|
27963
28392
|
function collectRunFiles(runsDir) {
|
|
27964
|
-
if (!
|
|
28393
|
+
if (!fs24.existsSync(runsDir)) return [];
|
|
27965
28394
|
const files = [];
|
|
27966
28395
|
const walk = (dir) => {
|
|
27967
|
-
for (const entry of
|
|
27968
|
-
const full =
|
|
28396
|
+
for (const entry of fs24.readdirSync(dir, { withFileTypes: true })) {
|
|
28397
|
+
const full = path22.join(dir, entry.name);
|
|
27969
28398
|
if (entry.isDirectory()) {
|
|
27970
28399
|
walk(full);
|
|
27971
28400
|
} else if (entry.name.endsWith(".json")) {
|
|
@@ -27979,30 +28408,30 @@ function collectRunFiles(runsDir) {
|
|
|
27979
28408
|
function resolveAgentWorkDir(harnessRoot, agentId) {
|
|
27980
28409
|
if (agentId === void 0) return void 0;
|
|
27981
28410
|
const entry = readSlotRegistry(harnessRoot, agentId);
|
|
27982
|
-
if (entry?.workDir &&
|
|
28411
|
+
if (entry?.workDir && fs24.existsSync(entry.workDir)) return entry.workDir;
|
|
27983
28412
|
const env3 = readHarnessEnv(harnessRoot);
|
|
27984
|
-
const projectName = env3.HARNESS_PROJECT_NAME ??
|
|
27985
|
-
const worktreeDir =
|
|
28413
|
+
const projectName = env3.HARNESS_PROJECT_NAME ?? path22.basename(harnessRoot);
|
|
28414
|
+
const worktreeDir = path22.join(os4.homedir(), "worktrees", `${projectName}-agent-${agentId}`);
|
|
27986
28415
|
const relPrefix = gitPrefix(harnessRoot);
|
|
27987
28416
|
const sessionEnvFiles = [];
|
|
27988
|
-
const worktreesRoot =
|
|
27989
|
-
if (
|
|
28417
|
+
const worktreesRoot = path22.join(os4.homedir(), "worktrees");
|
|
28418
|
+
if (fs24.existsSync(worktreesRoot)) {
|
|
27990
28419
|
const suffix = `-har-agent-${agentId}-`;
|
|
27991
|
-
for (const entry2 of
|
|
28420
|
+
for (const entry2 of fs24.readdirSync(worktreesRoot, { withFileTypes: true })) {
|
|
27992
28421
|
if (!entry2.isDirectory() || !entry2.name.includes(suffix)) continue;
|
|
27993
|
-
const sessionDir =
|
|
28422
|
+
const sessionDir = path22.join(worktreesRoot, entry2.name);
|
|
27994
28423
|
if (!sameGitCheckout(harnessRoot, sessionDir)) continue;
|
|
27995
|
-
sessionEnvFiles.push(
|
|
28424
|
+
sessionEnvFiles.push(path22.join(sessionDir, relPrefix, `.env.agent.${agentId}`));
|
|
27996
28425
|
}
|
|
27997
28426
|
}
|
|
27998
28427
|
const candidates = [
|
|
27999
|
-
|
|
28000
|
-
|
|
28428
|
+
path22.join(worktreeDir, `.env.agent.${agentId}`),
|
|
28429
|
+
path22.join(harnessRoot, `.env.agent.${agentId}`),
|
|
28001
28430
|
...sessionEnvFiles.sort()
|
|
28002
28431
|
];
|
|
28003
28432
|
for (const envFile of candidates) {
|
|
28004
|
-
if (!
|
|
28005
|
-
const content =
|
|
28433
|
+
if (!fs24.existsSync(envFile)) continue;
|
|
28434
|
+
const content = fs24.readFileSync(envFile, "utf8");
|
|
28006
28435
|
const match = content.match(/^REPO_ROOT=(.+)$/m);
|
|
28007
28436
|
if (match) return match[1].trim();
|
|
28008
28437
|
}
|
|
@@ -28014,11 +28443,11 @@ function createRun(ctx, meta) {
|
|
|
28014
28443
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
28015
28444
|
const runId = crypto2.randomUUID();
|
|
28016
28445
|
const relativePath = buildRunRelativePath(meta.stageId, meta.agentId, startedAt, runId, runsDir);
|
|
28017
|
-
const runFilePath =
|
|
28018
|
-
|
|
28446
|
+
const runFilePath = path22.join(runsDir, relativePath);
|
|
28447
|
+
fs24.mkdirSync(path22.dirname(runFilePath), { recursive: true });
|
|
28019
28448
|
const run2 = RunRecordSchema.parse({
|
|
28020
28449
|
runId,
|
|
28021
|
-
repoPath:
|
|
28450
|
+
repoPath: path22.resolve(ctx.repoPath),
|
|
28022
28451
|
harnessRoot,
|
|
28023
28452
|
stageId: meta.stageId,
|
|
28024
28453
|
kind: meta.kind,
|
|
@@ -28029,7 +28458,7 @@ function createRun(ctx, meta) {
|
|
|
28029
28458
|
relativePath,
|
|
28030
28459
|
trigger: ctx.trigger ?? "cli"
|
|
28031
28460
|
});
|
|
28032
|
-
|
|
28461
|
+
fs24.writeFileSync(runFilePath, JSON.stringify(run2, null, 2) + "\n");
|
|
28033
28462
|
return run2;
|
|
28034
28463
|
}
|
|
28035
28464
|
function finishRun(repoPath, runId, update) {
|
|
@@ -28048,13 +28477,13 @@ function finishRun(repoPath, runId, update) {
|
|
|
28048
28477
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
28049
28478
|
});
|
|
28050
28479
|
const runPath = resolveRunFilePath(harnessRoot, finished);
|
|
28051
|
-
|
|
28480
|
+
fs24.writeFileSync(runPath, JSON.stringify(finished, null, 2) + "\n");
|
|
28052
28481
|
return finished;
|
|
28053
28482
|
}
|
|
28054
28483
|
function findRunRecord(harnessRoot, runId) {
|
|
28055
28484
|
const runsDir = getRunsDir(harnessRoot);
|
|
28056
28485
|
for (const filePath of collectRunFiles(runsDir)) {
|
|
28057
|
-
const parsed = RunRecordSchema.safeParse(JSON.parse(
|
|
28486
|
+
const parsed = RunRecordSchema.safeParse(JSON.parse(fs24.readFileSync(filePath, "utf8")));
|
|
28058
28487
|
if (parsed.success && parsed.data.runId === runId) {
|
|
28059
28488
|
return parsed.data;
|
|
28060
28489
|
}
|
|
@@ -28066,10 +28495,10 @@ function getRun(repoPath, runId) {
|
|
|
28066
28495
|
}
|
|
28067
28496
|
function listRuns(repoPath, filter = {}) {
|
|
28068
28497
|
const runsDir = getRunsDir(resolveHarnessRoot(repoPath));
|
|
28069
|
-
if (!
|
|
28498
|
+
if (!fs24.existsSync(runsDir)) return [];
|
|
28070
28499
|
const runs = [];
|
|
28071
28500
|
for (const filePath of collectRunFiles(runsDir)) {
|
|
28072
|
-
const parsed = RunRecordSchema.safeParse(JSON.parse(
|
|
28501
|
+
const parsed = RunRecordSchema.safeParse(JSON.parse(fs24.readFileSync(filePath, "utf8")));
|
|
28073
28502
|
if (!parsed.success) continue;
|
|
28074
28503
|
if (filter.stageId && parsed.data.stageId !== filter.stageId) continue;
|
|
28075
28504
|
runs.push(parsed.data);
|
|
@@ -28081,11 +28510,552 @@ function listRuns(repoPath, filter = {}) {
|
|
|
28081
28510
|
return runs;
|
|
28082
28511
|
}
|
|
28083
28512
|
|
|
28513
|
+
// src/core/slot-preflight.ts
|
|
28514
|
+
var path26 = __toESM(require("path"));
|
|
28515
|
+
|
|
28516
|
+
// src/core/control-port.ts
|
|
28517
|
+
var import_child_process4 = require("child_process");
|
|
28518
|
+
var fs26 = __toESM(require("fs"));
|
|
28519
|
+
var path24 = __toESM(require("path"));
|
|
28520
|
+
|
|
28521
|
+
// src/core/slot-ports.ts
|
|
28522
|
+
var import_child_process3 = require("child_process");
|
|
28523
|
+
var fs25 = __toESM(require("fs"));
|
|
28524
|
+
var path23 = __toESM(require("path"));
|
|
28525
|
+
function portStep(env3) {
|
|
28526
|
+
return Number(env3.HARNESS_PORT_STEP ?? 10);
|
|
28527
|
+
}
|
|
28528
|
+
function defaultAppPort(base, agentId, step) {
|
|
28529
|
+
return base + agentId * step;
|
|
28530
|
+
}
|
|
28531
|
+
function slotPortLaneEnd(defaultPort, step) {
|
|
28532
|
+
return defaultPort + step - 1;
|
|
28533
|
+
}
|
|
28534
|
+
function isPortInUse(port, host = "127.0.0.1") {
|
|
28535
|
+
try {
|
|
28536
|
+
(0, import_child_process3.execSync)(`bash -c 'exec 3<>/dev/tcp/${host}/${port}'`, { stdio: "ignore" });
|
|
28537
|
+
return true;
|
|
28538
|
+
} catch {
|
|
28539
|
+
return false;
|
|
28540
|
+
}
|
|
28541
|
+
}
|
|
28542
|
+
function pickFreePort(start, end) {
|
|
28543
|
+
for (let port = start; port <= end; port++) {
|
|
28544
|
+
if (!isPortInUse(port)) return port;
|
|
28545
|
+
}
|
|
28546
|
+
return void 0;
|
|
28547
|
+
}
|
|
28548
|
+
function allocatePort(defaultPort, scanStart, scanEnd) {
|
|
28549
|
+
if (!isPortInUse(defaultPort)) return defaultPort;
|
|
28550
|
+
return pickFreePort(scanStart, scanEnd);
|
|
28551
|
+
}
|
|
28552
|
+
function allocateAppPorts(repoPath, agentId) {
|
|
28553
|
+
const harnessRoot = resolveHarnessRoot(repoPath);
|
|
28554
|
+
const env3 = readHarnessEnv(harnessRoot);
|
|
28555
|
+
const step = portStep(env3);
|
|
28556
|
+
const feDefault = defaultAppPort(Number(env3.HARNESS_FE_BASE_PORT ?? 3e3), agentId, step);
|
|
28557
|
+
const apiDefault = defaultAppPort(Number(env3.HARNESS_API_BASE_PORT ?? 8e3), agentId, step);
|
|
28558
|
+
const debugDefault = 9200 + agentId * step;
|
|
28559
|
+
const frontend = allocatePort(feDefault, feDefault, slotPortLaneEnd(feDefault, step));
|
|
28560
|
+
const api = allocatePort(apiDefault, apiDefault, slotPortLaneEnd(apiDefault, step));
|
|
28561
|
+
const debug2 = allocatePort(debugDefault, debugDefault, debugDefault + step - 1);
|
|
28562
|
+
if (frontend === void 0) {
|
|
28563
|
+
return { error: `No free frontend port in range ${feDefault}-${slotPortLaneEnd(feDefault, step)}` };
|
|
28564
|
+
}
|
|
28565
|
+
if (api === void 0) {
|
|
28566
|
+
return { error: `No free API port in range ${apiDefault}-${slotPortLaneEnd(apiDefault, step)}` };
|
|
28567
|
+
}
|
|
28568
|
+
if (debug2 === void 0) {
|
|
28569
|
+
return { error: `No free debug port in range ${debugDefault}-${debugDefault + step - 1}` };
|
|
28570
|
+
}
|
|
28571
|
+
return {
|
|
28572
|
+
frontend,
|
|
28573
|
+
api,
|
|
28574
|
+
debug: debug2,
|
|
28575
|
+
allocated: frontend !== feDefault || api !== apiDefault || debug2 !== debugDefault
|
|
28576
|
+
};
|
|
28577
|
+
}
|
|
28578
|
+
function harnessUsesPm2(repoPath) {
|
|
28579
|
+
const harnessRoot = resolveHarnessRoot(repoPath);
|
|
28580
|
+
return fs25.existsSync(path23.join(harnessRoot, ".har", "ecosystem.agent.template.cjs"));
|
|
28581
|
+
}
|
|
28582
|
+
|
|
28583
|
+
// src/core/control-port.ts
|
|
28584
|
+
function parseControlHostPort(apiUrl = getControlApiUrl()) {
|
|
28585
|
+
try {
|
|
28586
|
+
const url = new URL(apiUrl);
|
|
28587
|
+
if (url.port) return Number(url.port);
|
|
28588
|
+
return url.protocol === "https:" ? 443 : 80;
|
|
28589
|
+
} catch {
|
|
28590
|
+
return 3847;
|
|
28591
|
+
}
|
|
28592
|
+
}
|
|
28593
|
+
function portPublishedInDocker(ports, port) {
|
|
28594
|
+
const re2 = new RegExp(`:${port}->|:${port}/`);
|
|
28595
|
+
return re2.test(ports) || ports.includes(`0.0.0.0:${port}`);
|
|
28596
|
+
}
|
|
28597
|
+
function dockerOnPort(containers, port) {
|
|
28598
|
+
return containers.find((c2) => portPublishedInDocker(c2.ports, port));
|
|
28599
|
+
}
|
|
28600
|
+
function controlContainerOnPort(containers, port) {
|
|
28601
|
+
return containers.find(
|
|
28602
|
+
(c2) => /control/i.test(c2.name) && portPublishedInDocker(c2.ports, port)
|
|
28603
|
+
);
|
|
28604
|
+
}
|
|
28605
|
+
function listDockerContainers() {
|
|
28606
|
+
try {
|
|
28607
|
+
const raw = (0, import_child_process4.execSync)("docker ps --format {{.Names}}\\t{{.Ports}}", {
|
|
28608
|
+
encoding: "utf8",
|
|
28609
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
28610
|
+
timeout: 3e3
|
|
28611
|
+
});
|
|
28612
|
+
return raw.split("\n").map((line) => line.trim()).filter(Boolean).map((line) => {
|
|
28613
|
+
const tab = line.indexOf(" ");
|
|
28614
|
+
if (tab === -1) return { name: line, ports: "" };
|
|
28615
|
+
return { name: line.slice(0, tab), ports: line.slice(tab + 1) };
|
|
28616
|
+
});
|
|
28617
|
+
} catch {
|
|
28618
|
+
return [];
|
|
28619
|
+
}
|
|
28620
|
+
}
|
|
28621
|
+
function formatControlPortBlocker(containerName, port, label) {
|
|
28622
|
+
const suffix = label ? ` (${label})` : "";
|
|
28623
|
+
return `har control up (container "${containerName}") occupies port ${port}${suffix}.`;
|
|
28624
|
+
}
|
|
28625
|
+
function formatControlDefaultPortWarning(containerName, defaultPort, allocatedPort) {
|
|
28626
|
+
return `har control up holds port ${defaultPort} (container "${containerName}"). Harness will use port ${allocatedPort} instead. Run har control down to reclaim the default port.`;
|
|
28627
|
+
}
|
|
28628
|
+
function controlDefaultPortWarnings(containers, defaultFrontendPort, allocatedFrontend) {
|
|
28629
|
+
if (allocatedFrontend === defaultFrontendPort) return [];
|
|
28630
|
+
const control = controlContainerOnPort(containers, defaultFrontendPort);
|
|
28631
|
+
if (!control) return [];
|
|
28632
|
+
return [formatControlDefaultPortWarning(control.name, defaultFrontendPort, allocatedFrontend)];
|
|
28633
|
+
}
|
|
28634
|
+
function inspectControlUpReadiness(repoPath) {
|
|
28635
|
+
const resolved = path24.resolve(repoPath);
|
|
28636
|
+
const port = parseControlHostPort();
|
|
28637
|
+
const containers = listDockerContainers();
|
|
28638
|
+
const control = controlContainerOnPort(containers, port);
|
|
28639
|
+
const warnings = [];
|
|
28640
|
+
const controlHarnessDir = path24.join(resolved, "control", ".har");
|
|
28641
|
+
const harnessSlot1Active = fs26.existsSync(controlHarnessDir) && readSlotRegistry(path24.join(resolved, "control"), 1)?.status === "active";
|
|
28642
|
+
if (control) {
|
|
28643
|
+
return {
|
|
28644
|
+
warnings,
|
|
28645
|
+
portInUse: true,
|
|
28646
|
+
controlAlreadyRunning: true,
|
|
28647
|
+
harnessSlot1Active
|
|
28648
|
+
};
|
|
28649
|
+
}
|
|
28650
|
+
if (harnessSlot1Active) {
|
|
28651
|
+
warnings.push(
|
|
28652
|
+
`Mission Control harness slot 1 is active under control/.har (port ${port}). Stop it with: cd control && har env teardown 1 \u2014 or use har control up only when the harness is down.`
|
|
28653
|
+
);
|
|
28654
|
+
}
|
|
28655
|
+
const portBusy = isPortInUse(port);
|
|
28656
|
+
if (portBusy) {
|
|
28657
|
+
const occupant = dockerOnPort(containers, port);
|
|
28658
|
+
const who = occupant ? `container "${occupant.name}"` : "another process";
|
|
28659
|
+
warnings.push(
|
|
28660
|
+
`Port ${port} is already in use by ${who}. har control up needs this port \u2014 free it first.`
|
|
28661
|
+
);
|
|
28662
|
+
}
|
|
28663
|
+
return {
|
|
28664
|
+
warnings,
|
|
28665
|
+
portInUse: portBusy,
|
|
28666
|
+
controlAlreadyRunning: false,
|
|
28667
|
+
harnessSlot1Active
|
|
28668
|
+
};
|
|
28669
|
+
}
|
|
28670
|
+
|
|
28671
|
+
// src/core/slot-launch-guard-occupied.ts
|
|
28672
|
+
var fs27 = __toESM(require("fs"));
|
|
28673
|
+
var os5 = __toESM(require("os"));
|
|
28674
|
+
var path25 = __toESM(require("path"));
|
|
28675
|
+
var import_child_process5 = require("child_process");
|
|
28676
|
+
function runGit(cwd, args) {
|
|
28677
|
+
try {
|
|
28678
|
+
return (0, import_child_process5.execSync)(`git ${args}`, {
|
|
28679
|
+
cwd,
|
|
28680
|
+
encoding: "utf8",
|
|
28681
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
28682
|
+
}).trim();
|
|
28683
|
+
} catch {
|
|
28684
|
+
return void 0;
|
|
28685
|
+
}
|
|
28686
|
+
}
|
|
28687
|
+
function worktreeDirty(worktreePath) {
|
|
28688
|
+
const porcelain = runGit(worktreePath, "status --porcelain");
|
|
28689
|
+
return porcelain !== void 0 && porcelain.length > 0;
|
|
28690
|
+
}
|
|
28691
|
+
function gitCommonDir2(cwd) {
|
|
28692
|
+
const out = runGit(cwd, "rev-parse --git-common-dir");
|
|
28693
|
+
return out ? path25.resolve(cwd, out) : void 0;
|
|
28694
|
+
}
|
|
28695
|
+
function sameGitCheckout2(a2, b2) {
|
|
28696
|
+
const left2 = gitCommonDir2(a2);
|
|
28697
|
+
const right2 = gitCommonDir2(b2);
|
|
28698
|
+
return left2 !== void 0 && right2 !== void 0 && left2 === right2;
|
|
28699
|
+
}
|
|
28700
|
+
function discoverWorktree(harnessRoot, agentId, projectName) {
|
|
28701
|
+
const session = readSlotRegistry(harnessRoot, agentId);
|
|
28702
|
+
if (session?.worktreePath && fs27.existsSync(session.worktreePath)) {
|
|
28703
|
+
return session.worktreePath;
|
|
28704
|
+
}
|
|
28705
|
+
const legacy = path25.join(os5.homedir(), "worktrees", `${projectName}-agent-${agentId}`);
|
|
28706
|
+
if (fs27.existsSync(legacy) && sameGitCheckout2(harnessRoot, legacy)) return legacy;
|
|
28707
|
+
const worktreesRoot = path25.join(os5.homedir(), "worktrees");
|
|
28708
|
+
if (!fs27.existsSync(worktreesRoot)) return void 0;
|
|
28709
|
+
const suffix = `-har-agent-${agentId}-`;
|
|
28710
|
+
const relPrefix = runGit(harnessRoot, "rev-parse --show-prefix") ?? "";
|
|
28711
|
+
for (const name of fs27.readdirSync(worktreesRoot)) {
|
|
28712
|
+
if (!name.includes(suffix)) continue;
|
|
28713
|
+
const candidate = path25.join(worktreesRoot, name);
|
|
28714
|
+
if (!sameGitCheckout2(harnessRoot, candidate)) continue;
|
|
28715
|
+
if (!fs27.existsSync(path25.join(candidate, relPrefix, `.env.agent.${agentId}`))) continue;
|
|
28716
|
+
return candidate;
|
|
28717
|
+
}
|
|
28718
|
+
return void 0;
|
|
28719
|
+
}
|
|
28720
|
+
function collectOccupiedSlot(repoPath, agentId) {
|
|
28721
|
+
const harnessRoot = resolveHarnessRoot(repoPath);
|
|
28722
|
+
const env3 = readHarnessEnv(harnessRoot);
|
|
28723
|
+
const projectName = env3.HARNESS_PROJECT_NAME ?? path25.basename(harnessRoot);
|
|
28724
|
+
const session = readSlotRegistry(harnessRoot, agentId);
|
|
28725
|
+
const worktreePath = discoverWorktree(harnessRoot, agentId, projectName);
|
|
28726
|
+
const workDir = session?.workDir;
|
|
28727
|
+
const envInWorkDir = workDir ? fs27.existsSync(path25.join(workDir, `.env.agent.${agentId}`)) : false;
|
|
28728
|
+
const envInRoot = fs27.existsSync(path25.join(harnessRoot, `.env.agent.${agentId}`));
|
|
28729
|
+
const envInWorktree = worktreePath ? fs27.existsSync(path25.join(worktreePath, `.env.agent.${agentId}`)) : false;
|
|
28730
|
+
const active = session !== void 0 && session.status !== "completed" || envInWorkDir || envInRoot || envInWorktree;
|
|
28731
|
+
if (!active) return void 0;
|
|
28732
|
+
const dirty = worktreePath ? worktreeDirty(worktreePath) : void 0;
|
|
28733
|
+
return {
|
|
28734
|
+
agentId,
|
|
28735
|
+
active: true,
|
|
28736
|
+
workDir: workDir ?? worktreePath,
|
|
28737
|
+
worktreePath,
|
|
28738
|
+
branch: session?.branch,
|
|
28739
|
+
sessionStatus: session?.status,
|
|
28740
|
+
lastError: session?.lastError,
|
|
28741
|
+
sessionCreatedAt: session?.createdAt,
|
|
28742
|
+
dirty,
|
|
28743
|
+
harnessUsage: "none"
|
|
28744
|
+
};
|
|
28745
|
+
}
|
|
28746
|
+
function formatOccupiedSlot(slot, resume) {
|
|
28747
|
+
const lines = [
|
|
28748
|
+
`Slot ${slot.agentId} is already in use.`,
|
|
28749
|
+
slot.worktreePath ? ` Worktree: ${slot.worktreePath}` : void 0,
|
|
28750
|
+
slot.branch ? ` Branch: ${slot.branch}` : void 0,
|
|
28751
|
+
slot.workDir ? ` Work dir: ${slot.workDir}` : void 0,
|
|
28752
|
+
slot.sessionStatus ? ` Status: ${slot.sessionStatus}` : void 0,
|
|
28753
|
+
slot.lastError ? ` Error: ${slot.lastError}` : void 0,
|
|
28754
|
+
slot.sessionCreatedAt ? ` Since: ${slot.sessionCreatedAt}` : void 0,
|
|
28755
|
+
slot.dirty ? " Git: dirty (uncommitted changes \u2014 commit or use force to discard)" : " Git: clean",
|
|
28756
|
+
""
|
|
28757
|
+
];
|
|
28758
|
+
if (resume || slot.sessionStatus === "failed" || slot.sessionStatus === "starting") {
|
|
28759
|
+
lines.push(
|
|
28760
|
+
"This session failed partway through launch. Resume without replacing:",
|
|
28761
|
+
` har env launch ${slot.agentId} --resume`,
|
|
28762
|
+
` har env recover ${slot.agentId}`,
|
|
28763
|
+
` ./.har/launch.sh ${slot.agentId} --resume`,
|
|
28764
|
+
""
|
|
28765
|
+
);
|
|
28766
|
+
}
|
|
28767
|
+
lines.push(
|
|
28768
|
+
"Replacing removes the worktree. The session branch is kept only if you committed.",
|
|
28769
|
+
"Gitignored paths (state/, runs/, local clones) are NOT preserved.",
|
|
28770
|
+
"",
|
|
28771
|
+
"To replace: pass confirmReplace=true (MCP), --replace (CLI), or answer y at the prompt.",
|
|
28772
|
+
"If the worktree is dirty, also pass force=true / --force after explicit user approval."
|
|
28773
|
+
);
|
|
28774
|
+
return lines.filter(Boolean).join("\n");
|
|
28775
|
+
}
|
|
28776
|
+
function checkLaunchGuard(repoPath, agentId, options = {}) {
|
|
28777
|
+
const slot = collectOccupiedSlot(repoPath, agentId);
|
|
28778
|
+
if (!slot?.active) {
|
|
28779
|
+
return { allowed: true };
|
|
28780
|
+
}
|
|
28781
|
+
const harnessRoot = resolveHarnessRoot(repoPath);
|
|
28782
|
+
const session = readSlotRegistry(harnessRoot, agentId);
|
|
28783
|
+
if (options.resume) {
|
|
28784
|
+
if (!isSlotResumable(session)) {
|
|
28785
|
+
return {
|
|
28786
|
+
allowed: false,
|
|
28787
|
+
blocked: true,
|
|
28788
|
+
slot,
|
|
28789
|
+
reason: [
|
|
28790
|
+
`Slot ${agentId} is not resumable (status=${session?.status ?? "none"}).`,
|
|
28791
|
+
"Only failed or starting sessions can be resumed.",
|
|
28792
|
+
"Use a normal launch with --replace to start fresh."
|
|
28793
|
+
].join("\n")
|
|
28794
|
+
};
|
|
28795
|
+
}
|
|
28796
|
+
return { allowed: true, slot };
|
|
28797
|
+
}
|
|
28798
|
+
if (isSlotResumable(session) && !options.confirmReplace) {
|
|
28799
|
+
return {
|
|
28800
|
+
allowed: false,
|
|
28801
|
+
blocked: true,
|
|
28802
|
+
slot,
|
|
28803
|
+
reason: formatOccupiedSlot(slot)
|
|
28804
|
+
};
|
|
28805
|
+
}
|
|
28806
|
+
if (!options.confirmReplace) {
|
|
28807
|
+
return {
|
|
28808
|
+
allowed: false,
|
|
28809
|
+
blocked: true,
|
|
28810
|
+
slot,
|
|
28811
|
+
reason: formatOccupiedSlot(slot)
|
|
28812
|
+
};
|
|
28813
|
+
}
|
|
28814
|
+
if (slot.dirty && !options.force) {
|
|
28815
|
+
return {
|
|
28816
|
+
allowed: false,
|
|
28817
|
+
blocked: true,
|
|
28818
|
+
slot,
|
|
28819
|
+
reason: [
|
|
28820
|
+
formatOccupiedSlot(slot),
|
|
28821
|
+
"",
|
|
28822
|
+
"The occupied worktree has uncommitted changes.",
|
|
28823
|
+
"Pass force=true / --force to discard them (only after explicit user approval)."
|
|
28824
|
+
].join("\n")
|
|
28825
|
+
};
|
|
28826
|
+
}
|
|
28827
|
+
return { allowed: true, slot };
|
|
28828
|
+
}
|
|
28829
|
+
|
|
28830
|
+
// src/core/slot-preflight.ts
|
|
28831
|
+
var import_child_process6 = require("child_process");
|
|
28832
|
+
function listPm2Processes() {
|
|
28833
|
+
try {
|
|
28834
|
+
const raw = (0, import_child_process6.execSync)("npx --yes pm2 jlist", {
|
|
28835
|
+
encoding: "utf8",
|
|
28836
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
28837
|
+
timeout: 3e3
|
|
28838
|
+
});
|
|
28839
|
+
const procs = JSON.parse(raw);
|
|
28840
|
+
return Array.isArray(procs) ? procs : void 0;
|
|
28841
|
+
} catch {
|
|
28842
|
+
return void 0;
|
|
28843
|
+
}
|
|
28844
|
+
}
|
|
28845
|
+
function detectForeignPm2(projectName, agentId, procs) {
|
|
28846
|
+
if (!procs) return void 0;
|
|
28847
|
+
const slotPrefix = `har-${projectName}-agent-${agentId}-`;
|
|
28848
|
+
const legacyPrefix = `agent-${agentId}-`;
|
|
28849
|
+
const foreign = procs.filter(
|
|
28850
|
+
(p2) => p2.name && (p2.name.startsWith("har-") && p2.name.includes(`-agent-${agentId}-`) && !p2.name.startsWith(slotPrefix) || p2.name.startsWith(legacyPrefix) && !p2.name.startsWith("har-"))
|
|
28851
|
+
);
|
|
28852
|
+
if (foreign.length === 0) return void 0;
|
|
28853
|
+
return {
|
|
28854
|
+
processes: foreign.map((p2) => ({
|
|
28855
|
+
name: p2.name,
|
|
28856
|
+
cwd: p2.pm2_env?.pm_cwd ?? p2.pm2_env?.cwd
|
|
28857
|
+
}))
|
|
28858
|
+
};
|
|
28859
|
+
}
|
|
28860
|
+
function infraEnabled(env3, service) {
|
|
28861
|
+
return ` ${env3.HARNESS_INFRA_SERVICES ?? ""} `.includes(` ${service} `);
|
|
28862
|
+
}
|
|
28863
|
+
function checkInfraPort(env3, varName, defaultPort, scanStart, scanEnd) {
|
|
28864
|
+
const current = env3[varName] ? Number(env3[varName]) : void 0;
|
|
28865
|
+
if (current !== void 0 && !isPortInUse(current)) {
|
|
28866
|
+
return { port: current };
|
|
28867
|
+
}
|
|
28868
|
+
if (current !== void 0 && isPortInUse(current)) {
|
|
28869
|
+
const alt = pickFreePort(scanStart, scanEnd);
|
|
28870
|
+
if (alt !== void 0) return { port: alt };
|
|
28871
|
+
return { error: `No free ${varName} port in range ${scanStart}-${scanEnd}` };
|
|
28872
|
+
}
|
|
28873
|
+
const port = pickFreePort(defaultPort, scanEnd) ?? pickFreePort(scanStart, scanEnd);
|
|
28874
|
+
if (port === void 0) {
|
|
28875
|
+
return { error: `No free ${varName} port in range ${scanStart}-${scanEnd}` };
|
|
28876
|
+
}
|
|
28877
|
+
return { port };
|
|
28878
|
+
}
|
|
28879
|
+
function inspectSlotReadiness(repoPath, agentId, options = {}) {
|
|
28880
|
+
const harnessRoot = resolveHarnessRoot(repoPath);
|
|
28881
|
+
const env3 = readHarnessEnv(harnessRoot);
|
|
28882
|
+
const projectName = env3.HARNESS_PROJECT_NAME ?? path26.basename(harnessRoot);
|
|
28883
|
+
const usesPm2 = harnessUsesPm2(repoPath);
|
|
28884
|
+
const blockers = [];
|
|
28885
|
+
const remediations = [];
|
|
28886
|
+
const warnings = [];
|
|
28887
|
+
const ports = {};
|
|
28888
|
+
const session = readSlotRegistry(harnessRoot, agentId);
|
|
28889
|
+
const guard = options.occupied !== void 0 ? options.occupied.active ? options.resume && isSlotResumable(session) ? { allowed: true } : !options.confirmReplace ? {
|
|
28890
|
+
allowed: false,
|
|
28891
|
+
blocked: true,
|
|
28892
|
+
reason: `Slot ${agentId} is already in use.`
|
|
28893
|
+
} : options.occupied.dirty && !options.force ? {
|
|
28894
|
+
allowed: false,
|
|
28895
|
+
blocked: true,
|
|
28896
|
+
reason: `Slot ${agentId} worktree has uncommitted changes.`
|
|
28897
|
+
} : { allowed: true } : { allowed: true } : checkLaunchGuard(repoPath, agentId, options);
|
|
28898
|
+
if (!guard.allowed) {
|
|
28899
|
+
const resumable = isSlotResumable(session);
|
|
28900
|
+
blockers.push({
|
|
28901
|
+
code: options.occupied?.dirty ? "slot_dirty" : resumable ? "slot_resumable" : "slot_occupied",
|
|
28902
|
+
message: guard.reason ?? `Slot ${agentId} is occupied.`,
|
|
28903
|
+
remediation: resumable ? `Resume the partial launch: har env launch ${agentId} --resume (or har env recover ${agentId})` : guard.slot?.dirty ? "Commit changes in the worktree, or pass --force after explicit user approval." : "Pass --replace (CLI), confirmReplace=true (MCP), or answer y at the launch prompt."
|
|
28904
|
+
});
|
|
28905
|
+
remediations.push(
|
|
28906
|
+
resumable ? `har env launch ${agentId} --resume` : guard.slot?.dirty ? "har env preflight <id> --replace --force" : "har env preflight <id> --replace"
|
|
28907
|
+
);
|
|
28908
|
+
}
|
|
28909
|
+
const pm2Procs = usesPm2 ? options.pm2Processes !== void 0 ? options.pm2Processes : listPm2Processes() : void 0;
|
|
28910
|
+
const foreign = usesPm2 ? detectForeignPm2(projectName, agentId, pm2Procs) : void 0;
|
|
28911
|
+
if (foreign) {
|
|
28912
|
+
const names = foreign.processes.map((p2) => p2.name).join(", ");
|
|
28913
|
+
blockers.push({
|
|
28914
|
+
code: "foreign_pm2",
|
|
28915
|
+
message: `Foreign PM2 processes match agent ${agentId}: ${names}`,
|
|
28916
|
+
remediation: "Stop the other harness session (./.har/teardown.sh in that repo) or use a different agent slot.",
|
|
28917
|
+
details: { processes: foreign.processes }
|
|
28918
|
+
});
|
|
28919
|
+
remediations.push("Inspect with: npx pm2 jlist | grep agent-" + agentId);
|
|
28920
|
+
}
|
|
28921
|
+
if (usesPm2 && pm2Procs) {
|
|
28922
|
+
const slotPrefix = `har-${projectName}-agent-${agentId}-`;
|
|
28923
|
+
const owned = pm2Procs.filter((p2) => p2.name?.startsWith(slotPrefix));
|
|
28924
|
+
if (owned.length > 0 && !session) {
|
|
28925
|
+
blockers.push({
|
|
28926
|
+
code: "registry_missing",
|
|
28927
|
+
message: `PM2 processes exist for ${projectName} agent ${agentId} but the slot registry is missing.`,
|
|
28928
|
+
remediation: "Run teardown in this harness or delete PM2 processes manually, then relaunch."
|
|
28929
|
+
});
|
|
28930
|
+
}
|
|
28931
|
+
if (owned.length > 0 && session?.projectName && session.projectName !== projectName) {
|
|
28932
|
+
blockers.push({
|
|
28933
|
+
code: "project_mismatch",
|
|
28934
|
+
message: `Slot registry projectName=${session.projectName} does not match harness ${projectName}.`,
|
|
28935
|
+
remediation: "Teardown the stale session and relaunch with --replace."
|
|
28936
|
+
});
|
|
28937
|
+
}
|
|
28938
|
+
}
|
|
28939
|
+
let allocatedPorts = false;
|
|
28940
|
+
if (usesPm2 && (options.allocatePorts ?? true)) {
|
|
28941
|
+
const alloc = allocateAppPorts(repoPath, agentId);
|
|
28942
|
+
if ("error" in alloc) {
|
|
28943
|
+
blockers.push({
|
|
28944
|
+
code: "ports_exhausted",
|
|
28945
|
+
message: alloc.error,
|
|
28946
|
+
remediation: "Free a port in the slot lane or stop the process/container using it."
|
|
28947
|
+
});
|
|
28948
|
+
} else {
|
|
28949
|
+
ports.frontend = alloc.frontend;
|
|
28950
|
+
ports.api = alloc.api;
|
|
28951
|
+
ports.debug = alloc.debug;
|
|
28952
|
+
allocatedPorts = alloc.allocated;
|
|
28953
|
+
const containers = options.dockerContainers ?? listDockerContainers();
|
|
28954
|
+
const feDefault = defaultAppPort(
|
|
28955
|
+
Number(env3.HARNESS_FE_BASE_PORT ?? 3e3),
|
|
28956
|
+
agentId,
|
|
28957
|
+
portStep(env3)
|
|
28958
|
+
);
|
|
28959
|
+
warnings.push(...controlDefaultPortWarnings(containers, feDefault, alloc.frontend));
|
|
28960
|
+
for (const [label, port] of Object.entries({ frontend: alloc.frontend, api: alloc.api })) {
|
|
28961
|
+
const control = controlContainerOnPort(containers, port);
|
|
28962
|
+
if (control) {
|
|
28963
|
+
blockers.push({
|
|
28964
|
+
code: "control_port_conflict",
|
|
28965
|
+
message: formatControlPortBlocker(control.name, port, label),
|
|
28966
|
+
remediation: "Run: har control down \u2014 or launch a different slot id.",
|
|
28967
|
+
details: { container: control.name, port, label }
|
|
28968
|
+
});
|
|
28969
|
+
remediations.push("har control down");
|
|
28970
|
+
} else {
|
|
28971
|
+
const occupant = dockerOnPort(containers, port);
|
|
28972
|
+
if (occupant && !occupant.name.startsWith(`har-${projectName}-`)) {
|
|
28973
|
+
blockers.push({
|
|
28974
|
+
code: "docker_port_conflict",
|
|
28975
|
+
message: `Docker container "${occupant.name}" binds port ${port} (${label}).`,
|
|
28976
|
+
remediation: `Stop the container: docker stop ${occupant.name}`,
|
|
28977
|
+
details: { container: occupant.name, port, label }
|
|
28978
|
+
});
|
|
28979
|
+
}
|
|
28980
|
+
}
|
|
28981
|
+
}
|
|
28982
|
+
}
|
|
28983
|
+
}
|
|
28984
|
+
if (infraEnabled(env3, "db")) {
|
|
28985
|
+
const dbDefault = Number(env3.HARNESS_DB_PORT_DEFAULT ?? 15432);
|
|
28986
|
+
const dbStart = Number(env3.HARNESS_DB_PORT_SCAN_START ?? dbDefault);
|
|
28987
|
+
const dbEnd = Number(env3.HARNESS_DB_PORT_SCAN_END ?? dbDefault + 67);
|
|
28988
|
+
const dbCheck = checkInfraPort(env3, "AGENT_DB_PORT", dbDefault, dbStart, dbEnd);
|
|
28989
|
+
if (dbCheck.error) {
|
|
28990
|
+
blockers.push({
|
|
28991
|
+
code: "db_port_exhausted",
|
|
28992
|
+
message: dbCheck.error,
|
|
28993
|
+
remediation: `Free a port in ${dbStart}-${dbEnd} or stop har-*-db-1 containers from other projects.`
|
|
28994
|
+
});
|
|
28995
|
+
} else if (dbCheck.port !== void 0) {
|
|
28996
|
+
ports.db = dbCheck.port;
|
|
28997
|
+
const occupant = dockerOnPort(
|
|
28998
|
+
options.dockerContainers ?? listDockerContainers(),
|
|
28999
|
+
dbCheck.port
|
|
29000
|
+
);
|
|
29001
|
+
if (occupant && !occupant.name.startsWith(`har-${projectName}-`) && !occupant.name.includes("-db-")) {
|
|
29002
|
+
blockers.push({
|
|
29003
|
+
code: "db_port_conflict",
|
|
29004
|
+
message: `Port ${dbCheck.port} (database) is held by "${occupant.name}".`,
|
|
29005
|
+
remediation: `Stop the container or pick another slot.`,
|
|
29006
|
+
details: { container: occupant.name, port: dbCheck.port }
|
|
29007
|
+
});
|
|
29008
|
+
}
|
|
29009
|
+
}
|
|
29010
|
+
}
|
|
29011
|
+
const canLaunch = blockers.length === 0;
|
|
29012
|
+
return {
|
|
29013
|
+
canLaunch,
|
|
29014
|
+
verdict: canLaunch ? "ready" : "blocked",
|
|
29015
|
+
blockers,
|
|
29016
|
+
remediations: [...new Set(remediations)],
|
|
29017
|
+
ports: Object.keys(ports).length > 0 ? ports : void 0,
|
|
29018
|
+
allocatedPorts: allocatedPorts || void 0,
|
|
29019
|
+
warnings: warnings.length > 0 ? warnings : void 0
|
|
29020
|
+
};
|
|
29021
|
+
}
|
|
29022
|
+
function formatPreflightReport(agentId, readiness) {
|
|
29023
|
+
const lines = [];
|
|
29024
|
+
if (readiness.canLaunch) {
|
|
29025
|
+
lines.push(`Slot ${agentId}: ready to launch.`);
|
|
29026
|
+
if (readiness.ports) {
|
|
29027
|
+
const p2 = readiness.ports;
|
|
29028
|
+
const parts = [
|
|
29029
|
+
p2.frontend !== void 0 ? `frontend=${p2.frontend}` : void 0,
|
|
29030
|
+
p2.api !== void 0 ? `api=${p2.api}` : void 0,
|
|
29031
|
+
p2.debug !== void 0 ? `debug=${p2.debug}` : void 0,
|
|
29032
|
+
p2.db !== void 0 ? `db=${p2.db}` : void 0
|
|
29033
|
+
].filter(Boolean);
|
|
29034
|
+
if (parts.length) lines.push(` Ports: ${parts.join(" ")}`);
|
|
29035
|
+
if (readiness.allocatedPorts && !readiness.warnings?.length) {
|
|
29036
|
+
lines.push(" (alternate ports selected \u2014 defaults were busy)");
|
|
29037
|
+
}
|
|
29038
|
+
}
|
|
29039
|
+
if (readiness.warnings?.length) {
|
|
29040
|
+
for (const w2 of readiness.warnings) {
|
|
29041
|
+
lines.push(` WARN: ${w2}`);
|
|
29042
|
+
}
|
|
29043
|
+
}
|
|
29044
|
+
return lines.join("\n");
|
|
29045
|
+
}
|
|
29046
|
+
lines.push(`Slot ${agentId}: launch blocked.`);
|
|
29047
|
+
for (const b2 of readiness.blockers) {
|
|
29048
|
+
lines.push(` [${b2.code}] ${b2.message}`);
|
|
29049
|
+
if (b2.remediation) lines.push(` \u2192 ${b2.remediation}`);
|
|
29050
|
+
}
|
|
29051
|
+
return lines.join("\n");
|
|
29052
|
+
}
|
|
29053
|
+
|
|
28084
29054
|
// src/core/slot-status.ts
|
|
28085
29055
|
var BYPASS_WARNING_MS = 15 * 60 * 1e3;
|
|
28086
29056
|
function readGitRemote(repoPath) {
|
|
28087
29057
|
try {
|
|
28088
|
-
const remote = (0,
|
|
29058
|
+
const remote = (0, import_child_process7.execSync)("git remote get-url origin", {
|
|
28089
29059
|
cwd: repoPath,
|
|
28090
29060
|
encoding: "utf8",
|
|
28091
29061
|
stdio: ["pipe", "pipe", "ignore"]
|
|
@@ -28095,9 +29065,9 @@ function readGitRemote(repoPath) {
|
|
|
28095
29065
|
return void 0;
|
|
28096
29066
|
}
|
|
28097
29067
|
}
|
|
28098
|
-
function
|
|
29068
|
+
function runGit2(cwd, args) {
|
|
28099
29069
|
try {
|
|
28100
|
-
return (0,
|
|
29070
|
+
return (0, import_child_process7.execSync)(`git ${args}`, {
|
|
28101
29071
|
cwd,
|
|
28102
29072
|
encoding: "utf8",
|
|
28103
29073
|
stdio: ["pipe", "pipe", "ignore"]
|
|
@@ -28107,40 +29077,40 @@ function runGit(cwd, args) {
|
|
|
28107
29077
|
}
|
|
28108
29078
|
}
|
|
28109
29079
|
function readWorktreeBranch(worktreePath) {
|
|
28110
|
-
return
|
|
29080
|
+
return runGit2(worktreePath, "rev-parse --abbrev-ref HEAD");
|
|
28111
29081
|
}
|
|
28112
|
-
function
|
|
28113
|
-
const out =
|
|
28114
|
-
return out ?
|
|
29082
|
+
function gitCommonDir3(cwd) {
|
|
29083
|
+
const out = runGit2(cwd, "rev-parse --git-common-dir");
|
|
29084
|
+
return out ? path27.resolve(cwd, out) : void 0;
|
|
28115
29085
|
}
|
|
28116
|
-
function
|
|
28117
|
-
const left2 =
|
|
28118
|
-
const right2 =
|
|
29086
|
+
function sameGitCheckout3(a2, b2) {
|
|
29087
|
+
const left2 = gitCommonDir3(a2);
|
|
29088
|
+
const right2 = gitCommonDir3(b2);
|
|
28119
29089
|
return left2 !== void 0 && right2 !== void 0 && left2 === right2;
|
|
28120
29090
|
}
|
|
28121
29091
|
function gitPrefix2(cwd) {
|
|
28122
|
-
const out =
|
|
29092
|
+
const out = runGit2(cwd, "rev-parse --show-prefix");
|
|
28123
29093
|
return out ?? "";
|
|
28124
29094
|
}
|
|
28125
29095
|
function discoverSessionWorktreePath(harnessRoot, agentId) {
|
|
28126
|
-
const worktreesRoot =
|
|
28127
|
-
if (!
|
|
29096
|
+
const worktreesRoot = path27.join(os6.homedir(), "worktrees");
|
|
29097
|
+
if (!fs28.existsSync(worktreesRoot)) return void 0;
|
|
28128
29098
|
const suffix = `-har-agent-${agentId}-`;
|
|
28129
29099
|
const relPrefix = gitPrefix2(harnessRoot);
|
|
28130
|
-
const matches =
|
|
28131
|
-
(candidate) =>
|
|
29100
|
+
const matches = fs28.readdirSync(worktreesRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.includes(suffix)).map((entry) => path27.join(worktreesRoot, entry.name)).filter(
|
|
29101
|
+
(candidate) => sameGitCheckout3(harnessRoot, candidate) && fs28.existsSync(path27.join(candidate, relPrefix, `.env.agent.${agentId}`))
|
|
28132
29102
|
);
|
|
28133
29103
|
return matches.sort()[0];
|
|
28134
29104
|
}
|
|
28135
29105
|
function collectWorktreeDrift(harnessRoot, worktreePath, baseCommit) {
|
|
28136
29106
|
const drift = {};
|
|
28137
|
-
drift.detachedHead =
|
|
28138
|
-
const porcelain =
|
|
29107
|
+
drift.detachedHead = runGit2(worktreePath, "symbolic-ref -q HEAD") === void 0;
|
|
29108
|
+
const porcelain = runGit2(worktreePath, "status --porcelain");
|
|
28139
29109
|
if (porcelain !== void 0) drift.dirty = porcelain.length > 0;
|
|
28140
29110
|
if (baseCommit) {
|
|
28141
|
-
const ahead =
|
|
29111
|
+
const ahead = runGit2(worktreePath, `rev-list --count ${baseCommit}..HEAD`);
|
|
28142
29112
|
if (ahead !== void 0) drift.ahead = Number(ahead);
|
|
28143
|
-
const behind =
|
|
29113
|
+
const behind = runGit2(harnessRoot, `rev-list --count ${baseCommit}..HEAD`);
|
|
28144
29114
|
if (behind !== void 0) {
|
|
28145
29115
|
drift.behind = Number(behind);
|
|
28146
29116
|
drift.stale = drift.behind > 0;
|
|
@@ -28171,20 +29141,50 @@ function lastBuildPass(run2) {
|
|
|
28171
29141
|
const build = data.verification?.stages?.find((s2) => s2.name === "build");
|
|
28172
29142
|
return build?.pass;
|
|
28173
29143
|
}
|
|
28174
|
-
function
|
|
29144
|
+
function listPm2Processes2() {
|
|
29145
|
+
try {
|
|
29146
|
+
const raw = (0, import_child_process7.execSync)("npx --yes pm2 jlist", {
|
|
29147
|
+
encoding: "utf8",
|
|
29148
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
29149
|
+
timeout: 3e3
|
|
29150
|
+
});
|
|
29151
|
+
const procs = JSON.parse(raw);
|
|
29152
|
+
return Array.isArray(procs) ? procs : void 0;
|
|
29153
|
+
} catch {
|
|
29154
|
+
return void 0;
|
|
29155
|
+
}
|
|
29156
|
+
}
|
|
29157
|
+
function detectPm2Issue(projectName, agentId, session, procs) {
|
|
29158
|
+
if (!procs) return void 0;
|
|
29159
|
+
const slotPrefix = `har-${projectName}-agent-${agentId}-`;
|
|
29160
|
+
const legacyPrefix = `agent-${agentId}-`;
|
|
29161
|
+
const owned = procs.filter((p2) => p2.name?.startsWith(slotPrefix));
|
|
29162
|
+
const foreign = procs.filter(
|
|
29163
|
+
(p2) => p2.name && (p2.name.startsWith("har-") && p2.name.includes(`-agent-${agentId}-`) && !p2.name.startsWith(slotPrefix) || p2.name.startsWith(legacyPrefix) && !p2.name.startsWith("har-"))
|
|
29164
|
+
);
|
|
29165
|
+
if (foreign.length > 0) return "foreign_pm2";
|
|
29166
|
+
if (owned.length > 0) {
|
|
29167
|
+
if (!session) return "registry_missing";
|
|
29168
|
+
if (session.projectName && session.projectName !== projectName) {
|
|
29169
|
+
return "project_mismatch";
|
|
29170
|
+
}
|
|
29171
|
+
}
|
|
29172
|
+
return void 0;
|
|
29173
|
+
}
|
|
29174
|
+
function collectSlotStatus(harnessRoot, agentId, runs, pm2Procs) {
|
|
28175
29175
|
const env3 = readHarnessEnv(harnessRoot);
|
|
28176
|
-
const projectName = env3.HARNESS_PROJECT_NAME ??
|
|
29176
|
+
const projectName = env3.HARNESS_PROJECT_NAME ?? path27.basename(harnessRoot);
|
|
28177
29177
|
const session = readSlotRegistry(harnessRoot, agentId);
|
|
28178
|
-
const legacyWorktreePath =
|
|
28179
|
-
|
|
29178
|
+
const legacyWorktreePath = path27.join(
|
|
29179
|
+
os6.homedir(),
|
|
28180
29180
|
"worktrees",
|
|
28181
29181
|
`${projectName}-agent-${agentId}`
|
|
28182
29182
|
);
|
|
28183
|
-
const worktreePath = session?.worktreePath &&
|
|
29183
|
+
const worktreePath = session?.worktreePath && fs28.existsSync(session.worktreePath) ? session.worktreePath : fs28.existsSync(legacyWorktreePath) ? legacyWorktreePath : discoverSessionWorktreePath(harnessRoot, agentId);
|
|
28184
29184
|
const workDir = resolveAgentWorkDir(harnessRoot, agentId);
|
|
28185
|
-
const envInWorkDir = workDir ?
|
|
28186
|
-
const envInRoot =
|
|
28187
|
-
const envInWorktree = worktreePath ?
|
|
29185
|
+
const envInWorkDir = workDir ? fs28.existsSync(path27.join(workDir, `.env.agent.${agentId}`)) : false;
|
|
29186
|
+
const envInRoot = fs28.existsSync(path27.join(harnessRoot, `.env.agent.${agentId}`));
|
|
29187
|
+
const envInWorktree = worktreePath ? fs28.existsSync(path27.join(worktreePath, `.env.agent.${agentId}`)) : false;
|
|
28188
29188
|
const active = session !== void 0 && session.status !== "completed" || envInWorkDir || envInRoot || envInWorktree;
|
|
28189
29189
|
const latest = latestRunForAgent(runs, agentId);
|
|
28190
29190
|
const verifyRun = runs.find((r2) => r2.agentId === agentId && r2.stageId === "verify");
|
|
@@ -28197,6 +29197,12 @@ function collectSlotStatus(harnessRoot, agentId, runs) {
|
|
|
28197
29197
|
}
|
|
28198
29198
|
}
|
|
28199
29199
|
const drift = worktreePath ? collectWorktreeDrift(harnessRoot, worktreePath, session?.baseCommit) : {};
|
|
29200
|
+
const pm2Issue = detectPm2Issue(projectName, agentId, session, pm2Procs);
|
|
29201
|
+
const readiness = inspectSlotReadiness(harnessRoot, agentId, {
|
|
29202
|
+
allocatePorts: true,
|
|
29203
|
+
occupied: { active, dirty: drift.dirty }
|
|
29204
|
+
});
|
|
29205
|
+
const resumeHint = session?.status === "failed" || session?.status === "starting" ? `har env launch ${agentId} --resume (alias: har env recover ${agentId})` : void 0;
|
|
28200
29206
|
return {
|
|
28201
29207
|
agentId,
|
|
28202
29208
|
active,
|
|
@@ -28204,6 +29210,9 @@ function collectSlotStatus(harnessRoot, agentId, runs) {
|
|
|
28204
29210
|
worktreePath,
|
|
28205
29211
|
branch: session?.branch ?? (worktreePath ? readWorktreeBranch(worktreePath) : void 0),
|
|
28206
29212
|
previewUrls,
|
|
29213
|
+
ports: session?.ports,
|
|
29214
|
+
pm2Issue,
|
|
29215
|
+
readiness,
|
|
28207
29216
|
harnessUsage: deriveHarnessUsage(latest, active),
|
|
28208
29217
|
lastRunId: latest?.runId,
|
|
28209
29218
|
lastRunAt: latest?.startedAt,
|
|
@@ -28217,6 +29226,7 @@ function collectSlotStatus(harnessRoot, agentId, runs) {
|
|
|
28217
29226
|
purpose: session?.purpose,
|
|
28218
29227
|
sessionStatus: session?.status,
|
|
28219
29228
|
lastError: session?.lastError,
|
|
29229
|
+
resumeHint,
|
|
28220
29230
|
...drift
|
|
28221
29231
|
};
|
|
28222
29232
|
}
|
|
@@ -28225,25 +29235,26 @@ function collectEnvironmentStatus(repoPath) {
|
|
|
28225
29235
|
const runs = listRuns(harnessRoot, { limit: 200 });
|
|
28226
29236
|
const manifest = readManifest(harnessRoot);
|
|
28227
29237
|
const slotIds = getAgentSlotIds(harnessRoot);
|
|
29238
|
+
const pm2Procs = listPm2Processes2();
|
|
28228
29239
|
return {
|
|
28229
|
-
repoPath:
|
|
29240
|
+
repoPath: path27.resolve(repoPath),
|
|
28230
29241
|
harnessRoot,
|
|
28231
29242
|
gitRemote: readGitRemote(harnessRoot),
|
|
28232
29243
|
profile: manifest?.profile,
|
|
28233
|
-
slots: slotIds.map((id) => collectSlotStatus(harnessRoot, id, runs)),
|
|
29244
|
+
slots: slotIds.map((id) => collectSlotStatus(harnessRoot, id, runs, pm2Procs)),
|
|
28234
29245
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
28235
29246
|
};
|
|
28236
29247
|
}
|
|
28237
29248
|
|
|
28238
29249
|
// src/core/validations.ts
|
|
28239
29250
|
var crypto3 = __toESM(require("crypto"));
|
|
28240
|
-
var
|
|
28241
|
-
var
|
|
29251
|
+
var fs30 = __toESM(require("fs"));
|
|
29252
|
+
var path29 = __toESM(require("path"));
|
|
28242
29253
|
|
|
28243
29254
|
// src/core/change-batch.ts
|
|
28244
|
-
var
|
|
28245
|
-
var
|
|
28246
|
-
var
|
|
29255
|
+
var fs29 = __toESM(require("fs"));
|
|
29256
|
+
var os7 = __toESM(require("os"));
|
|
29257
|
+
var path28 = __toESM(require("path"));
|
|
28247
29258
|
function git(checkoutDir, args, env3) {
|
|
28248
29259
|
const result = run(`git ${args}`, { cwd: checkoutDir, env: env3 });
|
|
28249
29260
|
if (result.code !== 0) {
|
|
@@ -28259,7 +29270,7 @@ function isCheckoutRoot(checkoutDir) {
|
|
|
28259
29270
|
const toplevel = tryGit(checkoutDir, "rev-parse --show-toplevel");
|
|
28260
29271
|
if (!toplevel) return false;
|
|
28261
29272
|
try {
|
|
28262
|
-
return
|
|
29273
|
+
return fs29.realpathSync(toplevel) === fs29.realpathSync(checkoutDir);
|
|
28263
29274
|
} catch {
|
|
28264
29275
|
return false;
|
|
28265
29276
|
}
|
|
@@ -28281,7 +29292,7 @@ function isMergeOrRebaseInProgress(checkoutDir) {
|
|
|
28281
29292
|
if (tryGit(checkoutDir, "rev-parse -q --verify MERGE_HEAD") !== void 0) return true;
|
|
28282
29293
|
for (const dir of ["rebase-merge", "rebase-apply"]) {
|
|
28283
29294
|
const gitPath = tryGit(checkoutDir, `rev-parse --git-path ${dir}`);
|
|
28284
|
-
if (gitPath &&
|
|
29295
|
+
if (gitPath && fs29.existsSync(path28.resolve(checkoutDir, gitPath))) return true;
|
|
28285
29296
|
}
|
|
28286
29297
|
return false;
|
|
28287
29298
|
}
|
|
@@ -28304,8 +29315,8 @@ function parseNameStatus(output) {
|
|
|
28304
29315
|
return files;
|
|
28305
29316
|
}
|
|
28306
29317
|
function computeWorktreeSnapshot(checkoutDir) {
|
|
28307
|
-
const tmpDir =
|
|
28308
|
-
const tmpIndex =
|
|
29318
|
+
const tmpDir = fs29.mkdtempSync(path28.join(os7.tmpdir(), "har-idx-"));
|
|
29319
|
+
const tmpIndex = path28.join(tmpDir, "index");
|
|
28309
29320
|
const env3 = { GIT_INDEX_FILE: tmpIndex };
|
|
28310
29321
|
try {
|
|
28311
29322
|
const headTree = getHeadTree(checkoutDir);
|
|
@@ -28328,35 +29339,35 @@ function computeWorktreeSnapshot(checkoutDir) {
|
|
|
28328
29339
|
changedFiles: parseNameStatus(diffOutput)
|
|
28329
29340
|
};
|
|
28330
29341
|
} finally {
|
|
28331
|
-
|
|
29342
|
+
fs29.rmSync(tmpDir, { recursive: true, force: true });
|
|
28332
29343
|
}
|
|
28333
29344
|
}
|
|
28334
29345
|
|
|
28335
29346
|
// src/core/validations.ts
|
|
28336
29347
|
var VALIDATIONS_DIR = "validations";
|
|
28337
29348
|
function getValidationsDir(checkoutDir) {
|
|
28338
|
-
return
|
|
29349
|
+
return path29.join(checkoutDir, ".har", VALIDATIONS_DIR);
|
|
28339
29350
|
}
|
|
28340
29351
|
function validationPath(checkoutDir, treeHash) {
|
|
28341
|
-
return
|
|
29352
|
+
return path29.join(getValidationsDir(checkoutDir), `${treeHash}.json`);
|
|
28342
29353
|
}
|
|
28343
29354
|
function writeRecord(checkoutDir, record2) {
|
|
28344
29355
|
const dir = getValidationsDir(checkoutDir);
|
|
28345
|
-
|
|
28346
|
-
|
|
29356
|
+
fs30.mkdirSync(dir, { recursive: true });
|
|
29357
|
+
fs30.writeFileSync(
|
|
28347
29358
|
validationPath(checkoutDir, record2.treeHash),
|
|
28348
29359
|
`${JSON.stringify(record2, null, 2)}
|
|
28349
29360
|
`
|
|
28350
29361
|
);
|
|
28351
29362
|
}
|
|
28352
29363
|
function ensureValidationsIgnored(checkoutDir) {
|
|
28353
|
-
const harDir =
|
|
28354
|
-
if (!
|
|
28355
|
-
const gitignorePath =
|
|
28356
|
-
const content =
|
|
29364
|
+
const harDir = path29.join(checkoutDir, ".har");
|
|
29365
|
+
if (!fs30.existsSync(harDir)) return;
|
|
29366
|
+
const gitignorePath = path29.join(harDir, ".gitignore");
|
|
29367
|
+
const content = fs30.existsSync(gitignorePath) ? fs30.readFileSync(gitignorePath, "utf8") : "";
|
|
28357
29368
|
if (content.split("\n").some((line) => line.trim() === `${VALIDATIONS_DIR}/`)) return;
|
|
28358
29369
|
const suffix = content.length === 0 || content.endsWith("\n") ? "" : "\n";
|
|
28359
|
-
|
|
29370
|
+
fs30.writeFileSync(gitignorePath, `${content}${suffix}${VALIDATIONS_DIR}/
|
|
28360
29371
|
`);
|
|
28361
29372
|
}
|
|
28362
29373
|
function recordValidation(input) {
|
|
@@ -28373,8 +29384,8 @@ function recordValidation(input) {
|
|
|
28373
29384
|
treeHash: snapshot.treeHash,
|
|
28374
29385
|
headSha: snapshot.headSha,
|
|
28375
29386
|
branch: snapshot.branch,
|
|
28376
|
-
workDir:
|
|
28377
|
-
harnessRoot:
|
|
29387
|
+
workDir: path29.resolve(input.checkoutDir),
|
|
29388
|
+
harnessRoot: path29.resolve(input.harnessRoot),
|
|
28378
29389
|
agentId: input.agentId,
|
|
28379
29390
|
status: input.status,
|
|
28380
29391
|
full: input.full,
|
|
@@ -28386,16 +29397,16 @@ function recordValidation(input) {
|
|
|
28386
29397
|
committedAt: existing?.committedAt
|
|
28387
29398
|
});
|
|
28388
29399
|
writeRecord(input.checkoutDir, record2);
|
|
28389
|
-
if (
|
|
29400
|
+
if (path29.resolve(input.harnessRoot) !== path29.resolve(input.checkoutDir)) {
|
|
28390
29401
|
writeRecord(input.harnessRoot, record2);
|
|
28391
29402
|
}
|
|
28392
29403
|
return record2;
|
|
28393
29404
|
}
|
|
28394
29405
|
function findValidation(checkoutDir, treeHash) {
|
|
28395
29406
|
const file = validationPath(checkoutDir, treeHash);
|
|
28396
|
-
if (!
|
|
29407
|
+
if (!fs30.existsSync(file)) return void 0;
|
|
28397
29408
|
try {
|
|
28398
|
-
return ValidationRecordSchema.parse(JSON.parse(
|
|
29409
|
+
return ValidationRecordSchema.parse(JSON.parse(fs30.readFileSync(file, "utf8")));
|
|
28399
29410
|
} catch {
|
|
28400
29411
|
return void 0;
|
|
28401
29412
|
}
|
|
@@ -28410,19 +29421,19 @@ function attachCommit(checkoutDir, treeHash, commitSha) {
|
|
|
28410
29421
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
28411
29422
|
};
|
|
28412
29423
|
writeRecord(checkoutDir, updated);
|
|
28413
|
-
if (record2.harnessRoot &&
|
|
29424
|
+
if (record2.harnessRoot && path29.resolve(record2.harnessRoot) !== path29.resolve(checkoutDir) && fs30.existsSync(path29.join(record2.harnessRoot, ".har"))) {
|
|
28414
29425
|
writeRecord(record2.harnessRoot, updated);
|
|
28415
29426
|
}
|
|
28416
29427
|
return updated;
|
|
28417
29428
|
}
|
|
28418
29429
|
function listValidations(harnessRoot) {
|
|
28419
29430
|
const dir = getValidationsDir(harnessRoot);
|
|
28420
|
-
if (!
|
|
29431
|
+
if (!fs30.existsSync(dir)) return [];
|
|
28421
29432
|
const records = [];
|
|
28422
|
-
for (const entry of
|
|
29433
|
+
for (const entry of fs30.readdirSync(dir)) {
|
|
28423
29434
|
if (!entry.endsWith(".json")) continue;
|
|
28424
29435
|
try {
|
|
28425
|
-
records.push(ValidationRecordSchema.parse(JSON.parse(
|
|
29436
|
+
records.push(ValidationRecordSchema.parse(JSON.parse(fs30.readFileSync(path29.join(dir, entry), "utf8"))));
|
|
28426
29437
|
} catch {
|
|
28427
29438
|
}
|
|
28428
29439
|
}
|
|
@@ -28480,7 +29491,7 @@ async function waitForControlApi(apiUrl = getControlApiUrl(), timeoutMs = 6e4, i
|
|
|
28480
29491
|
const deadline = Date.now() + timeoutMs;
|
|
28481
29492
|
while (Date.now() < deadline) {
|
|
28482
29493
|
if (await isControlApiReachable(apiUrl)) return true;
|
|
28483
|
-
await new Promise((
|
|
29494
|
+
await new Promise((resolve26) => setTimeout(resolve26, intervalMs));
|
|
28484
29495
|
}
|
|
28485
29496
|
return false;
|
|
28486
29497
|
}
|
|
@@ -28492,7 +29503,7 @@ async function syncAllKnownReposWithControl(options) {
|
|
|
28492
29503
|
}
|
|
28493
29504
|
const repoPaths = new Set(listRegisteredRepos());
|
|
28494
29505
|
if (options?.cwd) {
|
|
28495
|
-
const cwd =
|
|
29506
|
+
const cwd = path30.resolve(options.cwd);
|
|
28496
29507
|
if (readManifest(cwd)) repoPaths.add(cwd);
|
|
28497
29508
|
}
|
|
28498
29509
|
try {
|
|
@@ -28500,7 +29511,7 @@ async function syncAllKnownReposWithControl(options) {
|
|
|
28500
29511
|
if (listResponse.ok) {
|
|
28501
29512
|
const repos = await listResponse.json();
|
|
28502
29513
|
for (const repo of repos) {
|
|
28503
|
-
const resolved =
|
|
29514
|
+
const resolved = path30.resolve(repo.path);
|
|
28504
29515
|
if (readManifest(resolved)) repoPaths.add(resolved);
|
|
28505
29516
|
}
|
|
28506
29517
|
}
|
|
@@ -28519,7 +29530,7 @@ async function syncAllKnownReposWithControl(options) {
|
|
|
28519
29530
|
return { synced, failed };
|
|
28520
29531
|
}
|
|
28521
29532
|
async function registerRepoWithControl(options) {
|
|
28522
|
-
const repoPath =
|
|
29533
|
+
const repoPath = path30.resolve(options.repoPath);
|
|
28523
29534
|
const apiUrl = options.apiUrl ?? getControlApiUrl();
|
|
28524
29535
|
const manifest = readManifest(repoPath);
|
|
28525
29536
|
const stagesRegistry = readStageRegistry(repoPath);
|
|
@@ -28532,7 +29543,7 @@ async function registerRepoWithControl(options) {
|
|
|
28532
29543
|
return postJson(`${apiUrl}/api/repos`, body, options.dryRun);
|
|
28533
29544
|
}
|
|
28534
29545
|
async function syncRepoWithControl(options) {
|
|
28535
|
-
const repoPath =
|
|
29546
|
+
const repoPath = path30.resolve(options.repoPath);
|
|
28536
29547
|
const apiUrl = options.apiUrl ?? getControlApiUrl();
|
|
28537
29548
|
if (options.cloud) {
|
|
28538
29549
|
const remote = createRemoteExecutor();
|
|
@@ -28560,7 +29571,7 @@ async function syncRepoWithControl(options) {
|
|
|
28560
29571
|
const listResponse = await fetch(`${apiUrl}/api/repos`);
|
|
28561
29572
|
if (!listResponse.ok) throw new Error("Failed to list repos from Control API");
|
|
28562
29573
|
const repos = await listResponse.json();
|
|
28563
|
-
const existing = repos.find((r2) =>
|
|
29574
|
+
const existing = repos.find((r2) => path30.resolve(r2.path) === repoPath);
|
|
28564
29575
|
if (!existing) throw new Error(`Repo not registered: ${repoPath}`);
|
|
28565
29576
|
await syncRepoRunsAndSlots(apiUrl, existing.id, repoPath, options.dryRun);
|
|
28566
29577
|
return;
|
|
@@ -28616,53 +29627,19 @@ async function syncRepoWithControlAsync(repoPath) {
|
|
|
28616
29627
|
}
|
|
28617
29628
|
|
|
28618
29629
|
// src/core/slot-launch-guard.ts
|
|
28619
|
-
function
|
|
28620
|
-
const
|
|
28621
|
-
|
|
28622
|
-
slot.worktreePath ? ` Worktree: ${slot.worktreePath}` : void 0,
|
|
28623
|
-
slot.branch ? ` Branch: ${slot.branch}` : void 0,
|
|
28624
|
-
slot.workDir ? ` Work dir: ${slot.workDir}` : void 0,
|
|
28625
|
-
slot.sessionStatus ? ` Status: ${slot.sessionStatus}` : void 0,
|
|
28626
|
-
slot.lastError ? ` Error: ${slot.lastError}` : void 0,
|
|
28627
|
-
slot.sessionCreatedAt ? ` Since: ${slot.sessionCreatedAt}` : void 0,
|
|
28628
|
-
slot.dirty ? " Git: dirty (uncommitted changes \u2014 commit or use force to discard)" : " Git: clean",
|
|
28629
|
-
"",
|
|
28630
|
-
"Replacing removes the worktree. The session branch is kept only if you committed.",
|
|
28631
|
-
"Gitignored paths (state/, runs/, local clones) are NOT preserved.",
|
|
28632
|
-
"",
|
|
28633
|
-
"To replace: pass confirmReplace=true (MCP), --replace (CLI), or answer y at the prompt.",
|
|
28634
|
-
"If the worktree is dirty, also pass force=true / --force after explicit user approval."
|
|
28635
|
-
];
|
|
28636
|
-
return lines.filter(Boolean).join("\n");
|
|
28637
|
-
}
|
|
28638
|
-
function checkLaunchGuard(repoPath, agentId, options = {}) {
|
|
28639
|
-
const status = collectEnvironmentStatus(repoPath);
|
|
28640
|
-
const slot = status.slots.find((s2) => s2.agentId === agentId);
|
|
28641
|
-
if (!slot?.active) {
|
|
28642
|
-
return { allowed: true };
|
|
28643
|
-
}
|
|
28644
|
-
if (!options.confirmReplace) {
|
|
29630
|
+
function checkLaunchGuard2(repoPath, agentId, options = {}) {
|
|
29631
|
+
const readiness = inspectSlotReadiness(repoPath, agentId, options);
|
|
29632
|
+
if (!readiness.canLaunch) {
|
|
28645
29633
|
return {
|
|
28646
29634
|
allowed: false,
|
|
28647
29635
|
blocked: true,
|
|
28648
|
-
|
|
28649
|
-
|
|
29636
|
+
reason: formatPreflightReport(agentId, readiness),
|
|
29637
|
+
slot: checkLaunchGuard(repoPath, agentId, options).slot,
|
|
29638
|
+
readiness
|
|
28650
29639
|
};
|
|
28651
29640
|
}
|
|
28652
|
-
|
|
28653
|
-
|
|
28654
|
-
allowed: false,
|
|
28655
|
-
blocked: true,
|
|
28656
|
-
slot,
|
|
28657
|
-
reason: [
|
|
28658
|
-
formatOccupiedSlot(slot),
|
|
28659
|
-
"",
|
|
28660
|
-
"The occupied worktree has uncommitted changes.",
|
|
28661
|
-
"Pass force=true / --force to discard them (only after explicit user approval)."
|
|
28662
|
-
].join("\n")
|
|
28663
|
-
};
|
|
28664
|
-
}
|
|
28665
|
-
return { allowed: true, slot };
|
|
29641
|
+
const occupied = checkLaunchGuard(repoPath, agentId, options);
|
|
29642
|
+
return { allowed: true, slot: occupied.slot, readiness };
|
|
28666
29643
|
}
|
|
28667
29644
|
|
|
28668
29645
|
// src/core/run-service.ts
|
|
@@ -28748,11 +29725,26 @@ var RunService = class {
|
|
|
28748
29725
|
listArtifacts(options) {
|
|
28749
29726
|
return this.executor.listArtifacts({ repoPath: options.repoPath }, { stageId: options.stageId });
|
|
28750
29727
|
}
|
|
28751
|
-
async
|
|
28752
|
-
const
|
|
29728
|
+
async preflightEnvironment(options) {
|
|
29729
|
+
const readiness = inspectSlotReadiness(options.repoPath, options.agentId, {
|
|
28753
29730
|
confirmReplace: options.confirmReplace,
|
|
28754
29731
|
force: options.force
|
|
28755
29732
|
});
|
|
29733
|
+
const report = formatPreflightReport(options.agentId, readiness);
|
|
29734
|
+
return {
|
|
29735
|
+
code: readiness.canLaunch ? 0 : 1,
|
|
29736
|
+
stdout: report + "\n",
|
|
29737
|
+
stderr: readiness.canLaunch ? "" : report + "\n",
|
|
29738
|
+
readiness,
|
|
29739
|
+
blocked: !readiness.canLaunch
|
|
29740
|
+
};
|
|
29741
|
+
}
|
|
29742
|
+
async launchEnvironment(options) {
|
|
29743
|
+
const guard = checkLaunchGuard2(options.repoPath, options.agentId, {
|
|
29744
|
+
confirmReplace: options.confirmReplace,
|
|
29745
|
+
force: options.force,
|
|
29746
|
+
resume: options.resume
|
|
29747
|
+
});
|
|
28756
29748
|
if (!guard.allowed) {
|
|
28757
29749
|
const slot = guard.slot;
|
|
28758
29750
|
return {
|
|
@@ -28779,7 +29771,8 @@ var RunService = class {
|
|
|
28779
29771
|
worktree: options.worktree,
|
|
28780
29772
|
claude: options.claude,
|
|
28781
29773
|
confirmReplace: options.confirmReplace,
|
|
28782
|
-
force: options.force
|
|
29774
|
+
force: options.force,
|
|
29775
|
+
resume: options.resume
|
|
28783
29776
|
},
|
|
28784
29777
|
trigger: "cli"
|
|
28785
29778
|
});
|
|
@@ -28950,6 +29943,7 @@ var defaultRunService = new RunService();
|
|
|
28950
29943
|
var runStage = defaultRunService.runStage.bind(defaultRunService);
|
|
28951
29944
|
var listArtifacts = defaultRunService.listArtifacts.bind(defaultRunService);
|
|
28952
29945
|
var launchEnvironment = defaultRunService.launchEnvironment.bind(defaultRunService);
|
|
29946
|
+
var preflightEnvironment = defaultRunService.preflightEnvironment.bind(defaultRunService);
|
|
28953
29947
|
var runVerification = defaultRunService.runVerification.bind(defaultRunService);
|
|
28954
29948
|
var teardownEnvironment = defaultRunService.teardownEnvironment.bind(defaultRunService);
|
|
28955
29949
|
var completeEnvironment = defaultRunService.completeEnvironment.bind(defaultRunService);
|
|
@@ -29041,8 +30035,30 @@ var envCommand = {
|
|
|
29041
30035
|
type: "boolean",
|
|
29042
30036
|
default: false,
|
|
29043
30037
|
describe: "Discard uncommitted changes when replacing a dirty worktree (only after explicit approval)"
|
|
30038
|
+
}).option("resume", {
|
|
30039
|
+
type: "boolean",
|
|
30040
|
+
default: false,
|
|
30041
|
+
describe: "Resume a failed or partial launch without creating a new worktree"
|
|
29044
30042
|
}),
|
|
29045
30043
|
handleLaunch
|
|
30044
|
+
).command(
|
|
30045
|
+
"recover <id>",
|
|
30046
|
+
"Resume a failed or partial launch (alias for launch --resume)",
|
|
30047
|
+
(y2) => y2.positional("id", { type: "number", describe: "Agent slot id (see .har/stages.json agentSlots)" }).option("repo", { type: "string", default: "." }),
|
|
30048
|
+
handleRecover
|
|
30049
|
+
).command(
|
|
30050
|
+
"preflight <id>",
|
|
30051
|
+
"Check whether a slot can launch now (ports, PM2, Docker, occupied slot)",
|
|
30052
|
+
(y2) => y2.positional("id", { type: "number", describe: "Agent slot id (see .har/stages.json agentSlots)" }).option("repo", { type: "string", default: "." }).option("json", { type: "boolean", default: false, describe: "Structured JSON output" }).option("replace", {
|
|
30053
|
+
type: "boolean",
|
|
30054
|
+
default: false,
|
|
30055
|
+
describe: "Treat an occupied slot as replaceable (same as launch --replace)"
|
|
30056
|
+
}).option("force", {
|
|
30057
|
+
type: "boolean",
|
|
30058
|
+
default: false,
|
|
30059
|
+
describe: "Allow replacing a dirty worktree (only after explicit approval)"
|
|
30060
|
+
}),
|
|
30061
|
+
handlePreflight
|
|
29046
30062
|
).command(
|
|
29047
30063
|
"verify <id>",
|
|
29048
30064
|
"Run verification suite for an agent",
|
|
@@ -29091,12 +30107,12 @@ var envCommand = {
|
|
|
29091
30107
|
).demandCommand(1, "Use: runs list | runs get <runId>"),
|
|
29092
30108
|
() => {
|
|
29093
30109
|
}
|
|
29094
|
-
).demandCommand(1, "Please specify a subcommand: init, maintain, add-stage, launch, verify, complete, teardown, status, runs"),
|
|
30110
|
+
).demandCommand(1, "Please specify a subcommand: init, maintain, add-stage, launch, recover, verify, complete, teardown, status, runs"),
|
|
29095
30111
|
handler: () => {
|
|
29096
30112
|
}
|
|
29097
30113
|
};
|
|
29098
30114
|
async function handleInit(argv) {
|
|
29099
|
-
const repoPath =
|
|
30115
|
+
const repoPath = path31.resolve(argv.repo);
|
|
29100
30116
|
header("har env init");
|
|
29101
30117
|
info(`Repository: ${repoPath}`);
|
|
29102
30118
|
try {
|
|
@@ -29147,7 +30163,7 @@ async function handleInit(argv) {
|
|
|
29147
30163
|
}
|
|
29148
30164
|
}
|
|
29149
30165
|
async function handleMaintain(argv) {
|
|
29150
|
-
const repoPath =
|
|
30166
|
+
const repoPath = path31.resolve(argv.repo);
|
|
29151
30167
|
header("har env maintain");
|
|
29152
30168
|
info(`Repository: ${repoPath}`);
|
|
29153
30169
|
try {
|
|
@@ -29169,20 +30185,36 @@ async function handleMaintain(argv) {
|
|
|
29169
30185
|
info("Validating harness...");
|
|
29170
30186
|
printValidation(result.validation);
|
|
29171
30187
|
printDrift(result.drift);
|
|
29172
|
-
if (
|
|
29173
|
-
|
|
29174
|
-
process.exit(1);
|
|
30188
|
+
if (result.bundle) {
|
|
30189
|
+
printMaintainBundleSummary(result.bundle.report);
|
|
29175
30190
|
}
|
|
29176
|
-
if (argv.
|
|
29177
|
-
|
|
29178
|
-
|
|
30191
|
+
if (argv.finalize) {
|
|
30192
|
+
if (!result.validation.pass) {
|
|
30193
|
+
warn("Harness has validation errors \u2014 fix them before finalizing.");
|
|
30194
|
+
process.exit(1);
|
|
30195
|
+
}
|
|
29179
30196
|
info("Manifest updated \u2014 generator version and file checksums recorded.");
|
|
30197
|
+
} else if (argv.auto) {
|
|
30198
|
+
if (!result.validation.pass) {
|
|
30199
|
+
warn("Harness has validation errors after maintenance.");
|
|
30200
|
+
process.exit(1);
|
|
30201
|
+
}
|
|
30202
|
+
await handleAgentMdProposal(repoPath, argv.yes);
|
|
29180
30203
|
} else {
|
|
29181
|
-
|
|
30204
|
+
if (!result.validation.pass) {
|
|
30205
|
+
warn("Harness has validation errors \u2014 fix them before running --finalize.");
|
|
30206
|
+
}
|
|
30207
|
+
emitManualAdaptationPrompt(repoPath, "maintain", "default", result.bundle?.report);
|
|
29182
30208
|
info("After your coding agent finishes adapting, record it with: har env maintain --finalize");
|
|
29183
30209
|
}
|
|
29184
30210
|
divider();
|
|
29185
|
-
|
|
30211
|
+
if (argv.finalize) {
|
|
30212
|
+
success("Harness finalized!");
|
|
30213
|
+
} else if (result.bundle) {
|
|
30214
|
+
success("Maintenance bundle ready!");
|
|
30215
|
+
} else {
|
|
30216
|
+
success("Harness updated!");
|
|
30217
|
+
}
|
|
29186
30218
|
await handleCursorRule({
|
|
29187
30219
|
repoPath,
|
|
29188
30220
|
cursorRule: resolveCursorRuleFlag(argv.cursorRule, argv.noCursorRule),
|
|
@@ -29199,8 +30231,8 @@ function resolveCursorRuleFlag(cursorRule, noCursorRule) {
|
|
|
29199
30231
|
if (cursorRule) return true;
|
|
29200
30232
|
return void 0;
|
|
29201
30233
|
}
|
|
29202
|
-
function emitManualAdaptationPrompt(repoPath, mode, profile = "default") {
|
|
29203
|
-
const prompt = mode === "init" ? buildInitAdaptationPrompt(repoPath, profile) : buildMaintainAdaptationPrompt(repoPath);
|
|
30234
|
+
function emitManualAdaptationPrompt(repoPath, mode, profile = "default", bundleReport) {
|
|
30235
|
+
const prompt = mode === "init" ? buildInitAdaptationPrompt(repoPath, profile) : buildMaintainAdaptationPrompt(repoPath, bundleReport);
|
|
29204
30236
|
writeAdaptationPrompt(repoPath, prompt);
|
|
29205
30237
|
process.stderr.write("\n");
|
|
29206
30238
|
if (mode === "init") {
|
|
@@ -29209,14 +30241,18 @@ function emitManualAdaptationPrompt(repoPath, mode, profile = "default") {
|
|
|
29209
30241
|
info("Review harness drift with your coding agent:");
|
|
29210
30242
|
}
|
|
29211
30243
|
info(" Paste the prompt below (also saved to .har/ADAPT-PROMPT.md)");
|
|
29212
|
-
|
|
30244
|
+
if (mode === "init") {
|
|
30245
|
+
info(" TODO validation warnings are expected until adaptation is complete.");
|
|
30246
|
+
} else if (bundleReport) {
|
|
30247
|
+
info(" Reference templates are in .har/maintain/templates/");
|
|
30248
|
+
}
|
|
29213
30249
|
printAdaptationPrompt(prompt);
|
|
29214
30250
|
}
|
|
29215
30251
|
async function handleAgentMdProposal(repoPath, autoYes) {
|
|
29216
30252
|
if (autoYes) {
|
|
29217
30253
|
const proposal = readAgentMdProposal(repoPath);
|
|
29218
30254
|
if (proposal) {
|
|
29219
|
-
writeFileSafe(
|
|
30255
|
+
writeFileSafe(path31.join(repoPath, "AGENT.md"), proposal.content);
|
|
29220
30256
|
clearAgentMdProposal(repoPath);
|
|
29221
30257
|
info("Applied AGENT.md proposal (--yes)");
|
|
29222
30258
|
}
|
|
@@ -29225,7 +30261,7 @@ async function handleAgentMdProposal(repoPath, autoYes) {
|
|
|
29225
30261
|
await promptApplyAgentMdProposal(repoPath);
|
|
29226
30262
|
}
|
|
29227
30263
|
async function handleAddStage(argv) {
|
|
29228
|
-
const repoPath =
|
|
30264
|
+
const repoPath = path31.resolve(argv.repo);
|
|
29229
30265
|
if (argv.template !== "playwright") {
|
|
29230
30266
|
error(`Unknown stage template: ${argv.template ?? "(missing)"}. Available: playwright`);
|
|
29231
30267
|
process.exit(1);
|
|
@@ -29253,47 +30289,67 @@ async function handleAddStage(argv) {
|
|
|
29253
30289
|
process.exit(1);
|
|
29254
30290
|
}
|
|
29255
30291
|
}
|
|
30292
|
+
async function handlePreflight(argv) {
|
|
30293
|
+
const repo = path31.resolve(argv.repo);
|
|
30294
|
+
const agentId = validateAgentId(argv.id, repo);
|
|
30295
|
+
const result = await preflightEnvironment({
|
|
30296
|
+
repoPath: repo,
|
|
30297
|
+
agentId,
|
|
30298
|
+
confirmReplace: argv.replace,
|
|
30299
|
+
force: argv.force
|
|
30300
|
+
});
|
|
30301
|
+
if (argv.json) {
|
|
30302
|
+
const output = SlotReadinessSchema.parse(result.readiness);
|
|
30303
|
+
process.stdout.write(JSON.stringify(output, null, 2) + "\n");
|
|
30304
|
+
process.exit(result.code);
|
|
30305
|
+
return;
|
|
30306
|
+
}
|
|
30307
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
30308
|
+
process.exit(result.code);
|
|
30309
|
+
}
|
|
29256
30310
|
async function handleLaunch(argv) {
|
|
29257
|
-
const repo =
|
|
30311
|
+
const repo = path31.resolve(argv.repo);
|
|
29258
30312
|
const agentId = validateAgentId(argv.id, repo);
|
|
29259
30313
|
let confirmReplace = argv.replace;
|
|
29260
30314
|
let force = argv.force;
|
|
29261
|
-
|
|
29262
|
-
|
|
29263
|
-
if (!
|
|
29264
|
-
|
|
29265
|
-
|
|
29266
|
-
|
|
29267
|
-
|
|
29268
|
-
|
|
29269
|
-
|
|
29270
|
-
|
|
29271
|
-
|
|
29272
|
-
|
|
29273
|
-
|
|
29274
|
-
|
|
29275
|
-
|
|
29276
|
-
|
|
29277
|
-
|
|
29278
|
-
|
|
29279
|
-
|
|
29280
|
-
|
|
29281
|
-
if (!
|
|
29282
|
-
|
|
29283
|
-
|
|
29284
|
-
|
|
29285
|
-
|
|
29286
|
-
|
|
29287
|
-
|
|
29288
|
-
|
|
29289
|
-
|
|
29290
|
-
|
|
30315
|
+
if (!argv.resume) {
|
|
30316
|
+
const guard = checkLaunchGuard2(repo, agentId, { confirmReplace, force });
|
|
30317
|
+
if (!guard.allowed && guard.blocked) {
|
|
30318
|
+
if (!confirmReplace && process.stdin.isTTY && process.stdout.isTTY) {
|
|
30319
|
+
warn("Occupied slot \u2014 review before replacing:");
|
|
30320
|
+
console.error(guard.reason ?? "");
|
|
30321
|
+
const readline4 = await import("readline");
|
|
30322
|
+
const rl = readline4.createInterface({ input: process.stdin, output: process.stderr });
|
|
30323
|
+
const answer = await new Promise((resolve26) => {
|
|
30324
|
+
rl.question("Replace this slot? [y/N] ", resolve26);
|
|
30325
|
+
});
|
|
30326
|
+
rl.close();
|
|
30327
|
+
if (!/^[Yy]$/.test(answer.trim())) {
|
|
30328
|
+
error("Aborted \u2014 slot left unchanged.");
|
|
30329
|
+
process.exit(2);
|
|
30330
|
+
}
|
|
30331
|
+
confirmReplace = true;
|
|
30332
|
+
}
|
|
30333
|
+
}
|
|
30334
|
+
const guardAfterConfirm = checkLaunchGuard2(repo, agentId, { confirmReplace, force });
|
|
30335
|
+
if (!guardAfterConfirm.allowed && guardAfterConfirm.blocked && guardAfterConfirm.slot?.dirty) {
|
|
30336
|
+
if (!force && process.stdin.isTTY && process.stdout.isTTY) {
|
|
30337
|
+
warn("Worktree has uncommitted changes.");
|
|
30338
|
+
const readline4 = await import("readline");
|
|
30339
|
+
const rl = readline4.createInterface({ input: process.stdin, output: process.stderr });
|
|
30340
|
+
const answer = await new Promise((resolve26) => {
|
|
30341
|
+
rl.question("Discard uncommitted changes? [y/N] ", resolve26);
|
|
30342
|
+
});
|
|
30343
|
+
rl.close();
|
|
30344
|
+
if (!/^[Yy]$/.test(answer.trim())) {
|
|
30345
|
+
error("Aborted \u2014 uncommitted work preserved.");
|
|
30346
|
+
process.exit(2);
|
|
30347
|
+
}
|
|
30348
|
+
force = true;
|
|
30349
|
+
} else if (!force) {
|
|
30350
|
+
error(guardAfterConfirm.reason ?? "Dirty worktree requires --force.");
|
|
29291
30351
|
process.exit(2);
|
|
29292
30352
|
}
|
|
29293
|
-
force = true;
|
|
29294
|
-
} else if (!force) {
|
|
29295
|
-
error(guardAfterConfirm.reason ?? "Dirty worktree requires --force.");
|
|
29296
|
-
process.exit(2);
|
|
29297
30353
|
}
|
|
29298
30354
|
}
|
|
29299
30355
|
const result = await launchEnvironment({
|
|
@@ -29303,6 +30359,7 @@ async function handleLaunch(argv) {
|
|
|
29303
30359
|
claude: argv.claude,
|
|
29304
30360
|
confirmReplace,
|
|
29305
30361
|
force,
|
|
30362
|
+
resume: argv.resume,
|
|
29306
30363
|
capture: false
|
|
29307
30364
|
});
|
|
29308
30365
|
if (result.blocked) {
|
|
@@ -29316,8 +30373,19 @@ async function handleLaunch(argv) {
|
|
|
29316
30373
|
}
|
|
29317
30374
|
process.exit(result.code);
|
|
29318
30375
|
}
|
|
30376
|
+
async function handleRecover(argv) {
|
|
30377
|
+
return handleLaunch({
|
|
30378
|
+
id: argv.id,
|
|
30379
|
+
repo: argv.repo,
|
|
30380
|
+
worktree: true,
|
|
30381
|
+
claude: false,
|
|
30382
|
+
replace: false,
|
|
30383
|
+
force: false,
|
|
30384
|
+
resume: true
|
|
30385
|
+
});
|
|
30386
|
+
}
|
|
29319
30387
|
async function handleVerify(argv) {
|
|
29320
|
-
const repo =
|
|
30388
|
+
const repo = path31.resolve(argv.repo);
|
|
29321
30389
|
const agentId = validateAgentId(argv.id, repo);
|
|
29322
30390
|
const result = await runVerification({
|
|
29323
30391
|
repoPath: repo,
|
|
@@ -29329,7 +30397,7 @@ async function handleVerify(argv) {
|
|
|
29329
30397
|
process.exit(result.code);
|
|
29330
30398
|
}
|
|
29331
30399
|
async function handleTeardown(argv) {
|
|
29332
|
-
const repo =
|
|
30400
|
+
const repo = path31.resolve(argv.repo);
|
|
29333
30401
|
const agentId = validateAgentId(argv.id, repo);
|
|
29334
30402
|
const result = await teardownEnvironment({
|
|
29335
30403
|
repoPath: repo,
|
|
@@ -29340,7 +30408,7 @@ async function handleTeardown(argv) {
|
|
|
29340
30408
|
process.exit(result.code);
|
|
29341
30409
|
}
|
|
29342
30410
|
async function handleComplete(argv) {
|
|
29343
|
-
const repo =
|
|
30411
|
+
const repo = path31.resolve(argv.repo);
|
|
29344
30412
|
const agentId = validateAgentId(argv.id, repo);
|
|
29345
30413
|
const result = await completeEnvironment({
|
|
29346
30414
|
repoPath: repo,
|
|
@@ -29361,7 +30429,7 @@ async function handleComplete(argv) {
|
|
|
29361
30429
|
process.exit(result.code);
|
|
29362
30430
|
}
|
|
29363
30431
|
async function handleStatus(argv) {
|
|
29364
|
-
const repoPath =
|
|
30432
|
+
const repoPath = path31.resolve(argv.repo);
|
|
29365
30433
|
if (argv.json) {
|
|
29366
30434
|
const status = collectEnvironmentStatus(repoPath);
|
|
29367
30435
|
const output = EnvironmentStatusSchema.parse(status);
|
|
@@ -29374,7 +30442,7 @@ async function handleStatus(argv) {
|
|
|
29374
30442
|
});
|
|
29375
30443
|
}
|
|
29376
30444
|
async function handleRunsList(argv) {
|
|
29377
|
-
const repoPath =
|
|
30445
|
+
const repoPath = path31.resolve(argv.repo);
|
|
29378
30446
|
const runs = listRuns(repoPath, { stageId: argv.stage, limit: argv.limit });
|
|
29379
30447
|
if (argv.json) {
|
|
29380
30448
|
process.stdout.write(JSON.stringify({ runs }, null, 2) + "\n");
|
|
@@ -29391,7 +30459,7 @@ async function handleRunsList(argv) {
|
|
|
29391
30459
|
}
|
|
29392
30460
|
}
|
|
29393
30461
|
async function handleRunsGet(argv) {
|
|
29394
|
-
const run2 = getRun(
|
|
30462
|
+
const run2 = getRun(path31.resolve(argv.repo), argv.runId);
|
|
29395
30463
|
if (!run2) {
|
|
29396
30464
|
error(`Run not found: ${argv.runId}`);
|
|
29397
30465
|
process.exit(1);
|
|
@@ -29426,10 +30494,26 @@ function printDrift(drift) {
|
|
|
29426
30494
|
if (drift.extra.length > 0) {
|
|
29427
30495
|
warn(` Extra/stale files: ${drift.extra.join(", ")}`);
|
|
29428
30496
|
}
|
|
29429
|
-
if (
|
|
30497
|
+
if (drift.missingPortVars.length > 0) {
|
|
30498
|
+
warn(
|
|
30499
|
+
` Missing port documentation vars in harness.env: ${drift.missingPortVars.join(", ")}`
|
|
30500
|
+
);
|
|
30501
|
+
warn(" See maintain/templates/harness.env in the maintenance bundle.");
|
|
30502
|
+
}
|
|
30503
|
+
if (!drift.generatorVersion.outdated && drift.checksumMismatch.length === 0 && drift.missing.length === 0 && drift.extra.length === 0 && drift.missingPortVars.length === 0) {
|
|
29430
30504
|
success(" Harness matches bundled templates");
|
|
29431
30505
|
}
|
|
29432
30506
|
}
|
|
30507
|
+
function printMaintainBundleSummary(report) {
|
|
30508
|
+
const missing = report.actions.filter((a2) => a2.kind === "missing").length;
|
|
30509
|
+
const drifted = report.actions.filter((a2) => a2.kind === "drift").length;
|
|
30510
|
+
const stale = report.stale.length;
|
|
30511
|
+
info(`Maintenance bundle: .har/maintain/`);
|
|
30512
|
+
info(` ${missing} missing, ${drifted} drifted, ${stale} stale`);
|
|
30513
|
+
if (!report.validation.pass) {
|
|
30514
|
+
warn(` Validation: ${report.validation.errors.length} error(s) \u2014 blocks --finalize`);
|
|
30515
|
+
}
|
|
30516
|
+
}
|
|
29433
30517
|
function printNextSteps(auto2) {
|
|
29434
30518
|
console.error("");
|
|
29435
30519
|
console.error(" Read: .har/README.md");
|
|
@@ -29448,30 +30532,11 @@ function printNextSteps(auto2) {
|
|
|
29448
30532
|
}
|
|
29449
30533
|
|
|
29450
30534
|
// src/cli/commands/control.ts
|
|
29451
|
-
var
|
|
30535
|
+
var path33 = __toESM(require("path"));
|
|
29452
30536
|
|
|
29453
30537
|
// src/core/control-lifecycle.ts
|
|
29454
|
-
var
|
|
29455
|
-
var
|
|
29456
|
-
|
|
29457
|
-
// src/core/package-version.ts
|
|
29458
|
-
var fs26 = __toESM(require("fs"));
|
|
29459
|
-
var path26 = __toESM(require("path"));
|
|
29460
|
-
function getHarPackageVersion() {
|
|
29461
|
-
if (process.env.HAR_PACKAGE_VERSION) {
|
|
29462
|
-
return process.env.HAR_PACKAGE_VERSION;
|
|
29463
|
-
}
|
|
29464
|
-
const candidates = [
|
|
29465
|
-
path26.resolve(__dirname, "..", "package.json"),
|
|
29466
|
-
path26.resolve(__dirname, "..", "..", "package.json")
|
|
29467
|
-
];
|
|
29468
|
-
for (const packageJsonPath of candidates) {
|
|
29469
|
-
if (!fs26.existsSync(packageJsonPath)) continue;
|
|
29470
|
-
const pkg = JSON.parse(fs26.readFileSync(packageJsonPath, "utf8"));
|
|
29471
|
-
return pkg.version;
|
|
29472
|
-
}
|
|
29473
|
-
throw new Error("Could not resolve @osfactory/har package.json for version lookup");
|
|
29474
|
-
}
|
|
30538
|
+
var import_child_process8 = require("child_process");
|
|
30539
|
+
var path32 = __toESM(require("path"));
|
|
29475
30540
|
|
|
29476
30541
|
// src/core/control-image.ts
|
|
29477
30542
|
var DEFAULT_CONTROL_IMAGE = "theosfactory/har-control";
|
|
@@ -29490,13 +30555,13 @@ function shouldBuildControlLocally() {
|
|
|
29490
30555
|
|
|
29491
30556
|
// src/core/control-lifecycle.ts
|
|
29492
30557
|
function resolveControlDir() {
|
|
29493
|
-
return
|
|
30558
|
+
return path32.resolve(__dirname, "..", "control");
|
|
29494
30559
|
}
|
|
29495
30560
|
function resolveControlComposeFiles(options) {
|
|
29496
30561
|
const controlDir = resolveControlDir();
|
|
29497
|
-
const files = [
|
|
30562
|
+
const files = [path32.join(controlDir, "docker-compose.yml")];
|
|
29498
30563
|
if (options?.build ?? shouldBuildControlLocally()) {
|
|
29499
|
-
files.push(
|
|
30564
|
+
files.push(path32.join(controlDir, "docker-compose.build.yml"));
|
|
29500
30565
|
}
|
|
29501
30566
|
return files;
|
|
29502
30567
|
}
|
|
@@ -29511,7 +30576,7 @@ function runDockerCompose(args, options) {
|
|
|
29511
30576
|
const controlDir = resolveControlDir();
|
|
29512
30577
|
const composeFiles = resolveControlComposeFiles(options);
|
|
29513
30578
|
const composeArgs = composeFiles.flatMap((file) => ["-f", file]);
|
|
29514
|
-
const result = (0,
|
|
30579
|
+
const result = (0, import_child_process8.spawnSync)("docker", ["compose", ...composeArgs, ...args], {
|
|
29515
30580
|
cwd: controlDir,
|
|
29516
30581
|
stdio: "inherit",
|
|
29517
30582
|
env: buildDockerComposeEnv()
|
|
@@ -29611,6 +30676,27 @@ var controlCommand = {
|
|
|
29611
30676
|
};
|
|
29612
30677
|
async function handleUp(argv) {
|
|
29613
30678
|
header("har control up");
|
|
30679
|
+
const readiness = inspectControlUpReadiness(process.cwd());
|
|
30680
|
+
for (const message of readiness.warnings) {
|
|
30681
|
+
warn(message);
|
|
30682
|
+
}
|
|
30683
|
+
if (readiness.controlAlreadyRunning) {
|
|
30684
|
+
success(`Mission Control is already running at ${getControlApiUrl()}`);
|
|
30685
|
+
if (!isControlEnabled()) return;
|
|
30686
|
+
info("Syncing repositories with Mission Control...");
|
|
30687
|
+
const { synced: synced2, failed: failed2, apiReady: apiReady2 } = await syncReposAfterControlStart(process.cwd());
|
|
30688
|
+
if (!apiReady2) {
|
|
30689
|
+
warn("Mission Control API did not become ready \u2014 run har control sync later");
|
|
30690
|
+
return;
|
|
30691
|
+
}
|
|
30692
|
+
if (synced2 > 0) {
|
|
30693
|
+
success(`Synced ${synced2} ${synced2 === 1 ? "repository" : "repositories"} with Mission Control`);
|
|
30694
|
+
}
|
|
30695
|
+
if (failed2 > 0) {
|
|
30696
|
+
warn(`${failed2} ${failed2 === 1 ? "repository" : "repositories"} could not be synced`);
|
|
30697
|
+
}
|
|
30698
|
+
return;
|
|
30699
|
+
}
|
|
29614
30700
|
const { code, apiUrl, imageRef } = await startMissionControl({
|
|
29615
30701
|
detach: argv.detach,
|
|
29616
30702
|
build: argv.build
|
|
@@ -29644,7 +30730,7 @@ async function handleDown() {
|
|
|
29644
30730
|
process.exit(code);
|
|
29645
30731
|
}
|
|
29646
30732
|
async function handleRegister(argv) {
|
|
29647
|
-
const repoPath =
|
|
30733
|
+
const repoPath = path33.resolve(argv.repo);
|
|
29648
30734
|
header("har control register");
|
|
29649
30735
|
info(`Repository: ${repoPath}`);
|
|
29650
30736
|
try {
|
|
@@ -29664,7 +30750,7 @@ async function handleRegister(argv) {
|
|
|
29664
30750
|
}
|
|
29665
30751
|
}
|
|
29666
30752
|
async function handleSync(argv) {
|
|
29667
|
-
const repoPath =
|
|
30753
|
+
const repoPath = path33.resolve(argv.repo);
|
|
29668
30754
|
try {
|
|
29669
30755
|
await syncRepoWithControl({
|
|
29670
30756
|
repoPath,
|
|
@@ -29703,7 +30789,7 @@ async function handleWatch(argv) {
|
|
|
29703
30789
|
};
|
|
29704
30790
|
const tick = async () => {
|
|
29705
30791
|
if (argv.repo) {
|
|
29706
|
-
await syncOne(
|
|
30792
|
+
await syncOne(path33.resolve(argv.repo));
|
|
29707
30793
|
return;
|
|
29708
30794
|
}
|
|
29709
30795
|
try {
|
|
@@ -29734,12 +30820,12 @@ async function handleLogin(argv) {
|
|
|
29734
30820
|
}
|
|
29735
30821
|
|
|
29736
30822
|
// src/cli/commands/hooks.ts
|
|
29737
|
-
var
|
|
30823
|
+
var path35 = __toESM(require("path"));
|
|
29738
30824
|
|
|
29739
30825
|
// src/core/hooks.ts
|
|
29740
|
-
var
|
|
29741
|
-
var
|
|
29742
|
-
var
|
|
30826
|
+
var fs31 = __toESM(require("fs"));
|
|
30827
|
+
var os8 = __toESM(require("os"));
|
|
30828
|
+
var path34 = __toESM(require("path"));
|
|
29743
30829
|
var MARKER_START = "# >>> har commit gate (managed by `har hooks`) >>>";
|
|
29744
30830
|
var MARKER_END = "# <<< har commit gate <<<";
|
|
29745
30831
|
var PRE_COMMIT_BLOCK = [
|
|
@@ -29764,7 +30850,7 @@ function resolveCheckoutRoot(cwd) {
|
|
|
29764
30850
|
function resolveHooksDir(checkoutDir) {
|
|
29765
30851
|
const hooksPath = tryGit2(checkoutDir, "rev-parse --git-path hooks");
|
|
29766
30852
|
if (!hooksPath) throw new Error(`Not a git checkout: ${checkoutDir}`);
|
|
29767
|
-
return
|
|
30853
|
+
return path34.resolve(checkoutDir, hooksPath);
|
|
29768
30854
|
}
|
|
29769
30855
|
function getConfiguredHooksPath(checkoutDir) {
|
|
29770
30856
|
return tryGit2(checkoutDir, "config core.hooksPath") || void 0;
|
|
@@ -29772,9 +30858,9 @@ function getConfiguredHooksPath(checkoutDir) {
|
|
|
29772
30858
|
function defaultHarInvocation() {
|
|
29773
30859
|
const entry = process.argv[1];
|
|
29774
30860
|
if (entry && entry.endsWith(".js")) {
|
|
29775
|
-
return `"${process.execPath}" "${
|
|
30861
|
+
return `"${process.execPath}" "${path34.resolve(entry)}"`;
|
|
29776
30862
|
}
|
|
29777
|
-
if (entry) return `"${
|
|
30863
|
+
if (entry) return `"${path34.resolve(entry)}"`;
|
|
29778
30864
|
return "har";
|
|
29779
30865
|
}
|
|
29780
30866
|
function buildHookScript(harInvocation, subcommand, failOpenNotice) {
|
|
@@ -29795,40 +30881,40 @@ exit 0
|
|
|
29795
30881
|
`;
|
|
29796
30882
|
}
|
|
29797
30883
|
function upsertMarkedBlock(filePath, block) {
|
|
29798
|
-
if (!
|
|
29799
|
-
|
|
30884
|
+
if (!fs31.existsSync(filePath)) {
|
|
30885
|
+
fs31.writeFileSync(filePath, `#!/bin/sh
|
|
29800
30886
|
${block}
|
|
29801
30887
|
`, { mode: 493 });
|
|
29802
30888
|
return "created";
|
|
29803
30889
|
}
|
|
29804
|
-
const content =
|
|
30890
|
+
const content = fs31.readFileSync(filePath, "utf8");
|
|
29805
30891
|
if (content.includes(MARKER_START)) {
|
|
29806
30892
|
const pattern = new RegExp(
|
|
29807
30893
|
`${escapeRegExp(MARKER_START)}[\\s\\S]*?${escapeRegExp(MARKER_END)}`
|
|
29808
30894
|
);
|
|
29809
|
-
|
|
29810
|
-
|
|
30895
|
+
fs31.writeFileSync(filePath, content.replace(pattern, block));
|
|
30896
|
+
fs31.chmodSync(filePath, 493);
|
|
29811
30897
|
return "updated";
|
|
29812
30898
|
}
|
|
29813
30899
|
const suffix = content.endsWith("\n") ? "" : "\n";
|
|
29814
|
-
|
|
30900
|
+
fs31.writeFileSync(filePath, `${content}${suffix}
|
|
29815
30901
|
${block}
|
|
29816
30902
|
`);
|
|
29817
|
-
|
|
30903
|
+
fs31.chmodSync(filePath, 493);
|
|
29818
30904
|
return "appended";
|
|
29819
30905
|
}
|
|
29820
30906
|
function removeMarkedBlock(filePath) {
|
|
29821
|
-
if (!
|
|
29822
|
-
const content =
|
|
30907
|
+
if (!fs31.existsSync(filePath)) return false;
|
|
30908
|
+
const content = fs31.readFileSync(filePath, "utf8");
|
|
29823
30909
|
if (!content.includes(MARKER_START)) return false;
|
|
29824
30910
|
const pattern = new RegExp(
|
|
29825
30911
|
`\\n?${escapeRegExp(MARKER_START)}[\\s\\S]*?${escapeRegExp(MARKER_END)}\\n?`
|
|
29826
30912
|
);
|
|
29827
30913
|
const stripped = content.replace(pattern, "\n");
|
|
29828
30914
|
if (stripped.replace(/^#!\/bin\/sh\n?/, "").trim() === "") {
|
|
29829
|
-
|
|
30915
|
+
fs31.rmSync(filePath);
|
|
29830
30916
|
} else {
|
|
29831
|
-
|
|
30917
|
+
fs31.writeFileSync(filePath, stripped);
|
|
29832
30918
|
}
|
|
29833
30919
|
return true;
|
|
29834
30920
|
}
|
|
@@ -29850,20 +30936,20 @@ Or re-run with --force to write into that directory anyway.`
|
|
|
29850
30936
|
);
|
|
29851
30937
|
}
|
|
29852
30938
|
const hooksDir = resolveHooksDir(checkout);
|
|
29853
|
-
|
|
30939
|
+
fs31.mkdirSync(hooksDir, { recursive: true });
|
|
29854
30940
|
const invocation = options.harInvocation ?? defaultHarInvocation();
|
|
29855
|
-
|
|
29856
|
-
|
|
30941
|
+
fs31.writeFileSync(
|
|
30942
|
+
path34.join(hooksDir, "har-pre-commit"),
|
|
29857
30943
|
buildHookScript(invocation, "check", "har: binary not found; skipping commit gate (reinstall with har hooks install)"),
|
|
29858
30944
|
{ mode: 493 }
|
|
29859
30945
|
);
|
|
29860
|
-
|
|
29861
|
-
|
|
30946
|
+
fs31.writeFileSync(
|
|
30947
|
+
path34.join(hooksDir, "har-post-commit"),
|
|
29862
30948
|
buildHookScript(invocation, "record-commit", "har: binary not found; skipping commit association"),
|
|
29863
30949
|
{ mode: 493 }
|
|
29864
30950
|
);
|
|
29865
|
-
const preCommit = upsertMarkedBlock(
|
|
29866
|
-
const postCommit = upsertMarkedBlock(
|
|
30951
|
+
const preCommit = upsertMarkedBlock(path34.join(hooksDir, "pre-commit"), PRE_COMMIT_BLOCK);
|
|
30952
|
+
const postCommit = upsertMarkedBlock(path34.join(hooksDir, "post-commit"), POST_COMMIT_BLOCK);
|
|
29867
30953
|
ensureValidationsIgnored(checkout);
|
|
29868
30954
|
return { hooksDir, preCommit, postCommit };
|
|
29869
30955
|
}
|
|
@@ -29872,12 +30958,12 @@ function uninstallHooks(repoPath) {
|
|
|
29872
30958
|
if (!checkout) throw new Error(`Not a git repository: ${repoPath}`);
|
|
29873
30959
|
const hooksDir = resolveHooksDir(checkout);
|
|
29874
30960
|
let removed = false;
|
|
29875
|
-
removed = removeMarkedBlock(
|
|
29876
|
-
removed = removeMarkedBlock(
|
|
30961
|
+
removed = removeMarkedBlock(path34.join(hooksDir, "pre-commit")) || removed;
|
|
30962
|
+
removed = removeMarkedBlock(path34.join(hooksDir, "post-commit")) || removed;
|
|
29877
30963
|
for (const file of ["har-pre-commit", "har-post-commit"]) {
|
|
29878
|
-
const full =
|
|
29879
|
-
if (
|
|
29880
|
-
|
|
30964
|
+
const full = path34.join(hooksDir, file);
|
|
30965
|
+
if (fs31.existsSync(full)) {
|
|
30966
|
+
fs31.rmSync(full);
|
|
29881
30967
|
removed = true;
|
|
29882
30968
|
}
|
|
29883
30969
|
}
|
|
@@ -29894,9 +30980,9 @@ function getCommitGateConfig(checkoutDir) {
|
|
|
29894
30980
|
function isAgentWorktree(checkoutDir) {
|
|
29895
30981
|
const branch = getCurrentBranch(checkoutDir);
|
|
29896
30982
|
if (branch && /^har-agent-\d+$/.test(branch)) return true;
|
|
29897
|
-
const worktreesRoot =
|
|
29898
|
-
const resolved =
|
|
29899
|
-
return resolved.startsWith(`${worktreesRoot}${
|
|
30983
|
+
const worktreesRoot = path34.join(os8.homedir(), "worktrees");
|
|
30984
|
+
const resolved = path34.resolve(checkoutDir);
|
|
30985
|
+
return resolved.startsWith(`${worktreesRoot}${path34.sep}`) && /-agent-\d+$/.test(path34.basename(resolved));
|
|
29900
30986
|
}
|
|
29901
30987
|
function resolveEffectiveMode(checkoutDir, gate) {
|
|
29902
30988
|
if (!gate.enabled) return "off";
|
|
@@ -29909,15 +30995,15 @@ function getHooksStatus(repoPath) {
|
|
|
29909
30995
|
const hooksDir = resolveHooksDir(checkout);
|
|
29910
30996
|
const gate = getCommitGateConfig(checkout);
|
|
29911
30997
|
const hasBlock = (name) => {
|
|
29912
|
-
const file =
|
|
29913
|
-
return
|
|
30998
|
+
const file = path34.join(hooksDir, name);
|
|
30999
|
+
return fs31.existsSync(file) && fs31.readFileSync(file, "utf8").includes(MARKER_START);
|
|
29914
31000
|
};
|
|
29915
31001
|
return {
|
|
29916
31002
|
checkout,
|
|
29917
31003
|
hooksDir,
|
|
29918
31004
|
configuredHooksPath: getConfiguredHooksPath(checkout),
|
|
29919
|
-
preCommitInstalled: hasBlock("pre-commit") &&
|
|
29920
|
-
postCommitInstalled: hasBlock("post-commit") &&
|
|
31005
|
+
preCommitInstalled: hasBlock("pre-commit") && fs31.existsSync(path34.join(hooksDir, "har-pre-commit")),
|
|
31006
|
+
postCommitInstalled: hasBlock("post-commit") && fs31.existsSync(path34.join(hooksDir, "har-post-commit")),
|
|
29921
31007
|
gate,
|
|
29922
31008
|
effectiveMode: resolveEffectiveMode(checkout, gate)
|
|
29923
31009
|
};
|
|
@@ -29928,7 +31014,7 @@ function checkCommitGate(cwd) {
|
|
|
29928
31014
|
}
|
|
29929
31015
|
const checkout = resolveCheckoutRoot(cwd);
|
|
29930
31016
|
if (!checkout) return { exitCode: 0, messages: [] };
|
|
29931
|
-
if (!
|
|
31017
|
+
if (!fs31.existsSync(path34.join(checkout, ".har", "stages.json"))) {
|
|
29932
31018
|
return { exitCode: 0, messages: [] };
|
|
29933
31019
|
}
|
|
29934
31020
|
const gate = getCommitGateConfig(checkout);
|
|
@@ -30004,7 +31090,7 @@ function repoOption(y2) {
|
|
|
30004
31090
|
}
|
|
30005
31091
|
function handleInstall(argv) {
|
|
30006
31092
|
try {
|
|
30007
|
-
const result = installHooks({ repoPath:
|
|
31093
|
+
const result = installHooks({ repoPath: path35.resolve(argv.repo), force: argv.force });
|
|
30008
31094
|
success(`Commit gate installed in ${result.hooksDir}`);
|
|
30009
31095
|
info(`pre-commit: ${result.preCommit}, post-commit: ${result.postCommit}`);
|
|
30010
31096
|
info("Commits of unverified change batches will be blocked in agent worktrees.");
|
|
@@ -30016,7 +31102,7 @@ function handleInstall(argv) {
|
|
|
30016
31102
|
}
|
|
30017
31103
|
function handleUninstall(argv) {
|
|
30018
31104
|
try {
|
|
30019
|
-
const result = uninstallHooks(
|
|
31105
|
+
const result = uninstallHooks(path35.resolve(argv.repo));
|
|
30020
31106
|
if (result.removed) {
|
|
30021
31107
|
success(`Commit gate removed from ${result.hooksDir}`);
|
|
30022
31108
|
} else {
|
|
@@ -30029,7 +31115,7 @@ function handleUninstall(argv) {
|
|
|
30029
31115
|
}
|
|
30030
31116
|
function handleStatus2(argv) {
|
|
30031
31117
|
try {
|
|
30032
|
-
const status = getHooksStatus(
|
|
31118
|
+
const status = getHooksStatus(path35.resolve(argv.repo));
|
|
30033
31119
|
if (argv.json) {
|
|
30034
31120
|
process.stdout.write(`${JSON.stringify(status, null, 2)}
|
|
30035
31121
|
`);
|
|
@@ -30292,10 +31378,10 @@ function assignProp(target, prop, value) {
|
|
|
30292
31378
|
configurable: true
|
|
30293
31379
|
});
|
|
30294
31380
|
}
|
|
30295
|
-
function getElementAtPath(obj,
|
|
30296
|
-
if (!
|
|
31381
|
+
function getElementAtPath(obj, path37) {
|
|
31382
|
+
if (!path37)
|
|
30297
31383
|
return obj;
|
|
30298
|
-
return
|
|
31384
|
+
return path37.reduce((acc, key) => acc?.[key], obj);
|
|
30299
31385
|
}
|
|
30300
31386
|
function promiseAllObject(promisesObj) {
|
|
30301
31387
|
const keys = Object.keys(promisesObj);
|
|
@@ -30615,11 +31701,11 @@ function aborted(x2, startIndex = 0) {
|
|
|
30615
31701
|
}
|
|
30616
31702
|
return false;
|
|
30617
31703
|
}
|
|
30618
|
-
function prefixIssues(
|
|
31704
|
+
function prefixIssues(path37, issues) {
|
|
30619
31705
|
return issues.map((iss) => {
|
|
30620
31706
|
var _a3;
|
|
30621
31707
|
(_a3 = iss).path ?? (_a3.path = []);
|
|
30622
|
-
iss.path.unshift(
|
|
31708
|
+
iss.path.unshift(path37);
|
|
30623
31709
|
return iss;
|
|
30624
31710
|
});
|
|
30625
31711
|
}
|
|
@@ -35942,7 +37028,7 @@ var Protocol = class {
|
|
|
35942
37028
|
return;
|
|
35943
37029
|
}
|
|
35944
37030
|
const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
|
|
35945
|
-
await new Promise((
|
|
37031
|
+
await new Promise((resolve26) => setTimeout(resolve26, pollInterval));
|
|
35946
37032
|
options?.signal?.throwIfAborted();
|
|
35947
37033
|
}
|
|
35948
37034
|
} catch (error3) {
|
|
@@ -35959,7 +37045,7 @@ var Protocol = class {
|
|
|
35959
37045
|
*/
|
|
35960
37046
|
request(request, resultSchema, options) {
|
|
35961
37047
|
const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
|
|
35962
|
-
return new Promise((
|
|
37048
|
+
return new Promise((resolve26, reject) => {
|
|
35963
37049
|
const earlyReject = (error3) => {
|
|
35964
37050
|
reject(error3);
|
|
35965
37051
|
};
|
|
@@ -36037,7 +37123,7 @@ var Protocol = class {
|
|
|
36037
37123
|
if (!parseResult.success) {
|
|
36038
37124
|
reject(parseResult.error);
|
|
36039
37125
|
} else {
|
|
36040
|
-
|
|
37126
|
+
resolve26(parseResult.data);
|
|
36041
37127
|
}
|
|
36042
37128
|
} catch (error3) {
|
|
36043
37129
|
reject(error3);
|
|
@@ -36298,12 +37384,12 @@ var Protocol = class {
|
|
|
36298
37384
|
}
|
|
36299
37385
|
} catch {
|
|
36300
37386
|
}
|
|
36301
|
-
return new Promise((
|
|
37387
|
+
return new Promise((resolve26, reject) => {
|
|
36302
37388
|
if (signal.aborted) {
|
|
36303
37389
|
reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
|
|
36304
37390
|
return;
|
|
36305
37391
|
}
|
|
36306
|
-
const timeoutId = setTimeout(
|
|
37392
|
+
const timeoutId = setTimeout(resolve26, interval);
|
|
36307
37393
|
signal.addEventListener("abort", () => {
|
|
36308
37394
|
clearTimeout(timeoutId);
|
|
36309
37395
|
reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
|
|
@@ -37093,7 +38179,7 @@ var Server = class extends Protocol {
|
|
|
37093
38179
|
};
|
|
37094
38180
|
|
|
37095
38181
|
// src/mcp/server.ts
|
|
37096
|
-
var
|
|
38182
|
+
var path36 = __toESM(require("path"));
|
|
37097
38183
|
|
|
37098
38184
|
// node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js
|
|
37099
38185
|
var import_node_process2 = __toESM(require("node:process"), 1);
|
|
@@ -37176,12 +38262,12 @@ var StdioServerTransport = class {
|
|
|
37176
38262
|
this.onclose?.();
|
|
37177
38263
|
}
|
|
37178
38264
|
send(message) {
|
|
37179
|
-
return new Promise((
|
|
38265
|
+
return new Promise((resolve26) => {
|
|
37180
38266
|
const json = serializeMessage(message);
|
|
37181
38267
|
if (this._stdout.write(json)) {
|
|
37182
|
-
|
|
38268
|
+
resolve26();
|
|
37183
38269
|
} else {
|
|
37184
|
-
this._stdout.once("drain",
|
|
38270
|
+
this._stdout.once("drain", resolve26);
|
|
37185
38271
|
}
|
|
37186
38272
|
});
|
|
37187
38273
|
}
|
|
@@ -37263,7 +38349,8 @@ var LaunchEnvironmentInputSchema = external_exports.object({
|
|
|
37263
38349
|
),
|
|
37264
38350
|
force: external_exports.boolean().default(false).describe(
|
|
37265
38351
|
"Discard uncommitted changes in a dirty occupied worktree. Requires confirmReplace=true and explicit user approval \u2014 never set autonomously."
|
|
37266
|
-
)
|
|
38352
|
+
),
|
|
38353
|
+
resume: external_exports.boolean().default(false).describe("Resume a failed or partial launch without creating a new worktree.")
|
|
37267
38354
|
});
|
|
37268
38355
|
var LaunchEnvironmentOutputSchema = ShellRunOutputSchema.extend({
|
|
37269
38356
|
previewUrls: external_exports.record(external_exports.string()).optional(),
|
|
@@ -37280,6 +38367,16 @@ var LaunchEnvironmentOutputSchema = ShellRunOutputSchema.extend({
|
|
|
37280
38367
|
sessionCreatedAt: external_exports.string().optional()
|
|
37281
38368
|
}).optional()
|
|
37282
38369
|
});
|
|
38370
|
+
var PreflightEnvironmentInputSchema = external_exports.object({
|
|
38371
|
+
repo: external_exports.string().default("."),
|
|
38372
|
+
agentId: agentIdSchema,
|
|
38373
|
+
confirmReplace: external_exports.boolean().default(false).describe("Treat an occupied slot as replaceable (same as launch confirmReplace)."),
|
|
38374
|
+
force: external_exports.boolean().default(false).describe("Allow replacing a dirty worktree (requires explicit user approval).")
|
|
38375
|
+
});
|
|
38376
|
+
var PreflightEnvironmentOutputSchema = ShellRunOutputSchema.extend({
|
|
38377
|
+
readiness: SlotReadinessSchema,
|
|
38378
|
+
blocked: external_exports.boolean().optional()
|
|
38379
|
+
});
|
|
37283
38380
|
var CompleteEnvironmentInputSchema = external_exports.object({
|
|
37284
38381
|
repo: external_exports.string().default("."),
|
|
37285
38382
|
agentId: agentIdSchema,
|
|
@@ -37388,6 +38485,40 @@ var HAR_MCP_TOOLS = [
|
|
|
37388
38485
|
force: {
|
|
37389
38486
|
type: "boolean",
|
|
37390
38487
|
description: "Discard uncommitted changes when replacing a dirty worktree. Requires confirmReplace=true and explicit user approval."
|
|
38488
|
+
},
|
|
38489
|
+
resume: {
|
|
38490
|
+
type: "boolean",
|
|
38491
|
+
description: "Resume a failed or partial launch (status failed/starting) without --replace. Preserves worktree and env."
|
|
38492
|
+
}
|
|
38493
|
+
},
|
|
38494
|
+
["agentId"]
|
|
38495
|
+
)
|
|
38496
|
+
},
|
|
38497
|
+
{
|
|
38498
|
+
name: "har_recover_environment",
|
|
38499
|
+
description: "Resume a failed or partial agent launch without replacing the worktree. Alias for har_launch_environment with resume=true.",
|
|
38500
|
+
inputSchema: objectJsonSchema(
|
|
38501
|
+
{
|
|
38502
|
+
repo: repoJsonProperty,
|
|
38503
|
+
agentId: agentIdJsonProperty
|
|
38504
|
+
},
|
|
38505
|
+
["agentId"]
|
|
38506
|
+
)
|
|
38507
|
+
},
|
|
38508
|
+
{
|
|
38509
|
+
name: "har_preflight_environment",
|
|
38510
|
+
description: "Readiness gate before launch: checks ports, foreign PM2, Docker conflicts, and occupied slot. Returns canLaunch with actionable blockers. Call before har_launch_environment.",
|
|
38511
|
+
inputSchema: objectJsonSchema(
|
|
38512
|
+
{
|
|
38513
|
+
repo: repoJsonProperty,
|
|
38514
|
+
agentId: agentIdJsonProperty,
|
|
38515
|
+
confirmReplace: {
|
|
38516
|
+
type: "boolean",
|
|
38517
|
+
description: "Treat an occupied slot as replaceable (same as launch confirmReplace)."
|
|
38518
|
+
},
|
|
38519
|
+
force: {
|
|
38520
|
+
type: "boolean",
|
|
38521
|
+
description: "Allow replacing a dirty worktree (only after explicit user approval)."
|
|
37391
38522
|
}
|
|
37392
38523
|
},
|
|
37393
38524
|
["agentId"]
|
|
@@ -37540,6 +38671,26 @@ async function handleMcpToolCall(toolName, args, defaultRepo = ".") {
|
|
|
37540
38671
|
claude: input.claude,
|
|
37541
38672
|
confirmReplace: input.confirmReplace,
|
|
37542
38673
|
force: input.force,
|
|
38674
|
+
resume: input.resume,
|
|
38675
|
+
capture: true
|
|
38676
|
+
});
|
|
38677
|
+
const parsed = LaunchEnvironmentOutputSchema.parse(result);
|
|
38678
|
+
return {
|
|
38679
|
+
...jsonContent(parsed),
|
|
38680
|
+
...result.blocked ? { isError: true } : {}
|
|
38681
|
+
};
|
|
38682
|
+
}
|
|
38683
|
+
case "har_recover_environment": {
|
|
38684
|
+
const input = LaunchEnvironmentInputSchema.parse({ ...args, repo, resume: true });
|
|
38685
|
+
const agentId = validateAgentId(input.agentId, repo);
|
|
38686
|
+
const result = await launchEnvironment({
|
|
38687
|
+
repoPath: repo,
|
|
38688
|
+
agentId,
|
|
38689
|
+
worktree: input.worktree,
|
|
38690
|
+
claude: input.claude,
|
|
38691
|
+
confirmReplace: false,
|
|
38692
|
+
force: false,
|
|
38693
|
+
resume: true,
|
|
37543
38694
|
capture: true
|
|
37544
38695
|
});
|
|
37545
38696
|
const parsed = LaunchEnvironmentOutputSchema.parse(result);
|
|
@@ -37548,6 +38699,21 @@ async function handleMcpToolCall(toolName, args, defaultRepo = ".") {
|
|
|
37548
38699
|
...result.blocked ? { isError: true } : {}
|
|
37549
38700
|
};
|
|
37550
38701
|
}
|
|
38702
|
+
case "har_preflight_environment": {
|
|
38703
|
+
const input = PreflightEnvironmentInputSchema.parse({ ...args, repo });
|
|
38704
|
+
const agentId = validateAgentId(input.agentId, repo);
|
|
38705
|
+
const result = await preflightEnvironment({
|
|
38706
|
+
repoPath: repo,
|
|
38707
|
+
agentId,
|
|
38708
|
+
confirmReplace: input.confirmReplace,
|
|
38709
|
+
force: input.force
|
|
38710
|
+
});
|
|
38711
|
+
const parsed = PreflightEnvironmentOutputSchema.parse(result);
|
|
38712
|
+
return {
|
|
38713
|
+
...jsonContent(parsed),
|
|
38714
|
+
...result.blocked ? { isError: true } : {}
|
|
38715
|
+
};
|
|
38716
|
+
}
|
|
37551
38717
|
case "har_run_stage": {
|
|
37552
38718
|
const input = RunStageInputSchema.parse({ ...args, repo });
|
|
37553
38719
|
const agentId = input.agentId !== void 0 ? validateAgentId(input.agentId, repo) : void 0;
|
|
@@ -37645,7 +38811,7 @@ async function handleMcpToolCall(toolName, args, defaultRepo = ".") {
|
|
|
37645
38811
|
const input = ControlUpInputSchema.parse({ ...args, repo });
|
|
37646
38812
|
const result = await startControlAndSync({
|
|
37647
38813
|
detach: input.detach,
|
|
37648
|
-
cwd:
|
|
38814
|
+
cwd: path36.resolve(input.repo)
|
|
37649
38815
|
});
|
|
37650
38816
|
if (result.code !== 0) {
|
|
37651
38817
|
return {
|
|
@@ -37709,7 +38875,7 @@ var mcpCommand = {
|
|
|
37709
38875
|
|
|
37710
38876
|
// src/cli/index.ts
|
|
37711
38877
|
async function runCli() {
|
|
37712
|
-
await yargs_default(hideBin(process.argv)).scriptName("har").usage("$0 <command> [options]").command(envCommand).command(controlCommand).command(hooksCommand).command(mcpCommand).demandCommand(1, "Please specify a command. Try: har env init").strict().help().version().parse();
|
|
38878
|
+
await yargs_default(hideBin(process.argv)).scriptName("har").usage("$0 <command> [options]").command(envCommand).command(controlCommand).command(hooksCommand).command(mcpCommand).demandCommand(1, "Please specify a command. Try: har env init").strict().help().version(getHarPackageVersion()).parse();
|
|
37713
38879
|
}
|
|
37714
38880
|
|
|
37715
38881
|
// src/index.ts
|