hunter-harness 0.2.28 → 0.2.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -26
- package/dist/bin.js +576 -187
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -1216,8 +1216,8 @@ async function assertNoSymlinks(root, relativePath) {
|
|
|
1216
1216
|
for (const segment of normalized.split("/")) {
|
|
1217
1217
|
current = join(current, segment);
|
|
1218
1218
|
try {
|
|
1219
|
-
const
|
|
1220
|
-
if (
|
|
1219
|
+
const stat8 = await lstat(current);
|
|
1220
|
+
if (stat8.isSymbolicLink()) {
|
|
1221
1221
|
throw new UnsafePathError("symbolic links are not managed");
|
|
1222
1222
|
}
|
|
1223
1223
|
} catch (error) {
|
|
@@ -1233,7 +1233,7 @@ async function assertNoSymlinks(root, relativePath) {
|
|
|
1233
1233
|
async function withRetry(action, options) {
|
|
1234
1234
|
const attempts = options.attempts ?? 3;
|
|
1235
1235
|
const sleep = options.sleep ?? (async (milliseconds) => {
|
|
1236
|
-
await new Promise((
|
|
1236
|
+
await new Promise((resolve11) => setTimeout(resolve11, milliseconds));
|
|
1237
1237
|
});
|
|
1238
1238
|
let lastError;
|
|
1239
1239
|
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
@@ -2263,6 +2263,12 @@ var MANAGED_NAMES = /* @__PURE__ */ new Set([
|
|
|
2263
2263
|
"harness-profile-java.md",
|
|
2264
2264
|
"harness-profile-java.mdc"
|
|
2265
2265
|
]);
|
|
2266
|
+
var AGENT_RULE_ROOTS = [
|
|
2267
|
+
".claude/rules",
|
|
2268
|
+
".cursor/rules",
|
|
2269
|
+
".codebuddy/.rules",
|
|
2270
|
+
".codebuddy/rules"
|
|
2271
|
+
];
|
|
2266
2272
|
function sha256(content) {
|
|
2267
2273
|
return createHash3("sha256").update(content).digest("hex");
|
|
2268
2274
|
}
|
|
@@ -2284,6 +2290,10 @@ async function readReceipt(root) {
|
|
|
2284
2290
|
}
|
|
2285
2291
|
return { schema_version: 1, source_hashes: {}, targets: {} };
|
|
2286
2292
|
}
|
|
2293
|
+
async function readManagedProjectRuleProjectionPaths(projectRoot) {
|
|
2294
|
+
const receipt = await readReceipt(resolve3(projectRoot));
|
|
2295
|
+
return new Set(Object.keys(receipt.targets).filter((target) => AGENT_RULE_ROOTS.some((ruleRoot) => target.startsWith(`${ruleRoot}/`))));
|
|
2296
|
+
}
|
|
2287
2297
|
async function markdownFiles(root) {
|
|
2288
2298
|
try {
|
|
2289
2299
|
return (await readdir2(root, { withFileTypes: true })).filter((entry) => entry.isFile() && [".md", ".mdc"].includes(extname(entry.name).toLowerCase())).map((entry) => entry.name).filter((name) => !MANAGED_NAMES.has(name)).sort();
|
|
@@ -2293,6 +2303,72 @@ async function markdownFiles(root) {
|
|
|
2293
2303
|
throw error;
|
|
2294
2304
|
}
|
|
2295
2305
|
}
|
|
2306
|
+
function portable(path) {
|
|
2307
|
+
return path.replaceAll("\\", "/");
|
|
2308
|
+
}
|
|
2309
|
+
function normalizeRuleContent(content) {
|
|
2310
|
+
return content.replace(/\r\n/g, "\n").trimEnd() + "\n";
|
|
2311
|
+
}
|
|
2312
|
+
function canonicalImportContent(content) {
|
|
2313
|
+
const normalized = content.replace(/\r\n/g, "\n");
|
|
2314
|
+
if (!normalized.startsWith("---\n"))
|
|
2315
|
+
return normalizeRuleContent(normalized);
|
|
2316
|
+
const closing = normalized.indexOf("\n---\n", 4);
|
|
2317
|
+
if (closing < 0)
|
|
2318
|
+
return null;
|
|
2319
|
+
const frontmatter = normalized.slice(4, closing);
|
|
2320
|
+
if (/^\s*(?:globs?|paths?)\s*:/im.test(frontmatter) || /^\s*alwaysApply\s*:\s*false\s*$/im.test(frontmatter)) {
|
|
2321
|
+
return null;
|
|
2322
|
+
}
|
|
2323
|
+
return normalizeRuleContent(normalized.slice(closing + 5));
|
|
2324
|
+
}
|
|
2325
|
+
async function collectImportCandidates(root, previous, result) {
|
|
2326
|
+
const candidates = [];
|
|
2327
|
+
for (const ruleRoot of AGENT_RULE_ROOTS) {
|
|
2328
|
+
for (const name of await markdownFiles(join4(root, ...ruleRoot.split("/")))) {
|
|
2329
|
+
const source = portable(`${ruleRoot}/${name}`);
|
|
2330
|
+
const content = await readFile3(join4(root, ...source.split("/")), "utf8");
|
|
2331
|
+
if (previous.targets[source] === sha256(content))
|
|
2332
|
+
continue;
|
|
2333
|
+
const canonical = canonicalImportContent(content);
|
|
2334
|
+
if (canonical === null) {
|
|
2335
|
+
result.agent_specific.push(source);
|
|
2336
|
+
continue;
|
|
2337
|
+
}
|
|
2338
|
+
candidates.push({
|
|
2339
|
+
source,
|
|
2340
|
+
destination: `${RULES_ROOT}/${basename(name, extname(name))}.md`,
|
|
2341
|
+
content: canonical
|
|
2342
|
+
});
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
return candidates;
|
|
2346
|
+
}
|
|
2347
|
+
async function importAgentRules(root, canonicalRoot, previous, result) {
|
|
2348
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
2349
|
+
for (const candidate of await collectImportCandidates(root, previous, result)) {
|
|
2350
|
+
const values = grouped.get(candidate.destination) ?? [];
|
|
2351
|
+
values.push(candidate);
|
|
2352
|
+
grouped.set(candidate.destination, values);
|
|
2353
|
+
}
|
|
2354
|
+
for (const [destination, candidates] of [...grouped].sort(([a], [b]) => a.localeCompare(b))) {
|
|
2355
|
+
const destinationPath = join4(canonicalRoot, basename(destination));
|
|
2356
|
+
const current = await optionalText(destinationPath);
|
|
2357
|
+
const distinct = new Set(candidates.map((candidate) => candidate.content));
|
|
2358
|
+
const representative = candidates.at(0);
|
|
2359
|
+
if (representative === void 0)
|
|
2360
|
+
continue;
|
|
2361
|
+
if (current === null && distinct.size === 1) {
|
|
2362
|
+
await atomicWrite(destinationPath, representative.content);
|
|
2363
|
+
result.migrated.push(destination);
|
|
2364
|
+
continue;
|
|
2365
|
+
}
|
|
2366
|
+
if (current !== null && distinct.size === 1 && normalizeRuleContent(current) === representative.content) {
|
|
2367
|
+
continue;
|
|
2368
|
+
}
|
|
2369
|
+
result.conflicts.push(...candidates.map((candidate) => candidate.source));
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2296
2372
|
function targetsFor(name, agents, surface) {
|
|
2297
2373
|
const stem = basename(name, extname(name));
|
|
2298
2374
|
const targets = [];
|
|
@@ -2319,23 +2395,15 @@ async function synchronizeProjectRules(projectRoot, agents, surface = "both") {
|
|
|
2319
2395
|
const canonicalRoot = join4(root, RULES_ROOT);
|
|
2320
2396
|
const result = {
|
|
2321
2397
|
migrated: [],
|
|
2398
|
+
agent_specific: [],
|
|
2322
2399
|
written: [],
|
|
2323
2400
|
removed: [],
|
|
2324
2401
|
unchanged: [],
|
|
2325
2402
|
conflicts: []
|
|
2326
2403
|
};
|
|
2327
2404
|
await mkdir4(canonicalRoot, { recursive: true });
|
|
2328
|
-
if ((await markdownFiles(canonicalRoot)).length === 0) {
|
|
2329
|
-
for (const name of await markdownFiles(join4(root, ".claude", "rules"))) {
|
|
2330
|
-
const destination = join4(canonicalRoot, `${basename(name, extname(name))}.md`);
|
|
2331
|
-
if (await optionalText(destination) !== null)
|
|
2332
|
-
continue;
|
|
2333
|
-
const content = await readFile3(join4(root, ".claude", "rules", name), "utf8");
|
|
2334
|
-
await atomicWrite(destination, content);
|
|
2335
|
-
result.migrated.push(`${RULES_ROOT}/${basename(destination)}`);
|
|
2336
|
-
}
|
|
2337
|
-
}
|
|
2338
2405
|
const previous = await readReceipt(root);
|
|
2406
|
+
await importAgentRules(root, canonicalRoot, previous, result);
|
|
2339
2407
|
const next = { schema_version: 1, source_hashes: {}, targets: {} };
|
|
2340
2408
|
const desired = /* @__PURE__ */ new Map();
|
|
2341
2409
|
for (const name of await markdownFiles(canonicalRoot)) {
|
|
@@ -2349,9 +2417,10 @@ async function synchronizeProjectRules(projectRoot, agents, surface = "both") {
|
|
|
2349
2417
|
const path = join4(root, target);
|
|
2350
2418
|
const current = await optionalText(path);
|
|
2351
2419
|
const incomingHash = sha256(content);
|
|
2352
|
-
|
|
2420
|
+
const canonicalCurrent = current === null ? null : canonicalImportContent(current);
|
|
2421
|
+
if (current === content || canonicalCurrent === content) {
|
|
2353
2422
|
result.unchanged.push(target);
|
|
2354
|
-
next.targets[target] = incomingHash;
|
|
2423
|
+
next.targets[target] = current === null ? incomingHash : sha256(current);
|
|
2355
2424
|
} else if (current === null || previous.targets[target] === sha256(current)) {
|
|
2356
2425
|
await atomicWrite(path, content);
|
|
2357
2426
|
result.written.push(target);
|
|
@@ -2411,6 +2480,7 @@ async function synchronizeProjectRules(projectRoot, agents, surface = "both") {
|
|
|
2411
2480
|
}
|
|
2412
2481
|
await atomicWrite(join4(root, RECEIPT_PATH), JSON.stringify(next, null, 2) + "\n");
|
|
2413
2482
|
result.migrated.sort();
|
|
2483
|
+
result.agent_specific.sort();
|
|
2414
2484
|
result.written.sort();
|
|
2415
2485
|
result.removed.sort();
|
|
2416
2486
|
result.unchanged.sort();
|
|
@@ -2918,16 +2988,237 @@ async function enrichContextIndexWithVerification(root, agents, profile, resourc
|
|
|
2918
2988
|
await writeFile4(contextPath, JSON.stringify(parsed, null, 2) + "\n", "utf8");
|
|
2919
2989
|
}
|
|
2920
2990
|
|
|
2921
|
-
// ../core/dist/project/
|
|
2991
|
+
// ../core/dist/project/rule-candidates.js
|
|
2922
2992
|
import { createHash as createHash6 } from "node:crypto";
|
|
2923
|
-
import { readFile as readFile6, readdir as readdir4,
|
|
2924
|
-
import { dirname as dirname4, join as join7, resolve as resolve6 } from "node:path";
|
|
2993
|
+
import { mkdir as mkdir5, readFile as readFile6, readdir as readdir4, rename as rename4, stat as stat3, writeFile as writeFile5 } from "node:fs/promises";
|
|
2994
|
+
import { basename as basename3, dirname as dirname4, join as join7, relative, resolve as resolve6 } from "node:path";
|
|
2995
|
+
var ARCHIVE_ROOT = ".harness/archive";
|
|
2996
|
+
var CANDIDATE_PATH = ".harness/knowledge/rule-candidates.json";
|
|
2997
|
+
var MAX_EVIDENCE_BYTES = 2 * 1024 * 1024;
|
|
2998
|
+
var EVIDENCE_NAMES = [
|
|
2999
|
+
/^review-findings.*\.json$/i,
|
|
3000
|
+
/^test-(?:report|results?|failures?).*\.json$/i,
|
|
3001
|
+
/^summary-data\.json$/i
|
|
3002
|
+
];
|
|
3003
|
+
function sha2562(content) {
|
|
3004
|
+
return createHash6("sha256").update(content).digest("hex");
|
|
3005
|
+
}
|
|
3006
|
+
function portable2(path) {
|
|
3007
|
+
return path.replaceAll("\\", "/");
|
|
3008
|
+
}
|
|
3009
|
+
function normalizeText(value) {
|
|
3010
|
+
return value.replace(/\s+/g, " ").trim();
|
|
3011
|
+
}
|
|
3012
|
+
function safeText(value, limit = 500) {
|
|
3013
|
+
if (typeof value !== "string")
|
|
3014
|
+
return null;
|
|
3015
|
+
const normalized = normalizeText(value).slice(0, limit);
|
|
3016
|
+
if (normalized.length < 8)
|
|
3017
|
+
return null;
|
|
3018
|
+
if (/(?:ignore|disregard)\s+(?:all\s+)?previous|system\s+prompt|developer\s+message/i.test(normalized)) {
|
|
3019
|
+
return null;
|
|
3020
|
+
}
|
|
3021
|
+
if (/-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----|gh[pousr]_[A-Za-z0-9]{20,}|(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?):\/\//i.test(normalized)) {
|
|
3022
|
+
return null;
|
|
3023
|
+
}
|
|
3024
|
+
return normalized;
|
|
3025
|
+
}
|
|
3026
|
+
function stringField(record, names) {
|
|
3027
|
+
for (const name of names) {
|
|
3028
|
+
const value = safeText(record[name]);
|
|
3029
|
+
if (value !== null)
|
|
3030
|
+
return value;
|
|
3031
|
+
}
|
|
3032
|
+
return null;
|
|
3033
|
+
}
|
|
3034
|
+
function severityOf(record) {
|
|
3035
|
+
const value = record.severity ?? record.level ?? record.priority ?? "unknown";
|
|
3036
|
+
return typeof value === "string" ? value.toLowerCase() : "unknown";
|
|
3037
|
+
}
|
|
3038
|
+
function highSeverity(severity) {
|
|
3039
|
+
return /^(?:red|critical|high|error|blocker|p0|p1)$/.test(severity);
|
|
3040
|
+
}
|
|
3041
|
+
function evidenceKind(path) {
|
|
3042
|
+
const name = basename3(path).toLowerCase();
|
|
3043
|
+
if (name.startsWith("review-"))
|
|
3044
|
+
return "review";
|
|
3045
|
+
if (name.startsWith("test-"))
|
|
3046
|
+
return "test";
|
|
3047
|
+
return "validation";
|
|
3048
|
+
}
|
|
3049
|
+
function observationFrom(record, path, archive) {
|
|
3050
|
+
const severity = severityOf(record);
|
|
3051
|
+
const suggestion = stringField(record, [
|
|
3052
|
+
"proposed_rule",
|
|
3053
|
+
"proposedRule",
|
|
3054
|
+
"suggestion",
|
|
3055
|
+
"recommendation",
|
|
3056
|
+
"remediation"
|
|
3057
|
+
]);
|
|
3058
|
+
const issue = stringField(record, ["issue", "message", "error", "failure"]);
|
|
3059
|
+
const title = stringField(record, ["title", "name", "id", "code"]) ?? issue;
|
|
3060
|
+
let proposedRule = suggestion;
|
|
3061
|
+
if (proposedRule === null && issue !== null && highSeverity(severity)) {
|
|
3062
|
+
proposedRule = `\u5FC5\u987B\u589E\u52A0\u53EF\u91CD\u590D\u9A8C\u8BC1\uFF0C\u9632\u6B62\u4EE5\u4E0B\u95EE\u9898\u518D\u6B21\u51FA\u73B0\uFF1A${issue}`;
|
|
3063
|
+
}
|
|
3064
|
+
if (proposedRule === null || title === null)
|
|
3065
|
+
return null;
|
|
3066
|
+
const recordId = record.id ?? record.code ?? record.name ?? null;
|
|
3067
|
+
return {
|
|
3068
|
+
title,
|
|
3069
|
+
proposedRule,
|
|
3070
|
+
severity,
|
|
3071
|
+
evidence: {
|
|
3072
|
+
archive,
|
|
3073
|
+
path,
|
|
3074
|
+
kind: evidenceKind(path),
|
|
3075
|
+
record_id: typeof recordId === "string" ? recordId.slice(0, 120) : null
|
|
3076
|
+
}
|
|
3077
|
+
};
|
|
3078
|
+
}
|
|
3079
|
+
function collectObservations(value, path, archive, output, rejected) {
|
|
3080
|
+
if (Array.isArray(value)) {
|
|
3081
|
+
for (const item2 of value)
|
|
3082
|
+
collectObservations(item2, path, archive, output, rejected);
|
|
3083
|
+
return;
|
|
3084
|
+
}
|
|
3085
|
+
if (value === null || typeof value !== "object")
|
|
3086
|
+
return;
|
|
3087
|
+
const record = value;
|
|
3088
|
+
const candidate = observationFrom(record, path, archive);
|
|
3089
|
+
if (candidate !== null)
|
|
3090
|
+
output.push(candidate);
|
|
3091
|
+
else if (Object.keys(record).some((key) => ["suggestion", "recommendation", "proposed_rule", "proposedRule"].includes(key)))
|
|
3092
|
+
rejected.count += 1;
|
|
3093
|
+
for (const nested of Object.values(record)) {
|
|
3094
|
+
if (nested !== null && typeof nested === "object") {
|
|
3095
|
+
collectObservations(nested, path, archive, output, rejected);
|
|
3096
|
+
}
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
async function evidenceFiles(root) {
|
|
3100
|
+
const archiveRoot = join7(root, ...ARCHIVE_ROOT.split("/"));
|
|
3101
|
+
const output = [];
|
|
3102
|
+
async function walk(directory) {
|
|
3103
|
+
let entries;
|
|
3104
|
+
try {
|
|
3105
|
+
entries = await readdir4(directory, { withFileTypes: true });
|
|
3106
|
+
} catch (error) {
|
|
3107
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
3108
|
+
return;
|
|
3109
|
+
throw error;
|
|
3110
|
+
}
|
|
3111
|
+
for (const entry of entries) {
|
|
3112
|
+
const path = join7(directory, entry.name);
|
|
3113
|
+
if (entry.isDirectory())
|
|
3114
|
+
await walk(path);
|
|
3115
|
+
else if (entry.isFile() && EVIDENCE_NAMES.some((pattern) => pattern.test(entry.name))) {
|
|
3116
|
+
if ((await stat3(path)).size <= MAX_EVIDENCE_BYTES)
|
|
3117
|
+
output.push(path);
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
3120
|
+
}
|
|
3121
|
+
await walk(archiveRoot);
|
|
3122
|
+
return output.sort();
|
|
3123
|
+
}
|
|
3124
|
+
function candidateKey(rule) {
|
|
3125
|
+
return normalizeText(rule).toLocaleLowerCase();
|
|
3126
|
+
}
|
|
3127
|
+
function buildCandidates(observations) {
|
|
3128
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
3129
|
+
for (const observation of observations) {
|
|
3130
|
+
const key = candidateKey(observation.proposedRule);
|
|
3131
|
+
const values = grouped.get(key) ?? [];
|
|
3132
|
+
values.push(observation);
|
|
3133
|
+
grouped.set(key, values);
|
|
3134
|
+
}
|
|
3135
|
+
const candidates = [];
|
|
3136
|
+
for (const [key, values] of grouped) {
|
|
3137
|
+
const archives = new Set(values.map((value) => value.evidence.archive));
|
|
3138
|
+
const highest = values.find((value) => highSeverity(value.severity));
|
|
3139
|
+
if (archives.size < 2 && highest === void 0)
|
|
3140
|
+
continue;
|
|
3141
|
+
const representative = highest ?? values.at(0);
|
|
3142
|
+
if (representative === void 0)
|
|
3143
|
+
continue;
|
|
3144
|
+
const evidence = [...new Map(values.map((value) => [
|
|
3145
|
+
`${value.evidence.path}\0${value.evidence.record_id ?? ""}`,
|
|
3146
|
+
value.evidence
|
|
3147
|
+
])).values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
3148
|
+
candidates.push({
|
|
3149
|
+
id: `rule_${sha2562(key).slice(0, 16)}`,
|
|
3150
|
+
status: "candidate",
|
|
3151
|
+
title: representative.title,
|
|
3152
|
+
proposed_rule: representative.proposedRule,
|
|
3153
|
+
confidence: archives.size >= 2 && highest !== void 0 ? "high" : "medium",
|
|
3154
|
+
severity: representative.severity,
|
|
3155
|
+
occurrences: evidence.length,
|
|
3156
|
+
evidence
|
|
3157
|
+
});
|
|
3158
|
+
}
|
|
3159
|
+
return candidates.sort((a, b) => a.id.localeCompare(b.id));
|
|
3160
|
+
}
|
|
3161
|
+
async function atomicWrite2(path, content) {
|
|
3162
|
+
await mkdir5(dirname4(path), { recursive: true });
|
|
3163
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
3164
|
+
await writeFile5(temporary, content, "utf8");
|
|
3165
|
+
await rename4(temporary, path);
|
|
3166
|
+
}
|
|
3167
|
+
async function synchronizeRuleCandidates(projectRoot, options = {}) {
|
|
3168
|
+
const root = resolve6(projectRoot);
|
|
3169
|
+
const files = await evidenceFiles(root);
|
|
3170
|
+
const sourceHashes = {};
|
|
3171
|
+
const observations = [];
|
|
3172
|
+
const rejected = { count: 0 };
|
|
3173
|
+
for (const path of files) {
|
|
3174
|
+
const relativePath = portable2(relative(root, path));
|
|
3175
|
+
const content2 = await readFile6(path, "utf8");
|
|
3176
|
+
sourceHashes[relativePath] = sha2562(content2);
|
|
3177
|
+
let parsed;
|
|
3178
|
+
try {
|
|
3179
|
+
parsed = JSON.parse(content2);
|
|
3180
|
+
} catch {
|
|
3181
|
+
continue;
|
|
3182
|
+
}
|
|
3183
|
+
const archive = relativePath.split("/")[2] ?? "unknown";
|
|
3184
|
+
collectObservations(parsed, relativePath, archive, observations, rejected);
|
|
3185
|
+
}
|
|
3186
|
+
const manifest = {
|
|
3187
|
+
schema_version: 1,
|
|
3188
|
+
source_hashes: sourceHashes,
|
|
3189
|
+
candidates: buildCandidates(observations)
|
|
3190
|
+
};
|
|
3191
|
+
const content = JSON.stringify(manifest, null, 2) + "\n";
|
|
3192
|
+
const destination = join7(root, ...CANDIDATE_PATH.split("/"));
|
|
3193
|
+
let current = null;
|
|
3194
|
+
try {
|
|
3195
|
+
current = await readFile6(destination, "utf8");
|
|
3196
|
+
} catch (error) {
|
|
3197
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
|
|
3198
|
+
throw error;
|
|
3199
|
+
}
|
|
3200
|
+
const changed = current !== content;
|
|
3201
|
+
if (changed && options.dryRun !== true)
|
|
3202
|
+
await atomicWrite2(destination, content);
|
|
3203
|
+
return {
|
|
3204
|
+
path: CANDIDATE_PATH,
|
|
3205
|
+
scanned: files.length,
|
|
3206
|
+
candidates: manifest.candidates.length,
|
|
3207
|
+
changed,
|
|
3208
|
+
rejected_untrusted: rejected.count
|
|
3209
|
+
};
|
|
3210
|
+
}
|
|
3211
|
+
|
|
3212
|
+
// ../core/dist/project/refresh.js
|
|
3213
|
+
import { createHash as createHash7 } from "node:crypto";
|
|
3214
|
+
import { readFile as readFile7, readdir as readdir5, rmdir } from "node:fs/promises";
|
|
3215
|
+
import { dirname as dirname5, join as join8, resolve as resolve7 } from "node:path";
|
|
2925
3216
|
import { parse as parseYaml3, stringify as stringifyYaml2 } from "yaml";
|
|
2926
3217
|
var INSTALLED_STATE_PATH = ".harness/state/local/installed-harness-bundle.json";
|
|
2927
3218
|
var CONTEXT_INDEX_PATH2 = ".harness/context-index.json";
|
|
2928
3219
|
async function fileHex2(path) {
|
|
2929
3220
|
try {
|
|
2930
|
-
return
|
|
3221
|
+
return createHash7("sha256").update(await readFile7(path)).digest("hex");
|
|
2931
3222
|
} catch (error) {
|
|
2932
3223
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
2933
3224
|
return null;
|
|
@@ -2937,7 +3228,7 @@ async function fileHex2(path) {
|
|
|
2937
3228
|
}
|
|
2938
3229
|
async function readOptionalText(path) {
|
|
2939
3230
|
try {
|
|
2940
|
-
return await
|
|
3231
|
+
return await readFile7(path, "utf8");
|
|
2941
3232
|
} catch (error) {
|
|
2942
3233
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
2943
3234
|
return "";
|
|
@@ -2946,7 +3237,7 @@ async function readOptionalText(path) {
|
|
|
2946
3237
|
}
|
|
2947
3238
|
}
|
|
2948
3239
|
async function readInstalledState(root) {
|
|
2949
|
-
const content = await readOptionalText(
|
|
3240
|
+
const content = await readOptionalText(join8(root, INSTALLED_STATE_PATH));
|
|
2950
3241
|
if (content === "") {
|
|
2951
3242
|
return {
|
|
2952
3243
|
profile: null,
|
|
@@ -3016,7 +3307,7 @@ async function readInstalledState(root) {
|
|
|
3016
3307
|
};
|
|
3017
3308
|
}
|
|
3018
3309
|
async function readInstalledAgentConfiguration(projectRoot) {
|
|
3019
|
-
const installed = await readInstalledState(
|
|
3310
|
+
const installed = await readInstalledState(resolve7(projectRoot));
|
|
3020
3311
|
return {
|
|
3021
3312
|
agents: installed.adapters,
|
|
3022
3313
|
profiles: Object.fromEntries(installed.adapters.map((agent) => [
|
|
@@ -3026,7 +3317,7 @@ async function readInstalledAgentConfiguration(projectRoot) {
|
|
|
3026
3317
|
};
|
|
3027
3318
|
}
|
|
3028
3319
|
async function readContextIndexBundleHash(root) {
|
|
3029
|
-
const content = await readOptionalText(
|
|
3320
|
+
const content = await readOptionalText(join8(root, CONTEXT_INDEX_PATH2));
|
|
3030
3321
|
if (content === "")
|
|
3031
3322
|
return null;
|
|
3032
3323
|
try {
|
|
@@ -3038,13 +3329,13 @@ async function readContextIndexBundleHash(root) {
|
|
|
3038
3329
|
}
|
|
3039
3330
|
}
|
|
3040
3331
|
async function pruneEmptyParentDirs(root, deletedPaths, boundaryPaths) {
|
|
3041
|
-
const boundaries = new Set(boundaryPaths.map((path) =>
|
|
3332
|
+
const boundaries = new Set(boundaryPaths.map((path) => join8(root, path)));
|
|
3042
3333
|
for (const deleted of deletedPaths) {
|
|
3043
|
-
let dir =
|
|
3334
|
+
let dir = dirname5(join8(root, deleted));
|
|
3044
3335
|
while (dir.startsWith(root) && !boundaries.has(dir)) {
|
|
3045
3336
|
let entries;
|
|
3046
3337
|
try {
|
|
3047
|
-
entries = await
|
|
3338
|
+
entries = await readdir5(dir);
|
|
3048
3339
|
} catch {
|
|
3049
3340
|
break;
|
|
3050
3341
|
}
|
|
@@ -3055,7 +3346,7 @@ async function pruneEmptyParentDirs(root, deletedPaths, boundaryPaths) {
|
|
|
3055
3346
|
} catch {
|
|
3056
3347
|
break;
|
|
3057
3348
|
}
|
|
3058
|
-
dir =
|
|
3349
|
+
dir = dirname5(dir);
|
|
3059
3350
|
}
|
|
3060
3351
|
}
|
|
3061
3352
|
}
|
|
@@ -3082,7 +3373,7 @@ function sortByTarget(items) {
|
|
|
3082
3373
|
return [...items].sort((left, right) => left.target_path.localeCompare(right.target_path));
|
|
3083
3374
|
}
|
|
3084
3375
|
async function reconcileContextIndex(root, agents, profiles, manifests, codebuddySurface2, verifications) {
|
|
3085
|
-
const existing = await readOptionalText(
|
|
3376
|
+
const existing = await readOptionalText(join8(root, CONTEXT_INDEX_PATH2));
|
|
3086
3377
|
let codebase = {
|
|
3087
3378
|
map: ".harness/codebase/map",
|
|
3088
3379
|
status: "missing"
|
|
@@ -3135,7 +3426,7 @@ async function reconcileContextIndex(root, agents, profiles, manifests, codebudd
|
|
|
3135
3426
|
}
|
|
3136
3427
|
async function projectTransitionOperation(root, agents, profiles, codebuddySurface2) {
|
|
3137
3428
|
const path = ".harness/project.yaml";
|
|
3138
|
-
const content = await readOptionalText(
|
|
3429
|
+
const content = await readOptionalText(join8(root, path));
|
|
3139
3430
|
if (content === "")
|
|
3140
3431
|
return null;
|
|
3141
3432
|
const project = parseYaml3(content);
|
|
@@ -3170,11 +3461,11 @@ function mergeTargets(targets) {
|
|
|
3170
3461
|
}).sort((left, right) => left.target_path.localeCompare(right.target_path));
|
|
3171
3462
|
}
|
|
3172
3463
|
async function reconcileMarkdownBlock(root, fileName, blockId, content, remove, ops, conflicts, preserved) {
|
|
3173
|
-
const original = await readOptionalText(
|
|
3464
|
+
const original = await readOptionalText(join8(root, fileName));
|
|
3174
3465
|
const synthetic = {
|
|
3175
3466
|
source_path: fileName,
|
|
3176
3467
|
target_path: fileName,
|
|
3177
|
-
sha256:
|
|
3468
|
+
sha256: createHash7("sha256").update(content).digest("hex"),
|
|
3178
3469
|
bytes: new TextEncoder().encode(content)
|
|
3179
3470
|
};
|
|
3180
3471
|
let next;
|
|
@@ -3189,7 +3480,7 @@ async function reconcileMarkdownBlock(root, fileName, blockId, content, remove,
|
|
|
3189
3480
|
next = refreshed.content;
|
|
3190
3481
|
}
|
|
3191
3482
|
} catch {
|
|
3192
|
-
const current = original === "" ? null :
|
|
3483
|
+
const current = original === "" ? null : createHash7("sha256").update(original).digest("hex");
|
|
3193
3484
|
preserved.push(item(synthetic, "preserve", "MALFORMED_MANAGED_BLOCK", current, synthetic.sha256));
|
|
3194
3485
|
conflicts.push(conflict(synthetic, "MALFORMED_MANAGED_BLOCK", current, synthetic.sha256));
|
|
3195
3486
|
return;
|
|
@@ -3209,7 +3500,7 @@ function stateWithoutInstalledAt(value) {
|
|
|
3209
3500
|
return copy;
|
|
3210
3501
|
}
|
|
3211
3502
|
async function refreshProject(options) {
|
|
3212
|
-
const root =
|
|
3503
|
+
const root = resolve7(options.projectRoot);
|
|
3213
3504
|
const installed = await readInstalledState(root);
|
|
3214
3505
|
const oldAgents = installed.adapters.length > 0 ? installed.adapters : ["claude-code"];
|
|
3215
3506
|
const selectedAgents = sortHarnessAgents(options.agents);
|
|
@@ -3293,7 +3584,7 @@ async function refreshProject(options) {
|
|
|
3293
3584
|
const newStateFiles = installed.files.filter((entry) => entry.owner === "shared" || !selectedSet.has(entry.owner));
|
|
3294
3585
|
for (const target of newManaged) {
|
|
3295
3586
|
const incoming = target.sha256;
|
|
3296
|
-
const current = await fileHex2(
|
|
3587
|
+
const current = await fileHex2(join8(root, target.target_path));
|
|
3297
3588
|
if (current === null) {
|
|
3298
3589
|
applied.push(item(target, "add", "MISSING_TARGET", null, incoming));
|
|
3299
3590
|
ops.push({ operation: "add", path: target.target_path, content: target.bytes });
|
|
@@ -3321,7 +3612,7 @@ async function refreshProject(options) {
|
|
|
3321
3612
|
}
|
|
3322
3613
|
}
|
|
3323
3614
|
for (const target of oldOnly) {
|
|
3324
|
-
const current = await fileHex2(
|
|
3615
|
+
const current = await fileHex2(join8(root, target.target_path));
|
|
3325
3616
|
if (current === null) {
|
|
3326
3617
|
continue;
|
|
3327
3618
|
}
|
|
@@ -3364,7 +3655,7 @@ async function refreshProject(options) {
|
|
|
3364
3655
|
const verifyEntries = [];
|
|
3365
3656
|
for (const target of verifyTargets) {
|
|
3366
3657
|
const rel = target.target_path.replace(/\\/g, "/");
|
|
3367
|
-
const actual = await fileHex2(
|
|
3658
|
+
const actual = await fileHex2(join8(root, target.target_path));
|
|
3368
3659
|
verifyEntries.push({ relpath: rel, sha256: actual ?? "" });
|
|
3369
3660
|
if (actual === null) {
|
|
3370
3661
|
verifyMismatches.push({ relpath: rel, expected: target.sha256, actual: "<missing>" });
|
|
@@ -3397,19 +3688,19 @@ async function refreshProject(options) {
|
|
|
3397
3688
|
owner: "shared",
|
|
3398
3689
|
target_path: "AGENTS.md",
|
|
3399
3690
|
block_id: AGENTS_CORE_BLOCK_ID,
|
|
3400
|
-
content_sha256:
|
|
3691
|
+
content_sha256: createHash7("sha256").update(AGENTS_MANAGED_BLOCK_CONTENT).digest("hex")
|
|
3401
3692
|
},
|
|
3402
3693
|
...selectedSet.has("claude-code") ? [{
|
|
3403
3694
|
owner: "claude-code",
|
|
3404
3695
|
target_path: "CLAUDE.md",
|
|
3405
3696
|
block_id: CLAUDE_BLOCK_ID,
|
|
3406
|
-
content_sha256:
|
|
3697
|
+
content_sha256: createHash7("sha256").update(CLAUDE_MANAGED_BLOCK_CONTENT).digest("hex")
|
|
3407
3698
|
}] : [],
|
|
3408
3699
|
...selectedSet.has("codebuddy") ? [{
|
|
3409
3700
|
owner: "codebuddy",
|
|
3410
3701
|
target_path: "CODEBUDDY.md",
|
|
3411
3702
|
block_id: CODEBUDDY_BLOCK_ID,
|
|
3412
|
-
content_sha256:
|
|
3703
|
+
content_sha256: createHash7("sha256").update(CODEBUDDY_MANAGED_BLOCK_CONTENT).digest("hex")
|
|
3413
3704
|
}] : []
|
|
3414
3705
|
].sort((left, right) => left.target_path.localeCompare(right.target_path) || left.block_id.localeCompare(right.block_id));
|
|
3415
3706
|
const filesByTarget = new Map(newStateFiles.map((entry) => [entry.target_path, entry]));
|
|
@@ -3425,7 +3716,7 @@ async function refreshProject(options) {
|
|
|
3425
3716
|
files: [...filesByTarget.values()].sort((left, right) => left.target_path.localeCompare(right.target_path) || left.source_path.localeCompare(right.source_path)),
|
|
3426
3717
|
managed_blocks: managedBlocks
|
|
3427
3718
|
};
|
|
3428
|
-
const existingState = await readOptionalText(
|
|
3719
|
+
const existingState = await readOptionalText(join8(root, INSTALLED_STATE_PATH));
|
|
3429
3720
|
let existingParsed = null;
|
|
3430
3721
|
try {
|
|
3431
3722
|
existingParsed = existingState === "" ? null : JSON.parse(existingState);
|
|
@@ -3480,7 +3771,7 @@ function buildMarkerCoreHash(text) {
|
|
|
3480
3771
|
}
|
|
3481
3772
|
}
|
|
3482
3773
|
async function collectFreshness(options) {
|
|
3483
|
-
const root =
|
|
3774
|
+
const root = resolve7(options.projectRoot);
|
|
3484
3775
|
const installed = await readInstalledState(root);
|
|
3485
3776
|
const codebuddySurface2 = options.codebuddySurface ?? "both";
|
|
3486
3777
|
const agents = [];
|
|
@@ -3525,18 +3816,18 @@ async function collectFreshness(options) {
|
|
|
3525
3816
|
identity.adapterHash = sha256Bytes(canonicalJson(targets.map((target) => ({ path: target.target_path.replace(/\\/g, "/"), sha256: target.sha256 })).sort((a, b) => a.path.localeCompare(b.path))));
|
|
3526
3817
|
const installedProjection = await Promise.all(targets.map(async (target) => ({
|
|
3527
3818
|
path: target.target_path.replace(/\\/g, "/"),
|
|
3528
|
-
sha256: await fileHex2(
|
|
3819
|
+
sha256: await fileHex2(join8(root, target.target_path))
|
|
3529
3820
|
})));
|
|
3530
3821
|
identity.installedAdapterHash = sha256Bytes(canonicalJson(installedProjection.sort((a, b) => a.path.localeCompare(b.path))));
|
|
3531
3822
|
const markerTarget = targets.find((target) => target.target_path.replace(/\\/g, "/").endsWith(`/${BUILD_MARKER_BUNDLE_PATH}`) || target.target_path === BUILD_MARKER_BUNDLE_PATH);
|
|
3532
3823
|
if (markerTarget !== void 0) {
|
|
3533
|
-
identity.installedCoreHash = buildMarkerCoreHash(await readOptionalText(
|
|
3824
|
+
identity.installedCoreHash = buildMarkerCoreHash(await readOptionalText(join8(root, markerTarget.target_path)));
|
|
3534
3825
|
}
|
|
3535
3826
|
const mismatchDetails = [];
|
|
3536
3827
|
const contentEntries = [];
|
|
3537
3828
|
for (const target of targets) {
|
|
3538
3829
|
const rel = target.target_path.replace(/\\/g, "/");
|
|
3539
|
-
const actual = await fileHex2(
|
|
3830
|
+
const actual = await fileHex2(join8(root, target.target_path));
|
|
3540
3831
|
contentEntries.push({ relpath: rel, sha256: actual ?? "" });
|
|
3541
3832
|
if (actual === null) {
|
|
3542
3833
|
mismatchDetails.push({ relpath: rel, expected: target.sha256, actual: "<missing>" });
|
|
@@ -3559,7 +3850,7 @@ async function collectFreshness(options) {
|
|
|
3559
3850
|
const drifted = [];
|
|
3560
3851
|
const missing = [];
|
|
3561
3852
|
for (const target of targets) {
|
|
3562
|
-
const current = await fileHex2(
|
|
3853
|
+
const current = await fileHex2(join8(root, target.target_path));
|
|
3563
3854
|
if (current === null) {
|
|
3564
3855
|
missing.push(target.target_path);
|
|
3565
3856
|
} else if (current !== target.sha256) {
|
|
@@ -3807,11 +4098,11 @@ function rawFindings(content) {
|
|
|
3807
4098
|
if (rule.id === "HH_PASSWORD_VALUE" && isObviousPlaceholder(value)) {
|
|
3808
4099
|
continue;
|
|
3809
4100
|
}
|
|
3810
|
-
const
|
|
4101
|
+
const relative4 = match[0].indexOf(value);
|
|
3811
4102
|
findings.push({
|
|
3812
4103
|
ruleId: rule.id,
|
|
3813
4104
|
severity: rule.severity,
|
|
3814
|
-
offset: match.index + Math.max(0,
|
|
4105
|
+
offset: match.index + Math.max(0, relative4),
|
|
3815
4106
|
value
|
|
3816
4107
|
});
|
|
3817
4108
|
}
|
|
@@ -3923,8 +4214,8 @@ function generateProposalPreview(input, scanOptions = {}) {
|
|
|
3923
4214
|
}
|
|
3924
4215
|
|
|
3925
4216
|
// ../core/dist/push/credentials.js
|
|
3926
|
-
import { readFile as
|
|
3927
|
-
import { join as
|
|
4217
|
+
import { readFile as readFile8, writeFile as writeFile6 } from "node:fs/promises";
|
|
4218
|
+
import { join as join9 } from "node:path";
|
|
3928
4219
|
import { parse as parseYaml4, stringify as stringifyYaml3 } from "yaml";
|
|
3929
4220
|
var CREDENTIALS_LOCAL_RELATIVE = ".harness/credentials.local.yaml";
|
|
3930
4221
|
var CREDENTIALS_GITIGNORE_LINES = [
|
|
@@ -3994,7 +4285,7 @@ function mergeLocalCredentials(existing, patch) {
|
|
|
3994
4285
|
}
|
|
3995
4286
|
async function readLocalCredentials(projectRoot) {
|
|
3996
4287
|
try {
|
|
3997
|
-
const raw = await
|
|
4288
|
+
const raw = await readFile8(join9(projectRoot, CREDENTIALS_LOCAL_RELATIVE), "utf8");
|
|
3998
4289
|
return parseLocalCredentials(parseYaml4(raw));
|
|
3999
4290
|
} catch (error) {
|
|
4000
4291
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
@@ -4005,13 +4296,13 @@ async function readLocalCredentials(projectRoot) {
|
|
|
4005
4296
|
}
|
|
4006
4297
|
async function writeLocalCredentials(projectRoot, credentials) {
|
|
4007
4298
|
const normalized = validateLocalCredentials(credentials);
|
|
4008
|
-
await
|
|
4299
|
+
await writeFile6(join9(projectRoot, CREDENTIALS_LOCAL_RELATIVE), stringifyYaml3(normalized, { sortMapEntries: true }) + "\n", "utf8");
|
|
4009
4300
|
}
|
|
4010
4301
|
async function ensureCredentialsGitignore(projectRoot) {
|
|
4011
|
-
const gitignorePath =
|
|
4302
|
+
const gitignorePath = join9(projectRoot, ".gitignore");
|
|
4012
4303
|
let content = "";
|
|
4013
4304
|
try {
|
|
4014
|
-
content = await
|
|
4305
|
+
content = await readFile8(gitignorePath, "utf8");
|
|
4015
4306
|
} catch (error) {
|
|
4016
4307
|
if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) {
|
|
4017
4308
|
throw error;
|
|
@@ -4034,7 +4325,7 @@ async function ensureCredentialsGitignore(projectRoot) {
|
|
|
4034
4325
|
return;
|
|
4035
4326
|
}
|
|
4036
4327
|
const suffix = content.length === 0 || content.endsWith("\n") ? "" : "\n";
|
|
4037
|
-
await
|
|
4328
|
+
await writeFile6(gitignorePath, content + suffix + missing.join("\n") + "\n", "utf8");
|
|
4038
4329
|
}
|
|
4039
4330
|
function resolvePushAuth(input) {
|
|
4040
4331
|
const tokenEnv = input.tokenEnv ?? input.projectTokenEnv;
|
|
@@ -4062,28 +4353,28 @@ function resolvePushAuth(input) {
|
|
|
4062
4353
|
}
|
|
4063
4354
|
|
|
4064
4355
|
// ../core/dist/push/push.js
|
|
4065
|
-
import { lstat as lstat3, readFile as
|
|
4066
|
-
import { join as
|
|
4356
|
+
import { lstat as lstat3, readFile as readFile12, readdir as readdir6, rm as rm5 } from "node:fs/promises";
|
|
4357
|
+
import { join as join13, relative as relative2, resolve as resolve9 } from "node:path";
|
|
4067
4358
|
import { parse as parseYaml5, stringify as stringifyYaml4 } from "yaml";
|
|
4068
4359
|
|
|
4069
4360
|
// ../core/dist/state/baseline.js
|
|
4070
|
-
import { readFile as
|
|
4071
|
-
import { join as
|
|
4361
|
+
import { readFile as readFile9 } from "node:fs/promises";
|
|
4362
|
+
import { join as join10 } from "node:path";
|
|
4072
4363
|
async function readBaseline(projectRoot) {
|
|
4073
|
-
const content = await
|
|
4364
|
+
const content = await readFile9(join10(stateLayout(projectRoot).baseline, "manifest.json"), "utf8");
|
|
4074
4365
|
return baselineManifestSchema.parse(JSON.parse(content));
|
|
4075
4366
|
}
|
|
4076
4367
|
|
|
4077
4368
|
// ../core/dist/state/locks.js
|
|
4078
4369
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
4079
|
-
import { readFile as
|
|
4080
|
-
import { join as
|
|
4370
|
+
import { readFile as readFile10, rename as rename5, rm as rm3, writeFile as writeFile7 } from "node:fs/promises";
|
|
4371
|
+
import { join as join11 } from "node:path";
|
|
4081
4372
|
async function readLock(path) {
|
|
4082
|
-
return JSON.parse(await
|
|
4373
|
+
return JSON.parse(await readFile10(path, "utf8"));
|
|
4083
4374
|
}
|
|
4084
4375
|
async function acquireProtocolLock(projectRoot, operation, options = {}) {
|
|
4085
4376
|
const layout = await ensureStateLayout(projectRoot);
|
|
4086
|
-
const lockPath =
|
|
4377
|
+
const lockPath = join11(layout.locks, "protocol.lock");
|
|
4087
4378
|
const now = options.now ?? Date.now();
|
|
4088
4379
|
const staleAfterMs = options.staleAfterMs ?? 15 * 60 * 1e3;
|
|
4089
4380
|
const record = {
|
|
@@ -4096,7 +4387,7 @@ async function acquireProtocolLock(projectRoot, operation, options = {}) {
|
|
|
4096
4387
|
};
|
|
4097
4388
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
4098
4389
|
try {
|
|
4099
|
-
await
|
|
4390
|
+
await writeFile7(lockPath, JSON.stringify(record, null, 2), { flag: "wx" });
|
|
4100
4391
|
return {
|
|
4101
4392
|
path: lockPath,
|
|
4102
4393
|
operation,
|
|
@@ -4118,15 +4409,15 @@ async function acquireProtocolLock(projectRoot, operation, options = {}) {
|
|
|
4118
4409
|
if (now - current.heartbeat_at_ms <= staleAfterMs) {
|
|
4119
4410
|
throw new Error("protocol lock is active for operation " + current.operation, { cause: error });
|
|
4120
4411
|
}
|
|
4121
|
-
await
|
|
4412
|
+
await rename5(lockPath, lockPath + ".stale-" + randomUUID3());
|
|
4122
4413
|
}
|
|
4123
4414
|
}
|
|
4124
4415
|
throw new Error("unable to acquire protocol lock");
|
|
4125
4416
|
}
|
|
4126
4417
|
|
|
4127
4418
|
// ../core/dist/sync/synchronize.js
|
|
4128
|
-
import { lstat as lstat2, readFile as
|
|
4129
|
-
import { join as
|
|
4419
|
+
import { lstat as lstat2, readFile as readFile11, rm as rm4 } from "node:fs/promises";
|
|
4420
|
+
import { join as join12, resolve as resolve8 } from "node:path";
|
|
4130
4421
|
|
|
4131
4422
|
// ../core/dist/update/conflicts.js
|
|
4132
4423
|
function operationTargetPath(operation) {
|
|
@@ -4304,7 +4595,8 @@ function planArtifactRebase(input) {
|
|
|
4304
4595
|
plan.alreadyApplied.push({ path: target, operation });
|
|
4305
4596
|
continue;
|
|
4306
4597
|
}
|
|
4307
|
-
const
|
|
4598
|
+
const projectionManaged = input.protocolOnlyPaths?.has(source) === true || input.protocolOnlyPaths?.has(target) === true;
|
|
4599
|
+
const staticDecision = projectionManaged ? { apply: false, reason: "protocol-only" } : decideUpdate(policy, false);
|
|
4308
4600
|
if (!staticDecision.apply) {
|
|
4309
4601
|
const acknowledgeReason = staticDecision.reason === "protocol-only" ? "protocol-only" : "policy-never";
|
|
4310
4602
|
const entry2 = acknowledgedEntry(operation, context, input.projectVersion, acknowledgeReason);
|
|
@@ -4408,8 +4700,8 @@ async function pathExists(path) {
|
|
|
4408
4700
|
}
|
|
4409
4701
|
}
|
|
4410
4702
|
async function optionalContent(root, path) {
|
|
4411
|
-
const full =
|
|
4412
|
-
return await pathExists(full) ?
|
|
4703
|
+
const full = join12(root, path);
|
|
4704
|
+
return await pathExists(full) ? readFile11(full, "utf8") : null;
|
|
4413
4705
|
}
|
|
4414
4706
|
function manifestPayloadHash(manifest) {
|
|
4415
4707
|
const payload = { ...manifest };
|
|
@@ -4421,10 +4713,10 @@ async function loadBlob(root, client, artifactId, operation, requestId, dryRun)
|
|
|
4421
4713
|
return null;
|
|
4422
4714
|
}
|
|
4423
4715
|
const hash = operation.content_sha256;
|
|
4424
|
-
const cacheRoot =
|
|
4425
|
-
const cachePath =
|
|
4716
|
+
const cacheRoot = join12(root, ".harness", "cache", "server-artifacts", artifactId);
|
|
4717
|
+
const cachePath = join12(cacheRoot, "blobs", hash.replace(":", "_"));
|
|
4426
4718
|
if (await pathExists(cachePath) && await sha256File(cachePath) === hash) {
|
|
4427
|
-
return
|
|
4719
|
+
return readFile11(cachePath, "utf8");
|
|
4428
4720
|
}
|
|
4429
4721
|
const bytes = await client.downloadArtifactBlob(artifactId, hash, requestId);
|
|
4430
4722
|
if (bytes.byteLength !== operation.size_bytes || sha256Bytes(bytes) !== hash) {
|
|
@@ -4515,7 +4807,7 @@ function applyBaselineUpdates(baseline, plan) {
|
|
|
4515
4807
|
}
|
|
4516
4808
|
return next;
|
|
4517
4809
|
}
|
|
4518
|
-
async function planSingleArtifact(root, baseline, manifest, client, requestId, dryRun, conflictStrategy, resolveOverrides) {
|
|
4810
|
+
async function planSingleArtifact(root, baseline, manifest, client, requestId, dryRun, conflictStrategy, resolveOverrides, protocolOnlyPaths) {
|
|
4519
4811
|
if (manifest.project_version === null) {
|
|
4520
4812
|
throw new Error("artifact manifest missing project_version");
|
|
4521
4813
|
}
|
|
@@ -4525,11 +4817,12 @@ async function planSingleArtifact(root, baseline, manifest, client, requestId, d
|
|
|
4525
4817
|
projectVersion: manifest.project_version,
|
|
4526
4818
|
contexts,
|
|
4527
4819
|
conflictStrategy,
|
|
4528
|
-
...resolveOverrides === void 0 ? {} : { resolveOverrides }
|
|
4820
|
+
...resolveOverrides === void 0 ? {} : { resolveOverrides },
|
|
4821
|
+
...protocolOnlyPaths === void 0 ? {} : { protocolOnlyPaths }
|
|
4529
4822
|
});
|
|
4530
4823
|
}
|
|
4531
4824
|
async function saveConflictReport(root, requestId, manifest, plan) {
|
|
4532
|
-
const reportPath =
|
|
4825
|
+
const reportPath = join12(root, ".harness", "reports", "conflicts-" + requestId + ".json");
|
|
4533
4826
|
await atomicWriteJson(reportPath, {
|
|
4534
4827
|
schema_version: 1,
|
|
4535
4828
|
request_id: requestId,
|
|
@@ -4540,7 +4833,7 @@ async function saveConflictReport(root, requestId, manifest, plan) {
|
|
|
4540
4833
|
});
|
|
4541
4834
|
}
|
|
4542
4835
|
async function synchronizeArtifacts(options, initialBaseline) {
|
|
4543
|
-
const root =
|
|
4836
|
+
const root = resolve8(options.projectRoot);
|
|
4544
4837
|
const projectId = options.project.project.project_id;
|
|
4545
4838
|
if (projectId === null) {
|
|
4546
4839
|
throw new Error("PROJECT_NOT_BOUND");
|
|
@@ -4589,7 +4882,7 @@ async function synchronizeArtifacts(options, initialBaseline) {
|
|
|
4589
4882
|
if (manifest.artifact_id !== discovery.artifact_id || manifest.project_id !== projectId || manifest.project_version === null || manifestPayloadHash(manifest) !== manifest.manifest_sha256) {
|
|
4590
4883
|
throw new Error("ARTIFACT_HASH_MISMATCH");
|
|
4591
4884
|
}
|
|
4592
|
-
let plan = await planSingleArtifact(root, baseline, manifest, options.client, options.requestId, options.dryRun, conflictStrategy, options.resolveOverrides);
|
|
4885
|
+
let plan = await planSingleArtifact(root, baseline, manifest, options.client, options.requestId, options.dryRun, conflictStrategy, options.resolveOverrides, options.protocolOnlyPaths);
|
|
4593
4886
|
if (plan.conflicts.length > 0 && options.confirmConflictStrategy !== void 0) {
|
|
4594
4887
|
const confirmed = await options.confirmConflictStrategy(plan.conflicts);
|
|
4595
4888
|
if (confirmed === false) {
|
|
@@ -4603,7 +4896,7 @@ async function synchronizeArtifacts(options, initialBaseline) {
|
|
|
4603
4896
|
aggregate.artifactId = manifest.artifact_id;
|
|
4604
4897
|
return aggregate;
|
|
4605
4898
|
}
|
|
4606
|
-
plan = await planSingleArtifact(root, baseline, manifest, options.client, options.requestId, options.dryRun, confirmed, options.resolveOverrides);
|
|
4899
|
+
plan = await planSingleArtifact(root, baseline, manifest, options.client, options.requestId, options.dryRun, confirmed, options.resolveOverrides, options.protocolOnlyPaths);
|
|
4607
4900
|
}
|
|
4608
4901
|
const paths = pathsFromPlan(plan);
|
|
4609
4902
|
aggregate.applied.push(...paths.applied);
|
|
@@ -4647,7 +4940,7 @@ async function synchronizeArtifacts(options, initialBaseline) {
|
|
|
4647
4940
|
}
|
|
4648
4941
|
continue;
|
|
4649
4942
|
}
|
|
4650
|
-
await atomicWriteJson(
|
|
4943
|
+
await atomicWriteJson(join12(root, ".harness", "cache", "server-artifacts", manifest.artifact_id, "manifest.json"), manifest);
|
|
4651
4944
|
const lock = await acquireProtocolLock(root, "update", { requestId: options.requestId });
|
|
4652
4945
|
try {
|
|
4653
4946
|
const nextBaseline = applyBaselineUpdates(baseline, plan);
|
|
@@ -4717,7 +5010,7 @@ function baselineEntryFromOperation(manifest, operation, localContent) {
|
|
|
4717
5010
|
};
|
|
4718
5011
|
}
|
|
4719
5012
|
async function advanceBaselineFromArtifact(options, baseline) {
|
|
4720
|
-
const root =
|
|
5013
|
+
const root = resolve8(options.projectRoot);
|
|
4721
5014
|
if (options.manifest.project_version === null) {
|
|
4722
5015
|
throw new Error("artifact manifest missing project_version");
|
|
4723
5016
|
}
|
|
@@ -4818,29 +5111,29 @@ async function walkFiles(root, current, output) {
|
|
|
4818
5111
|
if (!await exists3(current)) {
|
|
4819
5112
|
return;
|
|
4820
5113
|
}
|
|
4821
|
-
for (const item2 of await
|
|
4822
|
-
const path =
|
|
5114
|
+
for (const item2 of await readdir6(current, { withFileTypes: true })) {
|
|
5115
|
+
const path = join13(current, item2.name);
|
|
4823
5116
|
if (item2.isSymbolicLink()) {
|
|
4824
5117
|
throw new PushWorkflowError("symlink is not pushable", 6, "UNSAFE_SYMLINK");
|
|
4825
5118
|
}
|
|
4826
5119
|
if (item2.isDirectory()) {
|
|
4827
5120
|
await walkFiles(root, path, output);
|
|
4828
5121
|
} else if (item2.isFile()) {
|
|
4829
|
-
output.push(normalizeManagedPath(
|
|
5122
|
+
output.push(normalizeManagedPath(relative2(root, path).replaceAll("\\", "/")));
|
|
4830
5123
|
}
|
|
4831
5124
|
}
|
|
4832
5125
|
}
|
|
4833
5126
|
async function walkArchiveSummaries(root, output) {
|
|
4834
|
-
const archiveRoot =
|
|
5127
|
+
const archiveRoot = join13(root, ".harness", "archive");
|
|
4835
5128
|
if (!await exists3(archiveRoot))
|
|
4836
5129
|
return;
|
|
4837
|
-
for (const item2 of await
|
|
5130
|
+
for (const item2 of await readdir6(archiveRoot, { withFileTypes: true })) {
|
|
4838
5131
|
if (item2.isSymbolicLink()) {
|
|
4839
5132
|
throw new PushWorkflowError("symlink is not pushable", 6, "UNSAFE_SYMLINK");
|
|
4840
5133
|
}
|
|
4841
5134
|
if (!item2.isDirectory())
|
|
4842
5135
|
continue;
|
|
4843
|
-
const summaryPath =
|
|
5136
|
+
const summaryPath = join13(archiveRoot, item2.name, "reports", "final", "summary-data.json");
|
|
4844
5137
|
try {
|
|
4845
5138
|
const stats = await lstat3(summaryPath);
|
|
4846
5139
|
if (stats.isSymbolicLink()) {
|
|
@@ -4853,7 +5146,7 @@ async function walkArchiveSummaries(root, output) {
|
|
|
4853
5146
|
continue;
|
|
4854
5147
|
throw error;
|
|
4855
5148
|
}
|
|
4856
|
-
const relativePath = normalizeManagedPath(
|
|
5149
|
+
const relativePath = normalizeManagedPath(relative2(root, summaryPath).replaceAll("\\", "/"));
|
|
4857
5150
|
if (ARCHIVE_SUMMARY_PATH.test(relativePath)) {
|
|
4858
5151
|
output.push(relativePath);
|
|
4859
5152
|
}
|
|
@@ -4868,19 +5161,19 @@ function enabledHarnessAgents(project) {
|
|
|
4868
5161
|
async function walkHarnessEntries(root, directory, output) {
|
|
4869
5162
|
if (!await exists3(directory))
|
|
4870
5163
|
return;
|
|
4871
|
-
for (const item2 of await
|
|
5164
|
+
for (const item2 of await readdir6(directory, { withFileTypes: true })) {
|
|
4872
5165
|
if (item2.name.startsWith("harness-")) {
|
|
4873
|
-
const path =
|
|
5166
|
+
const path = join13(directory, item2.name);
|
|
4874
5167
|
if (item2.isDirectory()) {
|
|
4875
5168
|
await walkFiles(root, path, output);
|
|
4876
5169
|
} else if (item2.isFile()) {
|
|
4877
|
-
output.push(normalizeManagedPath(
|
|
5170
|
+
output.push(normalizeManagedPath(relative2(root, path).replaceAll("\\", "/")));
|
|
4878
5171
|
}
|
|
4879
5172
|
}
|
|
4880
5173
|
}
|
|
4881
5174
|
}
|
|
4882
5175
|
async function managedFiles(projectRoot, project) {
|
|
4883
|
-
const root =
|
|
5176
|
+
const root = resolve9(projectRoot);
|
|
4884
5177
|
const paths = [];
|
|
4885
5178
|
const adapters = getAdapters(enabledHarnessAgents(project));
|
|
4886
5179
|
const managedFiles2 = [
|
|
@@ -4889,26 +5182,26 @@ async function managedFiles(projectRoot, project) {
|
|
|
4889
5182
|
...adapters.some((adapter) => adapter.name === "codebuddy") ? ["CODEBUDDY.md"] : []
|
|
4890
5183
|
];
|
|
4891
5184
|
for (const path of managedFiles2) {
|
|
4892
|
-
if (await exists3(
|
|
5185
|
+
if (await exists3(join13(root, path))) {
|
|
4893
5186
|
paths.push(path);
|
|
4894
5187
|
}
|
|
4895
5188
|
}
|
|
4896
5189
|
for (const path of SHARED_MANAGED_ROOTS) {
|
|
4897
|
-
await walkFiles(root,
|
|
5190
|
+
await walkFiles(root, join13(root, path), paths);
|
|
4898
5191
|
}
|
|
4899
5192
|
await walkArchiveSummaries(root, paths);
|
|
4900
5193
|
for (const adapter of adapters) {
|
|
4901
5194
|
if (adapter.rulesRoot !== null) {
|
|
4902
|
-
await walkHarnessEntries(root,
|
|
5195
|
+
await walkHarnessEntries(root, join13(root, adapter.rulesRoot), paths);
|
|
4903
5196
|
}
|
|
4904
|
-
await walkHarnessEntries(root,
|
|
5197
|
+
await walkHarnessEntries(root, join13(root, adapter.skillsRoot), paths);
|
|
4905
5198
|
if (adapter.agentsRoot !== null) {
|
|
4906
|
-
await walkHarnessEntries(root,
|
|
5199
|
+
await walkHarnessEntries(root, join13(root, adapter.agentsRoot), paths);
|
|
4907
5200
|
}
|
|
4908
5201
|
}
|
|
4909
5202
|
const result = {};
|
|
4910
5203
|
for (const path of [...new Set(paths)].sort()) {
|
|
4911
|
-
result[path] = await
|
|
5204
|
+
result[path] = await readFile12(join13(root, path), "utf8");
|
|
4912
5205
|
}
|
|
4913
5206
|
return result;
|
|
4914
5207
|
}
|
|
@@ -4917,14 +5210,14 @@ function proposalBaseline(baseline) {
|
|
|
4917
5210
|
}
|
|
4918
5211
|
async function readProject(root) {
|
|
4919
5212
|
try {
|
|
4920
|
-
return projectConfigSchema.parse(parseYaml5(await
|
|
5213
|
+
return projectConfigSchema.parse(parseYaml5(await readFile12(join13(root, ".harness", "project.yaml"), "utf8")));
|
|
4921
5214
|
} catch {
|
|
4922
5215
|
throw new PushWorkflowError("project configuration is missing or invalid", 3, "PROJECT_CONFIG_INVALID");
|
|
4923
5216
|
}
|
|
4924
5217
|
}
|
|
4925
5218
|
async function readOptionalJson(path) {
|
|
4926
5219
|
try {
|
|
4927
|
-
return JSON.parse(await
|
|
5220
|
+
return JSON.parse(await readFile12(path, "utf8"));
|
|
4928
5221
|
} catch (error) {
|
|
4929
5222
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
4930
5223
|
return null;
|
|
@@ -4936,7 +5229,7 @@ async function clientIdFor(root, explicit) {
|
|
|
4936
5229
|
if (explicit !== void 0) {
|
|
4937
5230
|
return explicit;
|
|
4938
5231
|
}
|
|
4939
|
-
const path =
|
|
5232
|
+
const path = join13(root, ".harness", "state", "local", "client.json");
|
|
4940
5233
|
const existing = await readOptionalJson(path);
|
|
4941
5234
|
if (typeof existing?.client_id === "string" && /^cli_[A-Za-z0-9_-]+$/.test(existing.client_id)) {
|
|
4942
5235
|
return existing.client_id;
|
|
@@ -5132,7 +5425,7 @@ async function bindProject(root, project, baseline, projectId) {
|
|
|
5132
5425
|
return { project: nextProject, baseline: nextBaseline };
|
|
5133
5426
|
}
|
|
5134
5427
|
async function pushProject(options) {
|
|
5135
|
-
const root =
|
|
5428
|
+
const root = resolve9(options.projectRoot);
|
|
5136
5429
|
let project = await readProject(root);
|
|
5137
5430
|
let baseline = await readBaseline(root);
|
|
5138
5431
|
const profile = parseHarnessProfile(project.project.profiles[0]);
|
|
@@ -5198,7 +5491,7 @@ async function pushProject(options) {
|
|
|
5198
5491
|
if (options.confirmProposal !== void 0 && !await options.confirmProposal(preview)) {
|
|
5199
5492
|
return { preview, proposalId: null, projectId: project.project.project_id, cancelled: true };
|
|
5200
5493
|
}
|
|
5201
|
-
const workflowPath =
|
|
5494
|
+
const workflowPath = join13(root, ".harness", "state", "local", "push-workflow.json");
|
|
5202
5495
|
const priorWorkflow = await readOptionalJson(workflowPath);
|
|
5203
5496
|
const provisionalRequestId = priorWorkflow?.local_project_key === project.project.local_project_key ? priorWorkflow.request_id : uuidV7();
|
|
5204
5497
|
const lock = await acquireProtocolLock(root, "push", { requestId: provisionalRequestId });
|
|
@@ -5353,7 +5646,7 @@ async function pushProject(options) {
|
|
|
5353
5646
|
pushWarning = "BASELINE_ADVANCE_DEFERRED";
|
|
5354
5647
|
}
|
|
5355
5648
|
}
|
|
5356
|
-
await atomicWriteJson(
|
|
5649
|
+
await atomicWriteJson(join13(root, ".harness", "state", "local", "push-results", finalized.proposal_id + ".json"), {
|
|
5357
5650
|
schema_version: 1,
|
|
5358
5651
|
request_id: requestId,
|
|
5359
5652
|
project_id: projectId,
|
|
@@ -5398,14 +5691,14 @@ async function pushProject(options) {
|
|
|
5398
5691
|
}
|
|
5399
5692
|
|
|
5400
5693
|
// ../core/dist/state/cleanup.js
|
|
5401
|
-
import { readFile as
|
|
5402
|
-
import { join as
|
|
5694
|
+
import { readFile as readFile13, readdir as readdir7, rm as rm6 } from "node:fs/promises";
|
|
5695
|
+
import { join as join14 } from "node:path";
|
|
5403
5696
|
function isSafeEntryName(name) {
|
|
5404
5697
|
return name.length > 0 && !name.includes("/") && !name.includes("\\") && name !== "." && name !== ".." && !name.includes("\0");
|
|
5405
5698
|
}
|
|
5406
5699
|
async function listDir(path) {
|
|
5407
5700
|
try {
|
|
5408
|
-
return await
|
|
5701
|
+
return await readdir7(path);
|
|
5409
5702
|
} catch (error) {
|
|
5410
5703
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
5411
5704
|
return [];
|
|
@@ -5423,7 +5716,7 @@ async function cleanupProject(options) {
|
|
|
5423
5716
|
continue;
|
|
5424
5717
|
let journal;
|
|
5425
5718
|
try {
|
|
5426
|
-
journal = JSON.parse(await
|
|
5719
|
+
journal = JSON.parse(await readFile13(join14(layout.transactions, name, "journal.json"), "utf8"));
|
|
5427
5720
|
} catch {
|
|
5428
5721
|
continue;
|
|
5429
5722
|
}
|
|
@@ -5441,7 +5734,7 @@ async function cleanupProject(options) {
|
|
|
5441
5734
|
continue;
|
|
5442
5735
|
pruned.push(entry.id);
|
|
5443
5736
|
if (!options.dryRun) {
|
|
5444
|
-
await rm6(
|
|
5737
|
+
await rm6(join14(layout.transactions, entry.id), { recursive: true, force: true });
|
|
5445
5738
|
}
|
|
5446
5739
|
}
|
|
5447
5740
|
}
|
|
@@ -5450,7 +5743,7 @@ async function cleanupProject(options) {
|
|
|
5450
5743
|
continue;
|
|
5451
5744
|
removedCache.push(name);
|
|
5452
5745
|
if (!options.dryRun) {
|
|
5453
|
-
await rm6(
|
|
5746
|
+
await rm6(join14(layout.serverArtifacts, name), { recursive: true, force: true });
|
|
5454
5747
|
}
|
|
5455
5748
|
}
|
|
5456
5749
|
return {
|
|
@@ -5510,10 +5803,10 @@ var SKILL_TARGET_AGENTS = Object.freeze([
|
|
|
5510
5803
|
]);
|
|
5511
5804
|
|
|
5512
5805
|
// ../core/dist/transaction/recovery.js
|
|
5513
|
-
import { readFile as
|
|
5514
|
-
import { join as
|
|
5806
|
+
import { readFile as readFile14, readdir as readdir8, rm as rm7, stat as stat4 } from "node:fs/promises";
|
|
5807
|
+
import { join as join15 } from "node:path";
|
|
5515
5808
|
async function recoverTransaction(projectRoot, transactionId) {
|
|
5516
|
-
const journal = JSON.parse(await
|
|
5809
|
+
const journal = JSON.parse(await readFile14(join15(stateLayout(projectRoot).transactions, transactionId, "journal.json"), "utf8"));
|
|
5517
5810
|
if (journal.state === "committed") {
|
|
5518
5811
|
return { transactionId, status: "committed" };
|
|
5519
5812
|
}
|
|
@@ -5530,7 +5823,7 @@ async function listTransactions(projectRoot) {
|
|
|
5530
5823
|
const root = stateLayout(projectRoot).transactions;
|
|
5531
5824
|
let names;
|
|
5532
5825
|
try {
|
|
5533
|
-
names = await
|
|
5826
|
+
names = await readdir8(root);
|
|
5534
5827
|
} catch (error) {
|
|
5535
5828
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
5536
5829
|
return [];
|
|
@@ -5540,7 +5833,7 @@ async function listTransactions(projectRoot) {
|
|
|
5540
5833
|
const transactions = [];
|
|
5541
5834
|
for (const transactionId of names) {
|
|
5542
5835
|
try {
|
|
5543
|
-
const journal = JSON.parse(await
|
|
5836
|
+
const journal = JSON.parse(await readFile14(join15(root, transactionId, "journal.json"), "utf8"));
|
|
5544
5837
|
transactions.push({
|
|
5545
5838
|
transactionId,
|
|
5546
5839
|
kind: journal.kind,
|
|
@@ -5557,7 +5850,7 @@ async function pendingTransactions(projectRoot) {
|
|
|
5557
5850
|
}
|
|
5558
5851
|
async function pathExists2(path) {
|
|
5559
5852
|
try {
|
|
5560
|
-
await
|
|
5853
|
+
await stat4(path);
|
|
5561
5854
|
return true;
|
|
5562
5855
|
} catch (error) {
|
|
5563
5856
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
@@ -5571,11 +5864,11 @@ async function rollbackLatestCommittedUpdate(projectRoot) {
|
|
|
5571
5864
|
if (latest === void 0) {
|
|
5572
5865
|
throw new Error("no committed update transaction is available for rollback");
|
|
5573
5866
|
}
|
|
5574
|
-
const transactionRoot =
|
|
5575
|
-
const journal = JSON.parse(await
|
|
5576
|
-
const after = JSON.parse(await
|
|
5867
|
+
const transactionRoot = join15(stateLayout(projectRoot).transactions, latest.transactionId);
|
|
5868
|
+
const journal = JSON.parse(await readFile14(join15(transactionRoot, "journal.json"), "utf8"));
|
|
5869
|
+
const after = JSON.parse(await readFile14(join15(transactionRoot, "after", "manifest.json"), "utf8"));
|
|
5577
5870
|
for (const entry of after) {
|
|
5578
|
-
const target =
|
|
5871
|
+
const target = join15(projectRoot, entry.path);
|
|
5579
5872
|
const exists6 = await pathExists2(target);
|
|
5580
5873
|
if (exists6 !== entry.exists || exists6 && await sha256File(target) !== entry.hash) {
|
|
5581
5874
|
throw new Error("cannot rollback dirty path: " + entry.path);
|
|
@@ -5588,10 +5881,10 @@ async function rollbackLatestCommittedUpdate(projectRoot) {
|
|
|
5588
5881
|
continue;
|
|
5589
5882
|
}
|
|
5590
5883
|
seen.add(snapshot.path);
|
|
5591
|
-
const target =
|
|
5884
|
+
const target = join15(projectRoot, snapshot.path);
|
|
5592
5885
|
const exists6 = await pathExists2(target);
|
|
5593
5886
|
if (snapshot.existed && snapshot.snapshot_name !== null) {
|
|
5594
|
-
const content = await
|
|
5887
|
+
const content = await readFile14(join15(transactionRoot, "before", snapshot.snapshot_name));
|
|
5595
5888
|
operations.push({
|
|
5596
5889
|
operation: exists6 ? "modify" : "add",
|
|
5597
5890
|
path: snapshot.path,
|
|
@@ -5620,7 +5913,7 @@ async function cleanupOldTransactions(projectRoot, now = /* @__PURE__ */ new Dat
|
|
|
5620
5913
|
if (!removable) {
|
|
5621
5914
|
continue;
|
|
5622
5915
|
}
|
|
5623
|
-
await rm7(
|
|
5916
|
+
await rm7(join15(stateLayout(projectRoot).transactions, item2.transactionId), {
|
|
5624
5917
|
recursive: true,
|
|
5625
5918
|
force: true
|
|
5626
5919
|
});
|
|
@@ -5630,8 +5923,8 @@ async function cleanupOldTransactions(projectRoot, now = /* @__PURE__ */ new Dat
|
|
|
5630
5923
|
}
|
|
5631
5924
|
|
|
5632
5925
|
// ../core/dist/update/update.js
|
|
5633
|
-
import { readFile as
|
|
5634
|
-
import { join as
|
|
5926
|
+
import { readFile as readFile15 } from "node:fs/promises";
|
|
5927
|
+
import { join as join16, resolve as resolve10 } from "node:path";
|
|
5635
5928
|
import { parse as parseYaml8 } from "yaml";
|
|
5636
5929
|
var UpdateWorkflowError = class extends Error {
|
|
5637
5930
|
exitCode;
|
|
@@ -5644,10 +5937,10 @@ var UpdateWorkflowError = class extends Error {
|
|
|
5644
5937
|
}
|
|
5645
5938
|
};
|
|
5646
5939
|
async function updateProject(options) {
|
|
5647
|
-
const root =
|
|
5940
|
+
const root = resolve10(options.projectRoot);
|
|
5648
5941
|
let project;
|
|
5649
5942
|
try {
|
|
5650
|
-
project = projectConfigSchema.parse(parseYaml8(await
|
|
5943
|
+
project = projectConfigSchema.parse(parseYaml8(await readFile15(join16(root, ".harness", "project.yaml"), "utf8")));
|
|
5651
5944
|
} catch {
|
|
5652
5945
|
throw new UpdateWorkflowError("project configuration is invalid", 3, "PROJECT_CONFIG_INVALID");
|
|
5653
5946
|
}
|
|
@@ -5671,6 +5964,7 @@ async function updateProject(options) {
|
|
|
5671
5964
|
}
|
|
5672
5965
|
const requestId = uuidV7();
|
|
5673
5966
|
const baseline = await readBaseline(root);
|
|
5967
|
+
const protocolOnlyPaths = await readManagedProjectRuleProjectionPaths(root);
|
|
5674
5968
|
let parsedServerUrl;
|
|
5675
5969
|
try {
|
|
5676
5970
|
parsedServerUrl = new URL(serverUrl);
|
|
@@ -5692,6 +5986,7 @@ async function updateProject(options) {
|
|
|
5692
5986
|
client,
|
|
5693
5987
|
requestId,
|
|
5694
5988
|
dryRun: options.dryRun,
|
|
5989
|
+
protocolOnlyPaths,
|
|
5695
5990
|
...options.conflictStrategy === void 0 ? {} : { conflictStrategy: options.conflictStrategy },
|
|
5696
5991
|
...options.resolveOverrides === void 0 ? {} : { resolveOverrides: options.resolveOverrides },
|
|
5697
5992
|
...options.confirmConflictStrategy === void 0 ? {} : { confirmConflictStrategy: options.confirmConflictStrategy },
|
|
@@ -5702,7 +5997,13 @@ async function updateProject(options) {
|
|
|
5702
5997
|
const parsed = harnessAgentSchema.safeParse(agent);
|
|
5703
5998
|
return parsed.success ? [parsed.data] : [];
|
|
5704
5999
|
});
|
|
5705
|
-
await synchronizeProjectRules(root, agents, project.adapter_options?.codebuddy?.surface ?? "both");
|
|
6000
|
+
const projections = await synchronizeProjectRules(root, agents, project.adapter_options?.codebuddy?.surface ?? "both");
|
|
6001
|
+
for (const path of projections.conflicts) {
|
|
6002
|
+
result.conflicts.push({ path, operation: "modify", reason: "local-dirty" });
|
|
6003
|
+
if (!result.skipped.some((item2) => item2.path === path)) {
|
|
6004
|
+
result.skipped.push({ path, operation: "modify", reason: "local-dirty" });
|
|
6005
|
+
}
|
|
6006
|
+
}
|
|
5706
6007
|
}
|
|
5707
6008
|
return result;
|
|
5708
6009
|
} catch (error) {
|
|
@@ -5729,8 +6030,8 @@ async function updateProject(options) {
|
|
|
5729
6030
|
}
|
|
5730
6031
|
|
|
5731
6032
|
// src/config/init-config.ts
|
|
5732
|
-
import { isAbsolute as isAbsolute2, join as
|
|
5733
|
-
import { readFile as
|
|
6033
|
+
import { isAbsolute as isAbsolute2, join as join17 } from "node:path";
|
|
6034
|
+
import { readFile as readFile16 } from "node:fs/promises";
|
|
5734
6035
|
var InitConfigurationError = class extends Error {
|
|
5735
6036
|
exitCode;
|
|
5736
6037
|
code;
|
|
@@ -5813,9 +6114,9 @@ function hasOwn(record, key) {
|
|
|
5813
6114
|
async function resolveInitConfig(cwd, flags, prompts = {}, warnings = []) {
|
|
5814
6115
|
let fileConfig = {};
|
|
5815
6116
|
if (flags.config !== void 0) {
|
|
5816
|
-
const path = isAbsolute2(flags.config) ? flags.config :
|
|
6117
|
+
const path = isAbsolute2(flags.config) ? flags.config : join17(cwd, flags.config);
|
|
5817
6118
|
try {
|
|
5818
|
-
fileConfig = JSON.parse(await
|
|
6119
|
+
fileConfig = JSON.parse(await readFile16(path, "utf8"));
|
|
5819
6120
|
} catch (error) {
|
|
5820
6121
|
throw new InitConfigurationError(
|
|
5821
6122
|
"unable to read init config: " + (error instanceof Error ? error.message : String(error)),
|
|
@@ -5897,8 +6198,8 @@ function serializeCliResult(result) {
|
|
|
5897
6198
|
}
|
|
5898
6199
|
|
|
5899
6200
|
// src/config/codebuddy-setup.ts
|
|
5900
|
-
import { mkdir as
|
|
5901
|
-
import { basename as
|
|
6201
|
+
import { mkdir as mkdir6, readFile as readFile17, readdir as readdir9, stat as stat5, writeFile as writeFile8 } from "node:fs/promises";
|
|
6202
|
+
import { basename as basename4, dirname as dirname6, extname as extname2, join as join18 } from "node:path";
|
|
5902
6203
|
var MANAGED_RULE_NAMES = /* @__PURE__ */ new Set([
|
|
5903
6204
|
"harness-general.md",
|
|
5904
6205
|
"harness-general.mdc",
|
|
@@ -5909,7 +6210,7 @@ var SENSITIVE_ASSIGNMENT = /(?:password|passwd|token|secret|access[_-]?key|priva
|
|
|
5909
6210
|
var PRIVATE_KEY_BLOCK = /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/i;
|
|
5910
6211
|
async function exists4(path) {
|
|
5911
6212
|
try {
|
|
5912
|
-
await
|
|
6213
|
+
await stat5(path);
|
|
5913
6214
|
return true;
|
|
5914
6215
|
} catch {
|
|
5915
6216
|
return false;
|
|
@@ -5917,7 +6218,7 @@ async function exists4(path) {
|
|
|
5917
6218
|
}
|
|
5918
6219
|
async function readJsonObject(path) {
|
|
5919
6220
|
try {
|
|
5920
|
-
const parsed = JSON.parse(await
|
|
6221
|
+
const parsed = JSON.parse(await readFile17(path, "utf8"));
|
|
5921
6222
|
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
5922
6223
|
} catch (error) {
|
|
5923
6224
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") return {};
|
|
@@ -5925,10 +6226,10 @@ async function readJsonObject(path) {
|
|
|
5925
6226
|
}
|
|
5926
6227
|
}
|
|
5927
6228
|
async function inspectCodeBuddySetup(projectRoot, surface = "both") {
|
|
5928
|
-
const rulesRoot =
|
|
6229
|
+
const rulesRoot = join18(projectRoot, ".claude", "rules");
|
|
5929
6230
|
let ruleNames = [];
|
|
5930
6231
|
try {
|
|
5931
|
-
ruleNames = (await
|
|
6232
|
+
ruleNames = (await readdir9(rulesRoot, { withFileTypes: true })).filter((entry) => entry.isFile() && [".md", ".mdc"].includes(extname2(entry.name).toLowerCase())).map((entry) => entry.name).filter((name) => !MANAGED_RULE_NAMES.has(name)).sort();
|
|
5932
6233
|
} catch (error) {
|
|
5933
6234
|
if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) throw error;
|
|
5934
6235
|
}
|
|
@@ -5936,11 +6237,11 @@ async function inspectCodeBuddySetup(projectRoot, surface = "both") {
|
|
|
5936
6237
|
const currentClaudeRules = [];
|
|
5937
6238
|
const conflictingClaudeRules = [];
|
|
5938
6239
|
for (const name of ruleNames) {
|
|
5939
|
-
const sourceContent = await
|
|
6240
|
+
const sourceContent = await readFile17(join18(rulesRoot, name), "utf8");
|
|
5940
6241
|
const targetContents = await Promise.all(
|
|
5941
6242
|
destinationTargets(projectRoot, surface, name).map(async (target) => {
|
|
5942
6243
|
try {
|
|
5943
|
-
return await
|
|
6244
|
+
return await readFile17(target, "utf8");
|
|
5944
6245
|
} catch (error) {
|
|
5945
6246
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") return null;
|
|
5946
6247
|
throw error;
|
|
@@ -5954,22 +6255,22 @@ async function inspectCodeBuddySetup(projectRoot, surface = "both") {
|
|
|
5954
6255
|
currentClaudeRules.push(name);
|
|
5955
6256
|
}
|
|
5956
6257
|
}
|
|
5957
|
-
const mcp = await readJsonObject(
|
|
6258
|
+
const mcp = await readJsonObject(join18(projectRoot, ".mcp.json"));
|
|
5958
6259
|
const servers = mcp?.mcpServers;
|
|
5959
6260
|
const configured = servers !== null && typeof servers === "object" && !Array.isArray(servers) && Object.prototype.hasOwnProperty.call(servers, "codegraph");
|
|
5960
6261
|
return {
|
|
5961
6262
|
claudeRules,
|
|
5962
6263
|
currentClaudeRules,
|
|
5963
6264
|
conflictingClaudeRules,
|
|
5964
|
-
hasCodeGraphIndex: await exists4(
|
|
6265
|
+
hasCodeGraphIndex: await exists4(join18(projectRoot, ".codegraph")),
|
|
5965
6266
|
codeGraphConfigured: configured
|
|
5966
6267
|
};
|
|
5967
6268
|
}
|
|
5968
6269
|
function destinationTargets(root, surface, name) {
|
|
5969
|
-
const stem =
|
|
6270
|
+
const stem = basename4(name, extname2(name));
|
|
5970
6271
|
const targets = [];
|
|
5971
|
-
if (surface !== "cli") targets.push(
|
|
5972
|
-
if (surface !== "ide") targets.push(
|
|
6272
|
+
if (surface !== "cli") targets.push(join18(root, ".codebuddy", ".rules", `${stem}.mdc`));
|
|
6273
|
+
if (surface !== "ide") targets.push(join18(root, ".codebuddy", "rules", `${stem}.md`));
|
|
5973
6274
|
return targets;
|
|
5974
6275
|
}
|
|
5975
6276
|
async function applyCodeBuddySetup(options) {
|
|
@@ -5983,8 +6284,8 @@ async function applyCodeBuddySetup(options) {
|
|
|
5983
6284
|
if (options.syncClaudeRules) {
|
|
5984
6285
|
const plan = await inspectCodeBuddySetup(options.projectRoot, options.surface);
|
|
5985
6286
|
for (const name of plan.claudeRules) {
|
|
5986
|
-
const source =
|
|
5987
|
-
const content = await
|
|
6287
|
+
const source = join18(options.projectRoot, ".claude", "rules", name);
|
|
6288
|
+
const content = await readFile17(source, "utf8");
|
|
5988
6289
|
if (SENSITIVE_ASSIGNMENT.test(content) || PRIVATE_KEY_BLOCK.test(content)) {
|
|
5989
6290
|
result.skippedSensitive.push(name);
|
|
5990
6291
|
continue;
|
|
@@ -5994,14 +6295,14 @@ async function applyCodeBuddySetup(options) {
|
|
|
5994
6295
|
result.preserved.push(target);
|
|
5995
6296
|
continue;
|
|
5996
6297
|
}
|
|
5997
|
-
await
|
|
5998
|
-
await
|
|
6298
|
+
await mkdir6(dirname6(target), { recursive: true });
|
|
6299
|
+
await writeFile8(target, content, { encoding: "utf8", flag: "wx" });
|
|
5999
6300
|
result.copied.push(target);
|
|
6000
6301
|
}
|
|
6001
6302
|
}
|
|
6002
6303
|
}
|
|
6003
6304
|
if (options.configureCodeGraph) {
|
|
6004
|
-
const path =
|
|
6305
|
+
const path = join18(options.projectRoot, ".mcp.json");
|
|
6005
6306
|
const current = await readJsonObject(path);
|
|
6006
6307
|
if (current === null) {
|
|
6007
6308
|
result.warnings.push(".mcp.json \u4E0D\u662F\u6709\u6548 JSON\uFF0C\u5DF2\u4FDD\u7559\u539F\u6587\u4EF6\u5E76\u8DF3\u8FC7 CodeGraph MCP \u914D\u7F6E");
|
|
@@ -6016,7 +6317,7 @@ async function applyCodeBuddySetup(options) {
|
|
|
6016
6317
|
codegraph: { command: "codegraph", args: ["serve", "--mcp"] }
|
|
6017
6318
|
}
|
|
6018
6319
|
};
|
|
6019
|
-
await
|
|
6320
|
+
await writeFile8(path, JSON.stringify(next, null, 2) + "\n", "utf8");
|
|
6020
6321
|
result.mcpUpdated = true;
|
|
6021
6322
|
}
|
|
6022
6323
|
}
|
|
@@ -6025,13 +6326,13 @@ async function applyCodeBuddySetup(options) {
|
|
|
6025
6326
|
}
|
|
6026
6327
|
|
|
6027
6328
|
// src/commands/refresh.ts
|
|
6028
|
-
import { readFile as
|
|
6029
|
-
import { join as
|
|
6329
|
+
import { readFile as readFile18 } from "node:fs/promises";
|
|
6330
|
+
import { join as join19 } from "node:path";
|
|
6030
6331
|
import { parse as parseYaml9 } from "yaml";
|
|
6031
6332
|
async function detectProject(root) {
|
|
6032
6333
|
let content;
|
|
6033
6334
|
try {
|
|
6034
|
-
content = await
|
|
6335
|
+
content = await readFile18(join19(root, ".harness", "project.yaml"), "utf8");
|
|
6035
6336
|
} catch (error) {
|
|
6036
6337
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
6037
6338
|
return { status: "absent" };
|
|
@@ -6863,12 +7164,94 @@ async function runUpdate(options, dependencies) {
|
|
|
6863
7164
|
}
|
|
6864
7165
|
}
|
|
6865
7166
|
|
|
7167
|
+
// src/commands/rules-sync.ts
|
|
7168
|
+
function configuredAgents(config) {
|
|
7169
|
+
const agents = config.adapters.enabled.flatMap((value) => {
|
|
7170
|
+
const parsed = harnessAgentSchema.safeParse(value);
|
|
7171
|
+
return parsed.success ? [parsed.data] : [];
|
|
7172
|
+
});
|
|
7173
|
+
return sortHarnessAgents(agents.length > 0 ? agents : ["claude-code"]);
|
|
7174
|
+
}
|
|
7175
|
+
function configuredSurface(config, override) {
|
|
7176
|
+
const value = override ?? config.adapter_options?.codebuddy?.surface ?? "both";
|
|
7177
|
+
if (value === "both" || value === "ide" || value === "cli") return value;
|
|
7178
|
+
throw new Error("codebuddy surface \u5FC5\u987B\u4E3A both\u3001ide \u6216 cli");
|
|
7179
|
+
}
|
|
7180
|
+
async function runRulesSync(options, dependencies) {
|
|
7181
|
+
const detection = await detectProject(dependencies.cwd);
|
|
7182
|
+
if (detection.status === "absent") {
|
|
7183
|
+
dependencies.stderr("\u5C1A\u672A\u521D\u59CB\u5316 Hunter Harness\uFF1B\u8BF7\u5148\u8FD0\u884C `hunter-harness`\u3002\n");
|
|
7184
|
+
return 3;
|
|
7185
|
+
}
|
|
7186
|
+
if (detection.status === "invalid") {
|
|
7187
|
+
dependencies.stderr("PROJECT_CONFIG_INVALID\uFF1A.harness/project.yaml \u65E0\u6548\n");
|
|
7188
|
+
return 3;
|
|
7189
|
+
}
|
|
7190
|
+
try {
|
|
7191
|
+
const agents = options.agents === void 0 ? configuredAgents(detection.config) : parseAgentsInput(options.agents);
|
|
7192
|
+
const projections = await synchronizeProjectRules(
|
|
7193
|
+
dependencies.cwd,
|
|
7194
|
+
agents,
|
|
7195
|
+
configuredSurface(detection.config, options.codebuddySurface)
|
|
7196
|
+
);
|
|
7197
|
+
const learning = options.learn === false ? null : await synchronizeRuleCandidates(dependencies.cwd);
|
|
7198
|
+
const exitCode = projections.conflicts.length > 0 ? 5 : 0;
|
|
7199
|
+
const payload = {
|
|
7200
|
+
schema_version: 1,
|
|
7201
|
+
command: "rules-sync",
|
|
7202
|
+
request_id: uuidV7(),
|
|
7203
|
+
dry_run: false,
|
|
7204
|
+
ok: exitCode === 0,
|
|
7205
|
+
exit_code: exitCode,
|
|
7206
|
+
project_id: detection.config.project.project_id,
|
|
7207
|
+
summary: {
|
|
7208
|
+
migrated: projections.migrated.length,
|
|
7209
|
+
projected: projections.written.length,
|
|
7210
|
+
removed: projections.removed.length,
|
|
7211
|
+
unchanged: projections.unchanged.length,
|
|
7212
|
+
conflicts: projections.conflicts.length,
|
|
7213
|
+
agent_specific: projections.agent_specific.length,
|
|
7214
|
+
rule_candidates: learning?.candidates ?? 0
|
|
7215
|
+
},
|
|
7216
|
+
items: [
|
|
7217
|
+
...projections.migrated.map((path) => ({ path, status: "migrated" })),
|
|
7218
|
+
...projections.written.map((path) => ({ path, status: "projected" })),
|
|
7219
|
+
...projections.agent_specific.map((path) => ({ path, status: "agent-specific" })),
|
|
7220
|
+
...learning === null ? [] : [{
|
|
7221
|
+
path: learning.path,
|
|
7222
|
+
status: learning.changed ? "updated" : "unchanged",
|
|
7223
|
+
candidates: learning.candidates,
|
|
7224
|
+
scanned: learning.scanned
|
|
7225
|
+
}]
|
|
7226
|
+
],
|
|
7227
|
+
warnings: [
|
|
7228
|
+
...projections.conflicts.map((path) => `\u89C4\u5219\u5206\u6B67\u672A\u8986\u76D6\uFF1A${path}`),
|
|
7229
|
+
...projections.agent_specific.map((path) => `\u4FDD\u7559 Agent \u4E13\u5C5E\u89C4\u5219\uFF1A${path}`)
|
|
7230
|
+
],
|
|
7231
|
+
errors: []
|
|
7232
|
+
};
|
|
7233
|
+
if (options.json === true) {
|
|
7234
|
+
dependencies.stdout(serializeCliResult(payload));
|
|
7235
|
+
} else {
|
|
7236
|
+
dependencies.stdout(
|
|
7237
|
+
`\u89C4\u5219\u540C\u6B65\uFF1A\u8FC1\u79FB ${payload.summary.migrated}\uFF0C\u6295\u5F71 ${payload.summary.projected}\uFF0C\u51B2\u7A81 ${payload.summary.conflicts}\uFF0C\u5019\u9009 ${payload.summary.rule_candidates}\u3002
|
|
7238
|
+
`
|
|
7239
|
+
);
|
|
7240
|
+
for (const warning of payload.warnings) dependencies.stderr(warning + "\n");
|
|
7241
|
+
}
|
|
7242
|
+
return exitCode;
|
|
7243
|
+
} catch (error) {
|
|
7244
|
+
dependencies.stderr((error instanceof Error ? error.message : String(error)) + "\n");
|
|
7245
|
+
return 1;
|
|
7246
|
+
}
|
|
7247
|
+
}
|
|
7248
|
+
|
|
6866
7249
|
// src/commands/recovery.ts
|
|
6867
|
-
import { stat as
|
|
6868
|
-
import { join as
|
|
7250
|
+
import { stat as stat6 } from "node:fs/promises";
|
|
7251
|
+
import { join as join20 } from "node:path";
|
|
6869
7252
|
async function exists5(path) {
|
|
6870
7253
|
try {
|
|
6871
|
-
await
|
|
7254
|
+
await stat6(path);
|
|
6872
7255
|
return true;
|
|
6873
7256
|
} catch {
|
|
6874
7257
|
return false;
|
|
@@ -6925,7 +7308,7 @@ async function runRecoveryMenuIfApplicable(options, dependencies) {
|
|
|
6925
7308
|
return 5;
|
|
6926
7309
|
}
|
|
6927
7310
|
}
|
|
6928
|
-
const initialized = await exists5(
|
|
7311
|
+
const initialized = await exists5(join20(dependencies.cwd, ".harness", "project.yaml"));
|
|
6929
7312
|
if (!initialized || options.nonInteractive === true || explicitConfigure(options)) {
|
|
6930
7313
|
return null;
|
|
6931
7314
|
}
|
|
@@ -6968,8 +7351,8 @@ async function runRecoveryMenuIfApplicable(options, dependencies) {
|
|
|
6968
7351
|
}
|
|
6969
7352
|
|
|
6970
7353
|
// src/workflow-data/resolve.ts
|
|
6971
|
-
import { cp, mkdir as
|
|
6972
|
-
import { dirname as
|
|
7354
|
+
import { cp, mkdir as mkdir7, readdir as readdir10, readFile as readFile19, rm as rm8, stat as stat7 } from "node:fs/promises";
|
|
7355
|
+
import { dirname as dirname7, join as join21, relative as relative3 } from "node:path";
|
|
6973
7356
|
import { fileURLToPath } from "node:url";
|
|
6974
7357
|
var WorkflowDataResolutionError = class extends Error {
|
|
6975
7358
|
code;
|
|
@@ -6983,7 +7366,7 @@ var WorkflowDataResolutionError = class extends Error {
|
|
|
6983
7366
|
};
|
|
6984
7367
|
async function pathExists3(path) {
|
|
6985
7368
|
try {
|
|
6986
|
-
await
|
|
7369
|
+
await stat7(path);
|
|
6987
7370
|
return true;
|
|
6988
7371
|
} catch {
|
|
6989
7372
|
return false;
|
|
@@ -7003,17 +7386,17 @@ function workflowPackageName(family, env) {
|
|
|
7003
7386
|
return `${scope}/workflow-${family}`;
|
|
7004
7387
|
}
|
|
7005
7388
|
async function listFilesRecursive(root, base = root) {
|
|
7006
|
-
const entries = await
|
|
7389
|
+
const entries = await readdir10(root, { withFileTypes: true });
|
|
7007
7390
|
const files = [];
|
|
7008
7391
|
for (const entry of entries) {
|
|
7009
|
-
const absolute =
|
|
7392
|
+
const absolute = join21(root, entry.name);
|
|
7010
7393
|
if (entry.isDirectory()) {
|
|
7011
7394
|
files.push(...await listFilesRecursive(absolute, base));
|
|
7012
7395
|
continue;
|
|
7013
7396
|
}
|
|
7014
7397
|
if (!entry.isFile()) continue;
|
|
7015
|
-
const rel =
|
|
7016
|
-
files.push({ path: rel, content: await
|
|
7398
|
+
const rel = relative3(base, absolute).replaceAll("\\", "/");
|
|
7399
|
+
files.push({ path: rel, content: await readFile19(absolute, "utf8") });
|
|
7017
7400
|
}
|
|
7018
7401
|
return files;
|
|
7019
7402
|
}
|
|
@@ -7026,7 +7409,7 @@ async function verifyWorkflowPackageIntegrity(resourcesRoot) {
|
|
|
7026
7409
|
const manifest = await readWorkflowFamilyManifest(resourcesRoot);
|
|
7027
7410
|
const expected = manifest.content_sha256;
|
|
7028
7411
|
if (typeof expected !== "string" || expected.length === 0) return;
|
|
7029
|
-
const harnessRoot =
|
|
7412
|
+
const harnessRoot = join21(resourcesRoot, "harness");
|
|
7030
7413
|
if (!await pathExists3(harnessRoot)) {
|
|
7031
7414
|
throw new WorkflowDataResolutionError(
|
|
7032
7415
|
"\u5DE5\u4F5C\u6D41\u6570\u636E\u5305\u7F3A\u5C11 harness/\uFF0C\u65E0\u6CD5\u6821\u9A8C SHA-256",
|
|
@@ -7045,40 +7428,40 @@ async function verifyWorkflowPackageIntegrity(resourcesRoot) {
|
|
|
7045
7428
|
}
|
|
7046
7429
|
}
|
|
7047
7430
|
async function monorepoResourcesRoot() {
|
|
7048
|
-
const here =
|
|
7431
|
+
const here = dirname7(fileURLToPath(import.meta.url));
|
|
7049
7432
|
const candidates = [
|
|
7050
7433
|
// TypeScript source: packages/cli/src/workflow-data -> packages/workflow-data-harness.
|
|
7051
|
-
|
|
7434
|
+
join21(here, "../../../workflow-data-harness"),
|
|
7052
7435
|
// Bundled CLI: packages/cli/dist -> packages/workflow-data-harness.
|
|
7053
|
-
|
|
7436
|
+
join21(here, "../../workflow-data-harness"),
|
|
7054
7437
|
// Test/build layouts that preserve additional source directory levels.
|
|
7055
|
-
|
|
7438
|
+
join21(here, "../../../../packages/workflow-data-harness")
|
|
7056
7439
|
];
|
|
7057
7440
|
for (const candidate of candidates) {
|
|
7058
|
-
if (await pathExists3(
|
|
7441
|
+
if (await pathExists3(join21(candidate, "harness", "manifests"))) return candidate;
|
|
7059
7442
|
}
|
|
7060
7443
|
return null;
|
|
7061
7444
|
}
|
|
7062
7445
|
async function siblingWorkflowPackage(cwd) {
|
|
7063
|
-
const here =
|
|
7446
|
+
const here = dirname7(fileURLToPath(import.meta.url));
|
|
7064
7447
|
const candidates = [
|
|
7065
|
-
|
|
7448
|
+
join21(cwd, "node_modules", "@hunter-harness", "workflow-harness"),
|
|
7066
7449
|
// Published layout: node_modules/hunter-harness/dist/bin.js alongside the
|
|
7067
7450
|
// scoped workflow package in node_modules/@hunter-harness/workflow-harness.
|
|
7068
|
-
|
|
7451
|
+
join21(here, "..", "..", "@hunter-harness", "workflow-harness"),
|
|
7069
7452
|
// Monorepo bundled layout: packages/cli/dist/bin.js.
|
|
7070
|
-
|
|
7453
|
+
join21(here, "..", "..", "workflow-data-harness")
|
|
7071
7454
|
];
|
|
7072
7455
|
for (const candidate of candidates) {
|
|
7073
|
-
if (await pathExists3(
|
|
7456
|
+
if (await pathExists3(join21(candidate, "harness", "manifests"))) return candidate;
|
|
7074
7457
|
}
|
|
7075
7458
|
return null;
|
|
7076
7459
|
}
|
|
7077
7460
|
async function readCachedNpmPackageVersion(cacheRoot) {
|
|
7078
|
-
const manifestPath =
|
|
7461
|
+
const manifestPath = join21(cacheRoot, "package.json");
|
|
7079
7462
|
if (!await pathExists3(manifestPath)) return null;
|
|
7080
7463
|
try {
|
|
7081
|
-
const pkg = JSON.parse(await
|
|
7464
|
+
const pkg = JSON.parse(await readFile19(manifestPath, "utf8"));
|
|
7082
7465
|
return typeof pkg.version === "string" && pkg.version.length > 0 ? pkg.version : null;
|
|
7083
7466
|
} catch {
|
|
7084
7467
|
return null;
|
|
@@ -7100,13 +7483,13 @@ async function latestWorkflowCacheIsStale(cacheRoot, packageName, resolveManifes
|
|
|
7100
7483
|
}
|
|
7101
7484
|
}
|
|
7102
7485
|
async function materializeWorkflowPackageCache(cacheRoot, packageSpec, extract) {
|
|
7103
|
-
await
|
|
7104
|
-
const staging =
|
|
7486
|
+
await mkdir7(cacheRoot, { recursive: true });
|
|
7487
|
+
const staging = join21(cacheRoot, ".extract");
|
|
7105
7488
|
await rm8(staging, { recursive: true, force: true });
|
|
7106
|
-
await
|
|
7489
|
+
await mkdir7(staging, { recursive: true });
|
|
7107
7490
|
await extract(packageSpec, staging);
|
|
7108
|
-
const packageDir = await pathExists3(
|
|
7109
|
-
if (!await pathExists3(
|
|
7491
|
+
const packageDir = await pathExists3(join21(staging, "harness", "manifests")) ? staging : join21(staging, "package");
|
|
7492
|
+
if (!await pathExists3(join21(packageDir, "harness", "manifests"))) {
|
|
7110
7493
|
throw new WorkflowDataResolutionError(
|
|
7111
7494
|
"\u5DE5\u4F5C\u6D41\u6570\u636E\u5305\u5185\u5BB9\u65E0\u6548\uFF1A\u7F3A\u5C11 harness/manifests",
|
|
7112
7495
|
"WORKFLOW_DATA_INVALID",
|
|
@@ -7135,8 +7518,8 @@ async function resolveWorkflowResourcesRoot(options, argv = []) {
|
|
|
7135
7518
|
const packageName = workflowPackageName(family, options.env);
|
|
7136
7519
|
const packageSpec = version === "latest" ? packageName : `${packageName}@${version}`;
|
|
7137
7520
|
const cacheKey = packageSpec.replace("/", "+");
|
|
7138
|
-
const cacheRoot =
|
|
7139
|
-
if (await pathExists3(
|
|
7521
|
+
const cacheRoot = join21(options.cwd, ".harness", "cache", "workflow-packages", cacheKey);
|
|
7522
|
+
if (await pathExists3(join21(cacheRoot, "harness", "manifests"))) {
|
|
7140
7523
|
if (version === "latest") {
|
|
7141
7524
|
const stale = await latestWorkflowCacheIsStale(cacheRoot, packageName, options.pacoteManifest);
|
|
7142
7525
|
if (stale) {
|
|
@@ -7182,9 +7565,9 @@ function describeWorkflowDataFetchFailure(error, packageSpec) {
|
|
|
7182
7565
|
return `\u65E0\u6CD5\u83B7\u53D6\u5DE5\u4F5C\u6D41\u6570\u636E\u5305 ${packageSpec}\uFF1A\u672C\u5730\u7F13\u5B58\u4E0D\u5B58\u5728\u4E14\u83B7\u53D6\u5931\u8D25\u3002\u53EF${hintRoot}\u3002\u539F\u56E0\uFF1A${code !== "" ? code + " " : ""}${detail}`;
|
|
7183
7566
|
}
|
|
7184
7567
|
async function readWorkflowFamilyManifest(resourcesRoot) {
|
|
7185
|
-
const manifestPath =
|
|
7568
|
+
const manifestPath = join21(resourcesRoot, "hunter-workflow-family.json");
|
|
7186
7569
|
if (!await pathExists3(manifestPath)) return {};
|
|
7187
|
-
return JSON.parse(await
|
|
7570
|
+
return JSON.parse(await readFile19(manifestPath, "utf8"));
|
|
7188
7571
|
}
|
|
7189
7572
|
|
|
7190
7573
|
// src/bin.ts
|
|
@@ -7298,6 +7681,12 @@ async function runCli(argv, overrides = {}) {
|
|
|
7298
7681
|
dependencies
|
|
7299
7682
|
);
|
|
7300
7683
|
});
|
|
7684
|
+
program.command("rules-sync").description("\u6536\u655B\u516C\u5171\u89C4\u5219\u3001\u5237\u65B0 Agent \u6295\u5F71\u5E76\u63D0\u70BC\u5386\u53F2\u89C4\u5219\u5019\u9009").option("--agents <csv>").option("--codebuddy-surface <surface>").option("--no-learn", "\u53EA\u540C\u6B65\u89C4\u5219\uFF0C\u4E0D\u8BFB\u53D6\u5386\u53F2 review/test \u8BC1\u636E").option("--json").action(async (options) => {
|
|
7685
|
+
exitCode = await runRulesSync(
|
|
7686
|
+
{ ...program.opts(), ...options },
|
|
7687
|
+
dependencies
|
|
7688
|
+
);
|
|
7689
|
+
});
|
|
7301
7690
|
try {
|
|
7302
7691
|
await program.parseAsync(["node", "hunter-harness", ...argv]);
|
|
7303
7692
|
return exitCode;
|