hunter-harness 0.2.29 → 0.2.31
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 +421 -170
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -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((resolve13) => setTimeout(resolve13, milliseconds));
|
|
1237
1237
|
});
|
|
1238
1238
|
let lastError;
|
|
1239
1239
|
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
@@ -2290,6 +2290,10 @@ async function readReceipt(root) {
|
|
|
2290
2290
|
}
|
|
2291
2291
|
return { schema_version: 1, source_hashes: {}, targets: {} };
|
|
2292
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
|
+
}
|
|
2293
2297
|
async function markdownFiles(root) {
|
|
2294
2298
|
try {
|
|
2295
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();
|
|
@@ -2305,18 +2309,39 @@ function portable(path) {
|
|
|
2305
2309
|
function normalizeRuleContent(content) {
|
|
2306
2310
|
return content.replace(/\r\n/g, "\n").trimEnd() + "\n";
|
|
2307
2311
|
}
|
|
2308
|
-
function
|
|
2312
|
+
function parseAgentRule(content) {
|
|
2309
2313
|
const normalized = content.replace(/\r\n/g, "\n");
|
|
2310
|
-
if (!normalized.startsWith("---\n"))
|
|
2311
|
-
return
|
|
2314
|
+
if (!normalized.startsWith("---\n")) {
|
|
2315
|
+
return {
|
|
2316
|
+
canonical: normalizeRuleContent(normalized),
|
|
2317
|
+
globalFrontmatterPrefix: null
|
|
2318
|
+
};
|
|
2319
|
+
}
|
|
2312
2320
|
const closing = normalized.indexOf("\n---\n", 4);
|
|
2313
|
-
if (closing < 0)
|
|
2314
|
-
return null;
|
|
2321
|
+
if (closing < 0) {
|
|
2322
|
+
return { canonical: null, globalFrontmatterPrefix: null };
|
|
2323
|
+
}
|
|
2315
2324
|
const frontmatter = normalized.slice(4, closing);
|
|
2316
2325
|
if (/^\s*(?:globs?|paths?)\s*:/im.test(frontmatter) || /^\s*alwaysApply\s*:\s*false\s*$/im.test(frontmatter)) {
|
|
2317
|
-
return null;
|
|
2326
|
+
return { canonical: null, globalFrontmatterPrefix: null };
|
|
2318
2327
|
}
|
|
2319
|
-
|
|
2328
|
+
const bodyStart = closing + 5;
|
|
2329
|
+
return {
|
|
2330
|
+
canonical: normalizeRuleContent(normalized.slice(bodyStart).replace(/^\n+/, "")),
|
|
2331
|
+
globalFrontmatterPrefix: normalized.slice(0, bodyStart)
|
|
2332
|
+
};
|
|
2333
|
+
}
|
|
2334
|
+
function canonicalImportContent(content) {
|
|
2335
|
+
return parseAgentRule(content).canonical;
|
|
2336
|
+
}
|
|
2337
|
+
function projectionContent(target, current, canonical) {
|
|
2338
|
+
if (current === null || !target.toLowerCase().endsWith(".mdc"))
|
|
2339
|
+
return canonical;
|
|
2340
|
+
const parsed = parseAgentRule(current);
|
|
2341
|
+
if (parsed.globalFrontmatterPrefix === null)
|
|
2342
|
+
return canonical;
|
|
2343
|
+
return `${parsed.globalFrontmatterPrefix}
|
|
2344
|
+
${canonical}`;
|
|
2320
2345
|
}
|
|
2321
2346
|
async function collectImportCandidates(root, previous, result) {
|
|
2322
2347
|
const candidates = [];
|
|
@@ -2418,9 +2443,10 @@ async function synchronizeProjectRules(projectRoot, agents, surface = "both") {
|
|
|
2418
2443
|
result.unchanged.push(target);
|
|
2419
2444
|
next.targets[target] = current === null ? incomingHash : sha256(current);
|
|
2420
2445
|
} else if (current === null || previous.targets[target] === sha256(current)) {
|
|
2421
|
-
|
|
2446
|
+
const projected = projectionContent(target, current, content);
|
|
2447
|
+
await atomicWrite(path, projected);
|
|
2422
2448
|
result.written.push(target);
|
|
2423
|
-
next.targets[target] =
|
|
2449
|
+
next.targets[target] = sha256(projected);
|
|
2424
2450
|
} else {
|
|
2425
2451
|
result.conflicts.push(target);
|
|
2426
2452
|
next.targets[target] = previous.targets[target] ?? sha256(current);
|
|
@@ -3205,16 +3231,157 @@ async function synchronizeRuleCandidates(projectRoot, options = {}) {
|
|
|
3205
3231
|
};
|
|
3206
3232
|
}
|
|
3207
3233
|
|
|
3208
|
-
// ../core/dist/project/
|
|
3234
|
+
// ../core/dist/project/rule-review.js
|
|
3209
3235
|
import { createHash as createHash7 } from "node:crypto";
|
|
3210
|
-
import { readFile as readFile7,
|
|
3211
|
-
import { dirname as dirname5, join as join8, resolve as resolve7 } from "node:path";
|
|
3236
|
+
import { mkdir as mkdir6, readFile as readFile7, rename as rename5, writeFile as writeFile6 } from "node:fs/promises";
|
|
3237
|
+
import { basename as basename4, dirname as dirname5, join as join8, relative as relative2, resolve as resolve7 } from "node:path";
|
|
3238
|
+
var CANDIDATE_PATH2 = ".harness/knowledge/rule-candidates.json";
|
|
3239
|
+
var DECISION_PATH = ".harness/knowledge/rule-decisions.json";
|
|
3240
|
+
var RULES_ROOT2 = ".harness/rules";
|
|
3241
|
+
function sha2563(content) {
|
|
3242
|
+
return createHash7("sha256").update(content).digest("hex");
|
|
3243
|
+
}
|
|
3244
|
+
async function readOptional2(path) {
|
|
3245
|
+
try {
|
|
3246
|
+
return await readFile7(path, "utf8");
|
|
3247
|
+
} catch (error) {
|
|
3248
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
3249
|
+
return null;
|
|
3250
|
+
throw error;
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
async function readJson(path, fallback) {
|
|
3254
|
+
const content = await readOptional2(path);
|
|
3255
|
+
if (content === null)
|
|
3256
|
+
return fallback;
|
|
3257
|
+
return JSON.parse(content);
|
|
3258
|
+
}
|
|
3259
|
+
async function atomicWrite3(path, content) {
|
|
3260
|
+
await mkdir6(dirname5(path), { recursive: true });
|
|
3261
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
3262
|
+
await writeFile6(temporary, content, "utf8");
|
|
3263
|
+
await rename5(temporary, path);
|
|
3264
|
+
}
|
|
3265
|
+
function candidateRevision(candidate) {
|
|
3266
|
+
return sha2563(JSON.stringify(candidate));
|
|
3267
|
+
}
|
|
3268
|
+
function portable3(path) {
|
|
3269
|
+
return path.replaceAll("\\", "/");
|
|
3270
|
+
}
|
|
3271
|
+
function ruleTarget2(root, input) {
|
|
3272
|
+
const portablePath = portable3(input);
|
|
3273
|
+
if (!portablePath.startsWith(`${RULES_ROOT2}/`) || !portablePath.toLowerCase().endsWith(".md") || basename4(portablePath) === "") {
|
|
3274
|
+
throw new Error(`RULE_PATCH_PATH_INVALID: ${input}`);
|
|
3275
|
+
}
|
|
3276
|
+
const target = resolve7(root, ...portablePath.split("/"));
|
|
3277
|
+
const rulesRoot = resolve7(root, ...RULES_ROOT2.split("/"));
|
|
3278
|
+
const relativePath = portable3(relative2(rulesRoot, target));
|
|
3279
|
+
if (relativePath === ".." || relativePath.startsWith("../")) {
|
|
3280
|
+
throw new Error(`RULE_PATCH_PATH_INVALID: ${input}`);
|
|
3281
|
+
}
|
|
3282
|
+
return target;
|
|
3283
|
+
}
|
|
3284
|
+
function validateDecision(decision, candidates) {
|
|
3285
|
+
const candidate = candidates.get(decision.candidate_id);
|
|
3286
|
+
if (candidate === void 0) {
|
|
3287
|
+
throw new Error(`RULE_CANDIDATE_UNKNOWN: ${decision.candidate_id}`);
|
|
3288
|
+
}
|
|
3289
|
+
if (candidateRevision(candidate) !== decision.candidate_revision) {
|
|
3290
|
+
throw new Error(`RULE_CANDIDATE_STALE: ${decision.candidate_id}`);
|
|
3291
|
+
}
|
|
3292
|
+
if (decision.dispositions.length === 0 || decision.reason.trim().length < 8) {
|
|
3293
|
+
throw new Error(`RULE_DECISION_INVALID: ${decision.candidate_id}`);
|
|
3294
|
+
}
|
|
3295
|
+
if (decision.dispositions.includes("defer") && decision.review_after === void 0) {
|
|
3296
|
+
throw new Error(`RULE_DECISION_REVIEW_AFTER_REQUIRED: ${decision.candidate_id}`);
|
|
3297
|
+
}
|
|
3298
|
+
if (decision.dispositions.includes("public-rule") !== (decision.rule_patch !== void 0)) {
|
|
3299
|
+
throw new Error(`RULE_PATCH_REQUIRED: ${decision.candidate_id}`);
|
|
3300
|
+
}
|
|
3301
|
+
return candidate;
|
|
3302
|
+
}
|
|
3303
|
+
async function readCandidates(root) {
|
|
3304
|
+
return readJson(join8(root, ...CANDIDATE_PATH2.split("/")), { schema_version: 1, source_hashes: {}, candidates: [] });
|
|
3305
|
+
}
|
|
3306
|
+
async function readDecisions(root) {
|
|
3307
|
+
return readJson(join8(root, ...DECISION_PATH.split("/")), { schema_version: 1, decisions: [] });
|
|
3308
|
+
}
|
|
3309
|
+
async function exportRuleReviewQueue(projectRoot) {
|
|
3310
|
+
const root = resolve7(projectRoot);
|
|
3311
|
+
const candidates = await readCandidates(root);
|
|
3312
|
+
const decisions = await readDecisions(root);
|
|
3313
|
+
const decided = new Set(decisions.decisions.map((decision) => `${decision.candidate_id}\0${decision.candidate_revision}`));
|
|
3314
|
+
const pending = candidates.candidates.flatMap((candidate) => {
|
|
3315
|
+
const revision = candidateRevision(candidate);
|
|
3316
|
+
return decided.has(`${candidate.id}\0${revision}`) ? [] : [{
|
|
3317
|
+
candidate_id: candidate.id,
|
|
3318
|
+
candidate_revision: revision,
|
|
3319
|
+
candidate
|
|
3320
|
+
}];
|
|
3321
|
+
});
|
|
3322
|
+
return {
|
|
3323
|
+
schema_version: 1,
|
|
3324
|
+
pending,
|
|
3325
|
+
decided: candidates.candidates.length - pending.length
|
|
3326
|
+
};
|
|
3327
|
+
}
|
|
3328
|
+
async function applyRuleReviewDecisions(projectRoot, input) {
|
|
3329
|
+
if (input.schema_version !== 1 || !Array.isArray(input.decisions)) {
|
|
3330
|
+
throw new Error("RULE_DECISIONS_INVALID");
|
|
3331
|
+
}
|
|
3332
|
+
const root = resolve7(projectRoot);
|
|
3333
|
+
const candidateManifest = await readCandidates(root);
|
|
3334
|
+
const candidates = new Map(candidateManifest.candidates.map((candidate) => [
|
|
3335
|
+
candidate.id,
|
|
3336
|
+
candidate
|
|
3337
|
+
]));
|
|
3338
|
+
const plannedPatches = [];
|
|
3339
|
+
for (const decision of input.decisions) {
|
|
3340
|
+
validateDecision(decision, candidates);
|
|
3341
|
+
if (decision.rule_patch === void 0)
|
|
3342
|
+
continue;
|
|
3343
|
+
const target = ruleTarget2(root, decision.rule_patch.target_path);
|
|
3344
|
+
const current = await readOptional2(target);
|
|
3345
|
+
const currentHash = current === null ? null : sha2563(current);
|
|
3346
|
+
if (currentHash !== decision.rule_patch.expected_sha256) {
|
|
3347
|
+
throw new Error(`RULE_PATCH_STALE: ${decision.rule_patch.target_path}`);
|
|
3348
|
+
}
|
|
3349
|
+
plannedPatches.push({ path: target, content: decision.rule_patch.content });
|
|
3350
|
+
}
|
|
3351
|
+
const currentManifest = await readDecisions(root);
|
|
3352
|
+
const replacements = new Map(input.decisions.map((decision) => [
|
|
3353
|
+
`${decision.candidate_id}\0${decision.candidate_revision}`,
|
|
3354
|
+
decision
|
|
3355
|
+
]));
|
|
3356
|
+
for (const decision of currentManifest.decisions) {
|
|
3357
|
+
const key = `${decision.candidate_id}\0${decision.candidate_revision}`;
|
|
3358
|
+
if (!replacements.has(key))
|
|
3359
|
+
replacements.set(key, decision);
|
|
3360
|
+
}
|
|
3361
|
+
const next = {
|
|
3362
|
+
schema_version: 1,
|
|
3363
|
+
decisions: [...replacements.values()].sort((left, right) => left.candidate_id.localeCompare(right.candidate_id) || left.candidate_revision.localeCompare(right.candidate_revision))
|
|
3364
|
+
};
|
|
3365
|
+
for (const patch of plannedPatches)
|
|
3366
|
+
await atomicWrite3(patch.path, patch.content);
|
|
3367
|
+
await atomicWrite3(join8(root, ...DECISION_PATH.split("/")), JSON.stringify(next, null, 2) + "\n");
|
|
3368
|
+
return {
|
|
3369
|
+
applied: plannedPatches.length,
|
|
3370
|
+
recorded: input.decisions.length,
|
|
3371
|
+
path: DECISION_PATH
|
|
3372
|
+
};
|
|
3373
|
+
}
|
|
3374
|
+
|
|
3375
|
+
// ../core/dist/project/refresh.js
|
|
3376
|
+
import { createHash as createHash8 } from "node:crypto";
|
|
3377
|
+
import { readFile as readFile8, readdir as readdir5, rmdir } from "node:fs/promises";
|
|
3378
|
+
import { dirname as dirname6, join as join9, resolve as resolve8 } from "node:path";
|
|
3212
3379
|
import { parse as parseYaml3, stringify as stringifyYaml2 } from "yaml";
|
|
3213
3380
|
var INSTALLED_STATE_PATH = ".harness/state/local/installed-harness-bundle.json";
|
|
3214
3381
|
var CONTEXT_INDEX_PATH2 = ".harness/context-index.json";
|
|
3215
3382
|
async function fileHex2(path) {
|
|
3216
3383
|
try {
|
|
3217
|
-
return
|
|
3384
|
+
return createHash8("sha256").update(await readFile8(path)).digest("hex");
|
|
3218
3385
|
} catch (error) {
|
|
3219
3386
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
3220
3387
|
return null;
|
|
@@ -3224,7 +3391,7 @@ async function fileHex2(path) {
|
|
|
3224
3391
|
}
|
|
3225
3392
|
async function readOptionalText(path) {
|
|
3226
3393
|
try {
|
|
3227
|
-
return await
|
|
3394
|
+
return await readFile8(path, "utf8");
|
|
3228
3395
|
} catch (error) {
|
|
3229
3396
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
3230
3397
|
return "";
|
|
@@ -3233,7 +3400,7 @@ async function readOptionalText(path) {
|
|
|
3233
3400
|
}
|
|
3234
3401
|
}
|
|
3235
3402
|
async function readInstalledState(root) {
|
|
3236
|
-
const content = await readOptionalText(
|
|
3403
|
+
const content = await readOptionalText(join9(root, INSTALLED_STATE_PATH));
|
|
3237
3404
|
if (content === "") {
|
|
3238
3405
|
return {
|
|
3239
3406
|
profile: null,
|
|
@@ -3303,7 +3470,7 @@ async function readInstalledState(root) {
|
|
|
3303
3470
|
};
|
|
3304
3471
|
}
|
|
3305
3472
|
async function readInstalledAgentConfiguration(projectRoot) {
|
|
3306
|
-
const installed = await readInstalledState(
|
|
3473
|
+
const installed = await readInstalledState(resolve8(projectRoot));
|
|
3307
3474
|
return {
|
|
3308
3475
|
agents: installed.adapters,
|
|
3309
3476
|
profiles: Object.fromEntries(installed.adapters.map((agent) => [
|
|
@@ -3313,7 +3480,7 @@ async function readInstalledAgentConfiguration(projectRoot) {
|
|
|
3313
3480
|
};
|
|
3314
3481
|
}
|
|
3315
3482
|
async function readContextIndexBundleHash(root) {
|
|
3316
|
-
const content = await readOptionalText(
|
|
3483
|
+
const content = await readOptionalText(join9(root, CONTEXT_INDEX_PATH2));
|
|
3317
3484
|
if (content === "")
|
|
3318
3485
|
return null;
|
|
3319
3486
|
try {
|
|
@@ -3325,9 +3492,9 @@ async function readContextIndexBundleHash(root) {
|
|
|
3325
3492
|
}
|
|
3326
3493
|
}
|
|
3327
3494
|
async function pruneEmptyParentDirs(root, deletedPaths, boundaryPaths) {
|
|
3328
|
-
const boundaries = new Set(boundaryPaths.map((path) =>
|
|
3495
|
+
const boundaries = new Set(boundaryPaths.map((path) => join9(root, path)));
|
|
3329
3496
|
for (const deleted of deletedPaths) {
|
|
3330
|
-
let dir =
|
|
3497
|
+
let dir = dirname6(join9(root, deleted));
|
|
3331
3498
|
while (dir.startsWith(root) && !boundaries.has(dir)) {
|
|
3332
3499
|
let entries;
|
|
3333
3500
|
try {
|
|
@@ -3342,7 +3509,7 @@ async function pruneEmptyParentDirs(root, deletedPaths, boundaryPaths) {
|
|
|
3342
3509
|
} catch {
|
|
3343
3510
|
break;
|
|
3344
3511
|
}
|
|
3345
|
-
dir =
|
|
3512
|
+
dir = dirname6(dir);
|
|
3346
3513
|
}
|
|
3347
3514
|
}
|
|
3348
3515
|
}
|
|
@@ -3369,7 +3536,7 @@ function sortByTarget(items) {
|
|
|
3369
3536
|
return [...items].sort((left, right) => left.target_path.localeCompare(right.target_path));
|
|
3370
3537
|
}
|
|
3371
3538
|
async function reconcileContextIndex(root, agents, profiles, manifests, codebuddySurface2, verifications) {
|
|
3372
|
-
const existing = await readOptionalText(
|
|
3539
|
+
const existing = await readOptionalText(join9(root, CONTEXT_INDEX_PATH2));
|
|
3373
3540
|
let codebase = {
|
|
3374
3541
|
map: ".harness/codebase/map",
|
|
3375
3542
|
status: "missing"
|
|
@@ -3422,7 +3589,7 @@ async function reconcileContextIndex(root, agents, profiles, manifests, codebudd
|
|
|
3422
3589
|
}
|
|
3423
3590
|
async function projectTransitionOperation(root, agents, profiles, codebuddySurface2) {
|
|
3424
3591
|
const path = ".harness/project.yaml";
|
|
3425
|
-
const content = await readOptionalText(
|
|
3592
|
+
const content = await readOptionalText(join9(root, path));
|
|
3426
3593
|
if (content === "")
|
|
3427
3594
|
return null;
|
|
3428
3595
|
const project = parseYaml3(content);
|
|
@@ -3457,11 +3624,11 @@ function mergeTargets(targets) {
|
|
|
3457
3624
|
}).sort((left, right) => left.target_path.localeCompare(right.target_path));
|
|
3458
3625
|
}
|
|
3459
3626
|
async function reconcileMarkdownBlock(root, fileName, blockId, content, remove, ops, conflicts, preserved) {
|
|
3460
|
-
const original = await readOptionalText(
|
|
3627
|
+
const original = await readOptionalText(join9(root, fileName));
|
|
3461
3628
|
const synthetic = {
|
|
3462
3629
|
source_path: fileName,
|
|
3463
3630
|
target_path: fileName,
|
|
3464
|
-
sha256:
|
|
3631
|
+
sha256: createHash8("sha256").update(content).digest("hex"),
|
|
3465
3632
|
bytes: new TextEncoder().encode(content)
|
|
3466
3633
|
};
|
|
3467
3634
|
let next;
|
|
@@ -3476,7 +3643,7 @@ async function reconcileMarkdownBlock(root, fileName, blockId, content, remove,
|
|
|
3476
3643
|
next = refreshed.content;
|
|
3477
3644
|
}
|
|
3478
3645
|
} catch {
|
|
3479
|
-
const current = original === "" ? null :
|
|
3646
|
+
const current = original === "" ? null : createHash8("sha256").update(original).digest("hex");
|
|
3480
3647
|
preserved.push(item(synthetic, "preserve", "MALFORMED_MANAGED_BLOCK", current, synthetic.sha256));
|
|
3481
3648
|
conflicts.push(conflict(synthetic, "MALFORMED_MANAGED_BLOCK", current, synthetic.sha256));
|
|
3482
3649
|
return;
|
|
@@ -3496,7 +3663,7 @@ function stateWithoutInstalledAt(value) {
|
|
|
3496
3663
|
return copy;
|
|
3497
3664
|
}
|
|
3498
3665
|
async function refreshProject(options) {
|
|
3499
|
-
const root =
|
|
3666
|
+
const root = resolve8(options.projectRoot);
|
|
3500
3667
|
const installed = await readInstalledState(root);
|
|
3501
3668
|
const oldAgents = installed.adapters.length > 0 ? installed.adapters : ["claude-code"];
|
|
3502
3669
|
const selectedAgents = sortHarnessAgents(options.agents);
|
|
@@ -3580,7 +3747,7 @@ async function refreshProject(options) {
|
|
|
3580
3747
|
const newStateFiles = installed.files.filter((entry) => entry.owner === "shared" || !selectedSet.has(entry.owner));
|
|
3581
3748
|
for (const target of newManaged) {
|
|
3582
3749
|
const incoming = target.sha256;
|
|
3583
|
-
const current = await fileHex2(
|
|
3750
|
+
const current = await fileHex2(join9(root, target.target_path));
|
|
3584
3751
|
if (current === null) {
|
|
3585
3752
|
applied.push(item(target, "add", "MISSING_TARGET", null, incoming));
|
|
3586
3753
|
ops.push({ operation: "add", path: target.target_path, content: target.bytes });
|
|
@@ -3608,7 +3775,7 @@ async function refreshProject(options) {
|
|
|
3608
3775
|
}
|
|
3609
3776
|
}
|
|
3610
3777
|
for (const target of oldOnly) {
|
|
3611
|
-
const current = await fileHex2(
|
|
3778
|
+
const current = await fileHex2(join9(root, target.target_path));
|
|
3612
3779
|
if (current === null) {
|
|
3613
3780
|
continue;
|
|
3614
3781
|
}
|
|
@@ -3651,7 +3818,7 @@ async function refreshProject(options) {
|
|
|
3651
3818
|
const verifyEntries = [];
|
|
3652
3819
|
for (const target of verifyTargets) {
|
|
3653
3820
|
const rel = target.target_path.replace(/\\/g, "/");
|
|
3654
|
-
const actual = await fileHex2(
|
|
3821
|
+
const actual = await fileHex2(join9(root, target.target_path));
|
|
3655
3822
|
verifyEntries.push({ relpath: rel, sha256: actual ?? "" });
|
|
3656
3823
|
if (actual === null) {
|
|
3657
3824
|
verifyMismatches.push({ relpath: rel, expected: target.sha256, actual: "<missing>" });
|
|
@@ -3684,19 +3851,19 @@ async function refreshProject(options) {
|
|
|
3684
3851
|
owner: "shared",
|
|
3685
3852
|
target_path: "AGENTS.md",
|
|
3686
3853
|
block_id: AGENTS_CORE_BLOCK_ID,
|
|
3687
|
-
content_sha256:
|
|
3854
|
+
content_sha256: createHash8("sha256").update(AGENTS_MANAGED_BLOCK_CONTENT).digest("hex")
|
|
3688
3855
|
},
|
|
3689
3856
|
...selectedSet.has("claude-code") ? [{
|
|
3690
3857
|
owner: "claude-code",
|
|
3691
3858
|
target_path: "CLAUDE.md",
|
|
3692
3859
|
block_id: CLAUDE_BLOCK_ID,
|
|
3693
|
-
content_sha256:
|
|
3860
|
+
content_sha256: createHash8("sha256").update(CLAUDE_MANAGED_BLOCK_CONTENT).digest("hex")
|
|
3694
3861
|
}] : [],
|
|
3695
3862
|
...selectedSet.has("codebuddy") ? [{
|
|
3696
3863
|
owner: "codebuddy",
|
|
3697
3864
|
target_path: "CODEBUDDY.md",
|
|
3698
3865
|
block_id: CODEBUDDY_BLOCK_ID,
|
|
3699
|
-
content_sha256:
|
|
3866
|
+
content_sha256: createHash8("sha256").update(CODEBUDDY_MANAGED_BLOCK_CONTENT).digest("hex")
|
|
3700
3867
|
}] : []
|
|
3701
3868
|
].sort((left, right) => left.target_path.localeCompare(right.target_path) || left.block_id.localeCompare(right.block_id));
|
|
3702
3869
|
const filesByTarget = new Map(newStateFiles.map((entry) => [entry.target_path, entry]));
|
|
@@ -3712,7 +3879,7 @@ async function refreshProject(options) {
|
|
|
3712
3879
|
files: [...filesByTarget.values()].sort((left, right) => left.target_path.localeCompare(right.target_path) || left.source_path.localeCompare(right.source_path)),
|
|
3713
3880
|
managed_blocks: managedBlocks
|
|
3714
3881
|
};
|
|
3715
|
-
const existingState = await readOptionalText(
|
|
3882
|
+
const existingState = await readOptionalText(join9(root, INSTALLED_STATE_PATH));
|
|
3716
3883
|
let existingParsed = null;
|
|
3717
3884
|
try {
|
|
3718
3885
|
existingParsed = existingState === "" ? null : JSON.parse(existingState);
|
|
@@ -3767,7 +3934,7 @@ function buildMarkerCoreHash(text) {
|
|
|
3767
3934
|
}
|
|
3768
3935
|
}
|
|
3769
3936
|
async function collectFreshness(options) {
|
|
3770
|
-
const root =
|
|
3937
|
+
const root = resolve8(options.projectRoot);
|
|
3771
3938
|
const installed = await readInstalledState(root);
|
|
3772
3939
|
const codebuddySurface2 = options.codebuddySurface ?? "both";
|
|
3773
3940
|
const agents = [];
|
|
@@ -3812,18 +3979,18 @@ async function collectFreshness(options) {
|
|
|
3812
3979
|
identity.adapterHash = sha256Bytes(canonicalJson(targets.map((target) => ({ path: target.target_path.replace(/\\/g, "/"), sha256: target.sha256 })).sort((a, b) => a.path.localeCompare(b.path))));
|
|
3813
3980
|
const installedProjection = await Promise.all(targets.map(async (target) => ({
|
|
3814
3981
|
path: target.target_path.replace(/\\/g, "/"),
|
|
3815
|
-
sha256: await fileHex2(
|
|
3982
|
+
sha256: await fileHex2(join9(root, target.target_path))
|
|
3816
3983
|
})));
|
|
3817
3984
|
identity.installedAdapterHash = sha256Bytes(canonicalJson(installedProjection.sort((a, b) => a.path.localeCompare(b.path))));
|
|
3818
3985
|
const markerTarget = targets.find((target) => target.target_path.replace(/\\/g, "/").endsWith(`/${BUILD_MARKER_BUNDLE_PATH}`) || target.target_path === BUILD_MARKER_BUNDLE_PATH);
|
|
3819
3986
|
if (markerTarget !== void 0) {
|
|
3820
|
-
identity.installedCoreHash = buildMarkerCoreHash(await readOptionalText(
|
|
3987
|
+
identity.installedCoreHash = buildMarkerCoreHash(await readOptionalText(join9(root, markerTarget.target_path)));
|
|
3821
3988
|
}
|
|
3822
3989
|
const mismatchDetails = [];
|
|
3823
3990
|
const contentEntries = [];
|
|
3824
3991
|
for (const target of targets) {
|
|
3825
3992
|
const rel = target.target_path.replace(/\\/g, "/");
|
|
3826
|
-
const actual = await fileHex2(
|
|
3993
|
+
const actual = await fileHex2(join9(root, target.target_path));
|
|
3827
3994
|
contentEntries.push({ relpath: rel, sha256: actual ?? "" });
|
|
3828
3995
|
if (actual === null) {
|
|
3829
3996
|
mismatchDetails.push({ relpath: rel, expected: target.sha256, actual: "<missing>" });
|
|
@@ -3846,7 +4013,7 @@ async function collectFreshness(options) {
|
|
|
3846
4013
|
const drifted = [];
|
|
3847
4014
|
const missing = [];
|
|
3848
4015
|
for (const target of targets) {
|
|
3849
|
-
const current = await fileHex2(
|
|
4016
|
+
const current = await fileHex2(join9(root, target.target_path));
|
|
3850
4017
|
if (current === null) {
|
|
3851
4018
|
missing.push(target.target_path);
|
|
3852
4019
|
} else if (current !== target.sha256) {
|
|
@@ -4094,11 +4261,11 @@ function rawFindings(content) {
|
|
|
4094
4261
|
if (rule.id === "HH_PASSWORD_VALUE" && isObviousPlaceholder(value)) {
|
|
4095
4262
|
continue;
|
|
4096
4263
|
}
|
|
4097
|
-
const
|
|
4264
|
+
const relative5 = match[0].indexOf(value);
|
|
4098
4265
|
findings.push({
|
|
4099
4266
|
ruleId: rule.id,
|
|
4100
4267
|
severity: rule.severity,
|
|
4101
|
-
offset: match.index + Math.max(0,
|
|
4268
|
+
offset: match.index + Math.max(0, relative5),
|
|
4102
4269
|
value
|
|
4103
4270
|
});
|
|
4104
4271
|
}
|
|
@@ -4210,8 +4377,8 @@ function generateProposalPreview(input, scanOptions = {}) {
|
|
|
4210
4377
|
}
|
|
4211
4378
|
|
|
4212
4379
|
// ../core/dist/push/credentials.js
|
|
4213
|
-
import { readFile as
|
|
4214
|
-
import { join as
|
|
4380
|
+
import { readFile as readFile9, writeFile as writeFile7 } from "node:fs/promises";
|
|
4381
|
+
import { join as join10 } from "node:path";
|
|
4215
4382
|
import { parse as parseYaml4, stringify as stringifyYaml3 } from "yaml";
|
|
4216
4383
|
var CREDENTIALS_LOCAL_RELATIVE = ".harness/credentials.local.yaml";
|
|
4217
4384
|
var CREDENTIALS_GITIGNORE_LINES = [
|
|
@@ -4281,7 +4448,7 @@ function mergeLocalCredentials(existing, patch) {
|
|
|
4281
4448
|
}
|
|
4282
4449
|
async function readLocalCredentials(projectRoot) {
|
|
4283
4450
|
try {
|
|
4284
|
-
const raw = await
|
|
4451
|
+
const raw = await readFile9(join10(projectRoot, CREDENTIALS_LOCAL_RELATIVE), "utf8");
|
|
4285
4452
|
return parseLocalCredentials(parseYaml4(raw));
|
|
4286
4453
|
} catch (error) {
|
|
4287
4454
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
@@ -4292,13 +4459,13 @@ async function readLocalCredentials(projectRoot) {
|
|
|
4292
4459
|
}
|
|
4293
4460
|
async function writeLocalCredentials(projectRoot, credentials) {
|
|
4294
4461
|
const normalized = validateLocalCredentials(credentials);
|
|
4295
|
-
await
|
|
4462
|
+
await writeFile7(join10(projectRoot, CREDENTIALS_LOCAL_RELATIVE), stringifyYaml3(normalized, { sortMapEntries: true }) + "\n", "utf8");
|
|
4296
4463
|
}
|
|
4297
4464
|
async function ensureCredentialsGitignore(projectRoot) {
|
|
4298
|
-
const gitignorePath =
|
|
4465
|
+
const gitignorePath = join10(projectRoot, ".gitignore");
|
|
4299
4466
|
let content = "";
|
|
4300
4467
|
try {
|
|
4301
|
-
content = await
|
|
4468
|
+
content = await readFile9(gitignorePath, "utf8");
|
|
4302
4469
|
} catch (error) {
|
|
4303
4470
|
if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) {
|
|
4304
4471
|
throw error;
|
|
@@ -4321,7 +4488,7 @@ async function ensureCredentialsGitignore(projectRoot) {
|
|
|
4321
4488
|
return;
|
|
4322
4489
|
}
|
|
4323
4490
|
const suffix = content.length === 0 || content.endsWith("\n") ? "" : "\n";
|
|
4324
|
-
await
|
|
4491
|
+
await writeFile7(gitignorePath, content + suffix + missing.join("\n") + "\n", "utf8");
|
|
4325
4492
|
}
|
|
4326
4493
|
function resolvePushAuth(input) {
|
|
4327
4494
|
const tokenEnv = input.tokenEnv ?? input.projectTokenEnv;
|
|
@@ -4349,28 +4516,28 @@ function resolvePushAuth(input) {
|
|
|
4349
4516
|
}
|
|
4350
4517
|
|
|
4351
4518
|
// ../core/dist/push/push.js
|
|
4352
|
-
import { lstat as lstat3, readFile as
|
|
4353
|
-
import { join as
|
|
4519
|
+
import { lstat as lstat3, readFile as readFile13, readdir as readdir6, rm as rm5 } from "node:fs/promises";
|
|
4520
|
+
import { join as join14, relative as relative3, resolve as resolve10 } from "node:path";
|
|
4354
4521
|
import { parse as parseYaml5, stringify as stringifyYaml4 } from "yaml";
|
|
4355
4522
|
|
|
4356
4523
|
// ../core/dist/state/baseline.js
|
|
4357
|
-
import { readFile as
|
|
4358
|
-
import { join as
|
|
4524
|
+
import { readFile as readFile10 } from "node:fs/promises";
|
|
4525
|
+
import { join as join11 } from "node:path";
|
|
4359
4526
|
async function readBaseline(projectRoot) {
|
|
4360
|
-
const content = await
|
|
4527
|
+
const content = await readFile10(join11(stateLayout(projectRoot).baseline, "manifest.json"), "utf8");
|
|
4361
4528
|
return baselineManifestSchema.parse(JSON.parse(content));
|
|
4362
4529
|
}
|
|
4363
4530
|
|
|
4364
4531
|
// ../core/dist/state/locks.js
|
|
4365
4532
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
4366
|
-
import { readFile as
|
|
4367
|
-
import { join as
|
|
4533
|
+
import { readFile as readFile11, rename as rename6, rm as rm3, writeFile as writeFile8 } from "node:fs/promises";
|
|
4534
|
+
import { join as join12 } from "node:path";
|
|
4368
4535
|
async function readLock(path) {
|
|
4369
|
-
return JSON.parse(await
|
|
4536
|
+
return JSON.parse(await readFile11(path, "utf8"));
|
|
4370
4537
|
}
|
|
4371
4538
|
async function acquireProtocolLock(projectRoot, operation, options = {}) {
|
|
4372
4539
|
const layout = await ensureStateLayout(projectRoot);
|
|
4373
|
-
const lockPath =
|
|
4540
|
+
const lockPath = join12(layout.locks, "protocol.lock");
|
|
4374
4541
|
const now = options.now ?? Date.now();
|
|
4375
4542
|
const staleAfterMs = options.staleAfterMs ?? 15 * 60 * 1e3;
|
|
4376
4543
|
const record = {
|
|
@@ -4383,7 +4550,7 @@ async function acquireProtocolLock(projectRoot, operation, options = {}) {
|
|
|
4383
4550
|
};
|
|
4384
4551
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
4385
4552
|
try {
|
|
4386
|
-
await
|
|
4553
|
+
await writeFile8(lockPath, JSON.stringify(record, null, 2), { flag: "wx" });
|
|
4387
4554
|
return {
|
|
4388
4555
|
path: lockPath,
|
|
4389
4556
|
operation,
|
|
@@ -4405,15 +4572,15 @@ async function acquireProtocolLock(projectRoot, operation, options = {}) {
|
|
|
4405
4572
|
if (now - current.heartbeat_at_ms <= staleAfterMs) {
|
|
4406
4573
|
throw new Error("protocol lock is active for operation " + current.operation, { cause: error });
|
|
4407
4574
|
}
|
|
4408
|
-
await
|
|
4575
|
+
await rename6(lockPath, lockPath + ".stale-" + randomUUID3());
|
|
4409
4576
|
}
|
|
4410
4577
|
}
|
|
4411
4578
|
throw new Error("unable to acquire protocol lock");
|
|
4412
4579
|
}
|
|
4413
4580
|
|
|
4414
4581
|
// ../core/dist/sync/synchronize.js
|
|
4415
|
-
import { lstat as lstat2, readFile as
|
|
4416
|
-
import { join as
|
|
4582
|
+
import { lstat as lstat2, readFile as readFile12, rm as rm4 } from "node:fs/promises";
|
|
4583
|
+
import { join as join13, resolve as resolve9 } from "node:path";
|
|
4417
4584
|
|
|
4418
4585
|
// ../core/dist/update/conflicts.js
|
|
4419
4586
|
function operationTargetPath(operation) {
|
|
@@ -4591,7 +4758,8 @@ function planArtifactRebase(input) {
|
|
|
4591
4758
|
plan.alreadyApplied.push({ path: target, operation });
|
|
4592
4759
|
continue;
|
|
4593
4760
|
}
|
|
4594
|
-
const
|
|
4761
|
+
const projectionManaged = input.protocolOnlyPaths?.has(source) === true || input.protocolOnlyPaths?.has(target) === true;
|
|
4762
|
+
const staticDecision = projectionManaged ? { apply: false, reason: "protocol-only" } : decideUpdate(policy, false);
|
|
4595
4763
|
if (!staticDecision.apply) {
|
|
4596
4764
|
const acknowledgeReason = staticDecision.reason === "protocol-only" ? "protocol-only" : "policy-never";
|
|
4597
4765
|
const entry2 = acknowledgedEntry(operation, context, input.projectVersion, acknowledgeReason);
|
|
@@ -4695,8 +4863,8 @@ async function pathExists(path) {
|
|
|
4695
4863
|
}
|
|
4696
4864
|
}
|
|
4697
4865
|
async function optionalContent(root, path) {
|
|
4698
|
-
const full =
|
|
4699
|
-
return await pathExists(full) ?
|
|
4866
|
+
const full = join13(root, path);
|
|
4867
|
+
return await pathExists(full) ? readFile12(full, "utf8") : null;
|
|
4700
4868
|
}
|
|
4701
4869
|
function manifestPayloadHash(manifest) {
|
|
4702
4870
|
const payload = { ...manifest };
|
|
@@ -4708,10 +4876,10 @@ async function loadBlob(root, client, artifactId, operation, requestId, dryRun)
|
|
|
4708
4876
|
return null;
|
|
4709
4877
|
}
|
|
4710
4878
|
const hash = operation.content_sha256;
|
|
4711
|
-
const cacheRoot =
|
|
4712
|
-
const cachePath =
|
|
4879
|
+
const cacheRoot = join13(root, ".harness", "cache", "server-artifacts", artifactId);
|
|
4880
|
+
const cachePath = join13(cacheRoot, "blobs", hash.replace(":", "_"));
|
|
4713
4881
|
if (await pathExists(cachePath) && await sha256File(cachePath) === hash) {
|
|
4714
|
-
return
|
|
4882
|
+
return readFile12(cachePath, "utf8");
|
|
4715
4883
|
}
|
|
4716
4884
|
const bytes = await client.downloadArtifactBlob(artifactId, hash, requestId);
|
|
4717
4885
|
if (bytes.byteLength !== operation.size_bytes || sha256Bytes(bytes) !== hash) {
|
|
@@ -4802,7 +4970,7 @@ function applyBaselineUpdates(baseline, plan) {
|
|
|
4802
4970
|
}
|
|
4803
4971
|
return next;
|
|
4804
4972
|
}
|
|
4805
|
-
async function planSingleArtifact(root, baseline, manifest, client, requestId, dryRun, conflictStrategy, resolveOverrides) {
|
|
4973
|
+
async function planSingleArtifact(root, baseline, manifest, client, requestId, dryRun, conflictStrategy, resolveOverrides, protocolOnlyPaths) {
|
|
4806
4974
|
if (manifest.project_version === null) {
|
|
4807
4975
|
throw new Error("artifact manifest missing project_version");
|
|
4808
4976
|
}
|
|
@@ -4812,11 +4980,12 @@ async function planSingleArtifact(root, baseline, manifest, client, requestId, d
|
|
|
4812
4980
|
projectVersion: manifest.project_version,
|
|
4813
4981
|
contexts,
|
|
4814
4982
|
conflictStrategy,
|
|
4815
|
-
...resolveOverrides === void 0 ? {} : { resolveOverrides }
|
|
4983
|
+
...resolveOverrides === void 0 ? {} : { resolveOverrides },
|
|
4984
|
+
...protocolOnlyPaths === void 0 ? {} : { protocolOnlyPaths }
|
|
4816
4985
|
});
|
|
4817
4986
|
}
|
|
4818
4987
|
async function saveConflictReport(root, requestId, manifest, plan) {
|
|
4819
|
-
const reportPath =
|
|
4988
|
+
const reportPath = join13(root, ".harness", "reports", "conflicts-" + requestId + ".json");
|
|
4820
4989
|
await atomicWriteJson(reportPath, {
|
|
4821
4990
|
schema_version: 1,
|
|
4822
4991
|
request_id: requestId,
|
|
@@ -4827,7 +4996,7 @@ async function saveConflictReport(root, requestId, manifest, plan) {
|
|
|
4827
4996
|
});
|
|
4828
4997
|
}
|
|
4829
4998
|
async function synchronizeArtifacts(options, initialBaseline) {
|
|
4830
|
-
const root =
|
|
4999
|
+
const root = resolve9(options.projectRoot);
|
|
4831
5000
|
const projectId = options.project.project.project_id;
|
|
4832
5001
|
if (projectId === null) {
|
|
4833
5002
|
throw new Error("PROJECT_NOT_BOUND");
|
|
@@ -4876,7 +5045,7 @@ async function synchronizeArtifacts(options, initialBaseline) {
|
|
|
4876
5045
|
if (manifest.artifact_id !== discovery.artifact_id || manifest.project_id !== projectId || manifest.project_version === null || manifestPayloadHash(manifest) !== manifest.manifest_sha256) {
|
|
4877
5046
|
throw new Error("ARTIFACT_HASH_MISMATCH");
|
|
4878
5047
|
}
|
|
4879
|
-
let plan = await planSingleArtifact(root, baseline, manifest, options.client, options.requestId, options.dryRun, conflictStrategy, options.resolveOverrides);
|
|
5048
|
+
let plan = await planSingleArtifact(root, baseline, manifest, options.client, options.requestId, options.dryRun, conflictStrategy, options.resolveOverrides, options.protocolOnlyPaths);
|
|
4880
5049
|
if (plan.conflicts.length > 0 && options.confirmConflictStrategy !== void 0) {
|
|
4881
5050
|
const confirmed = await options.confirmConflictStrategy(plan.conflicts);
|
|
4882
5051
|
if (confirmed === false) {
|
|
@@ -4890,7 +5059,7 @@ async function synchronizeArtifacts(options, initialBaseline) {
|
|
|
4890
5059
|
aggregate.artifactId = manifest.artifact_id;
|
|
4891
5060
|
return aggregate;
|
|
4892
5061
|
}
|
|
4893
|
-
plan = await planSingleArtifact(root, baseline, manifest, options.client, options.requestId, options.dryRun, confirmed, options.resolveOverrides);
|
|
5062
|
+
plan = await planSingleArtifact(root, baseline, manifest, options.client, options.requestId, options.dryRun, confirmed, options.resolveOverrides, options.protocolOnlyPaths);
|
|
4894
5063
|
}
|
|
4895
5064
|
const paths = pathsFromPlan(plan);
|
|
4896
5065
|
aggregate.applied.push(...paths.applied);
|
|
@@ -4934,7 +5103,7 @@ async function synchronizeArtifacts(options, initialBaseline) {
|
|
|
4934
5103
|
}
|
|
4935
5104
|
continue;
|
|
4936
5105
|
}
|
|
4937
|
-
await atomicWriteJson(
|
|
5106
|
+
await atomicWriteJson(join13(root, ".harness", "cache", "server-artifacts", manifest.artifact_id, "manifest.json"), manifest);
|
|
4938
5107
|
const lock = await acquireProtocolLock(root, "update", { requestId: options.requestId });
|
|
4939
5108
|
try {
|
|
4940
5109
|
const nextBaseline = applyBaselineUpdates(baseline, plan);
|
|
@@ -5004,7 +5173,7 @@ function baselineEntryFromOperation(manifest, operation, localContent) {
|
|
|
5004
5173
|
};
|
|
5005
5174
|
}
|
|
5006
5175
|
async function advanceBaselineFromArtifact(options, baseline) {
|
|
5007
|
-
const root =
|
|
5176
|
+
const root = resolve9(options.projectRoot);
|
|
5008
5177
|
if (options.manifest.project_version === null) {
|
|
5009
5178
|
throw new Error("artifact manifest missing project_version");
|
|
5010
5179
|
}
|
|
@@ -5106,19 +5275,19 @@ async function walkFiles(root, current, output) {
|
|
|
5106
5275
|
return;
|
|
5107
5276
|
}
|
|
5108
5277
|
for (const item2 of await readdir6(current, { withFileTypes: true })) {
|
|
5109
|
-
const path =
|
|
5278
|
+
const path = join14(current, item2.name);
|
|
5110
5279
|
if (item2.isSymbolicLink()) {
|
|
5111
5280
|
throw new PushWorkflowError("symlink is not pushable", 6, "UNSAFE_SYMLINK");
|
|
5112
5281
|
}
|
|
5113
5282
|
if (item2.isDirectory()) {
|
|
5114
5283
|
await walkFiles(root, path, output);
|
|
5115
5284
|
} else if (item2.isFile()) {
|
|
5116
|
-
output.push(normalizeManagedPath(
|
|
5285
|
+
output.push(normalizeManagedPath(relative3(root, path).replaceAll("\\", "/")));
|
|
5117
5286
|
}
|
|
5118
5287
|
}
|
|
5119
5288
|
}
|
|
5120
5289
|
async function walkArchiveSummaries(root, output) {
|
|
5121
|
-
const archiveRoot =
|
|
5290
|
+
const archiveRoot = join14(root, ".harness", "archive");
|
|
5122
5291
|
if (!await exists3(archiveRoot))
|
|
5123
5292
|
return;
|
|
5124
5293
|
for (const item2 of await readdir6(archiveRoot, { withFileTypes: true })) {
|
|
@@ -5127,7 +5296,7 @@ async function walkArchiveSummaries(root, output) {
|
|
|
5127
5296
|
}
|
|
5128
5297
|
if (!item2.isDirectory())
|
|
5129
5298
|
continue;
|
|
5130
|
-
const summaryPath =
|
|
5299
|
+
const summaryPath = join14(archiveRoot, item2.name, "reports", "final", "summary-data.json");
|
|
5131
5300
|
try {
|
|
5132
5301
|
const stats = await lstat3(summaryPath);
|
|
5133
5302
|
if (stats.isSymbolicLink()) {
|
|
@@ -5140,7 +5309,7 @@ async function walkArchiveSummaries(root, output) {
|
|
|
5140
5309
|
continue;
|
|
5141
5310
|
throw error;
|
|
5142
5311
|
}
|
|
5143
|
-
const relativePath = normalizeManagedPath(
|
|
5312
|
+
const relativePath = normalizeManagedPath(relative3(root, summaryPath).replaceAll("\\", "/"));
|
|
5144
5313
|
if (ARCHIVE_SUMMARY_PATH.test(relativePath)) {
|
|
5145
5314
|
output.push(relativePath);
|
|
5146
5315
|
}
|
|
@@ -5157,17 +5326,17 @@ async function walkHarnessEntries(root, directory, output) {
|
|
|
5157
5326
|
return;
|
|
5158
5327
|
for (const item2 of await readdir6(directory, { withFileTypes: true })) {
|
|
5159
5328
|
if (item2.name.startsWith("harness-")) {
|
|
5160
|
-
const path =
|
|
5329
|
+
const path = join14(directory, item2.name);
|
|
5161
5330
|
if (item2.isDirectory()) {
|
|
5162
5331
|
await walkFiles(root, path, output);
|
|
5163
5332
|
} else if (item2.isFile()) {
|
|
5164
|
-
output.push(normalizeManagedPath(
|
|
5333
|
+
output.push(normalizeManagedPath(relative3(root, path).replaceAll("\\", "/")));
|
|
5165
5334
|
}
|
|
5166
5335
|
}
|
|
5167
5336
|
}
|
|
5168
5337
|
}
|
|
5169
5338
|
async function managedFiles(projectRoot, project) {
|
|
5170
|
-
const root =
|
|
5339
|
+
const root = resolve10(projectRoot);
|
|
5171
5340
|
const paths = [];
|
|
5172
5341
|
const adapters = getAdapters(enabledHarnessAgents(project));
|
|
5173
5342
|
const managedFiles2 = [
|
|
@@ -5176,26 +5345,26 @@ async function managedFiles(projectRoot, project) {
|
|
|
5176
5345
|
...adapters.some((adapter) => adapter.name === "codebuddy") ? ["CODEBUDDY.md"] : []
|
|
5177
5346
|
];
|
|
5178
5347
|
for (const path of managedFiles2) {
|
|
5179
|
-
if (await exists3(
|
|
5348
|
+
if (await exists3(join14(root, path))) {
|
|
5180
5349
|
paths.push(path);
|
|
5181
5350
|
}
|
|
5182
5351
|
}
|
|
5183
5352
|
for (const path of SHARED_MANAGED_ROOTS) {
|
|
5184
|
-
await walkFiles(root,
|
|
5353
|
+
await walkFiles(root, join14(root, path), paths);
|
|
5185
5354
|
}
|
|
5186
5355
|
await walkArchiveSummaries(root, paths);
|
|
5187
5356
|
for (const adapter of adapters) {
|
|
5188
5357
|
if (adapter.rulesRoot !== null) {
|
|
5189
|
-
await walkHarnessEntries(root,
|
|
5358
|
+
await walkHarnessEntries(root, join14(root, adapter.rulesRoot), paths);
|
|
5190
5359
|
}
|
|
5191
|
-
await walkHarnessEntries(root,
|
|
5360
|
+
await walkHarnessEntries(root, join14(root, adapter.skillsRoot), paths);
|
|
5192
5361
|
if (adapter.agentsRoot !== null) {
|
|
5193
|
-
await walkHarnessEntries(root,
|
|
5362
|
+
await walkHarnessEntries(root, join14(root, adapter.agentsRoot), paths);
|
|
5194
5363
|
}
|
|
5195
5364
|
}
|
|
5196
5365
|
const result = {};
|
|
5197
5366
|
for (const path of [...new Set(paths)].sort()) {
|
|
5198
|
-
result[path] = await
|
|
5367
|
+
result[path] = await readFile13(join14(root, path), "utf8");
|
|
5199
5368
|
}
|
|
5200
5369
|
return result;
|
|
5201
5370
|
}
|
|
@@ -5204,14 +5373,14 @@ function proposalBaseline(baseline) {
|
|
|
5204
5373
|
}
|
|
5205
5374
|
async function readProject(root) {
|
|
5206
5375
|
try {
|
|
5207
|
-
return projectConfigSchema.parse(parseYaml5(await
|
|
5376
|
+
return projectConfigSchema.parse(parseYaml5(await readFile13(join14(root, ".harness", "project.yaml"), "utf8")));
|
|
5208
5377
|
} catch {
|
|
5209
5378
|
throw new PushWorkflowError("project configuration is missing or invalid", 3, "PROJECT_CONFIG_INVALID");
|
|
5210
5379
|
}
|
|
5211
5380
|
}
|
|
5212
5381
|
async function readOptionalJson(path) {
|
|
5213
5382
|
try {
|
|
5214
|
-
return JSON.parse(await
|
|
5383
|
+
return JSON.parse(await readFile13(path, "utf8"));
|
|
5215
5384
|
} catch (error) {
|
|
5216
5385
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
5217
5386
|
return null;
|
|
@@ -5223,7 +5392,7 @@ async function clientIdFor(root, explicit) {
|
|
|
5223
5392
|
if (explicit !== void 0) {
|
|
5224
5393
|
return explicit;
|
|
5225
5394
|
}
|
|
5226
|
-
const path =
|
|
5395
|
+
const path = join14(root, ".harness", "state", "local", "client.json");
|
|
5227
5396
|
const existing = await readOptionalJson(path);
|
|
5228
5397
|
if (typeof existing?.client_id === "string" && /^cli_[A-Za-z0-9_-]+$/.test(existing.client_id)) {
|
|
5229
5398
|
return existing.client_id;
|
|
@@ -5419,7 +5588,7 @@ async function bindProject(root, project, baseline, projectId) {
|
|
|
5419
5588
|
return { project: nextProject, baseline: nextBaseline };
|
|
5420
5589
|
}
|
|
5421
5590
|
async function pushProject(options) {
|
|
5422
|
-
const root =
|
|
5591
|
+
const root = resolve10(options.projectRoot);
|
|
5423
5592
|
let project = await readProject(root);
|
|
5424
5593
|
let baseline = await readBaseline(root);
|
|
5425
5594
|
const profile = parseHarnessProfile(project.project.profiles[0]);
|
|
@@ -5485,7 +5654,7 @@ async function pushProject(options) {
|
|
|
5485
5654
|
if (options.confirmProposal !== void 0 && !await options.confirmProposal(preview)) {
|
|
5486
5655
|
return { preview, proposalId: null, projectId: project.project.project_id, cancelled: true };
|
|
5487
5656
|
}
|
|
5488
|
-
const workflowPath =
|
|
5657
|
+
const workflowPath = join14(root, ".harness", "state", "local", "push-workflow.json");
|
|
5489
5658
|
const priorWorkflow = await readOptionalJson(workflowPath);
|
|
5490
5659
|
const provisionalRequestId = priorWorkflow?.local_project_key === project.project.local_project_key ? priorWorkflow.request_id : uuidV7();
|
|
5491
5660
|
const lock = await acquireProtocolLock(root, "push", { requestId: provisionalRequestId });
|
|
@@ -5640,7 +5809,7 @@ async function pushProject(options) {
|
|
|
5640
5809
|
pushWarning = "BASELINE_ADVANCE_DEFERRED";
|
|
5641
5810
|
}
|
|
5642
5811
|
}
|
|
5643
|
-
await atomicWriteJson(
|
|
5812
|
+
await atomicWriteJson(join14(root, ".harness", "state", "local", "push-results", finalized.proposal_id + ".json"), {
|
|
5644
5813
|
schema_version: 1,
|
|
5645
5814
|
request_id: requestId,
|
|
5646
5815
|
project_id: projectId,
|
|
@@ -5685,8 +5854,8 @@ async function pushProject(options) {
|
|
|
5685
5854
|
}
|
|
5686
5855
|
|
|
5687
5856
|
// ../core/dist/state/cleanup.js
|
|
5688
|
-
import { readFile as
|
|
5689
|
-
import { join as
|
|
5857
|
+
import { readFile as readFile14, readdir as readdir7, rm as rm6 } from "node:fs/promises";
|
|
5858
|
+
import { join as join15 } from "node:path";
|
|
5690
5859
|
function isSafeEntryName(name) {
|
|
5691
5860
|
return name.length > 0 && !name.includes("/") && !name.includes("\\") && name !== "." && name !== ".." && !name.includes("\0");
|
|
5692
5861
|
}
|
|
@@ -5710,7 +5879,7 @@ async function cleanupProject(options) {
|
|
|
5710
5879
|
continue;
|
|
5711
5880
|
let journal;
|
|
5712
5881
|
try {
|
|
5713
|
-
journal = JSON.parse(await
|
|
5882
|
+
journal = JSON.parse(await readFile14(join15(layout.transactions, name, "journal.json"), "utf8"));
|
|
5714
5883
|
} catch {
|
|
5715
5884
|
continue;
|
|
5716
5885
|
}
|
|
@@ -5728,7 +5897,7 @@ async function cleanupProject(options) {
|
|
|
5728
5897
|
continue;
|
|
5729
5898
|
pruned.push(entry.id);
|
|
5730
5899
|
if (!options.dryRun) {
|
|
5731
|
-
await rm6(
|
|
5900
|
+
await rm6(join15(layout.transactions, entry.id), { recursive: true, force: true });
|
|
5732
5901
|
}
|
|
5733
5902
|
}
|
|
5734
5903
|
}
|
|
@@ -5737,7 +5906,7 @@ async function cleanupProject(options) {
|
|
|
5737
5906
|
continue;
|
|
5738
5907
|
removedCache.push(name);
|
|
5739
5908
|
if (!options.dryRun) {
|
|
5740
|
-
await rm6(
|
|
5909
|
+
await rm6(join15(layout.serverArtifacts, name), { recursive: true, force: true });
|
|
5741
5910
|
}
|
|
5742
5911
|
}
|
|
5743
5912
|
return {
|
|
@@ -5797,10 +5966,10 @@ var SKILL_TARGET_AGENTS = Object.freeze([
|
|
|
5797
5966
|
]);
|
|
5798
5967
|
|
|
5799
5968
|
// ../core/dist/transaction/recovery.js
|
|
5800
|
-
import { readFile as
|
|
5801
|
-
import { join as
|
|
5969
|
+
import { readFile as readFile15, readdir as readdir8, rm as rm7, stat as stat4 } from "node:fs/promises";
|
|
5970
|
+
import { join as join16 } from "node:path";
|
|
5802
5971
|
async function recoverTransaction(projectRoot, transactionId) {
|
|
5803
|
-
const journal = JSON.parse(await
|
|
5972
|
+
const journal = JSON.parse(await readFile15(join16(stateLayout(projectRoot).transactions, transactionId, "journal.json"), "utf8"));
|
|
5804
5973
|
if (journal.state === "committed") {
|
|
5805
5974
|
return { transactionId, status: "committed" };
|
|
5806
5975
|
}
|
|
@@ -5827,7 +5996,7 @@ async function listTransactions(projectRoot) {
|
|
|
5827
5996
|
const transactions = [];
|
|
5828
5997
|
for (const transactionId of names) {
|
|
5829
5998
|
try {
|
|
5830
|
-
const journal = JSON.parse(await
|
|
5999
|
+
const journal = JSON.parse(await readFile15(join16(root, transactionId, "journal.json"), "utf8"));
|
|
5831
6000
|
transactions.push({
|
|
5832
6001
|
transactionId,
|
|
5833
6002
|
kind: journal.kind,
|
|
@@ -5858,11 +6027,11 @@ async function rollbackLatestCommittedUpdate(projectRoot) {
|
|
|
5858
6027
|
if (latest === void 0) {
|
|
5859
6028
|
throw new Error("no committed update transaction is available for rollback");
|
|
5860
6029
|
}
|
|
5861
|
-
const transactionRoot =
|
|
5862
|
-
const journal = JSON.parse(await
|
|
5863
|
-
const after = JSON.parse(await
|
|
6030
|
+
const transactionRoot = join16(stateLayout(projectRoot).transactions, latest.transactionId);
|
|
6031
|
+
const journal = JSON.parse(await readFile15(join16(transactionRoot, "journal.json"), "utf8"));
|
|
6032
|
+
const after = JSON.parse(await readFile15(join16(transactionRoot, "after", "manifest.json"), "utf8"));
|
|
5864
6033
|
for (const entry of after) {
|
|
5865
|
-
const target =
|
|
6034
|
+
const target = join16(projectRoot, entry.path);
|
|
5866
6035
|
const exists6 = await pathExists2(target);
|
|
5867
6036
|
if (exists6 !== entry.exists || exists6 && await sha256File(target) !== entry.hash) {
|
|
5868
6037
|
throw new Error("cannot rollback dirty path: " + entry.path);
|
|
@@ -5875,10 +6044,10 @@ async function rollbackLatestCommittedUpdate(projectRoot) {
|
|
|
5875
6044
|
continue;
|
|
5876
6045
|
}
|
|
5877
6046
|
seen.add(snapshot.path);
|
|
5878
|
-
const target =
|
|
6047
|
+
const target = join16(projectRoot, snapshot.path);
|
|
5879
6048
|
const exists6 = await pathExists2(target);
|
|
5880
6049
|
if (snapshot.existed && snapshot.snapshot_name !== null) {
|
|
5881
|
-
const content = await
|
|
6050
|
+
const content = await readFile15(join16(transactionRoot, "before", snapshot.snapshot_name));
|
|
5882
6051
|
operations.push({
|
|
5883
6052
|
operation: exists6 ? "modify" : "add",
|
|
5884
6053
|
path: snapshot.path,
|
|
@@ -5907,7 +6076,7 @@ async function cleanupOldTransactions(projectRoot, now = /* @__PURE__ */ new Dat
|
|
|
5907
6076
|
if (!removable) {
|
|
5908
6077
|
continue;
|
|
5909
6078
|
}
|
|
5910
|
-
await rm7(
|
|
6079
|
+
await rm7(join16(stateLayout(projectRoot).transactions, item2.transactionId), {
|
|
5911
6080
|
recursive: true,
|
|
5912
6081
|
force: true
|
|
5913
6082
|
});
|
|
@@ -5917,8 +6086,8 @@ async function cleanupOldTransactions(projectRoot, now = /* @__PURE__ */ new Dat
|
|
|
5917
6086
|
}
|
|
5918
6087
|
|
|
5919
6088
|
// ../core/dist/update/update.js
|
|
5920
|
-
import { readFile as
|
|
5921
|
-
import { join as
|
|
6089
|
+
import { readFile as readFile16 } from "node:fs/promises";
|
|
6090
|
+
import { join as join17, resolve as resolve11 } from "node:path";
|
|
5922
6091
|
import { parse as parseYaml8 } from "yaml";
|
|
5923
6092
|
var UpdateWorkflowError = class extends Error {
|
|
5924
6093
|
exitCode;
|
|
@@ -5931,10 +6100,10 @@ var UpdateWorkflowError = class extends Error {
|
|
|
5931
6100
|
}
|
|
5932
6101
|
};
|
|
5933
6102
|
async function updateProject(options) {
|
|
5934
|
-
const root =
|
|
6103
|
+
const root = resolve11(options.projectRoot);
|
|
5935
6104
|
let project;
|
|
5936
6105
|
try {
|
|
5937
|
-
project = projectConfigSchema.parse(parseYaml8(await
|
|
6106
|
+
project = projectConfigSchema.parse(parseYaml8(await readFile16(join17(root, ".harness", "project.yaml"), "utf8")));
|
|
5938
6107
|
} catch {
|
|
5939
6108
|
throw new UpdateWorkflowError("project configuration is invalid", 3, "PROJECT_CONFIG_INVALID");
|
|
5940
6109
|
}
|
|
@@ -5958,6 +6127,7 @@ async function updateProject(options) {
|
|
|
5958
6127
|
}
|
|
5959
6128
|
const requestId = uuidV7();
|
|
5960
6129
|
const baseline = await readBaseline(root);
|
|
6130
|
+
const protocolOnlyPaths = await readManagedProjectRuleProjectionPaths(root);
|
|
5961
6131
|
let parsedServerUrl;
|
|
5962
6132
|
try {
|
|
5963
6133
|
parsedServerUrl = new URL(serverUrl);
|
|
@@ -5979,6 +6149,7 @@ async function updateProject(options) {
|
|
|
5979
6149
|
client,
|
|
5980
6150
|
requestId,
|
|
5981
6151
|
dryRun: options.dryRun,
|
|
6152
|
+
protocolOnlyPaths,
|
|
5982
6153
|
...options.conflictStrategy === void 0 ? {} : { conflictStrategy: options.conflictStrategy },
|
|
5983
6154
|
...options.resolveOverrides === void 0 ? {} : { resolveOverrides: options.resolveOverrides },
|
|
5984
6155
|
...options.confirmConflictStrategy === void 0 ? {} : { confirmConflictStrategy: options.confirmConflictStrategy },
|
|
@@ -5989,7 +6160,13 @@ async function updateProject(options) {
|
|
|
5989
6160
|
const parsed = harnessAgentSchema.safeParse(agent);
|
|
5990
6161
|
return parsed.success ? [parsed.data] : [];
|
|
5991
6162
|
});
|
|
5992
|
-
await synchronizeProjectRules(root, agents, project.adapter_options?.codebuddy?.surface ?? "both");
|
|
6163
|
+
const projections = await synchronizeProjectRules(root, agents, project.adapter_options?.codebuddy?.surface ?? "both");
|
|
6164
|
+
for (const path of projections.conflicts) {
|
|
6165
|
+
result.conflicts.push({ path, operation: "modify", reason: "local-dirty" });
|
|
6166
|
+
if (!result.skipped.some((item2) => item2.path === path)) {
|
|
6167
|
+
result.skipped.push({ path, operation: "modify", reason: "local-dirty" });
|
|
6168
|
+
}
|
|
6169
|
+
}
|
|
5993
6170
|
}
|
|
5994
6171
|
return result;
|
|
5995
6172
|
} catch (error) {
|
|
@@ -6016,8 +6193,8 @@ async function updateProject(options) {
|
|
|
6016
6193
|
}
|
|
6017
6194
|
|
|
6018
6195
|
// src/config/init-config.ts
|
|
6019
|
-
import { isAbsolute as isAbsolute2, join as
|
|
6020
|
-
import { readFile as
|
|
6196
|
+
import { isAbsolute as isAbsolute2, join as join18 } from "node:path";
|
|
6197
|
+
import { readFile as readFile17 } from "node:fs/promises";
|
|
6021
6198
|
var InitConfigurationError = class extends Error {
|
|
6022
6199
|
exitCode;
|
|
6023
6200
|
code;
|
|
@@ -6100,9 +6277,9 @@ function hasOwn(record, key) {
|
|
|
6100
6277
|
async function resolveInitConfig(cwd, flags, prompts = {}, warnings = []) {
|
|
6101
6278
|
let fileConfig = {};
|
|
6102
6279
|
if (flags.config !== void 0) {
|
|
6103
|
-
const path = isAbsolute2(flags.config) ? flags.config :
|
|
6280
|
+
const path = isAbsolute2(flags.config) ? flags.config : join18(cwd, flags.config);
|
|
6104
6281
|
try {
|
|
6105
|
-
fileConfig = JSON.parse(await
|
|
6282
|
+
fileConfig = JSON.parse(await readFile17(path, "utf8"));
|
|
6106
6283
|
} catch (error) {
|
|
6107
6284
|
throw new InitConfigurationError(
|
|
6108
6285
|
"unable to read init config: " + (error instanceof Error ? error.message : String(error)),
|
|
@@ -6184,8 +6361,8 @@ function serializeCliResult(result) {
|
|
|
6184
6361
|
}
|
|
6185
6362
|
|
|
6186
6363
|
// src/config/codebuddy-setup.ts
|
|
6187
|
-
import { mkdir as
|
|
6188
|
-
import { basename as
|
|
6364
|
+
import { mkdir as mkdir7, readFile as readFile18, readdir as readdir9, stat as stat5, writeFile as writeFile9 } from "node:fs/promises";
|
|
6365
|
+
import { basename as basename5, dirname as dirname7, extname as extname2, join as join19 } from "node:path";
|
|
6189
6366
|
var MANAGED_RULE_NAMES = /* @__PURE__ */ new Set([
|
|
6190
6367
|
"harness-general.md",
|
|
6191
6368
|
"harness-general.mdc",
|
|
@@ -6204,7 +6381,7 @@ async function exists4(path) {
|
|
|
6204
6381
|
}
|
|
6205
6382
|
async function readJsonObject(path) {
|
|
6206
6383
|
try {
|
|
6207
|
-
const parsed = JSON.parse(await
|
|
6384
|
+
const parsed = JSON.parse(await readFile18(path, "utf8"));
|
|
6208
6385
|
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
6209
6386
|
} catch (error) {
|
|
6210
6387
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") return {};
|
|
@@ -6212,7 +6389,7 @@ async function readJsonObject(path) {
|
|
|
6212
6389
|
}
|
|
6213
6390
|
}
|
|
6214
6391
|
async function inspectCodeBuddySetup(projectRoot, surface = "both") {
|
|
6215
|
-
const rulesRoot =
|
|
6392
|
+
const rulesRoot = join19(projectRoot, ".claude", "rules");
|
|
6216
6393
|
let ruleNames = [];
|
|
6217
6394
|
try {
|
|
6218
6395
|
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();
|
|
@@ -6223,11 +6400,11 @@ async function inspectCodeBuddySetup(projectRoot, surface = "both") {
|
|
|
6223
6400
|
const currentClaudeRules = [];
|
|
6224
6401
|
const conflictingClaudeRules = [];
|
|
6225
6402
|
for (const name of ruleNames) {
|
|
6226
|
-
const sourceContent = await
|
|
6403
|
+
const sourceContent = await readFile18(join19(rulesRoot, name), "utf8");
|
|
6227
6404
|
const targetContents = await Promise.all(
|
|
6228
6405
|
destinationTargets(projectRoot, surface, name).map(async (target) => {
|
|
6229
6406
|
try {
|
|
6230
|
-
return await
|
|
6407
|
+
return await readFile18(target, "utf8");
|
|
6231
6408
|
} catch (error) {
|
|
6232
6409
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") return null;
|
|
6233
6410
|
throw error;
|
|
@@ -6241,22 +6418,22 @@ async function inspectCodeBuddySetup(projectRoot, surface = "both") {
|
|
|
6241
6418
|
currentClaudeRules.push(name);
|
|
6242
6419
|
}
|
|
6243
6420
|
}
|
|
6244
|
-
const mcp = await readJsonObject(
|
|
6421
|
+
const mcp = await readJsonObject(join19(projectRoot, ".mcp.json"));
|
|
6245
6422
|
const servers = mcp?.mcpServers;
|
|
6246
6423
|
const configured = servers !== null && typeof servers === "object" && !Array.isArray(servers) && Object.prototype.hasOwnProperty.call(servers, "codegraph");
|
|
6247
6424
|
return {
|
|
6248
6425
|
claudeRules,
|
|
6249
6426
|
currentClaudeRules,
|
|
6250
6427
|
conflictingClaudeRules,
|
|
6251
|
-
hasCodeGraphIndex: await exists4(
|
|
6428
|
+
hasCodeGraphIndex: await exists4(join19(projectRoot, ".codegraph")),
|
|
6252
6429
|
codeGraphConfigured: configured
|
|
6253
6430
|
};
|
|
6254
6431
|
}
|
|
6255
6432
|
function destinationTargets(root, surface, name) {
|
|
6256
|
-
const stem =
|
|
6433
|
+
const stem = basename5(name, extname2(name));
|
|
6257
6434
|
const targets = [];
|
|
6258
|
-
if (surface !== "cli") targets.push(
|
|
6259
|
-
if (surface !== "ide") targets.push(
|
|
6435
|
+
if (surface !== "cli") targets.push(join19(root, ".codebuddy", ".rules", `${stem}.mdc`));
|
|
6436
|
+
if (surface !== "ide") targets.push(join19(root, ".codebuddy", "rules", `${stem}.md`));
|
|
6260
6437
|
return targets;
|
|
6261
6438
|
}
|
|
6262
6439
|
async function applyCodeBuddySetup(options) {
|
|
@@ -6270,8 +6447,8 @@ async function applyCodeBuddySetup(options) {
|
|
|
6270
6447
|
if (options.syncClaudeRules) {
|
|
6271
6448
|
const plan = await inspectCodeBuddySetup(options.projectRoot, options.surface);
|
|
6272
6449
|
for (const name of plan.claudeRules) {
|
|
6273
|
-
const source =
|
|
6274
|
-
const content = await
|
|
6450
|
+
const source = join19(options.projectRoot, ".claude", "rules", name);
|
|
6451
|
+
const content = await readFile18(source, "utf8");
|
|
6275
6452
|
if (SENSITIVE_ASSIGNMENT.test(content) || PRIVATE_KEY_BLOCK.test(content)) {
|
|
6276
6453
|
result.skippedSensitive.push(name);
|
|
6277
6454
|
continue;
|
|
@@ -6281,14 +6458,14 @@ async function applyCodeBuddySetup(options) {
|
|
|
6281
6458
|
result.preserved.push(target);
|
|
6282
6459
|
continue;
|
|
6283
6460
|
}
|
|
6284
|
-
await
|
|
6285
|
-
await
|
|
6461
|
+
await mkdir7(dirname7(target), { recursive: true });
|
|
6462
|
+
await writeFile9(target, content, { encoding: "utf8", flag: "wx" });
|
|
6286
6463
|
result.copied.push(target);
|
|
6287
6464
|
}
|
|
6288
6465
|
}
|
|
6289
6466
|
}
|
|
6290
6467
|
if (options.configureCodeGraph) {
|
|
6291
|
-
const path =
|
|
6468
|
+
const path = join19(options.projectRoot, ".mcp.json");
|
|
6292
6469
|
const current = await readJsonObject(path);
|
|
6293
6470
|
if (current === null) {
|
|
6294
6471
|
result.warnings.push(".mcp.json \u4E0D\u662F\u6709\u6548 JSON\uFF0C\u5DF2\u4FDD\u7559\u539F\u6587\u4EF6\u5E76\u8DF3\u8FC7 CodeGraph MCP \u914D\u7F6E");
|
|
@@ -6303,7 +6480,7 @@ async function applyCodeBuddySetup(options) {
|
|
|
6303
6480
|
codegraph: { command: "codegraph", args: ["serve", "--mcp"] }
|
|
6304
6481
|
}
|
|
6305
6482
|
};
|
|
6306
|
-
await
|
|
6483
|
+
await writeFile9(path, JSON.stringify(next, null, 2) + "\n", "utf8");
|
|
6307
6484
|
result.mcpUpdated = true;
|
|
6308
6485
|
}
|
|
6309
6486
|
}
|
|
@@ -6312,13 +6489,13 @@ async function applyCodeBuddySetup(options) {
|
|
|
6312
6489
|
}
|
|
6313
6490
|
|
|
6314
6491
|
// src/commands/refresh.ts
|
|
6315
|
-
import { readFile as
|
|
6316
|
-
import { join as
|
|
6492
|
+
import { readFile as readFile19 } from "node:fs/promises";
|
|
6493
|
+
import { join as join20 } from "node:path";
|
|
6317
6494
|
import { parse as parseYaml9 } from "yaml";
|
|
6318
6495
|
async function detectProject(root) {
|
|
6319
6496
|
let content;
|
|
6320
6497
|
try {
|
|
6321
|
-
content = await
|
|
6498
|
+
content = await readFile19(join20(root, ".harness", "project.yaml"), "utf8");
|
|
6322
6499
|
} catch (error) {
|
|
6323
6500
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
6324
6501
|
return { status: "absent" };
|
|
@@ -7181,6 +7358,7 @@ async function runRulesSync(options, dependencies) {
|
|
|
7181
7358
|
configuredSurface(detection.config, options.codebuddySurface)
|
|
7182
7359
|
);
|
|
7183
7360
|
const learning = options.learn === false ? null : await synchronizeRuleCandidates(dependencies.cwd);
|
|
7361
|
+
const ruleReview = learning === null ? null : await exportRuleReviewQueue(dependencies.cwd);
|
|
7184
7362
|
const exitCode = projections.conflicts.length > 0 ? 5 : 0;
|
|
7185
7363
|
const payload = {
|
|
7186
7364
|
schema_version: 1,
|
|
@@ -7197,7 +7375,8 @@ async function runRulesSync(options, dependencies) {
|
|
|
7197
7375
|
unchanged: projections.unchanged.length,
|
|
7198
7376
|
conflicts: projections.conflicts.length,
|
|
7199
7377
|
agent_specific: projections.agent_specific.length,
|
|
7200
|
-
rule_candidates: learning?.candidates ?? 0
|
|
7378
|
+
rule_candidates: learning?.candidates ?? 0,
|
|
7379
|
+
rule_review_pending: ruleReview?.pending.length ?? 0
|
|
7201
7380
|
},
|
|
7202
7381
|
items: [
|
|
7203
7382
|
...projections.migrated.map((path) => ({ path, status: "migrated" })),
|
|
@@ -7208,6 +7387,12 @@ async function runRulesSync(options, dependencies) {
|
|
|
7208
7387
|
status: learning.changed ? "updated" : "unchanged",
|
|
7209
7388
|
candidates: learning.candidates,
|
|
7210
7389
|
scanned: learning.scanned
|
|
7390
|
+
}],
|
|
7391
|
+
...ruleReview === null ? [] : [{
|
|
7392
|
+
path: ".harness/knowledge/rule-decisions.json",
|
|
7393
|
+
status: ruleReview.pending.length > 0 ? "pending-review" : "current",
|
|
7394
|
+
pending: ruleReview.pending.length,
|
|
7395
|
+
decided: ruleReview.decided
|
|
7211
7396
|
}]
|
|
7212
7397
|
],
|
|
7213
7398
|
warnings: [
|
|
@@ -7232,9 +7417,69 @@ async function runRulesSync(options, dependencies) {
|
|
|
7232
7417
|
}
|
|
7233
7418
|
}
|
|
7234
7419
|
|
|
7420
|
+
// src/commands/rules-review.ts
|
|
7421
|
+
import { readFile as readFile20 } from "node:fs/promises";
|
|
7422
|
+
import { resolve as resolve12 } from "node:path";
|
|
7423
|
+
async function runRulesReview(options, dependencies) {
|
|
7424
|
+
const detection = await detectProject(dependencies.cwd);
|
|
7425
|
+
if (detection.status === "absent") {
|
|
7426
|
+
dependencies.stderr("\u5C1A\u672A\u521D\u59CB\u5316 Hunter Harness\uFF1B\u8BF7\u5148\u8FD0\u884C `hunter-harness`\u3002\n");
|
|
7427
|
+
return 3;
|
|
7428
|
+
}
|
|
7429
|
+
if (detection.status === "invalid") {
|
|
7430
|
+
dependencies.stderr("PROJECT_CONFIG_INVALID\uFF1A.harness/project.yaml \u65E0\u6548\n");
|
|
7431
|
+
return 3;
|
|
7432
|
+
}
|
|
7433
|
+
try {
|
|
7434
|
+
const queue = await exportRuleReviewQueue(dependencies.cwd);
|
|
7435
|
+
let summary;
|
|
7436
|
+
let items;
|
|
7437
|
+
if (options.apply === void 0) {
|
|
7438
|
+
summary = { pending: queue.pending.length, decided: queue.decided };
|
|
7439
|
+
items = queue.pending;
|
|
7440
|
+
} else {
|
|
7441
|
+
const input = JSON.parse(
|
|
7442
|
+
await readFile20(resolve12(dependencies.cwd, options.apply), "utf8")
|
|
7443
|
+
);
|
|
7444
|
+
const result = await applyRuleReviewDecisions(dependencies.cwd, input);
|
|
7445
|
+
summary = { applied: result.applied, recorded: result.recorded };
|
|
7446
|
+
items = [{ path: result.path, status: "updated" }];
|
|
7447
|
+
}
|
|
7448
|
+
const payload = {
|
|
7449
|
+
schema_version: 1,
|
|
7450
|
+
command: "rules-review",
|
|
7451
|
+
request_id: uuidV7(),
|
|
7452
|
+
dry_run: false,
|
|
7453
|
+
ok: true,
|
|
7454
|
+
exit_code: 0,
|
|
7455
|
+
project_id: detection.config.project.project_id,
|
|
7456
|
+
summary,
|
|
7457
|
+
items,
|
|
7458
|
+
warnings: [],
|
|
7459
|
+
errors: []
|
|
7460
|
+
};
|
|
7461
|
+
if (options.json === true) dependencies.stdout(serializeCliResult(payload));
|
|
7462
|
+
else if (options.apply === void 0) {
|
|
7463
|
+
dependencies.stdout(
|
|
7464
|
+
`\u516C\u5171\u89C4\u5219\u5019\u9009\uFF1A\u5F85\u8BC4\u5BA1 ${summary.pending ?? 0}\uFF0C\u5DF2\u51B3\u5B9A ${summary.decided ?? 0}\u3002
|
|
7465
|
+
`
|
|
7466
|
+
);
|
|
7467
|
+
} else {
|
|
7468
|
+
dependencies.stdout(
|
|
7469
|
+
`\u516C\u5171\u89C4\u5219\u51B3\u7B56\uFF1A\u5E94\u7528 ${summary.applied ?? 0}\uFF0C\u8BB0\u5F55 ${summary.recorded ?? 0}\u3002
|
|
7470
|
+
`
|
|
7471
|
+
);
|
|
7472
|
+
}
|
|
7473
|
+
return 0;
|
|
7474
|
+
} catch (error) {
|
|
7475
|
+
dependencies.stderr((error instanceof Error ? error.message : String(error)) + "\n");
|
|
7476
|
+
return 1;
|
|
7477
|
+
}
|
|
7478
|
+
}
|
|
7479
|
+
|
|
7235
7480
|
// src/commands/recovery.ts
|
|
7236
7481
|
import { stat as stat6 } from "node:fs/promises";
|
|
7237
|
-
import { join as
|
|
7482
|
+
import { join as join21 } from "node:path";
|
|
7238
7483
|
async function exists5(path) {
|
|
7239
7484
|
try {
|
|
7240
7485
|
await stat6(path);
|
|
@@ -7294,7 +7539,7 @@ async function runRecoveryMenuIfApplicable(options, dependencies) {
|
|
|
7294
7539
|
return 5;
|
|
7295
7540
|
}
|
|
7296
7541
|
}
|
|
7297
|
-
const initialized = await exists5(
|
|
7542
|
+
const initialized = await exists5(join21(dependencies.cwd, ".harness", "project.yaml"));
|
|
7298
7543
|
if (!initialized || options.nonInteractive === true || explicitConfigure(options)) {
|
|
7299
7544
|
return null;
|
|
7300
7545
|
}
|
|
@@ -7337,8 +7582,8 @@ async function runRecoveryMenuIfApplicable(options, dependencies) {
|
|
|
7337
7582
|
}
|
|
7338
7583
|
|
|
7339
7584
|
// src/workflow-data/resolve.ts
|
|
7340
|
-
import { cp, mkdir as
|
|
7341
|
-
import { dirname as
|
|
7585
|
+
import { cp, mkdir as mkdir8, readdir as readdir10, readFile as readFile21, rm as rm8, stat as stat7 } from "node:fs/promises";
|
|
7586
|
+
import { dirname as dirname8, join as join22, relative as relative4 } from "node:path";
|
|
7342
7587
|
import { fileURLToPath } from "node:url";
|
|
7343
7588
|
var WorkflowDataResolutionError = class extends Error {
|
|
7344
7589
|
code;
|
|
@@ -7375,14 +7620,14 @@ async function listFilesRecursive(root, base = root) {
|
|
|
7375
7620
|
const entries = await readdir10(root, { withFileTypes: true });
|
|
7376
7621
|
const files = [];
|
|
7377
7622
|
for (const entry of entries) {
|
|
7378
|
-
const absolute =
|
|
7623
|
+
const absolute = join22(root, entry.name);
|
|
7379
7624
|
if (entry.isDirectory()) {
|
|
7380
7625
|
files.push(...await listFilesRecursive(absolute, base));
|
|
7381
7626
|
continue;
|
|
7382
7627
|
}
|
|
7383
7628
|
if (!entry.isFile()) continue;
|
|
7384
|
-
const rel =
|
|
7385
|
-
files.push({ path: rel, content: await
|
|
7629
|
+
const rel = relative4(base, absolute).replaceAll("\\", "/");
|
|
7630
|
+
files.push({ path: rel, content: await readFile21(absolute, "utf8") });
|
|
7386
7631
|
}
|
|
7387
7632
|
return files;
|
|
7388
7633
|
}
|
|
@@ -7395,7 +7640,7 @@ async function verifyWorkflowPackageIntegrity(resourcesRoot) {
|
|
|
7395
7640
|
const manifest = await readWorkflowFamilyManifest(resourcesRoot);
|
|
7396
7641
|
const expected = manifest.content_sha256;
|
|
7397
7642
|
if (typeof expected !== "string" || expected.length === 0) return;
|
|
7398
|
-
const harnessRoot =
|
|
7643
|
+
const harnessRoot = join22(resourcesRoot, "harness");
|
|
7399
7644
|
if (!await pathExists3(harnessRoot)) {
|
|
7400
7645
|
throw new WorkflowDataResolutionError(
|
|
7401
7646
|
"\u5DE5\u4F5C\u6D41\u6570\u636E\u5305\u7F3A\u5C11 harness/\uFF0C\u65E0\u6CD5\u6821\u9A8C SHA-256",
|
|
@@ -7414,40 +7659,40 @@ async function verifyWorkflowPackageIntegrity(resourcesRoot) {
|
|
|
7414
7659
|
}
|
|
7415
7660
|
}
|
|
7416
7661
|
async function monorepoResourcesRoot() {
|
|
7417
|
-
const here =
|
|
7662
|
+
const here = dirname8(fileURLToPath(import.meta.url));
|
|
7418
7663
|
const candidates = [
|
|
7419
7664
|
// TypeScript source: packages/cli/src/workflow-data -> packages/workflow-data-harness.
|
|
7420
|
-
|
|
7665
|
+
join22(here, "../../../workflow-data-harness"),
|
|
7421
7666
|
// Bundled CLI: packages/cli/dist -> packages/workflow-data-harness.
|
|
7422
|
-
|
|
7667
|
+
join22(here, "../../workflow-data-harness"),
|
|
7423
7668
|
// Test/build layouts that preserve additional source directory levels.
|
|
7424
|
-
|
|
7669
|
+
join22(here, "../../../../packages/workflow-data-harness")
|
|
7425
7670
|
];
|
|
7426
7671
|
for (const candidate of candidates) {
|
|
7427
|
-
if (await pathExists3(
|
|
7672
|
+
if (await pathExists3(join22(candidate, "harness", "manifests"))) return candidate;
|
|
7428
7673
|
}
|
|
7429
7674
|
return null;
|
|
7430
7675
|
}
|
|
7431
7676
|
async function siblingWorkflowPackage(cwd) {
|
|
7432
|
-
const here =
|
|
7677
|
+
const here = dirname8(fileURLToPath(import.meta.url));
|
|
7433
7678
|
const candidates = [
|
|
7434
|
-
|
|
7679
|
+
join22(cwd, "node_modules", "@hunter-harness", "workflow-harness"),
|
|
7435
7680
|
// Published layout: node_modules/hunter-harness/dist/bin.js alongside the
|
|
7436
7681
|
// scoped workflow package in node_modules/@hunter-harness/workflow-harness.
|
|
7437
|
-
|
|
7682
|
+
join22(here, "..", "..", "@hunter-harness", "workflow-harness"),
|
|
7438
7683
|
// Monorepo bundled layout: packages/cli/dist/bin.js.
|
|
7439
|
-
|
|
7684
|
+
join22(here, "..", "..", "workflow-data-harness")
|
|
7440
7685
|
];
|
|
7441
7686
|
for (const candidate of candidates) {
|
|
7442
|
-
if (await pathExists3(
|
|
7687
|
+
if (await pathExists3(join22(candidate, "harness", "manifests"))) return candidate;
|
|
7443
7688
|
}
|
|
7444
7689
|
return null;
|
|
7445
7690
|
}
|
|
7446
7691
|
async function readCachedNpmPackageVersion(cacheRoot) {
|
|
7447
|
-
const manifestPath =
|
|
7692
|
+
const manifestPath = join22(cacheRoot, "package.json");
|
|
7448
7693
|
if (!await pathExists3(manifestPath)) return null;
|
|
7449
7694
|
try {
|
|
7450
|
-
const pkg = JSON.parse(await
|
|
7695
|
+
const pkg = JSON.parse(await readFile21(manifestPath, "utf8"));
|
|
7451
7696
|
return typeof pkg.version === "string" && pkg.version.length > 0 ? pkg.version : null;
|
|
7452
7697
|
} catch {
|
|
7453
7698
|
return null;
|
|
@@ -7469,13 +7714,13 @@ async function latestWorkflowCacheIsStale(cacheRoot, packageName, resolveManifes
|
|
|
7469
7714
|
}
|
|
7470
7715
|
}
|
|
7471
7716
|
async function materializeWorkflowPackageCache(cacheRoot, packageSpec, extract) {
|
|
7472
|
-
await
|
|
7473
|
-
const staging =
|
|
7717
|
+
await mkdir8(cacheRoot, { recursive: true });
|
|
7718
|
+
const staging = join22(cacheRoot, ".extract");
|
|
7474
7719
|
await rm8(staging, { recursive: true, force: true });
|
|
7475
|
-
await
|
|
7720
|
+
await mkdir8(staging, { recursive: true });
|
|
7476
7721
|
await extract(packageSpec, staging);
|
|
7477
|
-
const packageDir = await pathExists3(
|
|
7478
|
-
if (!await pathExists3(
|
|
7722
|
+
const packageDir = await pathExists3(join22(staging, "harness", "manifests")) ? staging : join22(staging, "package");
|
|
7723
|
+
if (!await pathExists3(join22(packageDir, "harness", "manifests"))) {
|
|
7479
7724
|
throw new WorkflowDataResolutionError(
|
|
7480
7725
|
"\u5DE5\u4F5C\u6D41\u6570\u636E\u5305\u5185\u5BB9\u65E0\u6548\uFF1A\u7F3A\u5C11 harness/manifests",
|
|
7481
7726
|
"WORKFLOW_DATA_INVALID",
|
|
@@ -7504,8 +7749,8 @@ async function resolveWorkflowResourcesRoot(options, argv = []) {
|
|
|
7504
7749
|
const packageName = workflowPackageName(family, options.env);
|
|
7505
7750
|
const packageSpec = version === "latest" ? packageName : `${packageName}@${version}`;
|
|
7506
7751
|
const cacheKey = packageSpec.replace("/", "+");
|
|
7507
|
-
const cacheRoot =
|
|
7508
|
-
if (await pathExists3(
|
|
7752
|
+
const cacheRoot = join22(options.cwd, ".harness", "cache", "workflow-packages", cacheKey);
|
|
7753
|
+
if (await pathExists3(join22(cacheRoot, "harness", "manifests"))) {
|
|
7509
7754
|
if (version === "latest") {
|
|
7510
7755
|
const stale = await latestWorkflowCacheIsStale(cacheRoot, packageName, options.pacoteManifest);
|
|
7511
7756
|
if (stale) {
|
|
@@ -7551,9 +7796,9 @@ function describeWorkflowDataFetchFailure(error, packageSpec) {
|
|
|
7551
7796
|
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}`;
|
|
7552
7797
|
}
|
|
7553
7798
|
async function readWorkflowFamilyManifest(resourcesRoot) {
|
|
7554
|
-
const manifestPath =
|
|
7799
|
+
const manifestPath = join22(resourcesRoot, "hunter-workflow-family.json");
|
|
7555
7800
|
if (!await pathExists3(manifestPath)) return {};
|
|
7556
|
-
return JSON.parse(await
|
|
7801
|
+
return JSON.parse(await readFile21(manifestPath, "utf8"));
|
|
7557
7802
|
}
|
|
7558
7803
|
|
|
7559
7804
|
// src/bin.ts
|
|
@@ -7673,6 +7918,12 @@ async function runCli(argv, overrides = {}) {
|
|
|
7673
7918
|
dependencies
|
|
7674
7919
|
);
|
|
7675
7920
|
});
|
|
7921
|
+
program.command("rules-review").description("\u5BFC\u51FA\u5F85\u8BC4\u5BA1\u516C\u5171\u89C4\u5219\u5019\u9009\uFF0C\u6216\u5E94\u7528\u7ECF\u7528\u6237\u786E\u8BA4\u7684\u89C4\u5219\u51B3\u7B56").option("--apply <file>", "\u5E94\u7528\u5305\u542B\u5019\u9009 revision \u548C\u76EE\u6807 hash \u7684\u51B3\u7B56 JSON").option("--json").action(async (options) => {
|
|
7922
|
+
exitCode = await runRulesReview(
|
|
7923
|
+
{ ...program.opts(), ...options },
|
|
7924
|
+
dependencies
|
|
7925
|
+
);
|
|
7926
|
+
});
|
|
7676
7927
|
try {
|
|
7677
7928
|
await program.parseAsync(["node", "hunter-harness", ...argv]);
|
|
7678
7929
|
return exitCode;
|