@osfactory/har 0.61.2 → 0.62.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/dist/index.js CHANGED
@@ -33,6 +33,31 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
33
33
  mod
34
34
  ));
35
35
 
36
+ // src/core/package-version.ts
37
+ function getHarPackageVersion() {
38
+ if (process.env.HAR_PACKAGE_VERSION) {
39
+ return process.env.HAR_PACKAGE_VERSION;
40
+ }
41
+ const candidates = [
42
+ path.resolve(__dirname, "..", "package.json"),
43
+ path.resolve(__dirname, "..", "..", "package.json")
44
+ ];
45
+ for (const packageJsonPath of candidates) {
46
+ if (!fs.existsSync(packageJsonPath)) continue;
47
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
48
+ return pkg.version;
49
+ }
50
+ throw new Error("Could not resolve @osfactory/har package.json for version lookup");
51
+ }
52
+ var fs, path;
53
+ var init_package_version = __esm({
54
+ "src/core/package-version.ts"() {
55
+ "use strict";
56
+ fs = __toESM(require("fs"));
57
+ path = __toESM(require("path"));
58
+ }
59
+ });
60
+
36
61
  // src/core/telemetry-config.ts
37
62
  function getPreferencePath() {
38
63
  if (process.env.HAR_TELEMETRY_CONFIG_PATH) {
@@ -28779,12 +28804,10 @@ function run(command2, options = {}) {
28779
28804
  };
28780
28805
  }
28781
28806
  }
28782
- function runScript(scriptPath, args = [], options = {}) {
28783
- return runScriptCapture(scriptPath, args, { ...options, stream: true });
28784
- }
28785
28807
  function runScriptCapture(scriptPath, args = [], options = {}) {
28786
28808
  const stream = options.stream ?? false;
28787
- const { stream: _stream, ...spawnOptions } = options;
28809
+ const echoStdout = stream && (options.streamStdout ?? true);
28810
+ const { stream: _stream, streamStdout: _streamStdout, ...spawnOptions } = options;
28788
28811
  return new Promise((resolve51) => {
28789
28812
  const proc = (0, import_child_process.spawn)("bash", [scriptPath, ...args], {
28790
28813
  ...spawnOptions,
@@ -28794,7 +28817,7 @@ function runScriptCapture(scriptPath, args = [], options = {}) {
28794
28817
  let stderr = "";
28795
28818
  proc.stdout?.on("data", (d) => {
28796
28819
  stdout += d;
28797
- if (stream) process.stdout.write(d);
28820
+ if (echoStdout) process.stdout.write(d);
28798
28821
  });
28799
28822
  proc.stderr?.on("data", (d) => {
28800
28823
  stderr += d;
@@ -28805,7 +28828,8 @@ function runScriptCapture(scriptPath, args = [], options = {}) {
28805
28828
  }
28806
28829
  function runShellCommand(command2, options = {}) {
28807
28830
  const stream = options.stream ?? false;
28808
- const { stream: _stream, ...spawnOptions } = options;
28831
+ const echoStdout = stream && (options.streamStdout ?? true);
28832
+ const { stream: _stream, streamStdout: _streamStdout, ...spawnOptions } = options;
28809
28833
  return new Promise((resolve51) => {
28810
28834
  const proc = (0, import_child_process.spawn)("bash", ["-lc", command2], {
28811
28835
  ...spawnOptions,
@@ -28815,7 +28839,7 @@ function runShellCommand(command2, options = {}) {
28815
28839
  let stderr = "";
28816
28840
  proc.stdout?.on("data", (d) => {
28817
28841
  stdout += d;
28818
- if (stream) process.stdout.write(d);
28842
+ if (echoStdout) process.stdout.write(d);
28819
28843
  });
28820
28844
  proc.stderr?.on("data", (d) => {
28821
28845
  stderr += d;
@@ -29090,6 +29114,18 @@ function parseVerificationResult(stdout) {
29090
29114
  const parsed = VerificationResultSchema.safeParse(json);
29091
29115
  return parsed.success ? parsed.data : null;
29092
29116
  }
29117
+ function slimVerificationResult(verification) {
29118
+ if (!verification) return null;
29119
+ return {
29120
+ ...verification,
29121
+ stages: verification.stages.map((stage) => {
29122
+ if (!stage.pass || stage.output === void 0) return stage;
29123
+ const rest = { ...stage };
29124
+ delete rest.output;
29125
+ return rest;
29126
+ })
29127
+ };
29128
+ }
29093
29129
  function buildStageResult(input) {
29094
29130
  const verification = input.verification ?? parseVerificationResult(input.stdout) ?? void 0;
29095
29131
  const parsedStageResult = StageResultSchema.safeParse(extractJsonFromOutput(input.stdout));
@@ -29287,16 +29323,18 @@ function buildExecutionPlan(repoPath, stage, options) {
29287
29323
  env: mergedEnv
29288
29324
  };
29289
29325
  }
29290
- async function executePlan(plan, capture) {
29291
- const spawnOptions = { cwd: plan.cwd, env: plan.env };
29326
+ async function executePlan(plan, options) {
29327
+ const spawnOptions = {
29328
+ cwd: plan.cwd,
29329
+ env: plan.env,
29330
+ stream: !options.capture,
29331
+ streamStdout: options.streamStdout
29332
+ };
29292
29333
  if (plan.mode === "shell" && plan.shellCommand) {
29293
- return runShellCommand(plan.shellCommand, { ...spawnOptions, stream: !capture });
29334
+ return runShellCommand(plan.shellCommand, spawnOptions);
29294
29335
  }
29295
29336
  if (plan.scriptPath) {
29296
- if (capture) {
29297
- return runScriptCapture(plan.scriptPath, plan.args, spawnOptions);
29298
- }
29299
- return runScript(plan.scriptPath, plan.args, spawnOptions);
29337
+ return runScriptCapture(plan.scriptPath, plan.args, spawnOptions);
29300
29338
  }
29301
29339
  throw new Error("Invalid stage execution plan");
29302
29340
  }
@@ -29328,7 +29366,10 @@ var init_local_executor = __esm({
29328
29366
  const plan = buildExecutionPlan(repoPath, stage, options);
29329
29367
  const capture = options.capture ?? ctx.capture ?? true;
29330
29368
  const started = Date.now();
29331
- const result = await executePlan(plan, capture);
29369
+ const result = await executePlan(plan, {
29370
+ capture,
29371
+ streamStdout: stage.kind !== "verify"
29372
+ });
29332
29373
  const durationMs = Date.now() - started;
29333
29374
  const verification = stage.kind === "verify" ? parseVerificationResult(result.stdout) : void 0;
29334
29375
  const previewUrls = stage.kind === "launch" && result.code === 0 && options.agentId !== void 0 ? computePreviewUrls(repoPath, options.agentId) : void 0;
@@ -32865,6 +32906,7 @@ async function buildPortalPayload(repoPath, controlApiUrl, since, full = false)
32865
32906
  }
32866
32907
  const syncBody = {
32867
32908
  path: repoPath,
32909
+ cliVersion: getHarPackageVersion(),
32868
32910
  ...status.gitRemote ? { gitRemote: status.gitRemote } : {},
32869
32911
  ...manifest ? { manifest } : {},
32870
32912
  ...stagesRegistry ? { stagesRegistry } : {},
@@ -33287,6 +33329,7 @@ var init_control_sync = __esm({
33287
33329
  init_work_units();
33288
33330
  init_cloud_executor();
33289
33331
  init_telemetry_config();
33332
+ init_package_version();
33290
33333
  init_logging();
33291
33334
  init_usage_harvest();
33292
33335
  init_telemetry_env();
@@ -66476,26 +66519,8 @@ function isYargsInstance(y) {
66476
66519
  var Yargs = YargsFactory(esm_default);
66477
66520
  var yargs_default = Yargs;
66478
66521
 
66479
- // src/core/package-version.ts
66480
- var fs = __toESM(require("fs"));
66481
- var path = __toESM(require("path"));
66482
- function getHarPackageVersion() {
66483
- if (process.env.HAR_PACKAGE_VERSION) {
66484
- return process.env.HAR_PACKAGE_VERSION;
66485
- }
66486
- const candidates = [
66487
- path.resolve(__dirname, "..", "package.json"),
66488
- path.resolve(__dirname, "..", "..", "package.json")
66489
- ];
66490
- for (const packageJsonPath of candidates) {
66491
- if (!fs.existsSync(packageJsonPath)) continue;
66492
- const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
66493
- return pkg.version;
66494
- }
66495
- throw new Error("Could not resolve @osfactory/har package.json for version lookup");
66496
- }
66497
-
66498
66522
  // src/cli/index.ts
66523
+ init_package_version();
66499
66524
  init_telemetry_config();
66500
66525
  init_sync_context();
66501
66526
 
@@ -72190,6 +72215,7 @@ var import_child_process13 = require("child_process");
72190
72215
  var path52 = __toESM(require("path"));
72191
72216
 
72192
72217
  // src/core/control-image.ts
72218
+ init_package_version();
72193
72219
  var DEFAULT_CONTROL_IMAGE = "theosfactory/har-control";
72194
72220
  function getControlImageName() {
72195
72221
  return process.env.HAR_CONTROL_IMAGE ?? DEFAULT_CONTROL_IMAGE;
@@ -73264,6 +73290,7 @@ function writeOnboardingPreferences(overrides) {
73264
73290
  // src/cli/commands/env.ts
73265
73291
  init_schema2();
73266
73292
  init_validation();
73293
+ init_results();
73267
73294
  init_logging();
73268
73295
 
73269
73296
  // src/cli/help-text.ts
@@ -73511,7 +73538,11 @@ var envCommand = {
73511
73538
  ).command(
73512
73539
  "verify <id>",
73513
73540
  "Run verification suite for an agent",
73514
- (y) => y.positional("id", { type: "number", describe: "Agent slot id (see .har/stages.json agentSlots)" }).option("repo", { type: "string", default: "." }).option("full", { type: "boolean", default: false }),
73541
+ (y) => y.positional("id", { type: "number", describe: "Agent slot id (see .har/stages.json agentSlots)" }).option("repo", { type: "string", default: "." }).option("full", { type: "boolean", default: false }).option("json", {
73542
+ type: "boolean",
73543
+ default: false,
73544
+ describe: "Structured JSON (passing steps omit output; default is progress only)"
73545
+ }),
73515
73546
  handleVerify
73516
73547
  ).command(
73517
73548
  "teardown <id>",
@@ -73976,9 +74007,26 @@ async function handleVerify(argv) {
73976
74007
  repoPath: repo,
73977
74008
  agentId,
73978
74009
  full: argv.full,
73979
- capture: false
74010
+ capture: Boolean(argv.json)
73980
74011
  });
73981
- if (result.stdout) process.stdout.write(result.stdout);
74012
+ if (argv.json) {
74013
+ const payload = slimVerificationResult(result.verification) ?? {
74014
+ status: result.code === 0 ? "pass" : "fail",
74015
+ agent_id: agentId,
74016
+ stages: []
74017
+ };
74018
+ process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
74019
+ return finishCommand(result.code);
74020
+ }
74021
+ if (result.verification) {
74022
+ const failed = result.verification.stages.filter((stage) => !stage.pass).map((stage) => stage.name);
74023
+ const elapsed = result.verification.total_ms != null ? ` (${result.verification.total_ms}ms)` : "";
74024
+ if (failed.length > 0) {
74025
+ error(`Verification failed: ${failed.join(", ")}${elapsed}`);
74026
+ } else {
74027
+ success(`Verification ${result.verification.status}${elapsed}`);
74028
+ }
74029
+ }
73982
74030
  return finishCommand(result.code);
73983
74031
  }
73984
74032
  async function handleTeardown(argv) {
@@ -82466,6 +82514,7 @@ var StdioServerTransport = class {
82466
82514
  };
82467
82515
 
82468
82516
  // src/mcp/server.ts
82517
+ init_package_version();
82469
82518
  init_telemetry_config();
82470
82519
  init_runs();
82471
82520
  init_work_units();
@@ -82495,6 +82544,7 @@ function objectJsonSchema(properties, required2) {
82495
82544
  }
82496
82545
 
82497
82546
  // src/mcp/server.ts
82547
+ init_results();
82498
82548
  init_validation();
82499
82549
 
82500
82550
  // src/mcp/schemas.ts
@@ -82780,7 +82830,7 @@ var HAR_MCP_TOOLS = [
82780
82830
  },
82781
82831
  {
82782
82832
  name: "har_run_verification",
82783
- description: "Run the project verification pipeline for an agent slot.",
82833
+ description: "Run the project verification pipeline for an agent slot. Returns status, timing, and failed-step output (passing steps omit logs; stdout is not the raw JSON dump).",
82784
82834
  inputSchema: objectJsonSchema(
82785
82835
  {
82786
82836
  repo: repoJsonProperty,
@@ -83006,7 +83056,14 @@ async function handleMcpToolCall(toolName, args, defaultRepo = ".") {
83006
83056
  capture: true,
83007
83057
  trigger: "mcp"
83008
83058
  });
83009
- return jsonContent(RunVerificationOutputSchema.parse(result));
83059
+ return jsonContent(
83060
+ RunVerificationOutputSchema.parse({
83061
+ code: result.code,
83062
+ stdout: "",
83063
+ stderr: result.stderr,
83064
+ verification: slimVerificationResult(result.verification)
83065
+ })
83066
+ );
83010
83067
  }
83011
83068
  case "har_get_status": {
83012
83069
  const agentIdRaw = args.agentId;
@@ -83052,7 +83109,12 @@ async function handleMcpToolCall(toolName, args, defaultRepo = ".") {
83052
83109
  capture: true,
83053
83110
  trigger: "mcp"
83054
83111
  });
83055
- return jsonContent(CompleteEnvironmentOutputSchema.parse(result));
83112
+ return jsonContent(
83113
+ CompleteEnvironmentOutputSchema.parse({
83114
+ ...result,
83115
+ verification: slimVerificationResult(result.verification)
83116
+ })
83117
+ );
83056
83118
  }
83057
83119
  case "har_list_artifacts": {
83058
83120
  const input = ListArtifactsInputSchema.parse({ ...args, repo });
@@ -776,6 +776,29 @@ escape_step_output() {
776
776
  printf '%s' "$1" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const s=d.trim().split('\n').slice(0,50).join('\n');process.stdout.write(JSON.stringify(s))})" 2>/dev/null || echo '""'
777
777
  }
778
778
 
779
+ # Append one verify.sh step to RESULTS_JSON (caller global).
780
+ # Passing steps omit output — it already appeared as ✓ on stderr.
781
+ record_step_result() {
782
+ local name="$1"
783
+ local pass_bool="$2"
784
+ local elapsed="$3"
785
+ local output="${4:-}"
786
+ local step_json
787
+ if [ "$pass_bool" = "true" ]; then
788
+ step_json=$(NAME="$name" MS="$elapsed" node -e 'process.stdout.write(JSON.stringify({name:process.env.NAME,pass:true,ms:Number(process.env.MS)}))' 2>/dev/null) || return 0
789
+ else
790
+ local escaped
791
+ escaped=$(escape_step_output "$output")
792
+ step_json=$(NAME="$name" MS="$elapsed" node -e "process.stdout.write(JSON.stringify({name:process.env.NAME,pass:false,ms:Number(process.env.MS),output:$escaped}))" 2>/dev/null) || return 0
793
+ fi
794
+ RESULTS_JSON=$(echo "$RESULTS_JSON" | node -e "
795
+ const fs = require('fs');
796
+ const arr = JSON.parse(fs.readFileSync('/dev/stdin','utf8'));
797
+ arr.push($step_json);
798
+ process.stdout.write(JSON.stringify(arr));
799
+ " 2>/dev/null || echo "$RESULTS_JSON")
800
+ }
801
+
779
802
  # Emit "<id>\t<command>" for each registered stage listed in stages.json
780
803
  # verificationStages (used by verify --full). Ids without a matching registered
781
804
  # stage are inline verify.sh steps and skipped here; lifecycle stages
@@ -228,52 +228,291 @@ provision_node() {
228
228
  append_path_prefix "$dir/node_modules/.bin"
229
229
  }
230
230
 
231
+ # ── Python interpreter helpers ────────────────────────────────────────────────
232
+ # Resolve the project Python (requires-python, .python-version, uv) instead of
233
+ # blindly using PATH python3.
234
+
235
+ har_python_version_from_bin() {
236
+ local bin="$1"
237
+ "$bin" -c 'import sys; print(".".join(map(str, sys.version_info[:3])))' 2>/dev/null \
238
+ | tr -d '[:space:]'
239
+ }
240
+
241
+ har_python_version_to_sortable() {
242
+ local v="${1#python}"
243
+ v="${v#v}"
244
+ local major=0 minor=0 patch=0
245
+ IFS='.' read -r major minor patch _ <<< "$v"
246
+ major=${major:-0}
247
+ minor=${minor:-0}
248
+ patch=${patch:-0}
249
+ printf '%03d%03d%03d' "$major" "$minor" "$patch"
250
+ }
251
+
252
+ har_python_version_ge() {
253
+ local a b
254
+ a="$(har_python_version_to_sortable "$1")"
255
+ b="$(har_python_version_to_sortable "$2")"
256
+ [ "$a" -ge "$b" ]
257
+ }
258
+
259
+ har_python_requires_minimum() {
260
+ local spec="$1"
261
+ local ver=""
262
+
263
+ [ -n "$spec" ] || return 1
264
+
265
+ ver="$(printf '%s' "$spec" | sed -n \
266
+ -e 's/.*>=\([0-9][0-9]*\.[0-9][0-9]*\).*/\1/p' \
267
+ -e 's/.*~=\([0-9][0-9]*\.[0-9][0-9]*\).*/\1/p' \
268
+ -e 's/.*==\([0-9][0-9]*\.[0-9][0-9]*\).*/\1/p' \
269
+ | head -1)"
270
+
271
+ if [ -z "$ver" ]; then
272
+ ver="$(printf '%s' "$spec" | sed -n 's/^\([0-9][0-9]*\.[0-9][0-9]*\).*/\1/p')"
273
+ fi
274
+
275
+ [ -n "$ver" ] && echo "$ver"
276
+ }
277
+
278
+ har_python_read_dot_version() {
279
+ local dir="$1"
280
+ [ -f "$dir/.python-version" ] || return 1
281
+ head -1 "$dir/.python-version" | tr -d '[:space:]'
282
+ }
283
+
284
+ har_python_read_requires_spec() {
285
+ local dir="$1"
286
+ local spec=""
287
+
288
+ if [ -f "$dir/pyproject.toml" ]; then
289
+ spec="$(sed -n 's/^[[:space:]]*requires-python[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' \
290
+ "$dir/pyproject.toml" | head -1)"
291
+ fi
292
+ if [ -z "$spec" ] && [ -f "$dir/setup.cfg" ]; then
293
+ spec="$(sed -n '/^\[options\]/,/^\[/ s/^python_requires[[:space:]]*=[[:space:]]*\(.*\)/\1/p' \
294
+ "$dir/setup.cfg" | head -1 | tr -d ' "')"
295
+ fi
296
+ [ -n "$spec" ] && echo "$spec"
297
+ }
298
+
299
+ har_python_project_minimum() {
300
+ local dir="$1"
301
+ local req_min="" dot_ver="" higher=""
302
+
303
+ req_min="$(har_python_requires_minimum "$(har_python_read_requires_spec "$dir" || true)" || true)"
304
+ dot_ver="$(har_python_read_dot_version "$dir" || true)"
305
+ if [ -n "$dot_ver" ]; then
306
+ dot_ver="$(printf '%s' "$dot_ver" | sed 's/^v//; s/^\([0-9][0-9]*\.[0-9][0-9]*\).*/\1/')"
307
+ fi
308
+
309
+ if [ -n "$req_min" ] && [ -n "$dot_ver" ]; then
310
+ if har_python_version_ge "$dot_ver" "$req_min"; then
311
+ higher="$dot_ver"
312
+ else
313
+ higher="$req_min"
314
+ fi
315
+ elif [ -n "$req_min" ]; then
316
+ higher="$req_min"
317
+ elif [ -n "$dot_ver" ]; then
318
+ higher="$dot_ver"
319
+ fi
320
+
321
+ [ -n "$higher" ] && echo "$higher"
322
+ }
323
+
324
+ har_python_is_uv_project() {
325
+ local dir="$1"
326
+ [ -f "$dir/uv.lock" ] && return 0
327
+ [ -f "$dir/pyproject.toml" ] && grep -q '^\[tool\.uv\]' "$dir/pyproject.toml" 2>/dev/null
328
+ }
329
+
330
+ har_python_uv_find() {
331
+ local dir="$1"
332
+ local request="${2:-}"
333
+
334
+ command -v uv >/dev/null 2>&1 || return 1
335
+ if [ -n "$request" ]; then
336
+ (cd "$dir" && uv python find "$request" 2>/dev/null)
337
+ else
338
+ (cd "$dir" && uv python find 2>/dev/null)
339
+ fi
340
+ }
341
+
342
+ har_python_resolve_interpreter() {
343
+ local dir="$1"
344
+ local minimum="${2:-}"
345
+ local found=""
346
+
347
+ if har_python_is_uv_project "$dir"; then
348
+ found="$(har_python_uv_find "$dir" "$minimum" || true)"
349
+ if [ -n "$found" ] && [ -x "$found" ]; then
350
+ echo "$found"
351
+ return 0
352
+ fi
353
+ fi
354
+
355
+ if [ -f "$dir/.python-version" ]; then
356
+ found="$(har_python_uv_find "$dir" "$(har_python_read_dot_version "$dir")" || true)"
357
+ if [ -n "$found" ] && [ -x "$found" ]; then
358
+ echo "$found"
359
+ return 0
360
+ fi
361
+ if command -v pyenv >/dev/null 2>&1; then
362
+ found="$(cd "$dir" && pyenv which python 2>/dev/null || true)"
363
+ if [ -n "$found" ] && [ -x "$found" ]; then
364
+ echo "$found"
365
+ return 0
366
+ fi
367
+ fi
368
+ fi
369
+
370
+ if [ -n "$minimum" ]; then
371
+ found="$(har_python_uv_find "$dir" "$minimum" || true)"
372
+ if [ -n "$found" ] && [ -x "$found" ]; then
373
+ echo "$found"
374
+ return 0
375
+ fi
376
+ fi
377
+
378
+ if command -v python3 >/dev/null 2>&1; then
379
+ local sys_py sys_ver=""
380
+ sys_py="$(command -v python3)"
381
+ if [ -z "$minimum" ]; then
382
+ echo "$sys_py"
383
+ return 0
384
+ fi
385
+ sys_ver="$(har_python_version_from_bin "$sys_py")"
386
+ if [ -n "$sys_ver" ] && har_python_version_ge "$sys_ver" "$minimum"; then
387
+ echo "$sys_py"
388
+ return 0
389
+ fi
390
+ echo "$sys_py"
391
+ return 0
392
+ fi
393
+
394
+ return 1
395
+ }
396
+
397
+ har_python_warn_if_below_minimum() {
398
+ local bin="$1"
399
+ local minimum="$2"
400
+ local actual=""
401
+
402
+ [ -n "$minimum" ] || return 0
403
+ actual="$(har_python_version_from_bin "$bin")"
404
+ [ -n "$actual" ] || return 0
405
+ if ! har_python_version_ge "$actual" "$minimum"; then
406
+ pt_log "WARNING: resolved Python ${actual} is older than required ${minimum} (requires-python / .python-version)"
407
+ fi
408
+ }
409
+
410
+ har_python_create_venv() {
411
+ local dir="$1"
412
+ local venv_dir="$2"
413
+ local interpreter="$3"
414
+ local venv_rel="${venv_dir#"$dir"/}"
415
+
416
+ if har_python_is_uv_project "$dir" && command -v uv >/dev/null 2>&1; then
417
+ pt_log "Creating Python venv with uv at ${venv_rel}..."
418
+ if (cd "$dir" && uv venv --python "$interpreter" "$venv_dir" 2>/dev/null) \
419
+ || (cd "$dir" && uv venv "$venv_dir" 2>/dev/null); then
420
+ return 0
421
+ fi
422
+ pt_log "uv venv failed — falling back to ${interpreter} -m venv"
423
+ fi
424
+
425
+ pt_log "Creating Python venv at ${venv_rel}..."
426
+ "$interpreter" -m venv "$venv_dir"
427
+ }
428
+
429
+ har_python_install_deps() {
430
+ local dir="$1"
431
+ local venv_dir="$2"
432
+
433
+ if har_python_is_uv_project "$dir" && command -v uv >/dev/null 2>&1; then
434
+ pt_log "Syncing Python dependencies with uv..."
435
+ if (cd "$dir" && UV_PROJECT_ENVIRONMENT="$venv_dir" uv sync --quiet 2>/dev/null); then
436
+ return 0
437
+ fi
438
+ if (cd "$dir" && uv sync --quiet 2>/dev/null); then
439
+ return 0
440
+ fi
441
+ pt_log "uv sync failed — falling back to pip"
442
+ return 1
443
+ fi
444
+ return 1
445
+ }
446
+
231
447
  provision_python() {
232
448
  local dir="$1"
233
449
  local venv_rel="${HARNESS_PYTHON_VENV_DIR:-.har/venv}"
234
450
  local venv_dir="$dir/$venv_rel"
235
- local python_bin="python3"
451
+ local python_bin=""
452
+ local interpreter=""
453
+ local project_min=""
454
+
455
+ project_min="$(har_python_project_minimum "$dir" || true)"
456
+ interpreter="$(har_python_resolve_interpreter "$dir" "$project_min" || true)"
236
457
 
237
- if ! command -v python3 >/dev/null 2>&1; then
238
- pt_log "python3 not found on PATH — skipping venv provisioning"
458
+ if [ -z "$interpreter" ] || ! command -v "$interpreter" >/dev/null 2>&1; then
459
+ pt_log "No suitable Python interpreter found — skipping venv provisioning"
239
460
  append_env "HARNESS_ECOSYSTEM" "python"
240
461
  append_env "PYTHON_BIN" "${PYTHON_BIN:-python3}"
241
462
  return
242
463
  fi
243
464
 
465
+ if [ -n "$project_min" ]; then
466
+ har_python_warn_if_below_minimum "$interpreter" "$project_min"
467
+ fi
468
+
469
+ if [ -d "$venv_dir" ] && [ -x "$venv_dir/bin/python" ] && [ -n "$project_min" ]; then
470
+ local venv_ver=""
471
+ venv_ver="$(har_python_version_from_bin "$venv_dir/bin/python")"
472
+ if [ -n "$venv_ver" ] && ! har_python_version_ge "$venv_ver" "$project_min"; then
473
+ pt_log "Existing venv Python ${venv_ver} is older than required ${project_min} — recreating..."
474
+ rm -rf "$venv_dir"
475
+ fi
476
+ fi
477
+
244
478
  if [ ! -d "$venv_dir" ]; then
245
- pt_log "Creating Python venv at $venv_rel..."
246
- if ! python3 -m venv "$venv_dir"; then
479
+ if ! har_python_create_venv "$dir" "$venv_dir" "$interpreter"; then
247
480
  rm -rf "$venv_dir"
248
- pt_log "python3 -m venv failed (on Debian/Ubuntu install python3-venv) — using system python3"
481
+ pt_log "venv creation failed (on Debian/Ubuntu install python3-venv) — using resolved interpreter"
249
482
  append_env "HARNESS_ECOSYSTEM" "python"
250
- append_env "PYTHON_BIN" "$(command -v python3)"
483
+ append_env "PYTHON_BIN" "$interpreter"
251
484
  return
252
485
  fi
253
486
  fi
254
487
 
255
488
  if [ ! -x "$venv_dir/bin/python" ]; then
256
- pt_log "Python venv at $venv_rel is missing or broken — using system python3"
489
+ pt_log "Python venv at $venv_rel is missing or broken — using resolved interpreter"
257
490
  append_env "HARNESS_ECOSYSTEM" "python"
258
- append_env "PYTHON_BIN" "$(command -v python3)"
491
+ append_env "PYTHON_BIN" "$interpreter"
259
492
  return
260
493
  fi
261
494
 
262
495
  python_bin="$venv_dir/bin/python"
496
+ if [ -n "$project_min" ]; then
497
+ har_python_warn_if_below_minimum "$python_bin" "$project_min"
498
+ fi
499
+
263
500
  # shellcheck disable=SC1091
264
501
  source "$venv_dir/bin/activate"
265
502
 
266
503
  if ! run_install_cmd "$dir"; then
267
- pt_log "Installing Python dependencies..."
268
- if [ -f "$dir/pyproject.toml" ]; then
269
- (cd "$dir" && pip install -q -e ".[dev]" 2>/dev/null) || (cd "$dir" && pip install -q -e .)
270
- elif [ -f "$dir/requirements.txt" ]; then
271
- (cd "$dir" && pip install -q -r requirements.txt)
272
- elif [ -f "$dir/setup.py" ] || [ -f "$dir/setup.cfg" ]; then
273
- (cd "$dir" && pip install -q -e .)
274
- elif [ -f "$dir/Pipfile" ] && command -v pipenv >/dev/null 2>&1; then
275
- (cd "$dir" && pipenv install --dev)
276
- python_bin="$(cd "$dir" && pipenv --py)"
504
+ if ! har_python_install_deps "$dir" "$venv_dir"; then
505
+ pt_log "Installing Python dependencies..."
506
+ if [ -f "$dir/pyproject.toml" ]; then
507
+ (cd "$dir" && pip install -q -e ".[dev]" 2>/dev/null) || (cd "$dir" && pip install -q -e .)
508
+ elif [ -f "$dir/requirements.txt" ]; then
509
+ (cd "$dir" && pip install -q -r requirements.txt)
510
+ elif [ -f "$dir/setup.py" ] || [ -f "$dir/setup.cfg" ]; then
511
+ (cd "$dir" && pip install -q -e .)
512
+ elif [ -f "$dir/Pipfile" ] && command -v pipenv >/dev/null 2>&1; then
513
+ (cd "$dir" && pipenv install --dev)
514
+ python_bin="$(cd "$dir" && pipenv --py)"
515
+ fi
277
516
  fi
278
517
  fi
279
518
 
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env bash
2
2
  # Progressive verification pipeline for an agent environment.
3
- # Outputs JSON to stdout, human-readable progress to stderr.
3
+ # Outputs JSON to stdout (machine contract), human-readable progress to stderr.
4
+ # Passing steps omit `output`. `har env verify` streams progress only.
4
5
  #
5
6
  # Usage: ./.har/verify.sh <agent-id> [--full]
6
7
  #
@@ -73,7 +74,7 @@ run_step() {
73
74
  end=$(now_ms)
74
75
  elapsed=$(( end - start ))
75
76
 
76
- local pass_bool step_output_escaped
77
+ local pass_bool
77
78
  if [ "$exit_code" = "0" ]; then
78
79
  echo "✓ (${elapsed}ms)" >&2
79
80
  pass_bool="true"
@@ -84,14 +85,7 @@ run_step() {
84
85
  OVERALL_PASS=false
85
86
  fi
86
87
 
87
- step_output_escaped=$(escape_step_output "$output")
88
-
89
- RESULTS_JSON=$(echo "$RESULTS_JSON" | node -e "
90
- const fs = require('fs');
91
- let arr = JSON.parse(fs.readFileSync('/dev/stdin','utf8'));
92
- arr.push({name:'$name',pass:$pass_bool,ms:$elapsed,output:$step_output_escaped});
93
- process.stdout.write(JSON.stringify(arr));
94
- " 2>/dev/null || echo "$RESULTS_JSON")
88
+ record_step_result "$name" "$pass_bool" "$elapsed" "$output"
95
89
 
96
90
  if [ "$pass_bool" = "false" ] && [ -z "$FULL" ]; then
97
91
  return 1
@@ -114,7 +108,7 @@ run_http_step() {
114
108
  end=$(now_ms)
115
109
  elapsed=$(( end - start ))
116
110
 
117
- local pass_bool step_output_escaped
111
+ local pass_bool
118
112
  if [ "$exit_code" = "0" ]; then
119
113
  echo "✓ (${elapsed}ms)" >&2
120
114
  pass_bool="true"
@@ -124,14 +118,7 @@ run_http_step() {
124
118
  OVERALL_PASS=false
125
119
  fi
126
120
 
127
- step_output_escaped=$(escape_step_output "$output")
128
-
129
- RESULTS_JSON=$(echo "$RESULTS_JSON" | node -e "
130
- const fs = require('fs');
131
- let arr = JSON.parse(fs.readFileSync('/dev/stdin','utf8'));
132
- arr.push({name:'$name',pass:$pass_bool,ms:$elapsed,output:$step_output_escaped});
133
- process.stdout.write(JSON.stringify(arr));
134
- " 2>/dev/null || echo "$RESULTS_JSON")
121
+ record_step_result "$name" "$pass_bool" "$elapsed" "$output"
135
122
 
136
123
  if [ "$pass_bool" = "false" ] && [ -z "$FULL" ]; then
137
124
  return 1