hunter-harness 0.1.0 → 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 +1033 -219
- package/package.json +2 -2
- package/resources/harness/general/.harness-build.json +1 -1
- package/resources/harness/general/harness-archive/SKILL.md +215 -215
- package/resources/harness/general/harness-codebase-map/SKILL.md +112 -112
- package/resources/harness/general/harness-codebase-map/templates/map-summary.md +3 -3
- package/resources/harness/general/harness-knowledge-ingest/SKILL.md +246 -246
- package/resources/harness/general/harness-knowledge-query/SKILL.md +164 -164
- package/resources/harness/general/harness-plan/SKILL.md +127 -127
- package/resources/harness/general/harness-review/SKILL.md +156 -156
- package/resources/harness/general/harness-run/SKILL.md +132 -132
- package/resources/harness/general/harness-submit/SKILL.md +159 -159
- package/resources/harness/general/harness-sync/SKILL.md +82 -82
- package/resources/harness/general/harness-test/SKILL.md +180 -180
- package/resources/harness/general/scripts/harness_deploy.py +580 -580
- package/resources/harness/java/.harness-build.json +1 -1
- package/resources/harness/java/harness-apidoc/SKILL.md +86 -86
- package/resources/harness/java/harness-archive/SKILL.md +215 -215
- package/resources/harness/java/harness-codebase-map/SKILL.md +112 -112
- package/resources/harness/java/harness-codebase-map/templates/map-summary.md +3 -3
- package/resources/harness/java/harness-knowledge-ingest/SKILL.md +246 -246
- package/resources/harness/java/harness-knowledge-query/SKILL.md +164 -164
- package/resources/harness/java/harness-package/SKILL.md +87 -87
- package/resources/harness/java/harness-plan/SKILL.md +127 -127
- package/resources/harness/java/harness-review/SKILL.md +156 -156
- package/resources/harness/java/harness-run/SKILL.md +148 -148
- package/resources/harness/java/harness-submit/SKILL.md +166 -166
- package/resources/harness/java/harness-sync/SKILL.md +82 -82
- package/resources/harness/java/harness-test/SKILL.md +192 -192
- package/resources/harness/java/scripts/harness_deploy.py +580 -580
- package/resources/harness/manifests/general.json +13 -33
- package/resources/harness/manifests/java.json +15 -35
- package/resources/harness/migrations/0.1.1/general.json +358 -0
- package/resources/harness/migrations/0.1.1/java.json +398 -0
- package/resources/bootstrap-ir/manifest.json +0 -19
- package/resources/bootstrap-ir/skills/harness-apidoc.yaml +0 -24
- package/resources/bootstrap-ir/skills/harness-archive.yaml +0 -24
- package/resources/bootstrap-ir/skills/harness-codebase-map.yaml +0 -24
- package/resources/bootstrap-ir/skills/harness-knowledge-ingest.yaml +0 -24
- package/resources/bootstrap-ir/skills/harness-package.yaml +0 -24
- package/resources/bootstrap-ir/skills/harness-plan.yaml +0 -24
- package/resources/bootstrap-ir/skills/harness-review.yaml +0 -24
- package/resources/bootstrap-ir/skills/harness-run.yaml +0 -24
- package/resources/bootstrap-ir/skills/harness-skill-optimizer.yaml +0 -28
- package/resources/bootstrap-ir/skills/harness-submit.yaml +0 -24
- package/resources/bootstrap-ir/skills/harness-sync.yaml +0 -24
- package/resources/bootstrap-ir/skills/harness-test.yaml +0 -24
- package/resources/bootstrap-ir/templates/claude-code-skill.md +0 -12
- package/resources/harness/general/harness-knowledge-ingest/tests/fixtures/mcp-eval-project/.harness/archive/2026-01-10-ledger-reconciliation/reports/final/summary-data.json +0 -43
- package/resources/harness/general/harness-knowledge-ingest/tests/fixtures/mcp-eval-project/.harness/archive/2026-02-14-ledger-snapshot-followup/reports/final/summary-data.json +0 -38
- package/resources/harness/general/harness-knowledge-ingest/tests/fixtures/mcp-eval-project/.harness/archive/2026-03-05-webhook-contract/reports/final/summary-data.json +0 -36
- package/resources/harness/general/harness-knowledge-ingest/tests/fixtures/mcp-eval-project/.harness/project.yaml +0 -1
- package/resources/harness/general/harness-knowledge-ingest/tests/test_harness_knowledge.py +0 -1792
- package/resources/harness/java/harness-knowledge-ingest/tests/fixtures/mcp-eval-project/.harness/archive/2026-01-10-ledger-reconciliation/reports/final/summary-data.json +0 -43
- package/resources/harness/java/harness-knowledge-ingest/tests/fixtures/mcp-eval-project/.harness/archive/2026-02-14-ledger-snapshot-followup/reports/final/summary-data.json +0 -38
- package/resources/harness/java/harness-knowledge-ingest/tests/fixtures/mcp-eval-project/.harness/archive/2026-03-05-webhook-contract/reports/final/summary-data.json +0 -36
- package/resources/harness/java/harness-knowledge-ingest/tests/fixtures/mcp-eval-project/.harness/project.yaml +0 -1
- package/resources/harness/java/harness-knowledge-ingest/tests/test_harness_knowledge.py +0 -1792
- package/resources/manifest.json +0 -19
- package/resources/skills/harness-apidoc/SKILL.md +0 -50
- package/resources/skills/harness-archive/SKILL.md +0 -48
- package/resources/skills/harness-codebase-map/SKILL.md +0 -53
- package/resources/skills/harness-knowledge-ingest/SKILL.md +0 -48
- package/resources/skills/harness-package/SKILL.md +0 -48
- package/resources/skills/harness-plan/SKILL.md +0 -51
- package/resources/skills/harness-review/SKILL.md +0 -50
- package/resources/skills/harness-run/SKILL.md +0 -48
- package/resources/skills/harness-skill-optimizer/SKILL.md +0 -54
- package/resources/skills/harness-submit/SKILL.md +0 -47
- package/resources/skills/harness-sync/SKILL.md +0 -48
- package/resources/skills/harness-test/SKILL.md +0 -50
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(([,
|
|
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
|
|
793
|
-
const normalized = normalizeManagedPath(
|
|
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
|
|
821
|
-
if (
|
|
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((
|
|
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
|
}
|
|
@@ -1244,14 +1255,13 @@ function decideUpdate(policy, dirty) {
|
|
|
1244
1255
|
}
|
|
1245
1256
|
|
|
1246
1257
|
// ../core/dist/project/initialize.js
|
|
1247
|
-
import {
|
|
1248
|
-
import {
|
|
1249
|
-
import { basename, join as join4, resolve as resolve3 } from "node:path";
|
|
1258
|
+
import { readFile as readFile4, stat as stat2 } from "node:fs/promises";
|
|
1259
|
+
import { basename, join as join5, resolve as resolve3 } from "node:path";
|
|
1250
1260
|
import { parse as parseYaml2, stringify as stringifyYaml } from "yaml";
|
|
1251
1261
|
|
|
1252
1262
|
// ../core/dist/transaction/transaction.js
|
|
1253
1263
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
1254
|
-
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";
|
|
1255
1265
|
import { dirname as dirname2, join as join3 } from "node:path";
|
|
1256
1266
|
|
|
1257
1267
|
// ../core/dist/state/layout.js
|
|
@@ -1277,9 +1287,7 @@ async function ensureStateLayout(projectRoot) {
|
|
|
1277
1287
|
mkdir2(layout.baseline, { recursive: true }),
|
|
1278
1288
|
mkdir2(layout.transactions, { recursive: true }),
|
|
1279
1289
|
mkdir2(layout.locks, { recursive: true }),
|
|
1280
|
-
mkdir2(layout.local, { recursive: true })
|
|
1281
|
-
mkdir2(layout.serverArtifacts, { recursive: true }),
|
|
1282
|
-
mkdir2(layout.reports, { recursive: true })
|
|
1290
|
+
mkdir2(layout.local, { recursive: true })
|
|
1283
1291
|
]);
|
|
1284
1292
|
return layout;
|
|
1285
1293
|
}
|
|
@@ -1322,6 +1330,31 @@ async function writeJournal(transactionRoot, journal) {
|
|
|
1322
1330
|
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1323
1331
|
});
|
|
1324
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
|
+
}
|
|
1325
1358
|
async function snapshotPaths(projectRoot, transactionRoot, paths) {
|
|
1326
1359
|
const snapshots = [];
|
|
1327
1360
|
for (const path of paths) {
|
|
@@ -1461,7 +1494,6 @@ async function runTransaction(projectRoot, rawOperations, options = {}) {
|
|
|
1461
1494
|
await atomicWriteJson(join3(transactionRoot, "after", "manifest.json"), after);
|
|
1462
1495
|
journal.state = "committed";
|
|
1463
1496
|
await writeJournal(transactionRoot, journal);
|
|
1464
|
-
return { transactionId, status: "committed" };
|
|
1465
1497
|
} catch (error) {
|
|
1466
1498
|
if (error instanceof InterruptedTransactionError) {
|
|
1467
1499
|
throw error;
|
|
@@ -1471,6 +1503,186 @@ async function runTransaction(projectRoot, rawOperations, options = {}) {
|
|
|
1471
1503
|
await rollbackTransaction(projectRoot, transactionId);
|
|
1472
1504
|
throw error;
|
|
1473
1505
|
}
|
|
1506
|
+
await rm2(join3(transactionRoot, "staged"), { recursive: true, force: true });
|
|
1507
|
+
await pruneOlderSuccessful(layout, transactionId, options.kind);
|
|
1508
|
+
return { transactionId, status: "committed" };
|
|
1509
|
+
}
|
|
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
|
+
|
|
1532
|
+
// ../core/dist/project/profile-bundle.js
|
|
1533
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1534
|
+
import { readFile as readFile3, readdir as readdir2 } from "node:fs/promises";
|
|
1535
|
+
import { join as join4 } from "node:path";
|
|
1536
|
+
function isProfile(value) {
|
|
1537
|
+
return value === "general" || value === "java";
|
|
1538
|
+
}
|
|
1539
|
+
function validateRelativeBundlePath(path) {
|
|
1540
|
+
if (typeof path !== "string" || path.length === 0 || path.includes("\0") || path.startsWith("/") || path.startsWith("\\") || /^[A-Za-z]:/.test(path) || path.split("/").some((part) => part === "" || part === "." || part === "..")) {
|
|
1541
|
+
throw new Error("invalid Harness Bundle path");
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
async function loadProfileBundle(resourcesRoot, profile) {
|
|
1545
|
+
const raw = JSON.parse(await readFile3(join4(resourcesRoot, "harness", "manifests", `${profile}.json`), "utf8"));
|
|
1546
|
+
if (raw.schema_version !== 1 || raw.profile !== profile || typeof raw.bundle_version !== "string" || raw.generator !== "harness_deploy.py" || !Array.isArray(raw.files)) {
|
|
1547
|
+
throw new Error(`invalid ${profile} Harness Bundle manifest`);
|
|
1548
|
+
}
|
|
1549
|
+
const files = /* @__PURE__ */ new Map();
|
|
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)) {
|
|
1553
|
+
throw new Error(`invalid ${profile} Harness Bundle manifest entry`);
|
|
1554
|
+
}
|
|
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}`);
|
|
1558
|
+
}
|
|
1559
|
+
files.set(item2.path, bytes);
|
|
1560
|
+
}
|
|
1561
|
+
return { manifest: raw, files };
|
|
1562
|
+
}
|
|
1563
|
+
async function managedBundleTargets(resourcesRoot, profile) {
|
|
1564
|
+
const bundle = await loadProfileBundle(resourcesRoot, profile);
|
|
1565
|
+
const targets = new Set(bundleTargetContents(bundle).keys());
|
|
1566
|
+
if (profile === "java")
|
|
1567
|
+
targets.add(".claude/rules/harness-profile-java.md");
|
|
1568
|
+
return targets;
|
|
1569
|
+
}
|
|
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}`);
|
|
1580
|
+
}
|
|
1581
|
+
records.push({
|
|
1582
|
+
source_path: sourcePath,
|
|
1583
|
+
target_path: normalizeManagedPath(projectedTarget),
|
|
1584
|
+
sha256: manifestEntry.sha256,
|
|
1585
|
+
bytes
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
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));
|
|
1616
|
+
}
|
|
1617
|
+
function parseHarnessProfile(value) {
|
|
1618
|
+
return isProfile(value) ? value : null;
|
|
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;
|
|
1474
1686
|
}
|
|
1475
1687
|
|
|
1476
1688
|
// ../core/dist/project/uuid-v7.js
|
|
@@ -1498,7 +1710,7 @@ function uuidV7(now = Date.now()) {
|
|
|
1498
1710
|
// ../core/dist/project/initialize.js
|
|
1499
1711
|
async function readOptional(path) {
|
|
1500
1712
|
try {
|
|
1501
|
-
return await
|
|
1713
|
+
return await readFile4(path, "utf8");
|
|
1502
1714
|
} catch (error) {
|
|
1503
1715
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
1504
1716
|
return "";
|
|
@@ -1518,53 +1730,21 @@ async function exists2(path) {
|
|
|
1518
1730
|
}
|
|
1519
1731
|
}
|
|
1520
1732
|
async function existingProjectConfig(root) {
|
|
1521
|
-
const path =
|
|
1733
|
+
const path = join5(root, ".harness", "project.yaml");
|
|
1522
1734
|
const content = await readOptional(path);
|
|
1523
1735
|
return content === "" ? null : projectConfigSchema.parse(parseYaml2(content));
|
|
1524
1736
|
}
|
|
1525
1737
|
async function operationFor(root, path, content) {
|
|
1526
|
-
return await exists2(
|
|
1527
|
-
}
|
|
1528
|
-
async function readProfileBundle(resourcesRoot, profile) {
|
|
1529
|
-
const manifest = JSON.parse(await readFile3(join4(resourcesRoot, "harness", "manifests", `${profile}.json`), "utf8"));
|
|
1530
|
-
if (manifest.schema_version !== 1 || manifest.profile !== profile) {
|
|
1531
|
-
throw new Error(`invalid ${profile} Harness Bundle manifest`);
|
|
1532
|
-
}
|
|
1533
|
-
const files = /* @__PURE__ */ new Map();
|
|
1534
|
-
for (const item of manifest.files) {
|
|
1535
|
-
const bytes = await readFile3(join4(resourcesRoot, "harness", profile, item.path));
|
|
1536
|
-
if (createHash2("sha256").update(bytes).digest("hex") !== item.sha256) {
|
|
1537
|
-
throw new Error(`Harness Bundle hash mismatch: ${item.path}`);
|
|
1538
|
-
}
|
|
1539
|
-
files.set(item.path, bytes);
|
|
1540
|
-
}
|
|
1541
|
-
return { manifest, files };
|
|
1542
|
-
}
|
|
1543
|
-
function bundleTargets(bundleFiles) {
|
|
1544
|
-
const targets = /* @__PURE__ */ new Map();
|
|
1545
|
-
for (const [path, bytes] of bundleFiles) {
|
|
1546
|
-
targets.set(`.claude/skills/${path}`, bytes);
|
|
1547
|
-
const agent = /^agents\/([^/]+\.md)$/.exec(path);
|
|
1548
|
-
if (agent?.[1] !== void 0)
|
|
1549
|
-
targets.set(`.claude/agents/${agent[1]}`, bytes);
|
|
1550
|
-
}
|
|
1551
|
-
return targets;
|
|
1738
|
+
return await exists2(join5(root, path)) ? { operation: "modify", path, content } : { operation: "add", path, content };
|
|
1552
1739
|
}
|
|
1553
1740
|
var INSTALLED_BUNDLE_PATH = ".harness/state/local/installed-harness-bundle.json";
|
|
1554
|
-
async function previousInstalledFiles(root) {
|
|
1555
|
-
const content = await readOptional(join4(root, INSTALLED_BUNDLE_PATH));
|
|
1556
|
-
if (content === "")
|
|
1557
|
-
return [];
|
|
1558
|
-
const parsed = JSON.parse(content);
|
|
1559
|
-
return parsed.schema_version === 1 && Array.isArray(parsed.files) ? parsed.files : [];
|
|
1560
|
-
}
|
|
1561
1741
|
async function initializeProject(options) {
|
|
1562
1742
|
const root = resolve3(options.projectRoot);
|
|
1563
1743
|
const config = initConfigSchema.parse(options.config);
|
|
1564
1744
|
const existing = await existingProjectConfig(root);
|
|
1565
1745
|
const profile = config.profile;
|
|
1566
|
-
const bundle = await
|
|
1567
|
-
const
|
|
1746
|
+
const bundle = await loadProfileBundle(options.resourcesRoot, profile);
|
|
1747
|
+
const managed = await managedTargets(options.resourcesRoot, profile);
|
|
1568
1748
|
const bundleHash = sha256Bytes(canonicalJson(bundle.manifest.files));
|
|
1569
1749
|
const projectConfig = projectConfigSchema.parse({
|
|
1570
1750
|
harness: { name: "hunter-harness", schema_version: 1 },
|
|
@@ -1588,23 +1768,8 @@ async function initializeProject(options) {
|
|
|
1588
1768
|
artifact_manifest_hash: null,
|
|
1589
1769
|
files: {}
|
|
1590
1770
|
});
|
|
1591
|
-
const agentsContent = upsertManagedBlock(await readOptional(
|
|
1592
|
-
|
|
1593
|
-
"",
|
|
1594
|
-
"Use .harness/context-index.json to route rules, Knowledge, and codebase maps.",
|
|
1595
|
-
"Treat .claude/skills/harness-* as editable adapter working copies.",
|
|
1596
|
-
"Do not modify .harness/state or .harness/cache directly."
|
|
1597
|
-
].join("\n"));
|
|
1598
|
-
const claudeContent = upsertManagedBlock(await readOptional(join4(root, "CLAUDE.md")), [
|
|
1599
|
-
"@AGENTS.md",
|
|
1600
|
-
"",
|
|
1601
|
-
"# Hunter Harness",
|
|
1602
|
-
"",
|
|
1603
|
-
"- Rules: .claude/rules/",
|
|
1604
|
-
"- Skills: .claude/skills/harness-*/",
|
|
1605
|
-
"- Knowledge: .harness/knowledge/",
|
|
1606
|
-
"- Codebase map: .harness/codebase/map/"
|
|
1607
|
-
].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);
|
|
1608
1773
|
const files = /* @__PURE__ */ new Map([
|
|
1609
1774
|
[
|
|
1610
1775
|
".harness/project.yaml",
|
|
@@ -1632,44 +1797,29 @@ async function initializeProject(options) {
|
|
|
1632
1797
|
".harness/knowledge/index.json",
|
|
1633
1798
|
JSON.stringify({ schema_version: 1, generated_at: null, entries: [] }, null, 2) + "\n"
|
|
1634
1799
|
],
|
|
1635
|
-
[".harness/knowledge/_candidates/.gitkeep", ""],
|
|
1636
|
-
[".harness/knowledge/project-local/.gitkeep", ""],
|
|
1637
|
-
[".harness/codebase/map/.gitkeep", ""],
|
|
1638
|
-
[".harness/state/local/.gitkeep", ""],
|
|
1639
|
-
[".harness/cache/server-artifacts/.gitkeep", ""],
|
|
1640
|
-
[".harness/reports/.gitkeep", ""],
|
|
1641
|
-
[
|
|
1642
|
-
".harness/README.md",
|
|
1643
|
-
"# Hunter Harness\n\nUse npx hunter-harness, update, and push.\n"
|
|
1644
|
-
],
|
|
1645
1800
|
["AGENTS.md", agentsContent],
|
|
1646
|
-
["CLAUDE.md", claudeContent]
|
|
1647
|
-
[
|
|
1648
|
-
".claude/rules/harness-general.md",
|
|
1649
|
-
"# Hunter Harness Rules\n\n- Report evidence honestly.\n- Do not execute destructive actions without confirmation.\n"
|
|
1650
|
-
]
|
|
1801
|
+
["CLAUDE.md", claudeContent]
|
|
1651
1802
|
]);
|
|
1652
|
-
|
|
1653
|
-
files.set(
|
|
1654
|
-
}
|
|
1655
|
-
for (const [targetPath, bytes] of bundleFiles) {
|
|
1656
|
-
files.set(targetPath, bytes);
|
|
1803
|
+
for (const target of managed) {
|
|
1804
|
+
files.set(target.target_path, target.bytes);
|
|
1657
1805
|
}
|
|
1658
|
-
const
|
|
1659
|
-
|
|
1660
|
-
installedFileList.push(".claude/rules/harness-profile-java.md");
|
|
1661
|
-
const installedManifest = {
|
|
1662
|
-
schema_version: 1,
|
|
1806
|
+
const installedState = {
|
|
1807
|
+
schema_version: 2,
|
|
1663
1808
|
profile,
|
|
1664
|
-
|
|
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))
|
|
1665
1817
|
};
|
|
1666
|
-
files.set(INSTALLED_BUNDLE_PATH, JSON.stringify(
|
|
1667
|
-
const newManaged = new Set(installedManifest.files);
|
|
1668
|
-
const deleteOperations = (await previousInstalledFiles(root)).filter((path) => !newManaged.has(path)).map((path) => ({ operation: "delete", path }));
|
|
1818
|
+
files.set(INSTALLED_BUNDLE_PATH, JSON.stringify(installedState, null, 2) + "\n");
|
|
1669
1819
|
const paths = [...files.keys()].sort((left, right) => left.localeCompare(right));
|
|
1670
1820
|
if (!options.dryRun) {
|
|
1671
1821
|
const writeOperations = await Promise.all([...files.entries()].map(async ([path, content]) => operationFor(root, path, content)));
|
|
1672
|
-
await runTransaction(root,
|
|
1822
|
+
await runTransaction(root, writeOperations, { kind: "init" });
|
|
1673
1823
|
}
|
|
1674
1824
|
return {
|
|
1675
1825
|
projectConfig,
|
|
@@ -1679,6 +1829,325 @@ async function initializeProject(options) {
|
|
|
1679
1829
|
};
|
|
1680
1830
|
}
|
|
1681
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
|
+
|
|
1682
2151
|
// ../core/dist/proposal/diff.js
|
|
1683
2152
|
function operationPath(operation) {
|
|
1684
2153
|
return operation.operation === "rename" ? operation.to_path : operation.path;
|
|
@@ -1822,7 +2291,7 @@ function highEntropyCandidates(content) {
|
|
|
1822
2291
|
value: match[0],
|
|
1823
2292
|
offset: match.index,
|
|
1824
2293
|
entropy: shannonEntropy(match[0])
|
|
1825
|
-
})).filter((
|
|
2294
|
+
})).filter((item2) => item2.entropy >= 4.5);
|
|
1826
2295
|
}
|
|
1827
2296
|
|
|
1828
2297
|
// ../core/dist/security/scanner.js
|
|
@@ -1918,8 +2387,8 @@ function scanSensitiveFiles(files, options = {}) {
|
|
|
1918
2387
|
sha256Bytes(raw.value)
|
|
1919
2388
|
].join("\0"));
|
|
1920
2389
|
const overridable = raw.severity !== "high";
|
|
1921
|
-
const explicit = options.overrides?.find((
|
|
1922
|
-
const inline = ignores.find((
|
|
2390
|
+
const explicit = options.overrides?.find((item2) => item2.finding_fingerprint === fingerprint);
|
|
2391
|
+
const inline = ignores.find((item2) => item2.ruleId === raw.ruleId);
|
|
1923
2392
|
const override = overridable ? explicit ?? (inline === void 0 ? void 0 : {
|
|
1924
2393
|
finding_fingerprint: fingerprint,
|
|
1925
2394
|
actor: "inline-annotation",
|
|
@@ -1977,28 +2446,28 @@ function generateProposalPreview(input, scanOptions = {}) {
|
|
|
1977
2446
|
}
|
|
1978
2447
|
|
|
1979
2448
|
// ../core/dist/push/push.js
|
|
1980
|
-
import { lstat as lstat2, readFile as
|
|
1981
|
-
import { join as
|
|
1982
|
-
import { parse as
|
|
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";
|
|
1983
2452
|
|
|
1984
2453
|
// ../core/dist/state/baseline.js
|
|
1985
|
-
import { readFile as
|
|
1986
|
-
import { join as
|
|
2454
|
+
import { readFile as readFile6 } from "node:fs/promises";
|
|
2455
|
+
import { join as join7 } from "node:path";
|
|
1987
2456
|
async function readBaseline(projectRoot) {
|
|
1988
|
-
const content = await
|
|
2457
|
+
const content = await readFile6(join7(stateLayout(projectRoot).baseline, "manifest.json"), "utf8");
|
|
1989
2458
|
return baselineManifestSchema.parse(JSON.parse(content));
|
|
1990
2459
|
}
|
|
1991
2460
|
|
|
1992
2461
|
// ../core/dist/state/locks.js
|
|
1993
2462
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
1994
|
-
import { readFile as
|
|
1995
|
-
import { join as
|
|
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";
|
|
1996
2465
|
async function readLock(path) {
|
|
1997
|
-
return JSON.parse(await
|
|
2466
|
+
return JSON.parse(await readFile7(path, "utf8"));
|
|
1998
2467
|
}
|
|
1999
2468
|
async function acquireProtocolLock(projectRoot, operation, options = {}) {
|
|
2000
2469
|
const layout = await ensureStateLayout(projectRoot);
|
|
2001
|
-
const lockPath =
|
|
2470
|
+
const lockPath = join8(layout.locks, "protocol.lock");
|
|
2002
2471
|
const now = options.now ?? Date.now();
|
|
2003
2472
|
const staleAfterMs = options.staleAfterMs ?? 15 * 60 * 1e3;
|
|
2004
2473
|
const record = {
|
|
@@ -2061,23 +2530,7 @@ var MANAGED_FILES = [
|
|
|
2061
2530
|
".harness/project.yaml",
|
|
2062
2531
|
".harness/context-index.json"
|
|
2063
2532
|
];
|
|
2064
|
-
|
|
2065
|
-
async function installedBundlePaths(root) {
|
|
2066
|
-
try {
|
|
2067
|
-
const content = await readFile6(join7(root, INSTALLED_BUNDLE_PATH2), "utf8");
|
|
2068
|
-
const parsed = JSON.parse(content);
|
|
2069
|
-
if (parsed.schema_version === 1 && Array.isArray(parsed.files)) {
|
|
2070
|
-
return new Set(parsed.files);
|
|
2071
|
-
}
|
|
2072
|
-
} catch (error) {
|
|
2073
|
-
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
2074
|
-
return /* @__PURE__ */ new Set();
|
|
2075
|
-
}
|
|
2076
|
-
throw error;
|
|
2077
|
-
}
|
|
2078
|
-
return /* @__PURE__ */ new Set();
|
|
2079
|
-
}
|
|
2080
|
-
async function exists3(path) {
|
|
2533
|
+
async function exists4(path) {
|
|
2081
2534
|
try {
|
|
2082
2535
|
await lstat2(path);
|
|
2083
2536
|
return true;
|
|
@@ -2089,43 +2542,43 @@ async function exists3(path) {
|
|
|
2089
2542
|
}
|
|
2090
2543
|
}
|
|
2091
2544
|
async function walkFiles(root, current, output) {
|
|
2092
|
-
if (!await
|
|
2545
|
+
if (!await exists4(current)) {
|
|
2093
2546
|
return;
|
|
2094
2547
|
}
|
|
2095
|
-
for (const
|
|
2096
|
-
const path =
|
|
2097
|
-
if (
|
|
2548
|
+
for (const item2 of await readdir4(current, { withFileTypes: true })) {
|
|
2549
|
+
const path = join9(current, item2.name);
|
|
2550
|
+
if (item2.isSymbolicLink()) {
|
|
2098
2551
|
throw new PushWorkflowError("symlink is not pushable", 6, "UNSAFE_SYMLINK");
|
|
2099
2552
|
}
|
|
2100
|
-
if (
|
|
2553
|
+
if (item2.isDirectory()) {
|
|
2101
2554
|
await walkFiles(root, path, output);
|
|
2102
|
-
} else if (
|
|
2555
|
+
} else if (item2.isFile()) {
|
|
2103
2556
|
output.push(normalizeManagedPath(relative(root, path).replaceAll("\\", "/")));
|
|
2104
2557
|
}
|
|
2105
2558
|
}
|
|
2106
2559
|
}
|
|
2107
2560
|
async function managedFiles(projectRoot) {
|
|
2108
|
-
const root =
|
|
2561
|
+
const root = resolve5(projectRoot);
|
|
2109
2562
|
const paths = [];
|
|
2110
2563
|
for (const path of MANAGED_FILES) {
|
|
2111
|
-
if (await
|
|
2564
|
+
if (await exists4(join9(root, path))) {
|
|
2112
2565
|
paths.push(path);
|
|
2113
2566
|
}
|
|
2114
2567
|
}
|
|
2115
2568
|
for (const path of MANAGED_ROOTS) {
|
|
2116
|
-
await walkFiles(root,
|
|
2569
|
+
await walkFiles(root, join9(root, path), paths);
|
|
2117
2570
|
}
|
|
2118
|
-
const skillsRoot =
|
|
2119
|
-
if (await
|
|
2120
|
-
for (const
|
|
2121
|
-
if (
|
|
2122
|
-
await walkFiles(root,
|
|
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);
|
|
2123
2576
|
}
|
|
2124
2577
|
}
|
|
2125
2578
|
}
|
|
2126
2579
|
const result = {};
|
|
2127
2580
|
for (const path of [...new Set(paths)].sort()) {
|
|
2128
|
-
result[path] = await
|
|
2581
|
+
result[path] = await readFile8(join9(root, path), "utf8");
|
|
2129
2582
|
}
|
|
2130
2583
|
return result;
|
|
2131
2584
|
}
|
|
@@ -2134,14 +2587,14 @@ function proposalBaseline(baseline) {
|
|
|
2134
2587
|
}
|
|
2135
2588
|
async function readProject(root) {
|
|
2136
2589
|
try {
|
|
2137
|
-
return projectConfigSchema.parse(
|
|
2590
|
+
return projectConfigSchema.parse(parseYaml4(await readFile8(join9(root, ".harness", "project.yaml"), "utf8")));
|
|
2138
2591
|
} catch {
|
|
2139
2592
|
throw new PushWorkflowError("project configuration is missing or invalid", 3, "PROJECT_CONFIG_INVALID");
|
|
2140
2593
|
}
|
|
2141
2594
|
}
|
|
2142
2595
|
async function readOptionalJson(path) {
|
|
2143
2596
|
try {
|
|
2144
|
-
return JSON.parse(await
|
|
2597
|
+
return JSON.parse(await readFile8(path, "utf8"));
|
|
2145
2598
|
} catch (error) {
|
|
2146
2599
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
2147
2600
|
return null;
|
|
@@ -2153,7 +2606,7 @@ async function clientIdFor(root, explicit) {
|
|
|
2153
2606
|
if (explicit !== void 0) {
|
|
2154
2607
|
return explicit;
|
|
2155
2608
|
}
|
|
2156
|
-
const path =
|
|
2609
|
+
const path = join9(root, ".harness", "state", "local", "client.json");
|
|
2157
2610
|
const existing = await readOptionalJson(path);
|
|
2158
2611
|
if (typeof existing?.client_id === "string" && /^cli_[A-Za-z0-9_-]+$/.test(existing.client_id)) {
|
|
2159
2612
|
return existing.client_id;
|
|
@@ -2225,7 +2678,7 @@ async function bindProject(root, project, baseline, projectId) {
|
|
|
2225
2678
|
{
|
|
2226
2679
|
operation: "modify",
|
|
2227
2680
|
path: ".harness/project.yaml",
|
|
2228
|
-
content:
|
|
2681
|
+
content: stringifyYaml3(nextProject, { sortMapEntries: true })
|
|
2229
2682
|
},
|
|
2230
2683
|
{
|
|
2231
2684
|
operation: "modify",
|
|
@@ -2236,10 +2689,11 @@ async function bindProject(root, project, baseline, projectId) {
|
|
|
2236
2689
|
return { project: nextProject, baseline: nextBaseline };
|
|
2237
2690
|
}
|
|
2238
2691
|
async function pushProject(options) {
|
|
2239
|
-
const root =
|
|
2692
|
+
const root = resolve5(options.projectRoot);
|
|
2240
2693
|
let project = await readProject(root);
|
|
2241
2694
|
let baseline = await readBaseline(root);
|
|
2242
|
-
const
|
|
2695
|
+
const profile = parseHarnessProfile(project.project.profiles[0]);
|
|
2696
|
+
const installedPaths = profile === null ? /* @__PURE__ */ new Set() : await managedBundleTargets(options.resourcesRoot, profile);
|
|
2243
2697
|
let preview = makePreview(baseline, await managedFiles(root), options.confirmedProjectLocal ?? [], installedPaths);
|
|
2244
2698
|
if (preview.blocked) {
|
|
2245
2699
|
throw new PushWorkflowError("sensitive information scan blocked the proposal", 6, "SENSITIVE_CONTENT_BLOCKED");
|
|
@@ -2271,7 +2725,7 @@ async function pushProject(options) {
|
|
|
2271
2725
|
if (parsedServerUrl.protocol !== "https:") {
|
|
2272
2726
|
throw new PushWorkflowError("server_url must use HTTPS", 3, "SERVER_URL_INVALID");
|
|
2273
2727
|
}
|
|
2274
|
-
const workflowPath =
|
|
2728
|
+
const workflowPath = join9(root, ".harness", "state", "local", "push-workflow.json");
|
|
2275
2729
|
const priorWorkflow = await readOptionalJson(workflowPath);
|
|
2276
2730
|
const provisionalRequestId = priorWorkflow?.local_project_key === project.project.local_project_key ? priorWorkflow.request_id : uuidV7();
|
|
2277
2731
|
const lock = await acquireProtocolLock(root, "push", { requestId: provisionalRequestId });
|
|
@@ -2362,7 +2816,7 @@ async function pushProject(options) {
|
|
|
2362
2816
|
}
|
|
2363
2817
|
}
|
|
2364
2818
|
const finalized = await client.finalizeProposal(session.session_id, { schema_version: 1, manifest_sha256: proposalManifestHash }, requestId, workflow.finalize_idempotency_key);
|
|
2365
|
-
await atomicWriteJson(
|
|
2819
|
+
await atomicWriteJson(join9(root, ".harness", "state", "local", "push-results", finalized.proposal_id + ".json"), {
|
|
2366
2820
|
schema_version: 1,
|
|
2367
2821
|
request_id: requestId,
|
|
2368
2822
|
project_id: projectId,
|
|
@@ -2387,12 +2841,75 @@ async function pushProject(options) {
|
|
|
2387
2841
|
}
|
|
2388
2842
|
}
|
|
2389
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
|
+
|
|
2390
2907
|
// ../core/dist/skill/frontmatter.js
|
|
2391
|
-
import { parse as
|
|
2908
|
+
import { parse as parseYaml5 } from "yaml";
|
|
2392
2909
|
import { z as z12 } from "zod";
|
|
2393
2910
|
|
|
2394
2911
|
// ../core/dist/skill/fixer.js
|
|
2395
|
-
import { parse as
|
|
2912
|
+
import { parse as parseYaml6, stringify as stringifyYaml4 } from "yaml";
|
|
2396
2913
|
|
|
2397
2914
|
// ../core/dist/skill/agents.js
|
|
2398
2915
|
var AGENT_DESCRIPTORS = {
|
|
@@ -2431,10 +2948,10 @@ var AGENT_DESCRIPTORS = {
|
|
|
2431
2948
|
var INSTALLABLE_AGENTS = Object.keys(AGENT_DESCRIPTORS).filter((agent) => AGENT_DESCRIPTORS[agent]?.installable === true);
|
|
2432
2949
|
|
|
2433
2950
|
// ../core/dist/transaction/recovery.js
|
|
2434
|
-
import { readFile as
|
|
2435
|
-
import { join as
|
|
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";
|
|
2436
2953
|
async function recoverTransaction(projectRoot, transactionId) {
|
|
2437
|
-
const journal = JSON.parse(await
|
|
2954
|
+
const journal = JSON.parse(await readFile10(join11(stateLayout(projectRoot).transactions, transactionId, "journal.json"), "utf8"));
|
|
2438
2955
|
if (journal.state === "committed") {
|
|
2439
2956
|
return { transactionId, status: "committed" };
|
|
2440
2957
|
}
|
|
@@ -2451,7 +2968,7 @@ async function listTransactions(projectRoot) {
|
|
|
2451
2968
|
const root = stateLayout(projectRoot).transactions;
|
|
2452
2969
|
let names;
|
|
2453
2970
|
try {
|
|
2454
|
-
names = await
|
|
2971
|
+
names = await readdir6(root);
|
|
2455
2972
|
} catch (error) {
|
|
2456
2973
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
2457
2974
|
return [];
|
|
@@ -2461,7 +2978,7 @@ async function listTransactions(projectRoot) {
|
|
|
2461
2978
|
const transactions = [];
|
|
2462
2979
|
for (const transactionId of names) {
|
|
2463
2980
|
try {
|
|
2464
|
-
const journal = JSON.parse(await
|
|
2981
|
+
const journal = JSON.parse(await readFile10(join11(root, transactionId, "journal.json"), "utf8"));
|
|
2465
2982
|
transactions.push({
|
|
2466
2983
|
transactionId,
|
|
2467
2984
|
kind: journal.kind,
|
|
@@ -2474,11 +2991,11 @@ async function listTransactions(projectRoot) {
|
|
|
2474
2991
|
return transactions.sort((left, right) => right.createdAt.localeCompare(left.createdAt));
|
|
2475
2992
|
}
|
|
2476
2993
|
async function pendingTransactions(projectRoot) {
|
|
2477
|
-
return (await listTransactions(projectRoot)).filter((
|
|
2994
|
+
return (await listTransactions(projectRoot)).filter((item2) => RECOVERY_STATES.has(item2.state));
|
|
2478
2995
|
}
|
|
2479
2996
|
async function pathExists(path) {
|
|
2480
2997
|
try {
|
|
2481
|
-
await
|
|
2998
|
+
await stat4(path);
|
|
2482
2999
|
return true;
|
|
2483
3000
|
} catch (error) {
|
|
2484
3001
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
@@ -2488,17 +3005,17 @@ async function pathExists(path) {
|
|
|
2488
3005
|
}
|
|
2489
3006
|
}
|
|
2490
3007
|
async function rollbackLatestCommittedUpdate(projectRoot) {
|
|
2491
|
-
const latest = (await listTransactions(projectRoot)).find((
|
|
3008
|
+
const latest = (await listTransactions(projectRoot)).find((item2) => item2.kind === "update" && item2.state === "committed");
|
|
2492
3009
|
if (latest === void 0) {
|
|
2493
3010
|
throw new Error("no committed update transaction is available for rollback");
|
|
2494
3011
|
}
|
|
2495
|
-
const transactionRoot =
|
|
2496
|
-
const journal = JSON.parse(await
|
|
2497
|
-
const after = JSON.parse(await
|
|
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"));
|
|
2498
3015
|
for (const entry of after) {
|
|
2499
|
-
const target =
|
|
2500
|
-
const
|
|
2501
|
-
if (
|
|
3016
|
+
const target = join11(projectRoot, entry.path);
|
|
3017
|
+
const exists6 = await pathExists(target);
|
|
3018
|
+
if (exists6 !== entry.exists || exists6 && await sha256File(target) !== entry.hash) {
|
|
2502
3019
|
throw new Error("cannot rollback dirty path: " + entry.path);
|
|
2503
3020
|
}
|
|
2504
3021
|
}
|
|
@@ -2509,16 +3026,16 @@ async function rollbackLatestCommittedUpdate(projectRoot) {
|
|
|
2509
3026
|
continue;
|
|
2510
3027
|
}
|
|
2511
3028
|
seen.add(snapshot.path);
|
|
2512
|
-
const target =
|
|
2513
|
-
const
|
|
3029
|
+
const target = join11(projectRoot, snapshot.path);
|
|
3030
|
+
const exists6 = await pathExists(target);
|
|
2514
3031
|
if (snapshot.existed && snapshot.snapshot_name !== null) {
|
|
2515
|
-
const content = await
|
|
3032
|
+
const content = await readFile10(join11(transactionRoot, "before", snapshot.snapshot_name));
|
|
2516
3033
|
operations.push({
|
|
2517
|
-
operation:
|
|
3034
|
+
operation: exists6 ? "modify" : "add",
|
|
2518
3035
|
path: snapshot.path,
|
|
2519
3036
|
content
|
|
2520
3037
|
});
|
|
2521
|
-
} else if (
|
|
3038
|
+
} else if (exists6) {
|
|
2522
3039
|
operations.push({ operation: "delete", path: snapshot.path });
|
|
2523
3040
|
}
|
|
2524
3041
|
}
|
|
@@ -2529,23 +3046,23 @@ async function rollbackLatestCommittedUpdate(projectRoot) {
|
|
|
2529
3046
|
}
|
|
2530
3047
|
async function cleanupOldTransactions(projectRoot, now = /* @__PURE__ */ new Date()) {
|
|
2531
3048
|
const transactions = await listTransactions(projectRoot);
|
|
2532
|
-
const committedUpdates = transactions.filter((
|
|
2533
|
-
const keepCommitted = new Set(committedUpdates.slice(0, 10).map((
|
|
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));
|
|
2534
3051
|
const removed = [];
|
|
2535
|
-
for (const
|
|
2536
|
-
if (RECOVERY_STATES.has(
|
|
3052
|
+
for (const item2 of transactions) {
|
|
3053
|
+
if (RECOVERY_STATES.has(item2.state) || keepCommitted.has(item2.transactionId)) {
|
|
2537
3054
|
continue;
|
|
2538
3055
|
}
|
|
2539
|
-
const ageDays = (now.getTime() - Date.parse(
|
|
2540
|
-
const removable =
|
|
3056
|
+
const ageDays = (now.getTime() - Date.parse(item2.createdAt)) / 864e5;
|
|
3057
|
+
const removable = item2.state === "rolled_back" ? ageDays > 7 : item2.state === "committed" && ageDays > 30;
|
|
2541
3058
|
if (!removable) {
|
|
2542
3059
|
continue;
|
|
2543
3060
|
}
|
|
2544
|
-
await
|
|
3061
|
+
await rm6(join11(stateLayout(projectRoot).transactions, item2.transactionId), {
|
|
2545
3062
|
recursive: true,
|
|
2546
3063
|
force: true
|
|
2547
3064
|
});
|
|
2548
|
-
removed.push(
|
|
3065
|
+
removed.push(item2.transactionId);
|
|
2549
3066
|
}
|
|
2550
3067
|
return removed;
|
|
2551
3068
|
}
|
|
@@ -2573,9 +3090,9 @@ function managedBlockDirty(currentContent, managedBlockHash) {
|
|
|
2573
3090
|
}
|
|
2574
3091
|
|
|
2575
3092
|
// ../core/dist/update/update.js
|
|
2576
|
-
import { lstat as lstat3, readFile as
|
|
2577
|
-
import { join as
|
|
2578
|
-
import { parse as
|
|
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";
|
|
2579
3096
|
var UpdateWorkflowError = class extends Error {
|
|
2580
3097
|
exitCode;
|
|
2581
3098
|
code;
|
|
@@ -2598,7 +3115,7 @@ async function pathExists2(path) {
|
|
|
2598
3115
|
}
|
|
2599
3116
|
}
|
|
2600
3117
|
async function optionalContent(path) {
|
|
2601
|
-
return await pathExists2(path) ?
|
|
3118
|
+
return await pathExists2(path) ? readFile11(path, "utf8") : null;
|
|
2602
3119
|
}
|
|
2603
3120
|
function manifestPayloadHash(manifest) {
|
|
2604
3121
|
const payload = { ...manifest };
|
|
@@ -2616,14 +3133,14 @@ async function loadBlob(root, client, artifactId, operation, requestId, dryRun)
|
|
|
2616
3133
|
return null;
|
|
2617
3134
|
}
|
|
2618
3135
|
const hash = operation.content_sha256;
|
|
2619
|
-
const cacheRoot =
|
|
2620
|
-
const cachePath =
|
|
3136
|
+
const cacheRoot = join12(root, ".harness", "cache", "server-artifacts", artifactId);
|
|
3137
|
+
const cachePath = join12(cacheRoot, "blobs", hash.replace(":", "_"));
|
|
2621
3138
|
if (await pathExists2(cachePath) && await sha256File(cachePath) === hash) {
|
|
2622
|
-
return
|
|
3139
|
+
return readFile11(cachePath, "utf8");
|
|
2623
3140
|
}
|
|
2624
3141
|
const bytes = await client.downloadArtifactBlob(artifactId, hash, requestId);
|
|
2625
3142
|
if (bytes.byteLength !== operation.size_bytes || sha256Bytes(bytes) !== hash) {
|
|
2626
|
-
await
|
|
3143
|
+
await rm7(cachePath, { force: true });
|
|
2627
3144
|
throw new UpdateWorkflowError("artifact blob size or hash mismatch", 4, "ARTIFACT_HASH_MISMATCH");
|
|
2628
3145
|
}
|
|
2629
3146
|
if (!dryRun) {
|
|
@@ -2642,34 +3159,34 @@ function nextBaselineEntry(operation, finalContent, projectVersion) {
|
|
|
2642
3159
|
...block === null ? {} : { managed_block_hash: sha256Bytes(block) }
|
|
2643
3160
|
};
|
|
2644
3161
|
}
|
|
2645
|
-
function transactionOperation(
|
|
2646
|
-
if (
|
|
3162
|
+
function transactionOperation(item2) {
|
|
3163
|
+
if (item2.equivalent) {
|
|
2647
3164
|
return null;
|
|
2648
3165
|
}
|
|
2649
|
-
const operation =
|
|
3166
|
+
const operation = item2.operation;
|
|
2650
3167
|
const target = operationTargetPath(operation);
|
|
2651
3168
|
if (operation.operation === "delete") {
|
|
2652
|
-
return
|
|
3169
|
+
return item2.finalContent === null ? { operation: "delete", path: target } : { operation: "modify", path: target, content: item2.finalContent };
|
|
2653
3170
|
}
|
|
2654
3171
|
if (operation.operation === "rename") {
|
|
2655
3172
|
return {
|
|
2656
3173
|
operation: "rename",
|
|
2657
3174
|
from_path: operation.from_path,
|
|
2658
3175
|
to_path: operation.to_path,
|
|
2659
|
-
content:
|
|
3176
|
+
content: item2.finalContent ?? item2.content ?? ""
|
|
2660
3177
|
};
|
|
2661
3178
|
}
|
|
2662
3179
|
return {
|
|
2663
3180
|
operation: "modify",
|
|
2664
3181
|
path: target,
|
|
2665
|
-
content:
|
|
3182
|
+
content: item2.finalContent ?? item2.content ?? ""
|
|
2666
3183
|
};
|
|
2667
3184
|
}
|
|
2668
3185
|
async function updateProject(options) {
|
|
2669
|
-
const root =
|
|
3186
|
+
const root = resolve6(options.projectRoot);
|
|
2670
3187
|
let project;
|
|
2671
3188
|
try {
|
|
2672
|
-
project = projectConfigSchema.parse(
|
|
3189
|
+
project = projectConfigSchema.parse(parseYaml7(await readFile11(join12(root, ".harness", "project.yaml"), "utf8")));
|
|
2673
3190
|
} catch {
|
|
2674
3191
|
throw new UpdateWorkflowError("project configuration is invalid", 3, "PROJECT_CONFIG_INVALID");
|
|
2675
3192
|
}
|
|
@@ -2754,8 +3271,8 @@ async function updateProject(options) {
|
|
|
2754
3271
|
skipped.push({ path: target, operation: operation.operation, reason: "baseline-diverged" });
|
|
2755
3272
|
continue;
|
|
2756
3273
|
}
|
|
2757
|
-
const sourceContent = await optionalContent(
|
|
2758
|
-
const targetContent = target === source ? sourceContent : await optionalContent(
|
|
3274
|
+
const sourceContent = await optionalContent(join12(root, source));
|
|
3275
|
+
const targetContent = target === source ? sourceContent : await optionalContent(join12(root, target));
|
|
2759
3276
|
const incoming = blobs.get(operation) ?? null;
|
|
2760
3277
|
const incomingHash = contentHash(operation);
|
|
2761
3278
|
let equivalent = incomingHash !== null && targetContent !== null && sha256Bytes(targetContent) === incomingHash;
|
|
@@ -2800,7 +3317,7 @@ async function updateProject(options) {
|
|
|
2800
3317
|
}
|
|
2801
3318
|
prepared.push({ operation, content: incoming, finalContent, equivalent });
|
|
2802
3319
|
}
|
|
2803
|
-
const applied = prepared.map((
|
|
3320
|
+
const applied = prepared.map((item2) => operationTargetPath(item2.operation));
|
|
2804
3321
|
if (options.dryRun) {
|
|
2805
3322
|
return {
|
|
2806
3323
|
requestId,
|
|
@@ -2814,18 +3331,18 @@ async function updateProject(options) {
|
|
|
2814
3331
|
dryRun: true
|
|
2815
3332
|
};
|
|
2816
3333
|
}
|
|
2817
|
-
await atomicWriteJson(
|
|
3334
|
+
await atomicWriteJson(join12(root, ".harness", "cache", "server-artifacts", manifest.artifact_id, "manifest.json"), manifest);
|
|
2818
3335
|
const lock = await acquireProtocolLock(root, "update", { requestId });
|
|
2819
3336
|
try {
|
|
2820
3337
|
const nextBaseline = baselineManifestSchema.parse(structuredClone(baseline));
|
|
2821
|
-
for (const
|
|
2822
|
-
const target = operationTargetPath(
|
|
2823
|
-
nextBaseline.files[target] = nextBaselineEntry(
|
|
2824
|
-
if (
|
|
2825
|
-
nextBaseline.files[
|
|
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] = {
|
|
2826
3343
|
baseline_hash: null,
|
|
2827
3344
|
local_hash_at_apply: null,
|
|
2828
|
-
file_kind:
|
|
3345
|
+
file_kind: item2.operation.file_kind,
|
|
2829
3346
|
last_applied_version: manifest.project_version,
|
|
2830
3347
|
deleted: true
|
|
2831
3348
|
};
|
|
@@ -2847,7 +3364,7 @@ async function updateProject(options) {
|
|
|
2847
3364
|
skipped,
|
|
2848
3365
|
transaction_id: transactionId
|
|
2849
3366
|
};
|
|
2850
|
-
const operations = prepared.map((
|
|
3367
|
+
const operations = prepared.map((item2) => transactionOperation(item2)).filter((item2) => item2 !== null);
|
|
2851
3368
|
operations.push({
|
|
2852
3369
|
operation: "modify",
|
|
2853
3370
|
path: ".harness/state/baseline/manifest.json",
|
|
@@ -2895,8 +3412,8 @@ async function updateProject(options) {
|
|
|
2895
3412
|
}
|
|
2896
3413
|
|
|
2897
3414
|
// src/config/init-config.ts
|
|
2898
|
-
import { isAbsolute as isAbsolute2, join as
|
|
2899
|
-
import { readFile as
|
|
3415
|
+
import { isAbsolute as isAbsolute2, join as join13 } from "node:path";
|
|
3416
|
+
import { readFile as readFile12 } from "node:fs/promises";
|
|
2900
3417
|
var InitConfigurationError = class extends Error {
|
|
2901
3418
|
exitCode;
|
|
2902
3419
|
constructor(message, exitCode = 3, options) {
|
|
@@ -2914,9 +3431,9 @@ function normalizeProfile(value) {
|
|
|
2914
3431
|
async function resolveInitConfig(cwd, flags, promptMissing) {
|
|
2915
3432
|
let fileConfig = {};
|
|
2916
3433
|
if (flags.config !== void 0) {
|
|
2917
|
-
const path = isAbsolute2(flags.config) ? flags.config :
|
|
3434
|
+
const path = isAbsolute2(flags.config) ? flags.config : join13(cwd, flags.config);
|
|
2918
3435
|
try {
|
|
2919
|
-
fileConfig = JSON.parse(await
|
|
3436
|
+
fileConfig = JSON.parse(await readFile12(path, "utf8"));
|
|
2920
3437
|
} catch (error) {
|
|
2921
3438
|
throw new InitConfigurationError(
|
|
2922
3439
|
"unable to read init config: " + (error instanceof Error ? error.message : String(error)),
|
|
@@ -2958,8 +3475,175 @@ function serializeCliResult(result) {
|
|
|
2958
3475
|
return JSON.stringify(result) + "\n";
|
|
2959
3476
|
}
|
|
2960
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
|
+
|
|
2961
3642
|
// src/commands/configure.ts
|
|
2962
|
-
|
|
3643
|
+
function otherProfile(current) {
|
|
3644
|
+
return current === "general" ? "java" : "general";
|
|
3645
|
+
}
|
|
3646
|
+
async function runFirstInstall(options, dependencies) {
|
|
2963
3647
|
const requestId = uuidV7();
|
|
2964
3648
|
try {
|
|
2965
3649
|
const config = await resolveInitConfig(
|
|
@@ -3016,6 +3700,123 @@ async function runConfigure(options, dependencies) {
|
|
|
3016
3700
|
return exitCode;
|
|
3017
3701
|
}
|
|
3018
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
|
+
}
|
|
3019
3820
|
|
|
3020
3821
|
// src/commands/push.ts
|
|
3021
3822
|
async function runPush(options, dependencies) {
|
|
@@ -3027,6 +3828,7 @@ async function runPush(options, dependencies) {
|
|
|
3027
3828
|
try {
|
|
3028
3829
|
const result = await pushProject({
|
|
3029
3830
|
projectRoot: dependencies.cwd,
|
|
3831
|
+
resourcesRoot: dependencies.resourcesRoot,
|
|
3030
3832
|
...options.serverUrl === void 0 ? {} : { serverUrl: options.serverUrl },
|
|
3031
3833
|
...options.tokenEnv === void 0 ? {} : { tokenEnv: options.tokenEnv },
|
|
3032
3834
|
env: dependencies.env,
|
|
@@ -3125,7 +3927,7 @@ async function runUpdate(options, dependencies) {
|
|
|
3125
3927
|
result = await execute(options.dryRun === true);
|
|
3126
3928
|
}
|
|
3127
3929
|
const applied = new Set(result.applied);
|
|
3128
|
-
const skippedByPath = new Map(result.skipped.map((
|
|
3930
|
+
const skippedByPath = new Map(result.skipped.map((item2) => [item2.path, item2]));
|
|
3129
3931
|
const items = result.operations.map((operation) => {
|
|
3130
3932
|
const path = operation.operation === "rename" ? operation.to_path : operation.path;
|
|
3131
3933
|
const skipped = skippedByPath.get(path);
|
|
@@ -3187,11 +3989,11 @@ async function runUpdate(options, dependencies) {
|
|
|
3187
3989
|
}
|
|
3188
3990
|
|
|
3189
3991
|
// src/commands/recovery.ts
|
|
3190
|
-
import { stat as
|
|
3191
|
-
import { join as
|
|
3192
|
-
async function
|
|
3992
|
+
import { stat as stat5 } from "node:fs/promises";
|
|
3993
|
+
import { join as join15 } from "node:path";
|
|
3994
|
+
async function exists5(path) {
|
|
3193
3995
|
try {
|
|
3194
|
-
await
|
|
3996
|
+
await stat5(path);
|
|
3195
3997
|
return true;
|
|
3196
3998
|
} catch {
|
|
3197
3999
|
return false;
|
|
@@ -3248,7 +4050,7 @@ async function runRecoveryMenuIfApplicable(options, dependencies) {
|
|
|
3248
4050
|
return 5;
|
|
3249
4051
|
}
|
|
3250
4052
|
}
|
|
3251
|
-
const initialized = await
|
|
4053
|
+
const initialized = await exists5(join15(dependencies.cwd, ".harness", "project.yaml"));
|
|
3252
4054
|
if (!initialized || options.nonInteractive === true || explicitConfigure(options)) {
|
|
3253
4055
|
return null;
|
|
3254
4056
|
}
|
|
@@ -3316,7 +4118,7 @@ function addCommonOptions(command) {
|
|
|
3316
4118
|
}
|
|
3317
4119
|
async function runCli(argv, overrides = {}) {
|
|
3318
4120
|
const dependencies = defaultDependencies(overrides);
|
|
3319
|
-
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({
|
|
3320
4122
|
writeOut: dependencies.stdout,
|
|
3321
4123
|
writeErr: dependencies.stderr
|
|
3322
4124
|
});
|
|
@@ -3329,6 +4131,12 @@ async function runCli(argv, overrides = {}) {
|
|
|
3329
4131
|
}
|
|
3330
4132
|
exitCode = await runConfigure(options, dependencies);
|
|
3331
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
|
+
});
|
|
3332
4140
|
addCommonOptions(program.command("update")).description("Apply approved server artifacts").action(async (options) => {
|
|
3333
4141
|
exitCode = await runUpdate(
|
|
3334
4142
|
{ ...program.opts(), ...options },
|
|
@@ -3338,6 +4146,12 @@ async function runCli(argv, overrides = {}) {
|
|
|
3338
4146
|
addCommonOptions(program.command("push")).description("Create a governed proposal").action(async (options) => {
|
|
3339
4147
|
exitCode = await runPush({ ...program.opts(), ...options }, dependencies);
|
|
3340
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
|
+
});
|
|
3341
4155
|
try {
|
|
3342
4156
|
await program.parseAsync(["node", "hunter-harness", ...argv]);
|
|
3343
4157
|
return exitCode;
|