hunter-harness 0.2.6 → 0.2.8

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.
Files changed (2) hide show
  1. package/dist/bin.js +270 -85
  2. package/package.json +2 -2
package/dist/bin.js CHANGED
@@ -4,6 +4,7 @@
4
4
  import { realpathSync } from "node:fs";
5
5
  import { pathToFileURL } from "node:url";
6
6
  import { createInterface } from "node:readline/promises";
7
+ import { Writable } from "node:stream";
7
8
  import { Command, CommanderError } from "commander";
8
9
 
9
10
  // ../contracts/dist/ai-config.js
@@ -1670,6 +1671,9 @@ function affectedPaths(operation) {
1670
1671
  }
1671
1672
  async function writeJournal(transactionRoot, journal) {
1672
1673
  await atomicWriteJson(join3(transactionRoot, "journal.json"), journal);
1674
+ await writeStatus(transactionRoot, journal);
1675
+ }
1676
+ async function writeStatus(transactionRoot, journal) {
1673
1677
  await atomicWriteJson(join3(transactionRoot, "status.json"), {
1674
1678
  schema_version: 1,
1675
1679
  transaction_id: journal.transaction_id,
@@ -1679,6 +1683,16 @@ async function writeJournal(transactionRoot, journal) {
1679
1683
  updated_at: (/* @__PURE__ */ new Date()).toISOString()
1680
1684
  });
1681
1685
  }
1686
+ function journalOperation(operation) {
1687
+ if (operation.operation === "rename") {
1688
+ return {
1689
+ operation: "rename",
1690
+ from_path: operation.from_path,
1691
+ to_path: operation.to_path
1692
+ };
1693
+ }
1694
+ return { operation: operation.operation, path: operation.path };
1695
+ }
1682
1696
  async function pruneOlderSuccessful(layout, currentId, kind) {
1683
1697
  if (kind === void 0)
1684
1698
  return;
@@ -1799,12 +1813,12 @@ async function runTransaction(projectRoot, rawOperations, options = {}) {
1799
1813
  const snapshots = await snapshotPaths(projectRoot, transactionRoot, paths);
1800
1814
  await stageOperations(transactionRoot, operations);
1801
1815
  const journal = {
1802
- schema_version: 1,
1816
+ schema_version: 2,
1803
1817
  transaction_id: transactionId,
1804
1818
  ...options.kind === void 0 ? {} : { kind: options.kind },
1805
1819
  state: "prepared",
1806
1820
  created_at: (/* @__PURE__ */ new Date()).toISOString(),
1807
- operations,
1821
+ operations: operations.map(journalOperation),
1808
1822
  snapshots,
1809
1823
  applied_count: 0,
1810
1824
  failure: null
@@ -1820,7 +1834,7 @@ async function runTransaction(projectRoot, rawOperations, options = {}) {
1820
1834
  }
1821
1835
  await applyOperation(projectRoot, transactionRoot, operation, index, transactionId);
1822
1836
  journal.applied_count = index + 1;
1823
- await writeJournal(transactionRoot, journal);
1837
+ await writeStatus(transactionRoot, journal);
1824
1838
  if (options.interruptAfterApply === journal.applied_count) {
1825
1839
  journal.state = "interrupted";
1826
1840
  journal.failure = "injected interruption";
@@ -2346,6 +2360,7 @@ async function initializeProject(options) {
2346
2360
  }
2347
2361
  manifests.push({
2348
2362
  adapter: agent,
2363
+ profile,
2349
2364
  bundle_version: bundle.manifest.bundle_version,
2350
2365
  bundle_manifest_hash: bundleHash
2351
2366
  });
@@ -2450,9 +2465,9 @@ async function initializeProject(options) {
2450
2465
  return byTarget !== 0 ? byTarget : left.block_id.localeCompare(right.block_id);
2451
2466
  });
2452
2467
  const installedState = {
2453
- schema_version: 3,
2454
- profile,
2468
+ schema_version: 4,
2455
2469
  adapters: enabledAgents,
2470
+ profiles: Object.fromEntries(enabledAgents.map((agent) => [agent, profile])),
2456
2471
  installed_at: (/* @__PURE__ */ new Date()).toISOString(),
2457
2472
  manifests,
2458
2473
  files: mergedTargets.map((target) => ({
@@ -2507,17 +2522,35 @@ async function readOptionalText(path) {
2507
2522
  async function readInstalledState(root) {
2508
2523
  const content = await readOptionalText(join6(root, INSTALLED_STATE_PATH));
2509
2524
  if (content === "") {
2510
- return { profile: null, schemaVersion: null, adapters: [], trusted: /* @__PURE__ */ new Map() };
2525
+ return {
2526
+ profile: null,
2527
+ schemaVersion: null,
2528
+ adapters: [],
2529
+ profiles: /* @__PURE__ */ new Map(),
2530
+ trusted: /* @__PURE__ */ new Map(),
2531
+ files: [],
2532
+ manifests: [],
2533
+ managedBlocks: []
2534
+ };
2511
2535
  }
2512
2536
  let parsed;
2513
2537
  try {
2514
2538
  parsed = JSON.parse(content);
2515
2539
  } catch {
2516
- return { profile: null, schemaVersion: null, adapters: [], trusted: /* @__PURE__ */ new Map() };
2540
+ return {
2541
+ profile: null,
2542
+ schemaVersion: null,
2543
+ adapters: [],
2544
+ profiles: /* @__PURE__ */ new Map(),
2545
+ trusted: /* @__PURE__ */ new Map(),
2546
+ files: [],
2547
+ manifests: [],
2548
+ managedBlocks: []
2549
+ };
2517
2550
  }
2518
2551
  const profile = parseHarnessProfile(parsed.profile);
2519
2552
  const trusted = /* @__PURE__ */ new Map();
2520
- if ((parsed.schema_version === 2 || parsed.schema_version === 3) && Array.isArray(parsed.files)) {
2553
+ if ((parsed.schema_version === 2 || parsed.schema_version === 3 || parsed.schema_version === 4) && Array.isArray(parsed.files)) {
2521
2554
  for (const entry of parsed.files) {
2522
2555
  if (entry !== null && typeof entry === "object" && "target_path" in entry && "sha256" in entry) {
2523
2556
  const target = entry.target_path;
@@ -2529,8 +2562,42 @@ async function readInstalledState(root) {
2529
2562
  }
2530
2563
  }
2531
2564
  const schemaVersion = typeof parsed.schema_version === "number" ? parsed.schema_version : null;
2532
- const adapters = schemaVersion === 3 && Array.isArray(parsed.adapters) ? sortHarnessAgents(parsed.adapters.filter((value) => value === "claude-code" || value === "codex" || value === "cursor" || value === "codebuddy")) : schemaVersion === 1 || schemaVersion === 2 ? ["claude-code"] : [];
2533
- return { profile, schemaVersion, adapters, trusted };
2565
+ const adapters = (schemaVersion === 3 || schemaVersion === 4) && Array.isArray(parsed.adapters) ? sortHarnessAgents(parsed.adapters.filter((value) => value === "claude-code" || value === "codex" || value === "cursor" || value === "codebuddy")) : schemaVersion === 1 || schemaVersion === 2 ? ["claude-code"] : [];
2566
+ const profiles = /* @__PURE__ */ new Map();
2567
+ if (schemaVersion === 4 && parsed.profiles !== null && typeof parsed.profiles === "object" && !Array.isArray(parsed.profiles)) {
2568
+ for (const agent of adapters) {
2569
+ const value = parsed.profiles[agent];
2570
+ const agentProfile = parseHarnessProfile(value);
2571
+ if (agentProfile !== null)
2572
+ profiles.set(agent, agentProfile);
2573
+ }
2574
+ } else if (profile !== null) {
2575
+ for (const agent of adapters)
2576
+ profiles.set(agent, profile);
2577
+ }
2578
+ const files = Array.isArray(parsed.files) ? parsed.files.filter((entry) => entry !== null && typeof entry === "object" && typeof entry.target_path === "string" && typeof entry.source_path === "string" && typeof entry.sha256 === "string" && "owner" in entry) : [];
2579
+ const manifests = schemaVersion === 4 && Array.isArray(parsed.manifests) ? parsed.manifests.filter((entry) => entry !== null && typeof entry === "object" && typeof entry.adapter === "string" && typeof entry.profile === "string") : [];
2580
+ const managedBlocks = Array.isArray(parsed.managed_blocks) ? parsed.managed_blocks.filter((entry) => entry !== null && typeof entry === "object" && typeof entry.target_path === "string" && typeof entry.block_id === "string") : [];
2581
+ return {
2582
+ profile,
2583
+ schemaVersion,
2584
+ adapters,
2585
+ profiles,
2586
+ trusted,
2587
+ files,
2588
+ manifests,
2589
+ managedBlocks
2590
+ };
2591
+ }
2592
+ async function readInstalledAgentConfiguration(projectRoot) {
2593
+ const installed = await readInstalledState(resolve4(projectRoot));
2594
+ return {
2595
+ agents: installed.adapters,
2596
+ profiles: Object.fromEntries(installed.adapters.map((agent) => [
2597
+ agent,
2598
+ installed.profiles.get(agent) ?? installed.profile ?? "general"
2599
+ ]))
2600
+ };
2534
2601
  }
2535
2602
  async function readContextIndexBundleHash(root) {
2536
2603
  const content = await readOptionalText(join6(root, CONTEXT_INDEX_PATH));
@@ -2588,16 +2655,18 @@ function conflict(target, reason, oldSha, incomingSha) {
2588
2655
  function sortByTarget(items) {
2589
2656
  return [...items].sort((left, right) => left.target_path.localeCompare(right.target_path));
2590
2657
  }
2591
- async function reconcileContextIndex(root, profile, agents, manifests, codebuddySurface2) {
2658
+ async function reconcileContextIndex(root, agents, profiles, manifests, codebuddySurface2) {
2592
2659
  const existing = await readOptionalText(join6(root, CONTEXT_INDEX_PATH));
2593
- const context = { profile, codebuddySurface: codebuddySurface2 };
2594
2660
  const record = {
2595
2661
  schema_version: 2,
2596
2662
  project: {
2597
2663
  shared_instructions: "AGENTS.md",
2598
2664
  adapters: Object.fromEntries(agents.map((agent) => [
2599
2665
  agent,
2600
- getAdapter(agent).contextIndex(context)
2666
+ getAdapter(agent).contextIndex({
2667
+ profile: profiles.get(agent) ?? "general",
2668
+ codebuddySurface: codebuddySurface2
2669
+ })
2601
2670
  ]))
2602
2671
  },
2603
2672
  knowledge: { index: ".harness/knowledge/index.json" },
@@ -2616,23 +2685,25 @@ async function reconcileContextIndex(root, profile, agents, manifests, codebuddy
2616
2685
  content: next
2617
2686
  };
2618
2687
  }
2619
- async function projectTransitionOperation(root, previousProfile, profile, oldAgents, agents, codebuddySurface2) {
2620
- if (previousProfile === profile && oldAgents.length === agents.length && oldAgents.every((agent, index) => agent === agents[index]))
2621
- return null;
2688
+ async function projectTransitionOperation(root, agents, profiles, codebuddySurface2) {
2622
2689
  const path = ".harness/project.yaml";
2623
2690
  const content = await readOptionalText(join6(root, path));
2624
2691
  if (content === "")
2625
2692
  return null;
2626
2693
  const project = parseYaml3(content);
2694
+ const activeProfiles = [...new Set(agents.map((agent) => profiles.get(agent) ?? "general"))].sort();
2695
+ const next = stringifyYaml2({
2696
+ ...project,
2697
+ project: { ...project.project, profiles: activeProfiles },
2698
+ adapters: { enabled: agents },
2699
+ ...agents.includes("codebuddy") ? { adapter_options: { codebuddy: { surface: codebuddySurface2 } } } : { adapter_options: void 0 }
2700
+ }, { sortMapEntries: true });
2701
+ if (next === content)
2702
+ return null;
2627
2703
  return {
2628
2704
  operation: "modify",
2629
2705
  path,
2630
- content: stringifyYaml2({
2631
- ...project,
2632
- project: { ...project.project, profiles: [profile] },
2633
- adapters: { enabled: agents },
2634
- ...agents.includes("codebuddy") ? { adapter_options: { codebuddy: { surface: codebuddySurface2 } } } : { adapter_options: void 0 }
2635
- }, { sortMapEntries: true })
2706
+ content: next
2636
2707
  };
2637
2708
  }
2638
2709
  function mergeTargets(targets) {
@@ -2691,30 +2762,42 @@ function stateWithoutInstalledAt(value) {
2691
2762
  }
2692
2763
  async function refreshProject(options) {
2693
2764
  const root = resolve4(options.projectRoot);
2694
- const profile = options.profile;
2695
2765
  const installed = await readInstalledState(root);
2696
- const previousProfile = installed.profile;
2697
- const agents = sortHarnessAgents(options.agents);
2698
2766
  const oldAgents = installed.adapters.length > 0 ? installed.adapters : ["claude-code"];
2767
+ const selectedAgents = sortHarnessAgents(options.agents);
2768
+ const selectedSet = new Set(selectedAgents);
2769
+ const agents = sortHarnessAgents([...oldAgents, ...selectedAgents]);
2770
+ const profiles = new Map(installed.profiles);
2771
+ if (options.profile !== void 0) {
2772
+ for (const agent of selectedAgents)
2773
+ profiles.set(agent, options.profile);
2774
+ }
2775
+ const profile = options.profile ?? selectedAgents.map((agent) => profiles.get(agent)).find((value) => value !== void 0) ?? installed.profile ?? "general";
2776
+ const previousProfile = selectedAgents.map((agent) => installed.profiles.get(agent)).find((value) => value !== void 0) ?? installed.profile;
2699
2777
  const codebuddySurface2 = options.codebuddySurface ?? "both";
2700
- const context = { profile, codebuddySurface: codebuddySurface2 };
2701
2778
  const owned = [];
2702
2779
  const manifests = [];
2703
2780
  for (const agent of agents) {
2704
- const bundle = await loadAgentBundle(options.resourcesRoot, profile, agent);
2781
+ const agentProfile = profiles.get(agent) ?? profile;
2782
+ profiles.set(agent, agentProfile);
2783
+ const bundle = await loadAgentBundle(options.resourcesRoot, agentProfile, agent);
2705
2784
  manifests.push({
2706
2785
  adapter: agent,
2786
+ profile: agentProfile,
2707
2787
  bundle_version: bundle.manifest.bundle_version,
2708
2788
  bundle_manifest_hash: sha256Bytes(canonicalJson(bundle.manifest.files))
2709
2789
  });
2710
- for (const target of managedTargetsFor(getAdapter(agent), bundle, context)) {
2711
- owned.push({ ...target, owner: agent });
2790
+ if (selectedSet.has(agent)) {
2791
+ const context = { profile: agentProfile, codebuddySurface: codebuddySurface2 };
2792
+ for (const target of managedTargetsFor(getAdapter(agent), bundle, context)) {
2793
+ owned.push({ ...target, owner: agent });
2794
+ }
2712
2795
  }
2713
2796
  }
2714
2797
  const newManaged = mergeTargets(owned);
2715
2798
  let trusted = installed.trusted;
2716
2799
  let migrationOldPaths = null;
2717
- if (installed.schemaVersion === 1) {
2800
+ if (installed.schemaVersion === 1 && selectedSet.has("claude-code")) {
2718
2801
  const contextHash = await readContextIndexBundleHash(root);
2719
2802
  if (contextHash !== null) {
2720
2803
  const migrations = await loadMigrationManifests(options.resourcesRoot);
@@ -2738,12 +2821,18 @@ async function refreshProject(options) {
2738
2821
  });
2739
2822
  }
2740
2823
  }
2741
- } else if (previousProfile !== null) {
2742
- const oldContext = { profile: previousProfile, codebuddySurface: codebuddySurface2 };
2824
+ } else {
2743
2825
  const oldTargets = [];
2744
- for (const agent of oldAgents) {
2745
- const bundle = await loadAgentBundle(options.resourcesRoot, previousProfile, agent);
2746
- oldTargets.push(...managedTargetsFor(getAdapter(agent), bundle, oldContext));
2826
+ for (const agent of selectedAgents) {
2827
+ const oldProfile = installed.profiles.get(agent) ?? installed.profile;
2828
+ if (!oldAgents.includes(agent) || oldProfile === null || oldProfile === void 0) {
2829
+ continue;
2830
+ }
2831
+ const bundle = await loadAgentBundle(options.resourcesRoot, oldProfile, agent);
2832
+ oldTargets.push(...managedTargetsFor(getAdapter(agent), bundle, {
2833
+ profile: oldProfile,
2834
+ codebuddySurface: codebuddySurface2
2835
+ }));
2747
2836
  }
2748
2837
  oldOnly = oldTargets.filter((target) => !newTargetSet.has(target.target_path));
2749
2838
  }
@@ -2753,7 +2842,7 @@ async function refreshProject(options) {
2753
2842
  const unchanged = [];
2754
2843
  const conflicts = [];
2755
2844
  const ops = [];
2756
- const newStateFiles = [];
2845
+ const newStateFiles = installed.files.filter((entry) => entry.owner === "shared" || !selectedSet.has(entry.owner));
2757
2846
  for (const target of newManaged) {
2758
2847
  const incoming = target.sha256;
2759
2848
  const current = await fileHex(join6(root, target.target_path));
@@ -2801,41 +2890,50 @@ async function refreshProject(options) {
2801
2890
  }
2802
2891
  }
2803
2892
  await reconcileMarkdownBlock(root, "AGENTS.md", AGENTS_CORE_BLOCK_ID, AGENTS_MANAGED_BLOCK_CONTENT, false, ops, conflicts, preserved);
2804
- await reconcileMarkdownBlock(root, "CLAUDE.md", CLAUDE_BLOCK_ID, CLAUDE_MANAGED_BLOCK_CONTENT, !agents.includes("claude-code"), ops, conflicts, preserved);
2805
- await reconcileMarkdownBlock(root, "CODEBUDDY.md", CODEBUDDY_BLOCK_ID, CODEBUDDY_MANAGED_BLOCK_CONTENT, !agents.includes("codebuddy"), ops, conflicts, preserved);
2806
- const projectOperation = await projectTransitionOperation(root, previousProfile, profile, oldAgents, agents, codebuddySurface2);
2893
+ if (selectedSet.has("claude-code")) {
2894
+ await reconcileMarkdownBlock(root, "CLAUDE.md", CLAUDE_BLOCK_ID, CLAUDE_MANAGED_BLOCK_CONTENT, false, ops, conflicts, preserved);
2895
+ }
2896
+ if (selectedSet.has("codebuddy")) {
2897
+ await reconcileMarkdownBlock(root, "CODEBUDDY.md", CODEBUDDY_BLOCK_ID, CODEBUDDY_MANAGED_BLOCK_CONTENT, false, ops, conflicts, preserved);
2898
+ }
2899
+ const projectOperation = await projectTransitionOperation(root, agents, profiles, codebuddySurface2);
2807
2900
  if (projectOperation !== null)
2808
2901
  ops.push(projectOperation);
2809
- const contextOperation = await reconcileContextIndex(root, profile, agents, manifests, codebuddySurface2);
2902
+ const contextOperation = await reconcileContextIndex(root, agents, profiles, manifests, codebuddySurface2);
2810
2903
  if (contextOperation !== null)
2811
2904
  ops.push(contextOperation);
2812
2905
  const managedBlocks = [
2906
+ ...installed.managedBlocks.filter((entry) => entry.owner !== "shared" && !selectedSet.has(entry.owner)),
2813
2907
  {
2814
2908
  owner: "shared",
2815
2909
  target_path: "AGENTS.md",
2816
2910
  block_id: AGENTS_CORE_BLOCK_ID,
2817
2911
  content_sha256: createHash5("sha256").update(AGENTS_MANAGED_BLOCK_CONTENT).digest("hex")
2818
2912
  },
2819
- ...agents.includes("claude-code") ? [{
2913
+ ...selectedSet.has("claude-code") ? [{
2820
2914
  owner: "claude-code",
2821
2915
  target_path: "CLAUDE.md",
2822
2916
  block_id: CLAUDE_BLOCK_ID,
2823
2917
  content_sha256: createHash5("sha256").update(CLAUDE_MANAGED_BLOCK_CONTENT).digest("hex")
2824
2918
  }] : [],
2825
- ...agents.includes("codebuddy") ? [{
2919
+ ...selectedSet.has("codebuddy") ? [{
2826
2920
  owner: "codebuddy",
2827
2921
  target_path: "CODEBUDDY.md",
2828
2922
  block_id: CODEBUDDY_BLOCK_ID,
2829
2923
  content_sha256: createHash5("sha256").update(CODEBUDDY_MANAGED_BLOCK_CONTENT).digest("hex")
2830
2924
  }] : []
2831
2925
  ].sort((left, right) => left.target_path.localeCompare(right.target_path) || left.block_id.localeCompare(right.block_id));
2926
+ const filesByTarget = new Map(newStateFiles.map((entry) => [entry.target_path, entry]));
2832
2927
  const installedState = {
2833
- schema_version: 3,
2834
- profile,
2928
+ schema_version: 4,
2835
2929
  adapters: agents,
2930
+ profiles: Object.fromEntries(agents.map((agent) => [
2931
+ agent,
2932
+ profiles.get(agent) ?? "general"
2933
+ ])),
2836
2934
  installed_at: (/* @__PURE__ */ new Date()).toISOString(),
2837
- manifests,
2838
- files: newStateFiles.sort((left, right) => left.target_path.localeCompare(right.target_path) || left.source_path.localeCompare(right.source_path)),
2935
+ manifests: manifests.sort((left, right) => left.adapter.localeCompare(right.adapter)),
2936
+ files: [...filesByTarget.values()].sort((left, right) => left.target_path.localeCompare(right.target_path) || left.source_path.localeCompare(right.source_path)),
2839
2937
  managed_blocks: managedBlocks
2840
2938
  };
2841
2939
  const existingState = await readOptionalText(join6(root, INSTALLED_STATE_PATH));
@@ -2853,7 +2951,10 @@ async function refreshProject(options) {
2853
2951
  }
2854
2952
  if (!options.dryRun) {
2855
2953
  await runTransaction(root, ops, { kind: "refresh" });
2856
- await pruneEmptyParentDirs(root, removed.map((item2) => item2.target_path), getAdapters([.../* @__PURE__ */ new Set([...agents, ...oldAgents])]).flatMap((adapter) => adapter.pruneBoundaries(context)));
2954
+ await pruneEmptyParentDirs(root, removed.map((item2) => item2.target_path), getAdapters(selectedAgents).flatMap((adapter) => adapter.pruneBoundaries({
2955
+ profile: profiles.get(adapter.name) ?? profile,
2956
+ codebuddySurface: codebuddySurface2
2957
+ })));
2857
2958
  }
2858
2959
  return {
2859
2960
  profile,
@@ -3279,6 +3380,17 @@ async function ensureCredentialsGitignore(projectRoot) {
3279
3380
  }
3280
3381
  }
3281
3382
  const existing = new Set(content.split("\n").map((line) => line.trim()));
3383
+ const harnessDirectoryIgnored = [
3384
+ ".harness",
3385
+ ".harness/",
3386
+ "/.harness",
3387
+ "/.harness/",
3388
+ ".harness/**",
3389
+ "/.harness/**"
3390
+ ].some((line) => existing.has(line));
3391
+ if (harnessDirectoryIgnored) {
3392
+ return;
3393
+ }
3282
3394
  const missing = CREDENTIALS_GITIGNORE_LINES.filter((line) => !existing.has(line));
3283
3395
  if (missing.length === 0) {
3284
3396
  return;
@@ -3292,11 +3404,21 @@ function resolvePushAuth(input) {
3292
3404
  const localToken = input.local?.token?.trim();
3293
3405
  const token = envToken && envToken.length > 0 ? envToken : localToken && localToken.length > 0 ? localToken : void 0;
3294
3406
  const serverUrl = input.serverUrlFlag ?? input.local?.server_url ?? input.projectUrl ?? void 0;
3407
+ const missing = [];
3295
3408
  if (serverUrl === void 0 || serverUrl === null || serverUrl.trim() === "") {
3296
- return { code: "SERVER_URL_REQUIRED" };
3409
+ missing.push("url");
3297
3410
  }
3298
3411
  if (token === void 0) {
3299
- return { code: "TOKEN_INVALID" };
3412
+ missing.push("token");
3413
+ }
3414
+ if (missing.length > 0) {
3415
+ return {
3416
+ code: missing.includes("url") ? "SERVER_URL_REQUIRED" : "TOKEN_INVALID",
3417
+ missing
3418
+ };
3419
+ }
3420
+ if (serverUrl === void 0 || token === void 0) {
3421
+ throw new Error("push auth resolution invariant failed");
3300
3422
  }
3301
3423
  return { serverUrl, token };
3302
3424
  }
@@ -3634,9 +3756,6 @@ async function pushProject(options) {
3634
3756
  if (options.dryRun) {
3635
3757
  return { preview, proposalId: null, projectId: project.project.project_id };
3636
3758
  }
3637
- if (options.confirmProposal !== void 0 && !await options.confirmProposal(preview)) {
3638
- return { preview, proposalId: null, projectId: project.project.project_id, cancelled: true };
3639
- }
3640
3759
  const localCredentials = await readLocalCredentials(root);
3641
3760
  const auth = resolvePushAuth({
3642
3761
  ...options.serverUrl === void 0 ? {} : { serverUrlFlag: options.serverUrl },
@@ -3648,10 +3767,10 @@ async function pushProject(options) {
3648
3767
  });
3649
3768
  if ("code" in auth) {
3650
3769
  if (auth.code === "SERVER_URL_REQUIRED") {
3651
- throw new PushWorkflowError("server_url is required; use --server-url, project.yaml server.url, or " + CREDENTIALS_HINT, 3, "SERVER_URL_REQUIRED");
3770
+ throw new PushWorkflowError("server_url is required; use --server-url, project.yaml server.url, or " + CREDENTIALS_HINT, 3, "SERVER_URL_REQUIRED", { missing_credentials: auth.missing });
3652
3771
  }
3653
3772
  const tokenEnv2 = options.tokenEnv ?? project.server.token_env;
3654
- throw new PushWorkflowError("API token is unset; set " + tokenEnv2 + " or " + CREDENTIALS_HINT, 8, "TOKEN_INVALID");
3773
+ throw new PushWorkflowError("API token is unset; set " + tokenEnv2 + " or " + CREDENTIALS_HINT, 8, "TOKEN_INVALID", { missing_credentials: auth.missing });
3655
3774
  }
3656
3775
  const serverUrl = auth.serverUrl;
3657
3776
  const token = auth.token;
@@ -3668,6 +3787,9 @@ async function pushProject(options) {
3668
3787
  if (parsedServerUrl.protocol !== "https:") {
3669
3788
  throw new PushWorkflowError("server_url must use HTTPS", 3, "SERVER_URL_INVALID");
3670
3789
  }
3790
+ if (options.confirmProposal !== void 0 && !await options.confirmProposal(preview)) {
3791
+ return { preview, proposalId: null, projectId: project.project.project_id, cancelled: true };
3792
+ }
3671
3793
  const workflowPath = join10(root, ".harness", "state", "local", "push-workflow.json");
3672
3794
  const priorWorkflow = await readOptionalJson(workflowPath);
3673
3795
  const provisionalRequestId = priorWorkflow?.local_project_key === project.project.local_project_key ? priorWorkflow.request_id : uuidV7();
@@ -4568,9 +4690,9 @@ async function detectProject(root) {
4568
4690
  }
4569
4691
  return { status: "valid", config: parsed.data };
4570
4692
  }
4571
- function parseProfile(value, current) {
4693
+ function parseProfile(value) {
4572
4694
  if (value === void 0 || value === "") {
4573
- return current;
4695
+ return void 0;
4574
4696
  }
4575
4697
  if (value === "1" || value === "general") return "general";
4576
4698
  if (value === "2" || value === "java") return "java";
@@ -4655,7 +4777,7 @@ async function runRefresh(options, dependencies) {
4655
4777
  let targetProfile;
4656
4778
  let targetAgents;
4657
4779
  try {
4658
- targetProfile = parseProfile(options.profile, currentProfile);
4780
+ targetProfile = parseProfile(options.profile);
4659
4781
  targetAgents = options.agents === void 0 ? refreshAgents(detection.config) : parseAgentsInput(options.agents);
4660
4782
  } catch (error) {
4661
4783
  const message = error instanceof Error ? error.message : String(error);
@@ -4664,12 +4786,12 @@ async function runRefresh(options, dependencies) {
4664
4786
  return error instanceof InitConfigurationError ? error.exitCode : 3;
4665
4787
  }
4666
4788
  const dryRun = options.dryRun === true;
4667
- if ((targetProfile !== currentProfile || targetAgents.some((agent, index) => agent !== refreshAgents(detection.config)[index]) || targetAgents.length !== refreshAgents(detection.config).length) && !dryRun) {
4789
+ if ((targetProfile !== void 0 && targetProfile !== currentProfile || targetAgents.some((agent, index) => agent !== refreshAgents(detection.config)[index]) || targetAgents.length !== refreshAgents(detection.config).length) && !dryRun) {
4668
4790
  try {
4669
4791
  const preview = await refreshProject({
4670
4792
  projectRoot: dependencies.cwd,
4671
4793
  resourcesRoot: dependencies.resourcesRoot,
4672
- profile: targetProfile,
4794
+ ...targetProfile === void 0 ? {} : { profile: targetProfile },
4673
4795
  agents: targetAgents,
4674
4796
  codebuddySurface: codebuddySurface(detection.config),
4675
4797
  dryRun: true,
@@ -4694,7 +4816,7 @@ async function runRefresh(options, dependencies) {
4694
4816
  return 2;
4695
4817
  }
4696
4818
  } else if (!options.yes && !dryRun) {
4697
- const label = targetProfile === currentProfile ? `\u5237\u65B0\u5F53\u524D\u914D\u7F6E\uFF08${currentProfile}\uFF09` : `\u5207\u6362\u914D\u7F6E\uFF1A${currentProfile} \u2192 ${targetProfile}`;
4819
+ const label = targetProfile === currentProfile ? `\u5237\u65B0\u5F53\u524D\u914D\u7F6E\uFF08${currentProfile}\uFF09` : targetProfile === void 0 ? "\u5237\u65B0\u6240\u9009\u5DE5\u5177\u7684\u5F53\u524D\u914D\u7F6E" : `\u66F4\u65B0\u6240\u9009\u5DE5\u5177\u914D\u7F6E\uFF1A${currentProfile} \u2192 ${targetProfile}`;
4698
4820
  const answer = await dependencies.prompt(`${label}\uFF1F[y/N]\uFF1A`);
4699
4821
  if (!/^(?:y|yes)$/i.test(answer.trim())) {
4700
4822
  return 2;
@@ -4705,7 +4827,7 @@ async function runRefresh(options, dependencies) {
4705
4827
  const result = await refreshProject({
4706
4828
  projectRoot: dependencies.cwd,
4707
4829
  resourcesRoot: dependencies.resourcesRoot,
4708
- profile: targetProfile,
4830
+ ...targetProfile === void 0 ? {} : { profile: targetProfile },
4709
4831
  agents: targetAgents,
4710
4832
  codebuddySurface: codebuddySurface(detection.config),
4711
4833
  dryRun,
@@ -4720,7 +4842,7 @@ async function runRefresh(options, dependencies) {
4720
4842
  if (result.removed.length > 0) parts.push(`\u5DF2\u5220\u9664 ${result.removed.length} \u4E2A`);
4721
4843
  if (result.preserved.length > 0) parts.push(`\u5DF2\u4FDD\u7559 ${result.preserved.length} \u4E2A`);
4722
4844
  if (result.unchanged.length > 0) parts.push(`\u65E0\u9700\u53D8\u66F4 ${result.unchanged.length} \u4E2A`);
4723
- dependencies.stdout(`Harness \u5237\u65B0\uFF08${targetProfile}\uFF09\uFF1A${parts.join("\uFF0C") || "\u6CA1\u6709\u53D8\u66F4"}\u3002
4845
+ dependencies.stdout(`Harness \u5237\u65B0\uFF08${result.profile}\uFF09\uFF1A${parts.join("\uFF0C") || "\u6CA1\u6709\u53D8\u66F4"}\u3002
4724
4846
  `);
4725
4847
  }
4726
4848
  return output.exit_code;
@@ -4750,9 +4872,12 @@ async function runRefresh(options, dependencies) {
4750
4872
  }
4751
4873
 
4752
4874
  // src/commands/configure.ts
4753
- function otherProfile(current) {
4754
- return current === "general" ? "java" : "general";
4755
- }
4875
+ var AGENT_LABELS = {
4876
+ "claude-code": "Claude Code",
4877
+ codex: "Codex",
4878
+ cursor: "Cursor",
4879
+ codebuddy: "CodeBuddy"
4880
+ };
4756
4881
  async function runFirstInstall(options, dependencies) {
4757
4882
  const requestId = uuidV7();
4758
4883
  try {
@@ -4834,21 +4959,40 @@ async function runExistingProject(options, dependencies, currentProfile) {
4834
4959
  if (options.nonInteractive === true) {
4835
4960
  return runRefresh(refreshOptions, dependencies);
4836
4961
  }
4837
- const menu = await dependencies.prompt(
4838
- `Hunter Harness \u5DF2\u521D\u59CB\u5316\uFF08profile: ${currentProfile}\uFF09\u3002
4839
- 1. \u5237\u65B0\u5F53\u524D\u914D\u7F6E\uFF08\u9ED8\u8BA4\u4E14\u63A8\u8350\uFF09
4840
- 2. \u5207\u6362\u5230\u53E6\u4E00\u79CD\u914D\u7F6E
4841
- 3. \u53D6\u6D88
4842
- \u8BF7\u9009\u62E9 [1]: `
4843
- );
4844
- const choice = menu.trim();
4845
- if (choice === "3" || /^c/i.test(choice)) {
4846
- return 2;
4847
- }
4848
- if (choice === "2" || /^s/i.test(choice)) {
4849
- refreshOptions.profile = otherProfile(currentProfile);
4850
- } else {
4851
- refreshOptions.profile = currentProfile;
4962
+ const installed = await readInstalledAgentConfiguration(dependencies.cwd);
4963
+ const currentAgents = installed.agents.length > 0 ? installed.agents : ["claude-code"];
4964
+ const currentLines = currentAgents.map(
4965
+ (agent) => `- ${AGENT_LABELS[agent]}\uFF1A${installed.profiles[agent] ?? currentProfile}`
4966
+ ).join("\n");
4967
+ if (refreshOptions.agents === void 0) {
4968
+ const defaultSelection = currentAgents.map((agent) => String(HARNESS_AGENT_ORDER.indexOf(agent) + 1)).join(",");
4969
+ const answer = await dependencies.prompt(
4970
+ `Hunter Harness \u5F53\u524D\u914D\u7F6E\uFF1A
4971
+ ${currentLines}
4972
+ \u8BF7\u9009\u62E9\u672C\u6B21\u8981\u65B0\u589E\u6216\u5237\u65B0\u7684\u5DE5\u5177\uFF08\u53EF\u591A\u9009\uFF0C\u9017\u53F7\u5206\u9694\uFF1B\u672A\u9009\u62E9\u7684\u5DE5\u5177\u4FDD\u6301\u4E0D\u53D8\uFF09\uFF1A
4973
+ 1. Claude Code
4974
+ 2. Codex
4975
+ 3. Cursor
4976
+ 4. CodeBuddy
4977
+ \u8BF7\u8F93\u5165\u7F16\u53F7 [${defaultSelection}]\uFF0C\u6216\u8F93\u5165 0 \u53D6\u6D88\uFF1A`
4978
+ );
4979
+ if (answer.trim() === "0" || /^c/i.test(answer.trim())) return 2;
4980
+ refreshOptions.agents = answer.trim() === "" ? currentAgents.join(",") : answer.trim();
4981
+ }
4982
+ if (refreshOptions.profile === void 0) {
4983
+ const selected = parseAgentsInput(refreshOptions.agents);
4984
+ const selectedProfiles = new Set(selected.flatMap((agent) => {
4985
+ const profile = installed.profiles[agent];
4986
+ return profile === void 0 ? [] : [profile];
4987
+ }));
4988
+ const defaultProfile = selectedProfiles.size === 1 ? [...selectedProfiles][0] ?? currentProfile : currentProfile;
4989
+ const answer = await dependencies.prompt(
4990
+ `\u8BF7\u9009\u62E9\u6240\u9009\u5DE5\u5177\u4F7F\u7528\u7684 Harness \u914D\u7F6E\uFF1A
4991
+ 1. \u901A\u7528
4992
+ 2. Java
4993
+ \u8BF7\u8F93\u5165\u7F16\u53F7 [${defaultProfile === "java" ? "2" : "1"}]\uFF1A`
4994
+ );
4995
+ refreshOptions.profile = answer.trim() === "" ? defaultProfile : answer.trim();
4852
4996
  }
4853
4997
  refreshOptions.confirmed = true;
4854
4998
  return runRefresh(refreshOptions, dependencies);
@@ -4967,7 +5111,9 @@ function errorPayload(error) {
4967
5111
  }
4968
5112
  async function promptForCredentials(dependencies, missing) {
4969
5113
  const serverUrl = missing === "token" ? void 0 : (await dependencies.prompt("\u670D\u52A1\u7AEF URL (https://...): ")).trim();
4970
- const token = missing === "url" ? void 0 : (await dependencies.prompt("API Token: ")).trim();
5114
+ const token = missing === "url" ? void 0 : (await (dependencies.promptSecret ?? dependencies.prompt)(
5115
+ "API Token\uFF08\u8F93\u5165\u5C06\u9690\u85CF\uFF09: "
5116
+ )).trim();
4971
5117
  if ((missing === "url" || missing === "both") && (serverUrl === void 0 || serverUrl === "")) {
4972
5118
  return null;
4973
5119
  }
@@ -5076,7 +5222,8 @@ async function runPush(options, dependencies) {
5076
5222
  return 0;
5077
5223
  } catch (error) {
5078
5224
  if (attempt === 0 && options.nonInteractive !== true && options.dryRun !== true && error instanceof PushWorkflowError && (error.code === "SERVER_URL_REQUIRED" || error.code === "TOKEN_INVALID")) {
5079
- const missing = error.code === "SERVER_URL_REQUIRED" ? "url" : error.code === "TOKEN_INVALID" ? "token" : "both";
5225
+ const missingCredentials = error.details?.missing_credentials;
5226
+ const missing = missingCredentials?.includes("url") === true && missingCredentials.includes("token") ? "both" : missingCredentials?.includes("url") === true || error.code === "SERVER_URL_REQUIRED" ? "url" : "token";
5080
5227
  dependencies.stderr(
5081
5228
  error.message + "\n\u53EF\u5728\u4E0B\u65B9\u5F55\u5165\u5E76\u5199\u5165 .harness/credentials.local.yaml\u3002\n"
5082
5229
  );
@@ -5405,8 +5552,12 @@ async function verifyWorkflowPackageIntegrity(resourcesRoot) {
5405
5552
  async function monorepoResourcesRoot() {
5406
5553
  const here = dirname4(fileURLToPath(import.meta.url));
5407
5554
  const candidates = [
5408
- join17(here, "../../../../resources"),
5409
- join17(here, "../../../../../resources")
5555
+ // TypeScript source: packages/cli/src/workflow-data -> packages/workflow-data-harness.
5556
+ join17(here, "../../../workflow-data-harness"),
5557
+ // Bundled CLI: packages/cli/dist -> packages/workflow-data-harness.
5558
+ join17(here, "../../workflow-data-harness"),
5559
+ // Test/build layouts that preserve additional source directory levels.
5560
+ join17(here, "../../../../packages/workflow-data-harness")
5410
5561
  ];
5411
5562
  for (const candidate of candidates) {
5412
5563
  if (await pathExists3(join17(candidate, "harness", "manifests"))) return candidate;
@@ -5414,9 +5565,14 @@ async function monorepoResourcesRoot() {
5414
5565
  return null;
5415
5566
  }
5416
5567
  async function siblingWorkflowPackage(cwd) {
5568
+ const here = dirname4(fileURLToPath(import.meta.url));
5417
5569
  const candidates = [
5418
5570
  join17(cwd, "node_modules", "@hunter-harness", "workflow-harness"),
5419
- join17(dirname4(fileURLToPath(import.meta.url)), "..", "..", "workflow-data-harness")
5571
+ // Published layout: node_modules/hunter-harness/dist/bin.js alongside the
5572
+ // scoped workflow package in node_modules/@hunter-harness/workflow-harness.
5573
+ join17(here, "..", "..", "@hunter-harness", "workflow-harness"),
5574
+ // Monorepo bundled layout: packages/cli/dist/bin.js.
5575
+ join17(here, "..", "..", "workflow-data-harness")
5420
5576
  ];
5421
5577
  for (const candidate of candidates) {
5422
5578
  if (await pathExists3(join17(candidate, "harness", "manifests"))) return candidate;
@@ -5537,6 +5693,33 @@ async function readWorkflowFamilyManifest(resourcesRoot) {
5537
5693
  }
5538
5694
 
5539
5695
  // src/bin.ts
5696
+ async function promptSecret(question, input = process.stdin, output = process.stdout) {
5697
+ if (input.isTTY !== true || output.isTTY !== true) {
5698
+ const terminal2 = createInterface({ input, output });
5699
+ try {
5700
+ return await terminal2.question(question);
5701
+ } finally {
5702
+ terminal2.close();
5703
+ }
5704
+ }
5705
+ output.write(question);
5706
+ const mutedOutput = new Writable({
5707
+ write(_chunk, _encoding, callback) {
5708
+ callback();
5709
+ }
5710
+ });
5711
+ const terminal = createInterface({
5712
+ input,
5713
+ output: mutedOutput,
5714
+ terminal: true
5715
+ });
5716
+ try {
5717
+ return await terminal.question("");
5718
+ } finally {
5719
+ terminal.close();
5720
+ output.write("\n");
5721
+ }
5722
+ }
5540
5723
  function defaultDependencies(overrides) {
5541
5724
  return {
5542
5725
  cwd: overrides.cwd ?? process.cwd(),
@@ -5551,6 +5734,7 @@ function defaultDependencies(overrides) {
5551
5734
  terminal.close();
5552
5735
  }
5553
5736
  }),
5737
+ promptSecret: overrides.promptSecret ?? overrides.prompt ?? promptSecret,
5554
5738
  fetch: overrides.fetch ?? globalThis.fetch,
5555
5739
  env: overrides.env ?? process.env
5556
5740
  };
@@ -5634,5 +5818,6 @@ if (isDirectCliEntrypoint()) {
5634
5818
  }
5635
5819
  export {
5636
5820
  isDirectCliEntrypoint,
5821
+ promptSecret,
5637
5822
  runCli
5638
5823
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hunter-harness",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "description": "Local-first, server-governed agent harness",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -15,7 +15,7 @@
15
15
  ],
16
16
  "scripts": {
17
17
  "build": "node ../../node_modules/typescript/bin/tsc -p tsconfig.json && npm run bundle",
18
- "bundle": "node ../../node_modules/esbuild/bin/esbuild src/bin.ts --bundle --platform=node --format=esm --target=node24 --external:commander --external:yaml --external:zod --external:pacote --outfile=dist/bin.js",
18
+ "bundle": "node ../../scripts/bundle-cli.mjs",
19
19
  "typecheck": "node ../../node_modules/typescript/bin/tsc -p tsconfig.json --noEmit",
20
20
  "prepack": "npm run build"
21
21
  },