hunter-harness 0.1.1 → 0.1.2

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/bin.js CHANGED
@@ -414,7 +414,7 @@ function normalize(value) {
414
414
  return value.map(normalize);
415
415
  }
416
416
  if (value !== null && typeof value === "object") {
417
- return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key, normalize(item)]));
417
+ return Object.fromEntries(Object.entries(value).filter(([, item2]) => item2 !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, item2]) => [key, normalize(item2)]));
418
418
  }
419
419
  return value;
420
420
  }
@@ -789,8 +789,8 @@ function normalizeManagedPath(input) {
789
789
  }
790
790
  function assertNoCaseCollisions(paths) {
791
791
  const seen = /* @__PURE__ */ new Map();
792
- for (const item of paths) {
793
- const normalized = normalizeManagedPath(item);
792
+ for (const item2 of paths) {
793
+ const normalized = normalizeManagedPath(item2);
794
794
  const folded = normalized.toLocaleLowerCase("en-US");
795
795
  const existing = seen.get(folded);
796
796
  if (existing !== void 0 && existing !== normalized) {
@@ -817,8 +817,8 @@ async function assertNoSymlinks(root, relativePath) {
817
817
  for (const segment of normalized.split("/")) {
818
818
  current = join(current, segment);
819
819
  try {
820
- const stat5 = await lstat(current);
821
- if (stat5.isSymbolicLink()) {
820
+ const stat6 = await lstat(current);
821
+ if (stat6.isSymbolicLink()) {
822
822
  throw new UnsafePathError("symbolic links are not managed");
823
823
  }
824
824
  } catch (error) {
@@ -834,7 +834,7 @@ async function assertNoSymlinks(root, relativePath) {
834
834
  async function withRetry(action, options) {
835
835
  const attempts = options.attempts ?? 3;
836
836
  const sleep = options.sleep ?? (async (milliseconds) => {
837
- await new Promise((resolve6) => setTimeout(resolve6, milliseconds));
837
+ await new Promise((resolve7) => setTimeout(resolve7, milliseconds));
838
838
  });
839
839
  let lastError;
840
840
  for (let attempt = 1; attempt <= attempts; attempt += 1) {
@@ -1094,6 +1094,17 @@ function removeManagedBlock(original) {
1094
1094
  const after = original.slice(end).replace(/^(?:\r?\n){1,2}/, "");
1095
1095
  return before + after;
1096
1096
  }
1097
+ function refreshManagedBlock(original, blockContent) {
1098
+ const starts = markerCount(original, MANAGED_BLOCK_START);
1099
+ const ends = markerCount(original, MANAGED_BLOCK_END);
1100
+ const absent = starts === 0 && ends === 0;
1101
+ const malformed = !absent && (starts !== 1 || ends !== 1 || original.indexOf(MANAGED_BLOCK_START) > original.indexOf(MANAGED_BLOCK_END));
1102
+ if (malformed) {
1103
+ return { content: original, action: "preserved_conflict", conflict: true };
1104
+ }
1105
+ const action = absent ? "appended" : "refreshed";
1106
+ return { content: upsertManagedBlock(original, blockContent), action, conflict: false };
1107
+ }
1097
1108
  function escapeRe(value) {
1098
1109
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1099
1110
  }
@@ -1250,7 +1261,7 @@ import { parse as parseYaml2, stringify as stringifyYaml } from "yaml";
1250
1261
 
1251
1262
  // ../core/dist/transaction/transaction.js
1252
1263
  import { randomUUID as randomUUID2 } from "node:crypto";
1253
- import { copyFile, mkdir as mkdir3, readFile as readFile2, rename as rename2, rm as rm2, stat, writeFile as writeFile2 } from "node:fs/promises";
1264
+ import { copyFile, mkdir as mkdir3, readFile as readFile2, readdir, rename as rename2, rm as rm2, stat, writeFile as writeFile2 } from "node:fs/promises";
1254
1265
  import { dirname as dirname2, join as join3 } from "node:path";
1255
1266
 
1256
1267
  // ../core/dist/state/layout.js
@@ -1276,9 +1287,7 @@ async function ensureStateLayout(projectRoot) {
1276
1287
  mkdir2(layout.baseline, { recursive: true }),
1277
1288
  mkdir2(layout.transactions, { recursive: true }),
1278
1289
  mkdir2(layout.locks, { recursive: true }),
1279
- mkdir2(layout.local, { recursive: true }),
1280
- mkdir2(layout.serverArtifacts, { recursive: true }),
1281
- mkdir2(layout.reports, { recursive: true })
1290
+ mkdir2(layout.local, { recursive: true })
1282
1291
  ]);
1283
1292
  return layout;
1284
1293
  }
@@ -1321,6 +1330,31 @@ async function writeJournal(transactionRoot, journal) {
1321
1330
  updated_at: (/* @__PURE__ */ new Date()).toISOString()
1322
1331
  });
1323
1332
  }
1333
+ async function pruneOlderSuccessful(layout, currentId, kind) {
1334
+ if (kind === void 0)
1335
+ return;
1336
+ let entries;
1337
+ try {
1338
+ entries = await readdir(layout.transactions);
1339
+ } catch (error) {
1340
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
1341
+ return;
1342
+ throw error;
1343
+ }
1344
+ for (const name of entries) {
1345
+ if (name === currentId)
1346
+ continue;
1347
+ const txRoot = join3(layout.transactions, name);
1348
+ try {
1349
+ const journal = JSON.parse(await readFile2(join3(txRoot, "journal.json"), "utf8"));
1350
+ if (journal.state === "committed" && journal.kind === kind) {
1351
+ await rm2(txRoot, { recursive: true, force: true });
1352
+ }
1353
+ } catch {
1354
+ continue;
1355
+ }
1356
+ }
1357
+ }
1324
1358
  async function snapshotPaths(projectRoot, transactionRoot, paths) {
1325
1359
  const snapshots = [];
1326
1360
  for (const path of paths) {
@@ -1460,7 +1494,6 @@ async function runTransaction(projectRoot, rawOperations, options = {}) {
1460
1494
  await atomicWriteJson(join3(transactionRoot, "after", "manifest.json"), after);
1461
1495
  journal.state = "committed";
1462
1496
  await writeJournal(transactionRoot, journal);
1463
- return { transactionId, status: "committed" };
1464
1497
  } catch (error) {
1465
1498
  if (error instanceof InterruptedTransactionError) {
1466
1499
  throw error;
@@ -1470,11 +1503,35 @@ async function runTransaction(projectRoot, rawOperations, options = {}) {
1470
1503
  await rollbackTransaction(projectRoot, transactionId);
1471
1504
  throw error;
1472
1505
  }
1506
+ await rm2(join3(transactionRoot, "staged"), { recursive: true, force: true });
1507
+ await pruneOlderSuccessful(layout, transactionId, options.kind);
1508
+ return { transactionId, status: "committed" };
1473
1509
  }
1474
1510
 
1511
+ // ../core/dist/project/managed-content.js
1512
+ var AGENTS_MANAGED_BLOCK_CONTENT = [
1513
+ "# Hunter Harness",
1514
+ "",
1515
+ "Use .harness/context-index.json to route rules, Knowledge, and codebase maps.",
1516
+ "Treat .claude/skills/harness-* as editable adapter working copies.",
1517
+ "Do not modify .harness/state or .harness/cache directly."
1518
+ ].join("\n");
1519
+ var CLAUDE_MANAGED_BLOCK_CONTENT = [
1520
+ "@AGENTS.md",
1521
+ "",
1522
+ "# Hunter Harness",
1523
+ "",
1524
+ "- Rules: .claude/rules/",
1525
+ "- Skills: .claude/skills/harness-*/",
1526
+ "- Knowledge: .harness/knowledge/",
1527
+ "- Codebase map: .harness/codebase/map/"
1528
+ ].join("\n");
1529
+ var HARNESS_GENERAL_RULES_CONTENT = "# Hunter Harness Rules\n\n- Report evidence honestly.\n- Do not execute destructive actions without confirmation.\n";
1530
+ var HARNESS_JAVA_RULES_CONTENT = "# Java Profile\n\n- Verify builds and tests with the project build tool.\n";
1531
+
1475
1532
  // ../core/dist/project/profile-bundle.js
1476
1533
  import { createHash as createHash2 } from "node:crypto";
1477
- import { readFile as readFile3 } from "node:fs/promises";
1534
+ import { readFile as readFile3, readdir as readdir2 } from "node:fs/promises";
1478
1535
  import { join as join4 } from "node:path";
1479
1536
  function isProfile(value) {
1480
1537
  return value === "general" || value === "java";
@@ -1490,16 +1547,16 @@ async function loadProfileBundle(resourcesRoot, profile) {
1490
1547
  throw new Error(`invalid ${profile} Harness Bundle manifest`);
1491
1548
  }
1492
1549
  const files = /* @__PURE__ */ new Map();
1493
- for (const item of raw.files) {
1494
- validateRelativeBundlePath(item.path);
1495
- if (typeof item.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(item.sha256) || files.has(item.path)) {
1550
+ for (const item2 of raw.files) {
1551
+ validateRelativeBundlePath(item2.path);
1552
+ if (typeof item2.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(item2.sha256) || files.has(item2.path)) {
1496
1553
  throw new Error(`invalid ${profile} Harness Bundle manifest entry`);
1497
1554
  }
1498
- const bytes = await readFile3(join4(resourcesRoot, "harness", profile, item.path));
1499
- if (createHash2("sha256").update(bytes).digest("hex") !== item.sha256) {
1500
- throw new Error(`Harness Bundle hash mismatch: ${item.path}`);
1555
+ const bytes = await readFile3(join4(resourcesRoot, "harness", profile, item2.path));
1556
+ if (createHash2("sha256").update(bytes).digest("hex") !== item2.sha256) {
1557
+ throw new Error(`Harness Bundle hash mismatch: ${item2.path}`);
1501
1558
  }
1502
- files.set(item.path, bytes);
1559
+ files.set(item2.path, bytes);
1503
1560
  }
1504
1561
  return { manifest: raw, files };
1505
1562
  }
@@ -1510,22 +1567,123 @@ async function managedBundleTargets(resourcesRoot, profile) {
1510
1567
  targets.add(".claude/rules/harness-profile-java.md");
1511
1568
  return targets;
1512
1569
  }
1513
- function bundleTargetContents(bundle) {
1514
- const contents = /* @__PURE__ */ new Map();
1515
- for (const [path, bytes] of bundle.files) {
1516
- const skillTarget = `.claude/skills/${path}`;
1517
- contents.set(skillTarget, bytes);
1518
- const agent = /^agents\/([^/]+\.md)$/.exec(path);
1519
- if (agent?.[1] !== void 0) {
1520
- const agentTarget = `.claude/agents/${agent[1]}`;
1521
- contents.set(agentTarget, bytes);
1570
+ var AGENT_SOURCE_PATH = /^agents\/([^/]+\.md)$/;
1571
+ function projectBundle(bundle) {
1572
+ const records = [];
1573
+ for (const [sourcePath, bytes] of bundle.files) {
1574
+ validateRelativeBundlePath(sourcePath);
1575
+ const agent = AGENT_SOURCE_PATH.exec(sourcePath);
1576
+ const projectedTarget = agent?.[1] !== void 0 ? `.claude/agents/${agent[1]}` : `.claude/skills/${sourcePath}`;
1577
+ const manifestEntry = bundle.manifest.files.find((entry) => entry.path === sourcePath);
1578
+ if (manifestEntry === void 0) {
1579
+ throw new Error(`Harness Bundle missing manifest entry: ${sourcePath}`);
1522
1580
  }
1581
+ records.push({
1582
+ source_path: sourcePath,
1583
+ target_path: normalizeManagedPath(projectedTarget),
1584
+ sha256: manifestEntry.sha256,
1585
+ bytes
1586
+ });
1523
1587
  }
1524
- return contents;
1588
+ assertNoCaseCollisions(records.map((record) => record.target_path));
1589
+ return records.sort((left, right) => left.target_path.localeCompare(right.target_path));
1590
+ }
1591
+ function bundleTargetContents(bundle) {
1592
+ return new Map(projectBundle(bundle).map((record) => [record.target_path, record.bytes]));
1593
+ }
1594
+ function ruleTarget(sourcePath, targetPath, content) {
1595
+ return {
1596
+ source_path: sourcePath,
1597
+ target_path: targetPath,
1598
+ sha256: createHash2("sha256").update(content).digest("hex"),
1599
+ bytes: new TextEncoder().encode(content)
1600
+ };
1601
+ }
1602
+ async function managedTargets(resourcesRoot, profile) {
1603
+ const bundle = await loadProfileBundle(resourcesRoot, profile);
1604
+ const records = projectBundle(bundle).map((record) => ({
1605
+ source_path: record.source_path,
1606
+ target_path: record.target_path,
1607
+ sha256: record.sha256,
1608
+ bytes: record.bytes
1609
+ }));
1610
+ records.push(ruleTarget("rules/harness-general.md", ".claude/rules/harness-general.md", HARNESS_GENERAL_RULES_CONTENT));
1611
+ if (profile === "java") {
1612
+ records.push(ruleTarget("rules/harness-profile-java.md", ".claude/rules/harness-profile-java.md", HARNESS_JAVA_RULES_CONTENT));
1613
+ }
1614
+ assertNoCaseCollisions(records.map((record) => record.target_path));
1615
+ return records.sort((left, right) => left.target_path.localeCompare(right.target_path));
1525
1616
  }
1526
1617
  function parseHarnessProfile(value) {
1527
1618
  return isProfile(value) ? value : null;
1528
1619
  }
1620
+ function parseMigrationManifest(raw) {
1621
+ if (raw === null || typeof raw !== "object") {
1622
+ throw new Error("invalid Harness migration manifest");
1623
+ }
1624
+ const record = raw;
1625
+ if (record.schema_version !== 1 || !isProfile(record.profile) || typeof record.bundle_version !== "string" || !/^sha256:[a-f0-9]{64}$/.test(String(record.bundle_manifest_hash)) || !Array.isArray(record.projection)) {
1626
+ throw new Error("invalid Harness migration manifest");
1627
+ }
1628
+ const projection = [];
1629
+ for (const entry of record.projection) {
1630
+ if (entry === null || typeof entry !== "object") {
1631
+ throw new Error("invalid Harness migration manifest entry");
1632
+ }
1633
+ const item2 = entry;
1634
+ validateRelativeBundlePath(item2.source_path);
1635
+ if (typeof item2.target_path !== "string" || typeof item2.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(item2.sha256)) {
1636
+ throw new Error("invalid Harness migration manifest entry");
1637
+ }
1638
+ const targetPath = normalizeManagedPath(item2.target_path);
1639
+ if (!targetPath.startsWith(".claude/")) {
1640
+ throw new Error("invalid Harness migration manifest target");
1641
+ }
1642
+ projection.push({
1643
+ source_path: item2.source_path,
1644
+ target_path: targetPath,
1645
+ sha256: item2.sha256
1646
+ });
1647
+ }
1648
+ assertNoCaseCollisions(projection.map((item2) => item2.target_path));
1649
+ return {
1650
+ schema_version: 1,
1651
+ profile: record.profile,
1652
+ bundle_version: record.bundle_version,
1653
+ bundle_manifest_hash: record.bundle_manifest_hash,
1654
+ projection
1655
+ };
1656
+ }
1657
+ async function loadMigrationManifests(resourcesRoot) {
1658
+ const migrationsRoot = join4(resourcesRoot, "harness", "migrations");
1659
+ let versionDirs;
1660
+ try {
1661
+ versionDirs = await readdir2(migrationsRoot);
1662
+ } catch (error) {
1663
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
1664
+ return [];
1665
+ }
1666
+ throw error;
1667
+ }
1668
+ const manifests = [];
1669
+ for (const versionDir of versionDirs) {
1670
+ let files;
1671
+ try {
1672
+ files = await readdir2(join4(migrationsRoot, versionDir));
1673
+ } catch (error) {
1674
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
1675
+ continue;
1676
+ }
1677
+ throw error;
1678
+ }
1679
+ for (const file of files) {
1680
+ if (!file.endsWith(".json"))
1681
+ continue;
1682
+ manifests.push(parseMigrationManifest(JSON.parse(await readFile3(join4(migrationsRoot, versionDir, file), "utf8"))));
1683
+ }
1684
+ }
1685
+ return manifests;
1686
+ }
1529
1687
 
1530
1688
  // ../core/dist/project/uuid-v7.js
1531
1689
  import { randomBytes } from "node:crypto";
@@ -1580,24 +1738,13 @@ async function operationFor(root, path, content) {
1580
1738
  return await exists2(join5(root, path)) ? { operation: "modify", path, content } : { operation: "add", path, content };
1581
1739
  }
1582
1740
  var INSTALLED_BUNDLE_PATH = ".harness/state/local/installed-harness-bundle.json";
1583
- async function previousInstalledProfile(root) {
1584
- const content = await readOptional(join5(root, INSTALLED_BUNDLE_PATH));
1585
- if (content === "")
1586
- return null;
1587
- try {
1588
- const parsed = JSON.parse(content);
1589
- return parsed.schema_version === 1 ? parseHarnessProfile(parsed.profile) : null;
1590
- } catch {
1591
- return null;
1592
- }
1593
- }
1594
1741
  async function initializeProject(options) {
1595
1742
  const root = resolve3(options.projectRoot);
1596
1743
  const config = initConfigSchema.parse(options.config);
1597
1744
  const existing = await existingProjectConfig(root);
1598
1745
  const profile = config.profile;
1599
1746
  const bundle = await loadProfileBundle(options.resourcesRoot, profile);
1600
- const bundleFiles = bundleTargetContents(bundle);
1747
+ const managed = await managedTargets(options.resourcesRoot, profile);
1601
1748
  const bundleHash = sha256Bytes(canonicalJson(bundle.manifest.files));
1602
1749
  const projectConfig = projectConfigSchema.parse({
1603
1750
  harness: { name: "hunter-harness", schema_version: 1 },
@@ -1621,23 +1768,8 @@ async function initializeProject(options) {
1621
1768
  artifact_manifest_hash: null,
1622
1769
  files: {}
1623
1770
  });
1624
- const agentsContent = upsertManagedBlock(await readOptional(join5(root, "AGENTS.md")), [
1625
- "# Hunter Harness",
1626
- "",
1627
- "Use .harness/context-index.json to route rules, Knowledge, and codebase maps.",
1628
- "Treat .claude/skills/harness-* as editable adapter working copies.",
1629
- "Do not modify .harness/state or .harness/cache directly."
1630
- ].join("\n"));
1631
- const claudeContent = upsertManagedBlock(await readOptional(join5(root, "CLAUDE.md")), [
1632
- "@AGENTS.md",
1633
- "",
1634
- "# Hunter Harness",
1635
- "",
1636
- "- Rules: .claude/rules/",
1637
- "- Skills: .claude/skills/harness-*/",
1638
- "- Knowledge: .harness/knowledge/",
1639
- "- Codebase map: .harness/codebase/map/"
1640
- ].join("\n"));
1771
+ const agentsContent = upsertManagedBlock(await readOptional(join5(root, "AGENTS.md")), AGENTS_MANAGED_BLOCK_CONTENT);
1772
+ const claudeContent = upsertManagedBlock(await readOptional(join5(root, "CLAUDE.md")), CLAUDE_MANAGED_BLOCK_CONTENT);
1641
1773
  const files = /* @__PURE__ */ new Map([
1642
1774
  [
1643
1775
  ".harness/project.yaml",
@@ -1665,46 +1797,29 @@ async function initializeProject(options) {
1665
1797
  ".harness/knowledge/index.json",
1666
1798
  JSON.stringify({ schema_version: 1, generated_at: null, entries: [] }, null, 2) + "\n"
1667
1799
  ],
1668
- [".harness/knowledge/_candidates/.gitkeep", ""],
1669
- [".harness/knowledge/project-local/.gitkeep", ""],
1670
- [".harness/codebase/map/.gitkeep", ""],
1671
- [".harness/state/local/.gitkeep", ""],
1672
- [".harness/cache/server-artifacts/.gitkeep", ""],
1673
- [".harness/reports/.gitkeep", ""],
1674
- [
1675
- ".harness/README.md",
1676
- "# Hunter Harness\n\nUse npx hunter-harness, update, and push.\n"
1677
- ],
1678
1800
  ["AGENTS.md", agentsContent],
1679
- ["CLAUDE.md", claudeContent],
1680
- [
1681
- ".claude/rules/harness-general.md",
1682
- "# Hunter Harness Rules\n\n- Report evidence honestly.\n- Do not execute destructive actions without confirmation.\n"
1683
- ]
1801
+ ["CLAUDE.md", claudeContent]
1684
1802
  ]);
1685
- if (config.profile === "java") {
1686
- files.set(".claude/rules/harness-profile-java.md", "# Java Profile\n\n- Verify builds and tests with the project build tool.\n");
1687
- }
1688
- for (const [targetPath, bytes] of bundleFiles) {
1689
- files.set(targetPath, bytes);
1803
+ for (const target of managed) {
1804
+ files.set(target.target_path, target.bytes);
1690
1805
  }
1691
- const installedFileList = [...bundleFiles.keys()].sort();
1692
- if (profile === "java")
1693
- installedFileList.push(".claude/rules/harness-profile-java.md");
1694
- const installedManifest = {
1695
- schema_version: 1,
1806
+ const installedState = {
1807
+ schema_version: 2,
1696
1808
  profile,
1697
- files: installedFileList.sort()
1809
+ bundle_version: bundle.manifest.bundle_version,
1810
+ bundle_manifest_hash: bundleHash,
1811
+ installed_at: (/* @__PURE__ */ new Date()).toISOString(),
1812
+ files: managed.map((target) => ({
1813
+ source_path: target.source_path,
1814
+ target_path: target.target_path,
1815
+ sha256: target.sha256
1816
+ })).sort((left, right) => left.target_path.localeCompare(right.target_path))
1698
1817
  };
1699
- files.set(INSTALLED_BUNDLE_PATH, JSON.stringify(installedManifest, null, 2) + "\n");
1700
- const newManaged = await managedBundleTargets(options.resourcesRoot, profile);
1701
- const oldProfile = await previousInstalledProfile(root);
1702
- const oldManaged = oldProfile === null ? /* @__PURE__ */ new Set() : await managedBundleTargets(options.resourcesRoot, oldProfile);
1703
- const deleteOperations = [...oldManaged].filter((path) => !newManaged.has(path)).map((path) => ({ operation: "delete", path }));
1818
+ files.set(INSTALLED_BUNDLE_PATH, JSON.stringify(installedState, null, 2) + "\n");
1704
1819
  const paths = [...files.keys()].sort((left, right) => left.localeCompare(right));
1705
1820
  if (!options.dryRun) {
1706
1821
  const writeOperations = await Promise.all([...files.entries()].map(async ([path, content]) => operationFor(root, path, content)));
1707
- await runTransaction(root, [...deleteOperations, ...writeOperations], { kind: "init" });
1822
+ await runTransaction(root, writeOperations, { kind: "init" });
1708
1823
  }
1709
1824
  return {
1710
1825
  projectConfig,
@@ -1714,6 +1829,325 @@ async function initializeProject(options) {
1714
1829
  };
1715
1830
  }
1716
1831
 
1832
+ // ../core/dist/project/refresh.js
1833
+ import { createHash as createHash3 } from "node:crypto";
1834
+ import { readFile as readFile5, readdir as readdir3, rmdir, stat as stat3 } from "node:fs/promises";
1835
+ import { dirname as dirname3, join as join6, resolve as resolve4 } from "node:path";
1836
+ import { parse as parseYaml3, stringify as stringifyYaml2 } from "yaml";
1837
+ var INSTALLED_STATE_PATH = ".harness/state/local/installed-harness-bundle.json";
1838
+ var CONTEXT_INDEX_PATH = ".harness/context-index.json";
1839
+ async function exists3(path) {
1840
+ try {
1841
+ await stat3(path);
1842
+ return true;
1843
+ } catch (error) {
1844
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
1845
+ return false;
1846
+ }
1847
+ throw error;
1848
+ }
1849
+ }
1850
+ async function fileHex(path) {
1851
+ try {
1852
+ return createHash3("sha256").update(await readFile5(path)).digest("hex");
1853
+ } catch (error) {
1854
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
1855
+ return null;
1856
+ }
1857
+ throw error;
1858
+ }
1859
+ }
1860
+ async function readOptionalText(path) {
1861
+ try {
1862
+ return await readFile5(path, "utf8");
1863
+ } catch (error) {
1864
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
1865
+ return "";
1866
+ }
1867
+ throw error;
1868
+ }
1869
+ }
1870
+ async function readInstalledState(root) {
1871
+ const content = await readOptionalText(join6(root, INSTALLED_STATE_PATH));
1872
+ if (content === "") {
1873
+ return { profile: null, schemaVersion: null, trusted: /* @__PURE__ */ new Map() };
1874
+ }
1875
+ let parsed;
1876
+ try {
1877
+ parsed = JSON.parse(content);
1878
+ } catch {
1879
+ return { profile: null, schemaVersion: null, trusted: /* @__PURE__ */ new Map() };
1880
+ }
1881
+ const profile = parseHarnessProfile(parsed.profile);
1882
+ const trusted = /* @__PURE__ */ new Map();
1883
+ if (parsed.schema_version === 2 && Array.isArray(parsed.files)) {
1884
+ for (const entry of parsed.files) {
1885
+ if (entry !== null && typeof entry === "object" && "target_path" in entry && "sha256" in entry) {
1886
+ const target = entry.target_path;
1887
+ const sha = entry.sha256;
1888
+ if (typeof target === "string" && typeof sha === "string") {
1889
+ trusted.set(target, sha);
1890
+ }
1891
+ }
1892
+ }
1893
+ }
1894
+ return { profile, schemaVersion: typeof parsed.schema_version === "number" ? parsed.schema_version : null, trusted };
1895
+ }
1896
+ async function readContextIndexBundleHash(root) {
1897
+ const content = await readOptionalText(join6(root, CONTEXT_INDEX_PATH));
1898
+ if (content === "")
1899
+ return null;
1900
+ try {
1901
+ const record = JSON.parse(content);
1902
+ const hash = record.skill_bundle?.bundle_hash;
1903
+ return typeof hash === "string" ? hash : null;
1904
+ } catch {
1905
+ return null;
1906
+ }
1907
+ }
1908
+ async function pruneEmptyParentDirs(root, deletedPaths) {
1909
+ const claudeRoot = join6(root, ".claude");
1910
+ const boundaries = /* @__PURE__ */ new Set([
1911
+ claudeRoot,
1912
+ join6(claudeRoot, "skills"),
1913
+ join6(claudeRoot, "agents")
1914
+ ]);
1915
+ for (const deleted of deletedPaths) {
1916
+ let dir = dirname3(join6(root, deleted));
1917
+ while (dir.startsWith(claudeRoot) && !boundaries.has(dir)) {
1918
+ let entries;
1919
+ try {
1920
+ entries = await readdir3(dir);
1921
+ } catch {
1922
+ break;
1923
+ }
1924
+ if (entries.length > 0)
1925
+ break;
1926
+ try {
1927
+ await rmdir(dir);
1928
+ } catch {
1929
+ break;
1930
+ }
1931
+ dir = dirname3(dir);
1932
+ }
1933
+ }
1934
+ }
1935
+ function item(target, action, reason, oldSha, incomingSha) {
1936
+ return {
1937
+ source_path: target.source_path,
1938
+ target_path: target.target_path,
1939
+ action,
1940
+ reason,
1941
+ old_sha256: oldSha,
1942
+ incoming_sha256: incomingSha
1943
+ };
1944
+ }
1945
+ function conflict(target, reason, oldSha, incomingSha) {
1946
+ return {
1947
+ source_path: target.source_path,
1948
+ target_path: target.target_path,
1949
+ reason,
1950
+ old_sha256: oldSha,
1951
+ incoming_sha256: incomingSha
1952
+ };
1953
+ }
1954
+ function sortByTarget(items) {
1955
+ return [...items].sort((left, right) => left.target_path.localeCompare(right.target_path));
1956
+ }
1957
+ async function refreshMarkdownBlock(root, fileName, blockContent, ops, conflicts, preserved) {
1958
+ const original = await readOptionalText(join6(root, fileName));
1959
+ const current = original === "" ? null : createHash3("sha256").update(original).digest("hex");
1960
+ const refresh = refreshManagedBlock(original, blockContent);
1961
+ const synthetic = {
1962
+ source_path: fileName,
1963
+ target_path: fileName,
1964
+ sha256: createHash3("sha256").update(blockContent).digest("hex"),
1965
+ bytes: new TextEncoder().encode(blockContent)
1966
+ };
1967
+ if (refresh.conflict) {
1968
+ preserved.push(item(synthetic, "preserve", "MALFORMED_MANAGED_BLOCK", current, synthetic.sha256));
1969
+ conflicts.push(conflict(synthetic, "MALFORMED_MANAGED_BLOCK", current, synthetic.sha256));
1970
+ return;
1971
+ }
1972
+ if (refresh.content === original) {
1973
+ return;
1974
+ }
1975
+ ops.push({
1976
+ operation: original === "" ? "add" : "modify",
1977
+ path: fileName,
1978
+ content: refresh.content
1979
+ });
1980
+ }
1981
+ async function reconcileContextIndex(root, bundleVersion, bundleManifestHash) {
1982
+ const existing = await readOptionalText(join6(root, CONTEXT_INDEX_PATH));
1983
+ let record = {};
1984
+ if (existing !== "") {
1985
+ try {
1986
+ record = JSON.parse(existing);
1987
+ } catch {
1988
+ record = {};
1989
+ }
1990
+ }
1991
+ if (Object.keys(record).length === 0) {
1992
+ record = {
1993
+ schema_version: 1,
1994
+ project: { claude_md: "CLAUDE.md", agents_md: "AGENTS.md" },
1995
+ rules: [".claude/rules/harness-general.md"],
1996
+ knowledge: { index: ".harness/knowledge/index.json" },
1997
+ codebase: { map: ".harness/codebase/map", status: "missing" }
1998
+ };
1999
+ }
2000
+ record.skill_bundle = {
2001
+ registry_version: bundleVersion,
2002
+ bundle_hash: bundleManifestHash
2003
+ };
2004
+ return {
2005
+ operation: existing === "" ? "add" : "modify",
2006
+ path: CONTEXT_INDEX_PATH,
2007
+ content: JSON.stringify(record, null, 2) + "\n"
2008
+ };
2009
+ }
2010
+ async function profileTransitionOperation(root, previousProfile, profile) {
2011
+ if (previousProfile === null || previousProfile === profile)
2012
+ return null;
2013
+ const path = ".harness/project.yaml";
2014
+ const content = await readOptionalText(join6(root, path));
2015
+ const project = projectConfigSchema.parse(parseYaml3(content));
2016
+ return {
2017
+ operation: "modify",
2018
+ path,
2019
+ content: stringifyYaml2({
2020
+ ...project,
2021
+ project: { ...project.project, profiles: [profile] }
2022
+ }, { sortMapEntries: true })
2023
+ };
2024
+ }
2025
+ async function refreshProject(options) {
2026
+ const root = resolve4(options.projectRoot);
2027
+ const profile = options.profile;
2028
+ const newManaged = await managedTargets(options.resourcesRoot, profile);
2029
+ const newBundle = await loadProfileBundle(options.resourcesRoot, profile);
2030
+ const bundleManifestHash = sha256Bytes(canonicalJson(newBundle.manifest.files));
2031
+ const installed = await readInstalledState(root);
2032
+ const previousProfile = installed.profile;
2033
+ let trusted = installed.trusted;
2034
+ let migrationOldPaths = null;
2035
+ if (installed.schemaVersion === 1) {
2036
+ const contextHash = await readContextIndexBundleHash(root);
2037
+ if (contextHash !== null) {
2038
+ const migrations = await loadMigrationManifests(options.resourcesRoot);
2039
+ const match = migrations.find((m) => m.bundle_manifest_hash === contextHash && m.profile === installed.profile);
2040
+ if (match !== void 0) {
2041
+ trusted = new Map(match.projection.map((entry) => [entry.target_path, entry.sha256]));
2042
+ migrationOldPaths = new Set(match.projection.map((entry) => entry.target_path));
2043
+ }
2044
+ }
2045
+ }
2046
+ const newTargetSet = new Set(newManaged.map((target) => target.target_path));
2047
+ let oldOnly = [];
2048
+ if (migrationOldPaths !== null) {
2049
+ for (const targetPath of migrationOldPaths) {
2050
+ if (!newTargetSet.has(targetPath)) {
2051
+ oldOnly.push({
2052
+ source_path: targetPath,
2053
+ target_path: targetPath,
2054
+ sha256: trusted.get(targetPath) ?? "",
2055
+ bytes: new Uint8Array()
2056
+ });
2057
+ }
2058
+ }
2059
+ } else if (previousProfile !== null && previousProfile !== profile) {
2060
+ const oldManaged = await managedTargets(options.resourcesRoot, previousProfile);
2061
+ oldOnly = oldManaged.filter((target) => !newTargetSet.has(target.target_path));
2062
+ }
2063
+ const applied = [];
2064
+ const removed = [];
2065
+ const preserved = [];
2066
+ const unchanged = [];
2067
+ const conflicts = [];
2068
+ const ops = [];
2069
+ const newStateFiles = [];
2070
+ for (const target of newManaged) {
2071
+ const incoming = target.sha256;
2072
+ const current = await fileHex(join6(root, target.target_path));
2073
+ if (current === null) {
2074
+ applied.push(item(target, "add", "MISSING_TARGET", null, incoming));
2075
+ ops.push({ operation: "add", path: target.target_path, content: target.bytes });
2076
+ newStateFiles.push({ source_path: target.source_path, target_path: target.target_path, sha256: incoming });
2077
+ continue;
2078
+ }
2079
+ if (current === incoming) {
2080
+ unchanged.push(item(target, "unchanged", "ALREADY_CURRENT", current, incoming));
2081
+ newStateFiles.push({ source_path: target.source_path, target_path: target.target_path, sha256: incoming });
2082
+ continue;
2083
+ }
2084
+ const trustedHash = trusted.get(target.target_path);
2085
+ if (trustedHash !== void 0 && current === trustedHash || options.forceManaged) {
2086
+ const reason = options.forceManaged ? "FORCE_MANAGED" : "BASELINE_CLEAN";
2087
+ applied.push(item(target, "replace", reason, current, incoming));
2088
+ ops.push({ operation: "modify", path: target.target_path, content: target.bytes });
2089
+ newStateFiles.push({ source_path: target.source_path, target_path: target.target_path, sha256: incoming });
2090
+ } else {
2091
+ const reason = trustedHash === void 0 ? "LEGACY_BASELINE_UNKNOWN" : "LOCAL_MODIFICATION";
2092
+ preserved.push(item(target, "preserve", reason, current, incoming));
2093
+ conflicts.push(conflict(target, reason, current, incoming));
2094
+ if (trustedHash !== void 0) {
2095
+ newStateFiles.push({ source_path: target.source_path, target_path: target.target_path, sha256: trustedHash });
2096
+ }
2097
+ }
2098
+ }
2099
+ for (const target of oldOnly) {
2100
+ const current = await fileHex(join6(root, target.target_path));
2101
+ if (current === null) {
2102
+ continue;
2103
+ }
2104
+ const trustedHash = trusted.get(target.target_path);
2105
+ const clean = trustedHash !== void 0 && current === trustedHash;
2106
+ if (clean || options.forceManaged) {
2107
+ const reason = clean ? "BASELINE_CLEAN" : "FORCE_MANAGED";
2108
+ removed.push(item(target, "delete", reason, current, null));
2109
+ ops.push({ operation: "delete", path: target.target_path });
2110
+ } else {
2111
+ const reason = trustedHash === void 0 ? "LEGACY_BASELINE_UNKNOWN" : "LEGACY_PROFILE_FILE_MODIFIED";
2112
+ preserved.push(item(target, "preserve", reason, current, null));
2113
+ conflicts.push(conflict(target, reason, current, null));
2114
+ }
2115
+ }
2116
+ await refreshMarkdownBlock(root, "AGENTS.md", AGENTS_MANAGED_BLOCK_CONTENT, ops, conflicts, preserved);
2117
+ await refreshMarkdownBlock(root, "CLAUDE.md", CLAUDE_MANAGED_BLOCK_CONTENT, ops, conflicts, preserved);
2118
+ const profileOperation = await profileTransitionOperation(root, previousProfile, profile);
2119
+ if (profileOperation !== null)
2120
+ ops.push(profileOperation);
2121
+ ops.push(await reconcileContextIndex(root, newBundle.manifest.bundle_version, bundleManifestHash));
2122
+ const installedState = {
2123
+ schema_version: 2,
2124
+ profile,
2125
+ bundle_version: newBundle.manifest.bundle_version,
2126
+ bundle_manifest_hash: bundleManifestHash,
2127
+ installed_at: (/* @__PURE__ */ new Date()).toISOString(),
2128
+ files: newStateFiles.sort((left, right) => left.target_path.localeCompare(right.target_path))
2129
+ };
2130
+ ops.push({
2131
+ operation: await exists3(join6(root, INSTALLED_STATE_PATH)) ? "modify" : "add",
2132
+ path: INSTALLED_STATE_PATH,
2133
+ content: JSON.stringify(installedState, null, 2) + "\n"
2134
+ });
2135
+ if (!options.dryRun) {
2136
+ await runTransaction(root, ops, { kind: "refresh" });
2137
+ await pruneEmptyParentDirs(root, removed.map((item2) => item2.target_path));
2138
+ }
2139
+ return {
2140
+ profile,
2141
+ previous_profile: previousProfile,
2142
+ dry_run: options.dryRun,
2143
+ applied: sortByTarget(applied),
2144
+ removed: sortByTarget(removed),
2145
+ preserved: sortByTarget(preserved),
2146
+ unchanged: sortByTarget(unchanged),
2147
+ conflicts: sortByTarget(conflicts)
2148
+ };
2149
+ }
2150
+
1717
2151
  // ../core/dist/proposal/diff.js
1718
2152
  function operationPath(operation) {
1719
2153
  return operation.operation === "rename" ? operation.to_path : operation.path;
@@ -1857,7 +2291,7 @@ function highEntropyCandidates(content) {
1857
2291
  value: match[0],
1858
2292
  offset: match.index,
1859
2293
  entropy: shannonEntropy(match[0])
1860
- })).filter((item) => item.entropy >= 4.5);
2294
+ })).filter((item2) => item2.entropy >= 4.5);
1861
2295
  }
1862
2296
 
1863
2297
  // ../core/dist/security/scanner.js
@@ -1953,8 +2387,8 @@ function scanSensitiveFiles(files, options = {}) {
1953
2387
  sha256Bytes(raw.value)
1954
2388
  ].join("\0"));
1955
2389
  const overridable = raw.severity !== "high";
1956
- const explicit = options.overrides?.find((item) => item.finding_fingerprint === fingerprint);
1957
- const inline = ignores.find((item) => item.ruleId === raw.ruleId);
2390
+ const explicit = options.overrides?.find((item2) => item2.finding_fingerprint === fingerprint);
2391
+ const inline = ignores.find((item2) => item2.ruleId === raw.ruleId);
1958
2392
  const override = overridable ? explicit ?? (inline === void 0 ? void 0 : {
1959
2393
  finding_fingerprint: fingerprint,
1960
2394
  actor: "inline-annotation",
@@ -2012,28 +2446,28 @@ function generateProposalPreview(input, scanOptions = {}) {
2012
2446
  }
2013
2447
 
2014
2448
  // ../core/dist/push/push.js
2015
- import { lstat as lstat2, readFile as readFile7, readdir, rm as rm4 } from "node:fs/promises";
2016
- import { join as join8, relative, resolve as resolve4 } from "node:path";
2017
- import { parse as parseYaml3, stringify as stringifyYaml2 } from "yaml";
2449
+ import { lstat as lstat2, readFile as readFile8, readdir as readdir4, rm as rm4 } from "node:fs/promises";
2450
+ import { join as join9, relative, resolve as resolve5 } from "node:path";
2451
+ import { parse as parseYaml4, stringify as stringifyYaml3 } from "yaml";
2018
2452
 
2019
2453
  // ../core/dist/state/baseline.js
2020
- import { readFile as readFile5 } from "node:fs/promises";
2021
- import { join as join6 } from "node:path";
2454
+ import { readFile as readFile6 } from "node:fs/promises";
2455
+ import { join as join7 } from "node:path";
2022
2456
  async function readBaseline(projectRoot) {
2023
- const content = await readFile5(join6(stateLayout(projectRoot).baseline, "manifest.json"), "utf8");
2457
+ const content = await readFile6(join7(stateLayout(projectRoot).baseline, "manifest.json"), "utf8");
2024
2458
  return baselineManifestSchema.parse(JSON.parse(content));
2025
2459
  }
2026
2460
 
2027
2461
  // ../core/dist/state/locks.js
2028
2462
  import { randomUUID as randomUUID3 } from "node:crypto";
2029
- import { readFile as readFile6, rename as rename3, rm as rm3, writeFile as writeFile3 } from "node:fs/promises";
2030
- import { join as join7 } from "node:path";
2463
+ import { readFile as readFile7, rename as rename3, rm as rm3, writeFile as writeFile3 } from "node:fs/promises";
2464
+ import { join as join8 } from "node:path";
2031
2465
  async function readLock(path) {
2032
- return JSON.parse(await readFile6(path, "utf8"));
2466
+ return JSON.parse(await readFile7(path, "utf8"));
2033
2467
  }
2034
2468
  async function acquireProtocolLock(projectRoot, operation, options = {}) {
2035
2469
  const layout = await ensureStateLayout(projectRoot);
2036
- const lockPath = join7(layout.locks, "protocol.lock");
2470
+ const lockPath = join8(layout.locks, "protocol.lock");
2037
2471
  const now = options.now ?? Date.now();
2038
2472
  const staleAfterMs = options.staleAfterMs ?? 15 * 60 * 1e3;
2039
2473
  const record = {
@@ -2096,7 +2530,7 @@ var MANAGED_FILES = [
2096
2530
  ".harness/project.yaml",
2097
2531
  ".harness/context-index.json"
2098
2532
  ];
2099
- async function exists3(path) {
2533
+ async function exists4(path) {
2100
2534
  try {
2101
2535
  await lstat2(path);
2102
2536
  return true;
@@ -2108,43 +2542,43 @@ async function exists3(path) {
2108
2542
  }
2109
2543
  }
2110
2544
  async function walkFiles(root, current, output) {
2111
- if (!await exists3(current)) {
2545
+ if (!await exists4(current)) {
2112
2546
  return;
2113
2547
  }
2114
- for (const item of await readdir(current, { withFileTypes: true })) {
2115
- const path = join8(current, item.name);
2116
- if (item.isSymbolicLink()) {
2548
+ for (const item2 of await readdir4(current, { withFileTypes: true })) {
2549
+ const path = join9(current, item2.name);
2550
+ if (item2.isSymbolicLink()) {
2117
2551
  throw new PushWorkflowError("symlink is not pushable", 6, "UNSAFE_SYMLINK");
2118
2552
  }
2119
- if (item.isDirectory()) {
2553
+ if (item2.isDirectory()) {
2120
2554
  await walkFiles(root, path, output);
2121
- } else if (item.isFile()) {
2555
+ } else if (item2.isFile()) {
2122
2556
  output.push(normalizeManagedPath(relative(root, path).replaceAll("\\", "/")));
2123
2557
  }
2124
2558
  }
2125
2559
  }
2126
2560
  async function managedFiles(projectRoot) {
2127
- const root = resolve4(projectRoot);
2561
+ const root = resolve5(projectRoot);
2128
2562
  const paths = [];
2129
2563
  for (const path of MANAGED_FILES) {
2130
- if (await exists3(join8(root, path))) {
2564
+ if (await exists4(join9(root, path))) {
2131
2565
  paths.push(path);
2132
2566
  }
2133
2567
  }
2134
2568
  for (const path of MANAGED_ROOTS) {
2135
- await walkFiles(root, join8(root, path), paths);
2569
+ await walkFiles(root, join9(root, path), paths);
2136
2570
  }
2137
- const skillsRoot = join8(root, ".claude", "skills");
2138
- if (await exists3(skillsRoot)) {
2139
- for (const item of await readdir(skillsRoot, { withFileTypes: true })) {
2140
- if (item.isDirectory() && item.name.startsWith("harness-")) {
2141
- await walkFiles(root, join8(skillsRoot, item.name), paths);
2571
+ const skillsRoot = join9(root, ".claude", "skills");
2572
+ if (await exists4(skillsRoot)) {
2573
+ for (const item2 of await readdir4(skillsRoot, { withFileTypes: true })) {
2574
+ if (item2.isDirectory() && item2.name.startsWith("harness-")) {
2575
+ await walkFiles(root, join9(skillsRoot, item2.name), paths);
2142
2576
  }
2143
2577
  }
2144
2578
  }
2145
2579
  const result = {};
2146
2580
  for (const path of [...new Set(paths)].sort()) {
2147
- result[path] = await readFile7(join8(root, path), "utf8");
2581
+ result[path] = await readFile8(join9(root, path), "utf8");
2148
2582
  }
2149
2583
  return result;
2150
2584
  }
@@ -2153,14 +2587,14 @@ function proposalBaseline(baseline) {
2153
2587
  }
2154
2588
  async function readProject(root) {
2155
2589
  try {
2156
- return projectConfigSchema.parse(parseYaml3(await readFile7(join8(root, ".harness", "project.yaml"), "utf8")));
2590
+ return projectConfigSchema.parse(parseYaml4(await readFile8(join9(root, ".harness", "project.yaml"), "utf8")));
2157
2591
  } catch {
2158
2592
  throw new PushWorkflowError("project configuration is missing or invalid", 3, "PROJECT_CONFIG_INVALID");
2159
2593
  }
2160
2594
  }
2161
2595
  async function readOptionalJson(path) {
2162
2596
  try {
2163
- return JSON.parse(await readFile7(path, "utf8"));
2597
+ return JSON.parse(await readFile8(path, "utf8"));
2164
2598
  } catch (error) {
2165
2599
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
2166
2600
  return null;
@@ -2172,7 +2606,7 @@ async function clientIdFor(root, explicit) {
2172
2606
  if (explicit !== void 0) {
2173
2607
  return explicit;
2174
2608
  }
2175
- const path = join8(root, ".harness", "state", "local", "client.json");
2609
+ const path = join9(root, ".harness", "state", "local", "client.json");
2176
2610
  const existing = await readOptionalJson(path);
2177
2611
  if (typeof existing?.client_id === "string" && /^cli_[A-Za-z0-9_-]+$/.test(existing.client_id)) {
2178
2612
  return existing.client_id;
@@ -2244,7 +2678,7 @@ async function bindProject(root, project, baseline, projectId) {
2244
2678
  {
2245
2679
  operation: "modify",
2246
2680
  path: ".harness/project.yaml",
2247
- content: stringifyYaml2(nextProject, { sortMapEntries: true })
2681
+ content: stringifyYaml3(nextProject, { sortMapEntries: true })
2248
2682
  },
2249
2683
  {
2250
2684
  operation: "modify",
@@ -2255,7 +2689,7 @@ async function bindProject(root, project, baseline, projectId) {
2255
2689
  return { project: nextProject, baseline: nextBaseline };
2256
2690
  }
2257
2691
  async function pushProject(options) {
2258
- const root = resolve4(options.projectRoot);
2692
+ const root = resolve5(options.projectRoot);
2259
2693
  let project = await readProject(root);
2260
2694
  let baseline = await readBaseline(root);
2261
2695
  const profile = parseHarnessProfile(project.project.profiles[0]);
@@ -2291,7 +2725,7 @@ async function pushProject(options) {
2291
2725
  if (parsedServerUrl.protocol !== "https:") {
2292
2726
  throw new PushWorkflowError("server_url must use HTTPS", 3, "SERVER_URL_INVALID");
2293
2727
  }
2294
- const workflowPath = join8(root, ".harness", "state", "local", "push-workflow.json");
2728
+ const workflowPath = join9(root, ".harness", "state", "local", "push-workflow.json");
2295
2729
  const priorWorkflow = await readOptionalJson(workflowPath);
2296
2730
  const provisionalRequestId = priorWorkflow?.local_project_key === project.project.local_project_key ? priorWorkflow.request_id : uuidV7();
2297
2731
  const lock = await acquireProtocolLock(root, "push", { requestId: provisionalRequestId });
@@ -2382,7 +2816,7 @@ async function pushProject(options) {
2382
2816
  }
2383
2817
  }
2384
2818
  const finalized = await client.finalizeProposal(session.session_id, { schema_version: 1, manifest_sha256: proposalManifestHash }, requestId, workflow.finalize_idempotency_key);
2385
- await atomicWriteJson(join8(root, ".harness", "state", "local", "push-results", finalized.proposal_id + ".json"), {
2819
+ await atomicWriteJson(join9(root, ".harness", "state", "local", "push-results", finalized.proposal_id + ".json"), {
2386
2820
  schema_version: 1,
2387
2821
  request_id: requestId,
2388
2822
  project_id: projectId,
@@ -2407,12 +2841,75 @@ async function pushProject(options) {
2407
2841
  }
2408
2842
  }
2409
2843
 
2844
+ // ../core/dist/state/cleanup.js
2845
+ import { readFile as readFile9, readdir as readdir5, rm as rm5 } from "node:fs/promises";
2846
+ import { join as join10 } from "node:path";
2847
+ function isSafeEntryName(name) {
2848
+ return name.length > 0 && !name.includes("/") && !name.includes("\\") && name !== "." && name !== ".." && !name.includes("\0");
2849
+ }
2850
+ async function listDir(path) {
2851
+ try {
2852
+ return await readdir5(path);
2853
+ } catch (error) {
2854
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
2855
+ return [];
2856
+ }
2857
+ throw error;
2858
+ }
2859
+ }
2860
+ async function cleanupProject(options) {
2861
+ const layout = stateLayout(options.projectRoot);
2862
+ const pruned = [];
2863
+ const removedCache = [];
2864
+ const committedByKind = /* @__PURE__ */ new Map();
2865
+ for (const name of await listDir(layout.transactions)) {
2866
+ if (!isSafeEntryName(name))
2867
+ continue;
2868
+ let journal;
2869
+ try {
2870
+ journal = JSON.parse(await readFile9(join10(layout.transactions, name, "journal.json"), "utf8"));
2871
+ } catch {
2872
+ continue;
2873
+ }
2874
+ if (journal.state === "committed" && typeof journal.kind === "string") {
2875
+ const arr = committedByKind.get(journal.kind) ?? [];
2876
+ arr.push({ id: name, createdAt: typeof journal.created_at === "string" ? journal.created_at : "" });
2877
+ committedByKind.set(journal.kind, arr);
2878
+ }
2879
+ }
2880
+ for (const arr of committedByKind.values()) {
2881
+ arr.sort((left, right) => right.createdAt.localeCompare(left.createdAt));
2882
+ for (let index = 1; index < arr.length; index += 1) {
2883
+ const entry = arr[index];
2884
+ if (entry === void 0)
2885
+ continue;
2886
+ pruned.push(entry.id);
2887
+ if (!options.dryRun) {
2888
+ await rm5(join10(layout.transactions, entry.id), { recursive: true, force: true });
2889
+ }
2890
+ }
2891
+ }
2892
+ for (const name of await listDir(layout.serverArtifacts)) {
2893
+ if (!isSafeEntryName(name))
2894
+ continue;
2895
+ removedCache.push(name);
2896
+ if (!options.dryRun) {
2897
+ await rm5(join10(layout.serverArtifacts, name), { recursive: true, force: true });
2898
+ }
2899
+ }
2900
+ return {
2901
+ dry_run: options.dryRun,
2902
+ pruned_transactions: pruned.sort(),
2903
+ removed_cache: removedCache.sort()
2904
+ };
2905
+ }
2906
+
2410
2907
  // ../core/dist/skill/frontmatter.js
2411
- import { parse as parseYaml4 } from "yaml";
2908
+ import { parse as parseYaml5 } from "yaml";
2412
2909
  import { z as z12 } from "zod";
2413
2910
 
2414
2911
  // ../core/dist/skill/fixer.js
2415
- import { parse as parseYaml5, stringify as stringifyYaml3 } from "yaml";
2912
+ import { parse as parseYaml6, stringify as stringifyYaml4 } from "yaml";
2416
2913
 
2417
2914
  // ../core/dist/skill/agents.js
2418
2915
  var AGENT_DESCRIPTORS = {
@@ -2451,10 +2948,10 @@ var AGENT_DESCRIPTORS = {
2451
2948
  var INSTALLABLE_AGENTS = Object.keys(AGENT_DESCRIPTORS).filter((agent) => AGENT_DESCRIPTORS[agent]?.installable === true);
2452
2949
 
2453
2950
  // ../core/dist/transaction/recovery.js
2454
- import { readFile as readFile8, readdir as readdir2, rm as rm5, stat as stat3 } from "node:fs/promises";
2455
- import { join as join9 } from "node:path";
2951
+ import { readFile as readFile10, readdir as readdir6, rm as rm6, stat as stat4 } from "node:fs/promises";
2952
+ import { join as join11 } from "node:path";
2456
2953
  async function recoverTransaction(projectRoot, transactionId) {
2457
- const journal = JSON.parse(await readFile8(join9(stateLayout(projectRoot).transactions, transactionId, "journal.json"), "utf8"));
2954
+ const journal = JSON.parse(await readFile10(join11(stateLayout(projectRoot).transactions, transactionId, "journal.json"), "utf8"));
2458
2955
  if (journal.state === "committed") {
2459
2956
  return { transactionId, status: "committed" };
2460
2957
  }
@@ -2471,7 +2968,7 @@ async function listTransactions(projectRoot) {
2471
2968
  const root = stateLayout(projectRoot).transactions;
2472
2969
  let names;
2473
2970
  try {
2474
- names = await readdir2(root);
2971
+ names = await readdir6(root);
2475
2972
  } catch (error) {
2476
2973
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
2477
2974
  return [];
@@ -2481,7 +2978,7 @@ async function listTransactions(projectRoot) {
2481
2978
  const transactions = [];
2482
2979
  for (const transactionId of names) {
2483
2980
  try {
2484
- const journal = JSON.parse(await readFile8(join9(root, transactionId, "journal.json"), "utf8"));
2981
+ const journal = JSON.parse(await readFile10(join11(root, transactionId, "journal.json"), "utf8"));
2485
2982
  transactions.push({
2486
2983
  transactionId,
2487
2984
  kind: journal.kind,
@@ -2494,11 +2991,11 @@ async function listTransactions(projectRoot) {
2494
2991
  return transactions.sort((left, right) => right.createdAt.localeCompare(left.createdAt));
2495
2992
  }
2496
2993
  async function pendingTransactions(projectRoot) {
2497
- return (await listTransactions(projectRoot)).filter((item) => RECOVERY_STATES.has(item.state));
2994
+ return (await listTransactions(projectRoot)).filter((item2) => RECOVERY_STATES.has(item2.state));
2498
2995
  }
2499
2996
  async function pathExists(path) {
2500
2997
  try {
2501
- await stat3(path);
2998
+ await stat4(path);
2502
2999
  return true;
2503
3000
  } catch (error) {
2504
3001
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
@@ -2508,17 +3005,17 @@ async function pathExists(path) {
2508
3005
  }
2509
3006
  }
2510
3007
  async function rollbackLatestCommittedUpdate(projectRoot) {
2511
- const latest = (await listTransactions(projectRoot)).find((item) => item.kind === "update" && item.state === "committed");
3008
+ const latest = (await listTransactions(projectRoot)).find((item2) => item2.kind === "update" && item2.state === "committed");
2512
3009
  if (latest === void 0) {
2513
3010
  throw new Error("no committed update transaction is available for rollback");
2514
3011
  }
2515
- const transactionRoot = join9(stateLayout(projectRoot).transactions, latest.transactionId);
2516
- const journal = JSON.parse(await readFile8(join9(transactionRoot, "journal.json"), "utf8"));
2517
- const after = JSON.parse(await readFile8(join9(transactionRoot, "after", "manifest.json"), "utf8"));
3012
+ const transactionRoot = join11(stateLayout(projectRoot).transactions, latest.transactionId);
3013
+ const journal = JSON.parse(await readFile10(join11(transactionRoot, "journal.json"), "utf8"));
3014
+ const after = JSON.parse(await readFile10(join11(transactionRoot, "after", "manifest.json"), "utf8"));
2518
3015
  for (const entry of after) {
2519
- const target = join9(projectRoot, entry.path);
2520
- const exists5 = await pathExists(target);
2521
- if (exists5 !== entry.exists || exists5 && await sha256File(target) !== entry.hash) {
3016
+ const target = join11(projectRoot, entry.path);
3017
+ const exists6 = await pathExists(target);
3018
+ if (exists6 !== entry.exists || exists6 && await sha256File(target) !== entry.hash) {
2522
3019
  throw new Error("cannot rollback dirty path: " + entry.path);
2523
3020
  }
2524
3021
  }
@@ -2529,16 +3026,16 @@ async function rollbackLatestCommittedUpdate(projectRoot) {
2529
3026
  continue;
2530
3027
  }
2531
3028
  seen.add(snapshot.path);
2532
- const target = join9(projectRoot, snapshot.path);
2533
- const exists5 = await pathExists(target);
3029
+ const target = join11(projectRoot, snapshot.path);
3030
+ const exists6 = await pathExists(target);
2534
3031
  if (snapshot.existed && snapshot.snapshot_name !== null) {
2535
- const content = await readFile8(join9(transactionRoot, "before", snapshot.snapshot_name));
3032
+ const content = await readFile10(join11(transactionRoot, "before", snapshot.snapshot_name));
2536
3033
  operations.push({
2537
- operation: exists5 ? "modify" : "add",
3034
+ operation: exists6 ? "modify" : "add",
2538
3035
  path: snapshot.path,
2539
3036
  content
2540
3037
  });
2541
- } else if (exists5) {
3038
+ } else if (exists6) {
2542
3039
  operations.push({ operation: "delete", path: snapshot.path });
2543
3040
  }
2544
3041
  }
@@ -2549,23 +3046,23 @@ async function rollbackLatestCommittedUpdate(projectRoot) {
2549
3046
  }
2550
3047
  async function cleanupOldTransactions(projectRoot, now = /* @__PURE__ */ new Date()) {
2551
3048
  const transactions = await listTransactions(projectRoot);
2552
- const committedUpdates = transactions.filter((item) => item.kind === "update" && item.state === "committed");
2553
- const keepCommitted = new Set(committedUpdates.slice(0, 10).map((item) => item.transactionId));
3049
+ const committedUpdates = transactions.filter((item2) => item2.kind === "update" && item2.state === "committed");
3050
+ const keepCommitted = new Set(committedUpdates.slice(0, 10).map((item2) => item2.transactionId));
2554
3051
  const removed = [];
2555
- for (const item of transactions) {
2556
- if (RECOVERY_STATES.has(item.state) || keepCommitted.has(item.transactionId)) {
3052
+ for (const item2 of transactions) {
3053
+ if (RECOVERY_STATES.has(item2.state) || keepCommitted.has(item2.transactionId)) {
2557
3054
  continue;
2558
3055
  }
2559
- const ageDays = (now.getTime() - Date.parse(item.createdAt)) / 864e5;
2560
- const removable = item.state === "rolled_back" ? ageDays > 7 : item.state === "committed" && ageDays > 30;
3056
+ const ageDays = (now.getTime() - Date.parse(item2.createdAt)) / 864e5;
3057
+ const removable = item2.state === "rolled_back" ? ageDays > 7 : item2.state === "committed" && ageDays > 30;
2561
3058
  if (!removable) {
2562
3059
  continue;
2563
3060
  }
2564
- await rm5(join9(stateLayout(projectRoot).transactions, item.transactionId), {
3061
+ await rm6(join11(stateLayout(projectRoot).transactions, item2.transactionId), {
2565
3062
  recursive: true,
2566
3063
  force: true
2567
3064
  });
2568
- removed.push(item.transactionId);
3065
+ removed.push(item2.transactionId);
2569
3066
  }
2570
3067
  return removed;
2571
3068
  }
@@ -2593,9 +3090,9 @@ function managedBlockDirty(currentContent, managedBlockHash) {
2593
3090
  }
2594
3091
 
2595
3092
  // ../core/dist/update/update.js
2596
- import { lstat as lstat3, readFile as readFile9, rm as rm6 } from "node:fs/promises";
2597
- import { join as join10, resolve as resolve5 } from "node:path";
2598
- import { parse as parseYaml6 } from "yaml";
3093
+ import { lstat as lstat3, readFile as readFile11, rm as rm7 } from "node:fs/promises";
3094
+ import { join as join12, resolve as resolve6 } from "node:path";
3095
+ import { parse as parseYaml7 } from "yaml";
2599
3096
  var UpdateWorkflowError = class extends Error {
2600
3097
  exitCode;
2601
3098
  code;
@@ -2618,7 +3115,7 @@ async function pathExists2(path) {
2618
3115
  }
2619
3116
  }
2620
3117
  async function optionalContent(path) {
2621
- return await pathExists2(path) ? readFile9(path, "utf8") : null;
3118
+ return await pathExists2(path) ? readFile11(path, "utf8") : null;
2622
3119
  }
2623
3120
  function manifestPayloadHash(manifest) {
2624
3121
  const payload = { ...manifest };
@@ -2636,14 +3133,14 @@ async function loadBlob(root, client, artifactId, operation, requestId, dryRun)
2636
3133
  return null;
2637
3134
  }
2638
3135
  const hash = operation.content_sha256;
2639
- const cacheRoot = join10(root, ".harness", "cache", "server-artifacts", artifactId);
2640
- const cachePath = join10(cacheRoot, "blobs", hash.replace(":", "_"));
3136
+ const cacheRoot = join12(root, ".harness", "cache", "server-artifacts", artifactId);
3137
+ const cachePath = join12(cacheRoot, "blobs", hash.replace(":", "_"));
2641
3138
  if (await pathExists2(cachePath) && await sha256File(cachePath) === hash) {
2642
- return readFile9(cachePath, "utf8");
3139
+ return readFile11(cachePath, "utf8");
2643
3140
  }
2644
3141
  const bytes = await client.downloadArtifactBlob(artifactId, hash, requestId);
2645
3142
  if (bytes.byteLength !== operation.size_bytes || sha256Bytes(bytes) !== hash) {
2646
- await rm6(cachePath, { force: true });
3143
+ await rm7(cachePath, { force: true });
2647
3144
  throw new UpdateWorkflowError("artifact blob size or hash mismatch", 4, "ARTIFACT_HASH_MISMATCH");
2648
3145
  }
2649
3146
  if (!dryRun) {
@@ -2662,34 +3159,34 @@ function nextBaselineEntry(operation, finalContent, projectVersion) {
2662
3159
  ...block === null ? {} : { managed_block_hash: sha256Bytes(block) }
2663
3160
  };
2664
3161
  }
2665
- function transactionOperation(item) {
2666
- if (item.equivalent) {
3162
+ function transactionOperation(item2) {
3163
+ if (item2.equivalent) {
2667
3164
  return null;
2668
3165
  }
2669
- const operation = item.operation;
3166
+ const operation = item2.operation;
2670
3167
  const target = operationTargetPath(operation);
2671
3168
  if (operation.operation === "delete") {
2672
- return item.finalContent === null ? { operation: "delete", path: target } : { operation: "modify", path: target, content: item.finalContent };
3169
+ return item2.finalContent === null ? { operation: "delete", path: target } : { operation: "modify", path: target, content: item2.finalContent };
2673
3170
  }
2674
3171
  if (operation.operation === "rename") {
2675
3172
  return {
2676
3173
  operation: "rename",
2677
3174
  from_path: operation.from_path,
2678
3175
  to_path: operation.to_path,
2679
- content: item.finalContent ?? item.content ?? ""
3176
+ content: item2.finalContent ?? item2.content ?? ""
2680
3177
  };
2681
3178
  }
2682
3179
  return {
2683
3180
  operation: "modify",
2684
3181
  path: target,
2685
- content: item.finalContent ?? item.content ?? ""
3182
+ content: item2.finalContent ?? item2.content ?? ""
2686
3183
  };
2687
3184
  }
2688
3185
  async function updateProject(options) {
2689
- const root = resolve5(options.projectRoot);
3186
+ const root = resolve6(options.projectRoot);
2690
3187
  let project;
2691
3188
  try {
2692
- project = projectConfigSchema.parse(parseYaml6(await readFile9(join10(root, ".harness", "project.yaml"), "utf8")));
3189
+ project = projectConfigSchema.parse(parseYaml7(await readFile11(join12(root, ".harness", "project.yaml"), "utf8")));
2693
3190
  } catch {
2694
3191
  throw new UpdateWorkflowError("project configuration is invalid", 3, "PROJECT_CONFIG_INVALID");
2695
3192
  }
@@ -2774,8 +3271,8 @@ async function updateProject(options) {
2774
3271
  skipped.push({ path: target, operation: operation.operation, reason: "baseline-diverged" });
2775
3272
  continue;
2776
3273
  }
2777
- const sourceContent = await optionalContent(join10(root, source));
2778
- const targetContent = target === source ? sourceContent : await optionalContent(join10(root, target));
3274
+ const sourceContent = await optionalContent(join12(root, source));
3275
+ const targetContent = target === source ? sourceContent : await optionalContent(join12(root, target));
2779
3276
  const incoming = blobs.get(operation) ?? null;
2780
3277
  const incomingHash = contentHash(operation);
2781
3278
  let equivalent = incomingHash !== null && targetContent !== null && sha256Bytes(targetContent) === incomingHash;
@@ -2820,7 +3317,7 @@ async function updateProject(options) {
2820
3317
  }
2821
3318
  prepared.push({ operation, content: incoming, finalContent, equivalent });
2822
3319
  }
2823
- const applied = prepared.map((item) => operationTargetPath(item.operation));
3320
+ const applied = prepared.map((item2) => operationTargetPath(item2.operation));
2824
3321
  if (options.dryRun) {
2825
3322
  return {
2826
3323
  requestId,
@@ -2834,18 +3331,18 @@ async function updateProject(options) {
2834
3331
  dryRun: true
2835
3332
  };
2836
3333
  }
2837
- await atomicWriteJson(join10(root, ".harness", "cache", "server-artifacts", manifest.artifact_id, "manifest.json"), manifest);
3334
+ await atomicWriteJson(join12(root, ".harness", "cache", "server-artifacts", manifest.artifact_id, "manifest.json"), manifest);
2838
3335
  const lock = await acquireProtocolLock(root, "update", { requestId });
2839
3336
  try {
2840
3337
  const nextBaseline = baselineManifestSchema.parse(structuredClone(baseline));
2841
- for (const item of prepared) {
2842
- const target = operationTargetPath(item.operation);
2843
- nextBaseline.files[target] = nextBaselineEntry(item.operation, item.finalContent, manifest.project_version);
2844
- if (item.operation.operation === "rename") {
2845
- nextBaseline.files[item.operation.from_path] = {
3338
+ for (const item2 of prepared) {
3339
+ const target = operationTargetPath(item2.operation);
3340
+ nextBaseline.files[target] = nextBaselineEntry(item2.operation, item2.finalContent, manifest.project_version);
3341
+ if (item2.operation.operation === "rename") {
3342
+ nextBaseline.files[item2.operation.from_path] = {
2846
3343
  baseline_hash: null,
2847
3344
  local_hash_at_apply: null,
2848
- file_kind: item.operation.file_kind,
3345
+ file_kind: item2.operation.file_kind,
2849
3346
  last_applied_version: manifest.project_version,
2850
3347
  deleted: true
2851
3348
  };
@@ -2867,7 +3364,7 @@ async function updateProject(options) {
2867
3364
  skipped,
2868
3365
  transaction_id: transactionId
2869
3366
  };
2870
- const operations = prepared.map((item) => transactionOperation(item)).filter((item) => item !== null);
3367
+ const operations = prepared.map((item2) => transactionOperation(item2)).filter((item2) => item2 !== null);
2871
3368
  operations.push({
2872
3369
  operation: "modify",
2873
3370
  path: ".harness/state/baseline/manifest.json",
@@ -2915,8 +3412,8 @@ async function updateProject(options) {
2915
3412
  }
2916
3413
 
2917
3414
  // src/config/init-config.ts
2918
- import { isAbsolute as isAbsolute2, join as join11 } from "node:path";
2919
- import { readFile as readFile10 } from "node:fs/promises";
3415
+ import { isAbsolute as isAbsolute2, join as join13 } from "node:path";
3416
+ import { readFile as readFile12 } from "node:fs/promises";
2920
3417
  var InitConfigurationError = class extends Error {
2921
3418
  exitCode;
2922
3419
  constructor(message, exitCode = 3, options) {
@@ -2934,9 +3431,9 @@ function normalizeProfile(value) {
2934
3431
  async function resolveInitConfig(cwd, flags, promptMissing) {
2935
3432
  let fileConfig = {};
2936
3433
  if (flags.config !== void 0) {
2937
- const path = isAbsolute2(flags.config) ? flags.config : join11(cwd, flags.config);
3434
+ const path = isAbsolute2(flags.config) ? flags.config : join13(cwd, flags.config);
2938
3435
  try {
2939
- fileConfig = JSON.parse(await readFile10(path, "utf8"));
3436
+ fileConfig = JSON.parse(await readFile12(path, "utf8"));
2940
3437
  } catch (error) {
2941
3438
  throw new InitConfigurationError(
2942
3439
  "unable to read init config: " + (error instanceof Error ? error.message : String(error)),
@@ -2978,8 +3475,175 @@ function serializeCliResult(result) {
2978
3475
  return JSON.stringify(result) + "\n";
2979
3476
  }
2980
3477
 
3478
+ // src/commands/refresh.ts
3479
+ import { readFile as readFile13 } from "node:fs/promises";
3480
+ import { join as join14 } from "node:path";
3481
+ import { parse as parseYaml8 } from "yaml";
3482
+ async function detectProject(root) {
3483
+ let content;
3484
+ try {
3485
+ content = await readFile13(join14(root, ".harness", "project.yaml"), "utf8");
3486
+ } catch (error) {
3487
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
3488
+ return { status: "absent" };
3489
+ }
3490
+ throw error;
3491
+ }
3492
+ const parsed = projectConfigSchema.safeParse(parseYaml8(content));
3493
+ if (!parsed.success) {
3494
+ return { status: "invalid" };
3495
+ }
3496
+ return { status: "valid", config: parsed.data };
3497
+ }
3498
+ function parseProfile(value, current) {
3499
+ if (value === void 0 || value === "") {
3500
+ return current;
3501
+ }
3502
+ if (value === "1" || value === "general") return "general";
3503
+ if (value === "2" || value === "java") return "java";
3504
+ throw new Error("profile must be general or java");
3505
+ }
3506
+ function summarize(result) {
3507
+ const items = [
3508
+ ...result.applied.map((item2) => ({ ...item2, status: result.dry_run ? "planned" : "applied" })),
3509
+ ...result.removed.map((item2) => ({ ...item2, status: result.dry_run ? "planned" : "removed" })),
3510
+ ...result.preserved.map((item2) => ({ ...item2, status: "preserved" })),
3511
+ ...result.unchanged.map((item2) => ({ ...item2, status: "unchanged" }))
3512
+ ];
3513
+ const exitCode = result.conflicts.length > 0 ? 5 : 0;
3514
+ return {
3515
+ schema_version: 1,
3516
+ command: "refresh",
3517
+ request_id: uuidV7(),
3518
+ dry_run: result.dry_run,
3519
+ ok: exitCode === 0,
3520
+ exit_code: exitCode,
3521
+ project_id: null,
3522
+ summary: {
3523
+ applied: result.dry_run ? 0 : result.applied.length,
3524
+ removed: result.dry_run ? 0 : result.removed.length,
3525
+ preserved: result.preserved.length,
3526
+ unchanged: result.unchanged.length,
3527
+ conflicts: result.conflicts.length
3528
+ },
3529
+ items,
3530
+ warnings: result.conflicts,
3531
+ errors: []
3532
+ };
3533
+ }
3534
+ function renderProfileTransitionPreview(result) {
3535
+ const items = [
3536
+ ...result.applied,
3537
+ ...result.removed,
3538
+ ...result.preserved,
3539
+ ...result.unchanged
3540
+ ];
3541
+ return "Preview profile transition:\n" + items.map((item2) => `- ${item2.action}: ${item2.target_path} (${item2.reason})`).join("\n") + "\n";
3542
+ }
3543
+ async function runRefresh(options, dependencies) {
3544
+ const requestId = uuidV7();
3545
+ const detection = await detectProject(dependencies.cwd);
3546
+ if (detection.status === "absent") {
3547
+ dependencies.stderr("Hunter Harness is not initialized; run `hunter-harness` first.\n");
3548
+ return 3;
3549
+ }
3550
+ if (detection.status === "invalid") {
3551
+ dependencies.stderr("PROJECT_CONFIG_INVALID: .harness/project.yaml is invalid\n");
3552
+ return 3;
3553
+ }
3554
+ const currentProfile = detection.config.project.profiles[0] ?? "general";
3555
+ let targetProfile;
3556
+ try {
3557
+ targetProfile = parseProfile(options.profile, currentProfile);
3558
+ } catch (error) {
3559
+ const message = error instanceof Error ? error.message : String(error);
3560
+ dependencies.stderr(message + "\n");
3561
+ return 3;
3562
+ }
3563
+ const dryRun = options.dryRun === true;
3564
+ if (targetProfile !== currentProfile && !dryRun) {
3565
+ try {
3566
+ const preview = await refreshProject({
3567
+ projectRoot: dependencies.cwd,
3568
+ resourcesRoot: dependencies.resourcesRoot,
3569
+ profile: targetProfile,
3570
+ dryRun: true,
3571
+ forceManaged: options.forceManaged === true
3572
+ });
3573
+ const rendered = renderProfileTransitionPreview(preview);
3574
+ if (options.json === true) {
3575
+ dependencies.stderr(rendered);
3576
+ } else {
3577
+ dependencies.stdout(rendered);
3578
+ }
3579
+ } catch (error) {
3580
+ const message = error instanceof Error ? error.message : String(error);
3581
+ dependencies.stderr(message + "\n");
3582
+ return 1;
3583
+ }
3584
+ }
3585
+ if (options.confirmed !== true) {
3586
+ if (options.nonInteractive === true) {
3587
+ if (!options.yes && !dryRun) {
3588
+ dependencies.stderr("non-interactive refresh requires --yes\n");
3589
+ return 2;
3590
+ }
3591
+ } else if (!options.yes && !dryRun) {
3592
+ const label = targetProfile === currentProfile ? `Refresh current profile (${currentProfile})` : `Switch profile ${currentProfile} -> ${targetProfile}`;
3593
+ const answer = await dependencies.prompt(`${label}? [y/N]: `);
3594
+ if (!/^(?:y|yes)$/i.test(answer.trim())) {
3595
+ return 2;
3596
+ }
3597
+ }
3598
+ }
3599
+ try {
3600
+ const result = await refreshProject({
3601
+ projectRoot: dependencies.cwd,
3602
+ resourcesRoot: dependencies.resourcesRoot,
3603
+ profile: targetProfile,
3604
+ dryRun,
3605
+ forceManaged: options.forceManaged === true
3606
+ });
3607
+ const output = summarize(result);
3608
+ if (options.json === true) {
3609
+ dependencies.stdout(serializeCliResult({ ...output, request_id: requestId }));
3610
+ } else {
3611
+ const parts = [];
3612
+ if (result.applied.length > 0) parts.push(`applied ${result.applied.length}`);
3613
+ if (result.removed.length > 0) parts.push(`removed ${result.removed.length}`);
3614
+ if (result.preserved.length > 0) parts.push(`preserved ${result.preserved.length}`);
3615
+ if (result.unchanged.length > 0) parts.push(`unchanged ${result.unchanged.length}`);
3616
+ dependencies.stdout(`Harness refresh (${targetProfile}): ${parts.join(", ") || "no changes"}.
3617
+ `);
3618
+ }
3619
+ return output.exit_code;
3620
+ } catch (error) {
3621
+ const message = error instanceof Error ? error.message : String(error);
3622
+ dependencies.stderr(message + "\n");
3623
+ if (options.json === true) {
3624
+ dependencies.stdout(serializeCliResult({
3625
+ schema_version: 1,
3626
+ command: "refresh",
3627
+ request_id: requestId,
3628
+ dry_run: dryRun,
3629
+ ok: false,
3630
+ exit_code: 1,
3631
+ project_id: null,
3632
+ summary: { applied: 0, removed: 0, preserved: 0, unchanged: 0, conflicts: 0 },
3633
+ items: [],
3634
+ warnings: [],
3635
+ errors: [{ message }]
3636
+ }));
3637
+ }
3638
+ return 1;
3639
+ }
3640
+ }
3641
+
2981
3642
  // src/commands/configure.ts
2982
- async function runConfigure(options, dependencies) {
3643
+ function otherProfile(current) {
3644
+ return current === "general" ? "java" : "general";
3645
+ }
3646
+ async function runFirstInstall(options, dependencies) {
2983
3647
  const requestId = uuidV7();
2984
3648
  try {
2985
3649
  const config = await resolveInitConfig(
@@ -3036,6 +3700,123 @@ async function runConfigure(options, dependencies) {
3036
3700
  return exitCode;
3037
3701
  }
3038
3702
  }
3703
+ async function runExistingProject(options, dependencies, currentProfile) {
3704
+ const refreshOptions = {};
3705
+ if (options.profile !== void 0) refreshOptions.profile = options.profile;
3706
+ if (options.nonInteractive !== void 0) refreshOptions.nonInteractive = options.nonInteractive;
3707
+ if (options.yes !== void 0) refreshOptions.yes = options.yes;
3708
+ if (options.dryRun !== void 0) refreshOptions.dryRun = options.dryRun;
3709
+ if (options.json !== void 0) refreshOptions.json = options.json;
3710
+ if (options.forceManaged !== void 0) refreshOptions.forceManaged = options.forceManaged;
3711
+ if (options.nonInteractive === true) {
3712
+ return runRefresh(refreshOptions, dependencies);
3713
+ }
3714
+ const menu = await dependencies.prompt(
3715
+ `Hunter Harness \u5DF2\u521D\u59CB\u5316\uFF08profile: ${currentProfile}\uFF09\u3002
3716
+ 1. Refresh current profile\uFF08\u9ED8\u8BA4\u4E14\u63A8\u8350\uFF09
3717
+ 2. Switch to the other profile
3718
+ 3. Cancel
3719
+ \u8BF7\u9009\u62E9 [1]: `
3720
+ );
3721
+ const choice = menu.trim();
3722
+ if (choice === "3" || /^c/i.test(choice)) {
3723
+ return 2;
3724
+ }
3725
+ if (choice === "2" || /^s/i.test(choice)) {
3726
+ refreshOptions.profile = otherProfile(currentProfile);
3727
+ } else {
3728
+ refreshOptions.profile = currentProfile;
3729
+ }
3730
+ refreshOptions.confirmed = true;
3731
+ return runRefresh(refreshOptions, dependencies);
3732
+ }
3733
+ async function runConfigure(options, dependencies) {
3734
+ const detection = await detectProject(dependencies.cwd);
3735
+ if (detection.status === "invalid") {
3736
+ dependencies.stderr("PROJECT_CONFIG_INVALID: .harness/project.yaml is invalid; not initializing over it.\n");
3737
+ if (options.json === true) {
3738
+ dependencies.stdout(serializeCliResult({
3739
+ schema_version: 1,
3740
+ command: "configure",
3741
+ request_id: uuidV7(),
3742
+ dry_run: options.dryRun === true,
3743
+ ok: false,
3744
+ exit_code: 3,
3745
+ project_id: null,
3746
+ summary: { planned: 0, applied: 0 },
3747
+ items: [],
3748
+ warnings: [],
3749
+ errors: [{ code: "PROJECT_CONFIG_INVALID", message: "project.yaml is invalid" }]
3750
+ }));
3751
+ }
3752
+ return 3;
3753
+ }
3754
+ if (detection.status === "valid") {
3755
+ const currentProfile = detection.config.project.profiles[0] ?? "general";
3756
+ return runExistingProject(options, dependencies, currentProfile);
3757
+ }
3758
+ return runFirstInstall(options, dependencies);
3759
+ }
3760
+
3761
+ // src/commands/cleanup.ts
3762
+ async function runCleanup(options, dependencies) {
3763
+ const requestId = uuidV7();
3764
+ const dryRun = options.dryRun === true;
3765
+ if (options.nonInteractive === true && options.yes !== true && !dryRun) {
3766
+ dependencies.stderr("non-interactive cleanup requires --yes\n");
3767
+ return 2;
3768
+ }
3769
+ try {
3770
+ const result = await cleanupProject({ projectRoot: dependencies.cwd, dryRun });
3771
+ const output = {
3772
+ schema_version: 1,
3773
+ command: "cleanup",
3774
+ request_id: requestId,
3775
+ dry_run: result.dry_run,
3776
+ ok: true,
3777
+ exit_code: 0,
3778
+ project_id: null,
3779
+ summary: {
3780
+ pruned_transactions: result.pruned_transactions.length,
3781
+ removed_cache: result.removed_cache.length
3782
+ },
3783
+ items: [
3784
+ ...result.pruned_transactions.map((id) => ({ path: id, status: "pruned" })),
3785
+ ...result.removed_cache.map((id) => ({ path: id, status: "removed" }))
3786
+ ],
3787
+ warnings: [],
3788
+ errors: []
3789
+ };
3790
+ if (options.json === true) {
3791
+ dependencies.stdout(serializeCliResult(output));
3792
+ } else {
3793
+ dependencies.stdout(
3794
+ `Harness cleanup: pruned ${result.pruned_transactions.length} transaction(s), removed ${result.removed_cache.length} cache entr(ies).
3795
+ `
3796
+ );
3797
+ }
3798
+ return 0;
3799
+ } catch (error) {
3800
+ const message = error instanceof Error ? error.message : String(error);
3801
+ dependencies.stderr(message + "\n");
3802
+ if (options.json === true) {
3803
+ dependencies.stdout(serializeCliResult({
3804
+ schema_version: 1,
3805
+ command: "cleanup",
3806
+ request_id: requestId,
3807
+ dry_run: dryRun,
3808
+ ok: false,
3809
+ exit_code: 1,
3810
+ project_id: null,
3811
+ summary: { pruned_transactions: 0, removed_cache: 0 },
3812
+ items: [],
3813
+ warnings: [],
3814
+ errors: [{ message }]
3815
+ }));
3816
+ }
3817
+ return 1;
3818
+ }
3819
+ }
3039
3820
 
3040
3821
  // src/commands/push.ts
3041
3822
  async function runPush(options, dependencies) {
@@ -3146,7 +3927,7 @@ async function runUpdate(options, dependencies) {
3146
3927
  result = await execute(options.dryRun === true);
3147
3928
  }
3148
3929
  const applied = new Set(result.applied);
3149
- const skippedByPath = new Map(result.skipped.map((item) => [item.path, item]));
3930
+ const skippedByPath = new Map(result.skipped.map((item2) => [item2.path, item2]));
3150
3931
  const items = result.operations.map((operation) => {
3151
3932
  const path = operation.operation === "rename" ? operation.to_path : operation.path;
3152
3933
  const skipped = skippedByPath.get(path);
@@ -3208,11 +3989,11 @@ async function runUpdate(options, dependencies) {
3208
3989
  }
3209
3990
 
3210
3991
  // src/commands/recovery.ts
3211
- import { stat as stat4 } from "node:fs/promises";
3212
- import { join as join12 } from "node:path";
3213
- async function exists4(path) {
3992
+ import { stat as stat5 } from "node:fs/promises";
3993
+ import { join as join15 } from "node:path";
3994
+ async function exists5(path) {
3214
3995
  try {
3215
- await stat4(path);
3996
+ await stat5(path);
3216
3997
  return true;
3217
3998
  } catch {
3218
3999
  return false;
@@ -3269,7 +4050,7 @@ async function runRecoveryMenuIfApplicable(options, dependencies) {
3269
4050
  return 5;
3270
4051
  }
3271
4052
  }
3272
- const initialized = await exists4(join12(dependencies.cwd, ".harness", "project.yaml"));
4053
+ const initialized = await exists5(join15(dependencies.cwd, ".harness", "project.yaml"));
3273
4054
  if (!initialized || options.nonInteractive === true || explicitConfigure(options)) {
3274
4055
  return null;
3275
4056
  }
@@ -3337,7 +4118,7 @@ function addCommonOptions(command) {
3337
4118
  }
3338
4119
  async function runCli(argv, overrides = {}) {
3339
4120
  const dependencies = defaultDependencies(overrides);
3340
- const program = addCommonOptions(new Command()).name("hunter-harness").description("Local-first, server-governed agent harness").option("--profile <name>").option("--config <file>").showHelpAfterError().exitOverride().configureOutput({
4121
+ const program = addCommonOptions(new Command()).name("hunter-harness").description("Local-first, server-governed agent harness").option("--profile <name>").option("--config <file>").option("--force-managed").showHelpAfterError().exitOverride().configureOutput({
3341
4122
  writeOut: dependencies.stdout,
3342
4123
  writeErr: dependencies.stderr
3343
4124
  });
@@ -3350,6 +4131,12 @@ async function runCli(argv, overrides = {}) {
3350
4131
  }
3351
4132
  exitCode = await runConfigure(options, dependencies);
3352
4133
  });
4134
+ addCommonOptions(program.command("refresh")).description("Local Conservative Refresh of an installed Harness project").option("--profile <name>").option("--force-managed").action(async (options) => {
4135
+ exitCode = await runRefresh(
4136
+ { ...program.opts(), ...options },
4137
+ dependencies
4138
+ );
4139
+ });
3353
4140
  addCommonOptions(program.command("update")).description("Apply approved server artifacts").action(async (options) => {
3354
4141
  exitCode = await runUpdate(
3355
4142
  { ...program.opts(), ...options },
@@ -3359,6 +4146,12 @@ async function runCli(argv, overrides = {}) {
3359
4146
  addCommonOptions(program.command("push")).description("Create a governed proposal").action(async (options) => {
3360
4147
  exitCode = await runPush({ ...program.opts(), ...options }, dependencies);
3361
4148
  });
4149
+ addCommonOptions(program.command("cleanup")).description("Prune completed transactions and obsolete server cache").action(async (options) => {
4150
+ exitCode = await runCleanup(
4151
+ { ...program.opts(), ...options },
4152
+ dependencies
4153
+ );
4154
+ });
3362
4155
  try {
3363
4156
  await program.parseAsync(["node", "hunter-harness", ...argv]);
3364
4157
  return exitCode;