@harness-engineering/core 0.27.0 → 0.28.0
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/index.d.mts +116 -2
- package/dist/index.d.ts +116 -2
- package/dist/index.js +436 -200
- package/dist/index.mjs +420 -190
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -221,15 +221,15 @@ function validateConfig(data, schema) {
|
|
|
221
221
|
let message = "Configuration validation failed";
|
|
222
222
|
const suggestions = [];
|
|
223
223
|
if (firstError) {
|
|
224
|
-
const
|
|
225
|
-
const pathDisplay =
|
|
224
|
+
const path50 = firstError.path.join(".");
|
|
225
|
+
const pathDisplay = path50 ? ` at "${path50}"` : "";
|
|
226
226
|
if (firstError.code === "invalid_type") {
|
|
227
227
|
const received = firstError.received;
|
|
228
228
|
const expected = firstError.expected;
|
|
229
229
|
if (received === "undefined") {
|
|
230
230
|
code = "MISSING_FIELD";
|
|
231
231
|
message = `Missing required field${pathDisplay}: ${firstError.message}`;
|
|
232
|
-
suggestions.push(`Field "${
|
|
232
|
+
suggestions.push(`Field "${path50}" is required and must be of type "${expected}"`);
|
|
233
233
|
} else {
|
|
234
234
|
code = "INVALID_TYPE";
|
|
235
235
|
message = `Invalid type${pathDisplay}: ${firstError.message}`;
|
|
@@ -683,16 +683,16 @@ function makeFinding(input) {
|
|
|
683
683
|
if (input.suggestion) finding.suggestion = input.suggestion;
|
|
684
684
|
return finding;
|
|
685
685
|
}
|
|
686
|
-
function readTextSafe(
|
|
686
|
+
function readTextSafe(path50) {
|
|
687
687
|
try {
|
|
688
|
-
return readFileSync(
|
|
688
|
+
return readFileSync(path50, "utf-8");
|
|
689
689
|
} catch {
|
|
690
690
|
return null;
|
|
691
691
|
}
|
|
692
692
|
}
|
|
693
|
-
function safeFileSize(
|
|
693
|
+
function safeFileSize(path50) {
|
|
694
694
|
try {
|
|
695
|
-
return statSync(
|
|
695
|
+
return statSync(path50).size;
|
|
696
696
|
} catch {
|
|
697
697
|
return null;
|
|
698
698
|
}
|
|
@@ -1286,27 +1286,27 @@ function extractSections(content) {
|
|
|
1286
1286
|
}
|
|
1287
1287
|
return sections.map((section) => buildAgentMapSection(section, lines));
|
|
1288
1288
|
}
|
|
1289
|
-
function isExternalLink(
|
|
1290
|
-
return
|
|
1289
|
+
function isExternalLink(path50) {
|
|
1290
|
+
return path50.startsWith("http://") || path50.startsWith("https://") || path50.startsWith("#") || path50.startsWith("mailto:");
|
|
1291
1291
|
}
|
|
1292
1292
|
function resolveLinkPath(linkPath, baseDir) {
|
|
1293
1293
|
return linkPath.startsWith(".") ? join8(baseDir, linkPath) : linkPath;
|
|
1294
1294
|
}
|
|
1295
|
-
async function validateAgentsMap(
|
|
1296
|
-
const contentResult = await readFileContent(
|
|
1295
|
+
async function validateAgentsMap(path50 = "./AGENTS.md") {
|
|
1296
|
+
const contentResult = await readFileContent(path50);
|
|
1297
1297
|
if (!contentResult.ok) {
|
|
1298
1298
|
return Err(
|
|
1299
1299
|
createError(
|
|
1300
1300
|
"PARSE_ERROR",
|
|
1301
1301
|
`Failed to read AGENTS.md: ${contentResult.error.message}`,
|
|
1302
|
-
{ path:
|
|
1302
|
+
{ path: path50 },
|
|
1303
1303
|
["Ensure the file exists", "Check file permissions"]
|
|
1304
1304
|
)
|
|
1305
1305
|
);
|
|
1306
1306
|
}
|
|
1307
1307
|
const content = contentResult.value;
|
|
1308
1308
|
const sections = extractSections(content);
|
|
1309
|
-
const baseDir = dirname2(
|
|
1309
|
+
const baseDir = dirname2(path50);
|
|
1310
1310
|
const sectionTitles = sections.map((s) => s.title);
|
|
1311
1311
|
const missingSections = REQUIRED_SECTIONS.filter(
|
|
1312
1312
|
(required) => !sectionTitles.some((title) => title.toLowerCase().includes(required.toLowerCase()))
|
|
@@ -1913,8 +1913,8 @@ async function checkDocCoverage(domain, options = {}) {
|
|
|
1913
1913
|
|
|
1914
1914
|
// src/context/knowledge-map.ts
|
|
1915
1915
|
import { join as join14, basename as basename3 } from "path";
|
|
1916
|
-
function suggestFix(
|
|
1917
|
-
const targetName = basename3(
|
|
1916
|
+
function suggestFix(path50, existingFiles) {
|
|
1917
|
+
const targetName = basename3(path50).toLowerCase();
|
|
1918
1918
|
const similar = existingFiles.find((file) => {
|
|
1919
1919
|
const fileName = basename3(file).toLowerCase();
|
|
1920
1920
|
return fileName.includes(targetName) || targetName.includes(fileName);
|
|
@@ -1922,7 +1922,7 @@ function suggestFix(path48, existingFiles) {
|
|
|
1922
1922
|
if (similar) {
|
|
1923
1923
|
return `Did you mean "${similar}"?`;
|
|
1924
1924
|
}
|
|
1925
|
-
return `Create the file "${
|
|
1925
|
+
return `Create the file "${path50}" or remove the link`;
|
|
1926
1926
|
}
|
|
1927
1927
|
async function validateKnowledgeMap(rootDir = process.cwd()) {
|
|
1928
1928
|
const agentsPath = join14(rootDir, "AGENTS.md");
|
|
@@ -2399,8 +2399,8 @@ function createBoundaryValidator(schema, name) {
|
|
|
2399
2399
|
return Ok(result.data);
|
|
2400
2400
|
}
|
|
2401
2401
|
const suggestions = result.error.issues.map((issue) => {
|
|
2402
|
-
const
|
|
2403
|
-
return
|
|
2402
|
+
const path50 = issue.path.join(".");
|
|
2403
|
+
return path50 ? `${path50}: ${issue.message}` : issue.message;
|
|
2404
2404
|
});
|
|
2405
2405
|
return Err(
|
|
2406
2406
|
createError(
|
|
@@ -4279,12 +4279,12 @@ function parseDiffHeader(part) {
|
|
|
4279
4279
|
return headerMatch[2];
|
|
4280
4280
|
}
|
|
4281
4281
|
function parseDiffPart(part) {
|
|
4282
|
-
const
|
|
4283
|
-
if (!
|
|
4282
|
+
const path50 = parseDiffHeader(part);
|
|
4283
|
+
if (!path50) return null;
|
|
4284
4284
|
const additionRegex = /^\+(?!\+\+)/gm;
|
|
4285
4285
|
const deletionRegex = /^-(?!--)/gm;
|
|
4286
4286
|
return {
|
|
4287
|
-
path:
|
|
4287
|
+
path: path50,
|
|
4288
4288
|
status: detectFileStatus(part),
|
|
4289
4289
|
additions: (part.match(additionRegex) || []).length,
|
|
4290
4290
|
deletions: (part.match(deletionRegex) || []).length
|
|
@@ -9144,6 +9144,98 @@ var SecurityScanner = class {
|
|
|
9144
9144
|
}
|
|
9145
9145
|
};
|
|
9146
9146
|
|
|
9147
|
+
// src/security/osv-client.ts
|
|
9148
|
+
import * as fs30 from "fs";
|
|
9149
|
+
import * as path26 from "path";
|
|
9150
|
+
var OSV_ENDPOINT = "https://api.osv.dev/v1/query";
|
|
9151
|
+
var DEFAULT_CACHE_DIR = path26.join(".harness", "cache", "osv");
|
|
9152
|
+
var DEFAULT_TTL_HOURS = 24;
|
|
9153
|
+
function createOsvClient(options = {}) {
|
|
9154
|
+
const cacheDir = options.cacheDir ?? DEFAULT_CACHE_DIR;
|
|
9155
|
+
const ttlMs = (options.cacheTtlHours ?? DEFAULT_TTL_HOURS) * 60 * 60 * 1e3;
|
|
9156
|
+
const fetchFn = options.fetchFn ?? globalThis.fetch;
|
|
9157
|
+
const logger = options.logger ?? { warn: (m, ctx) => console.warn(m, ctx) };
|
|
9158
|
+
const strict = options.strict ?? false;
|
|
9159
|
+
return {
|
|
9160
|
+
async check(pkg) {
|
|
9161
|
+
const cacheKey = `${pkg.ecosystem}-${sanitizePkgName(pkg.name)}@${pkg.version ?? "latest"}.json`;
|
|
9162
|
+
const cachePath = path26.join(cacheDir, cacheKey);
|
|
9163
|
+
const cached = await readCache(cachePath, ttlMs);
|
|
9164
|
+
if (cached) {
|
|
9165
|
+
return { ...cached, source: "cache" };
|
|
9166
|
+
}
|
|
9167
|
+
try {
|
|
9168
|
+
const advisories = await queryOsv(fetchFn, pkg);
|
|
9169
|
+
const result = classify(advisories);
|
|
9170
|
+
await writeCache(cachePath, result);
|
|
9171
|
+
return { ...result, source: "network" };
|
|
9172
|
+
} catch (err) {
|
|
9173
|
+
const message = `OSV query failed for ${pkg.name}@${pkg.version ?? "latest"}: ${String(err)}`;
|
|
9174
|
+
if (strict) {
|
|
9175
|
+
throw new Error(message, { cause: err });
|
|
9176
|
+
}
|
|
9177
|
+
logger.warn(message, { pkg });
|
|
9178
|
+
return { malicious: [], other: [], source: "fail-open" };
|
|
9179
|
+
}
|
|
9180
|
+
},
|
|
9181
|
+
async clearCache() {
|
|
9182
|
+
try {
|
|
9183
|
+
await fs30.promises.rm(cacheDir, { recursive: true, force: true });
|
|
9184
|
+
} catch {
|
|
9185
|
+
}
|
|
9186
|
+
}
|
|
9187
|
+
};
|
|
9188
|
+
}
|
|
9189
|
+
async function queryOsv(fetchFn, pkg) {
|
|
9190
|
+
const body = {
|
|
9191
|
+
package: { ecosystem: pkg.ecosystem, name: pkg.name }
|
|
9192
|
+
};
|
|
9193
|
+
if (pkg.version) body.version = pkg.version;
|
|
9194
|
+
const response = await fetchFn(OSV_ENDPOINT, {
|
|
9195
|
+
method: "POST",
|
|
9196
|
+
headers: { "content-type": "application/json" },
|
|
9197
|
+
body: JSON.stringify(body)
|
|
9198
|
+
});
|
|
9199
|
+
if (!response.ok) {
|
|
9200
|
+
throw new Error(`OSV returned HTTP ${response.status}`);
|
|
9201
|
+
}
|
|
9202
|
+
const json = await response.json();
|
|
9203
|
+
return json.vulns ?? [];
|
|
9204
|
+
}
|
|
9205
|
+
function classify(advisories) {
|
|
9206
|
+
const malicious = [];
|
|
9207
|
+
const other = [];
|
|
9208
|
+
for (const a of advisories) {
|
|
9209
|
+
if (typeof a?.id === "string" && a.id.startsWith("MAL-")) {
|
|
9210
|
+
malicious.push(a);
|
|
9211
|
+
} else {
|
|
9212
|
+
other.push(a);
|
|
9213
|
+
}
|
|
9214
|
+
}
|
|
9215
|
+
return { malicious, other };
|
|
9216
|
+
}
|
|
9217
|
+
async function readCache(cachePath, ttlMs) {
|
|
9218
|
+
try {
|
|
9219
|
+
const stat2 = await fs30.promises.stat(cachePath);
|
|
9220
|
+
if (Date.now() - stat2.mtimeMs > ttlMs) return null;
|
|
9221
|
+
const buf = await fs30.promises.readFile(cachePath, "utf-8");
|
|
9222
|
+
const parsed = JSON.parse(buf);
|
|
9223
|
+
return { malicious: parsed.malicious ?? [], other: parsed.other ?? [] };
|
|
9224
|
+
} catch {
|
|
9225
|
+
return null;
|
|
9226
|
+
}
|
|
9227
|
+
}
|
|
9228
|
+
async function writeCache(cachePath, payload) {
|
|
9229
|
+
try {
|
|
9230
|
+
await fs30.promises.mkdir(path26.dirname(cachePath), { recursive: true });
|
|
9231
|
+
await fs30.promises.writeFile(cachePath, JSON.stringify(payload, null, 2), "utf-8");
|
|
9232
|
+
} catch {
|
|
9233
|
+
}
|
|
9234
|
+
}
|
|
9235
|
+
function sanitizePkgName(name) {
|
|
9236
|
+
return name.replace(/[/\\]/g, "__");
|
|
9237
|
+
}
|
|
9238
|
+
|
|
9147
9239
|
// src/security/injection-patterns.ts
|
|
9148
9240
|
var hiddenUnicodePatterns = [
|
|
9149
9241
|
{
|
|
@@ -9382,12 +9474,12 @@ var DESTRUCTIVE_BASH = [
|
|
|
9382
9474
|
|
|
9383
9475
|
// src/security/taint.ts
|
|
9384
9476
|
import { readFileSync as readFileSync22, writeFileSync as writeFileSync14, unlinkSync as unlinkSync2, mkdirSync as mkdirSync13, readdirSync as readdirSync3 } from "fs";
|
|
9385
|
-
import { join as
|
|
9477
|
+
import { join as join38, dirname as dirname10 } from "path";
|
|
9386
9478
|
var TAINT_DURATION_MS = 30 * 60 * 1e3;
|
|
9387
9479
|
var DEFAULT_SESSION_ID = "default";
|
|
9388
9480
|
function getTaintFilePath(projectRoot, sessionId) {
|
|
9389
9481
|
const id = sessionId || DEFAULT_SESSION_ID;
|
|
9390
|
-
return
|
|
9482
|
+
return join38(projectRoot, ".harness", `session-taint-${id}.json`);
|
|
9391
9483
|
}
|
|
9392
9484
|
function readTaint(projectRoot, sessionId) {
|
|
9393
9485
|
const filePath = getTaintFilePath(projectRoot, sessionId);
|
|
@@ -9437,7 +9529,7 @@ function writeTaint(projectRoot, sessionId, reason, findings, source) {
|
|
|
9437
9529
|
const id = sessionId || DEFAULT_SESSION_ID;
|
|
9438
9530
|
const filePath = getTaintFilePath(projectRoot, id);
|
|
9439
9531
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
9440
|
-
const dir =
|
|
9532
|
+
const dir = dirname10(filePath);
|
|
9441
9533
|
mkdirSync13(dir, { recursive: true });
|
|
9442
9534
|
const existing = readTaint(projectRoot, id);
|
|
9443
9535
|
const maxSeverity = findings.some((f) => f.severity === "high") ? "high" : "medium";
|
|
@@ -9469,14 +9561,14 @@ function clearTaint(projectRoot, sessionId) {
|
|
|
9469
9561
|
return 0;
|
|
9470
9562
|
}
|
|
9471
9563
|
}
|
|
9472
|
-
const harnessDir =
|
|
9564
|
+
const harnessDir = join38(projectRoot, ".harness");
|
|
9473
9565
|
let count = 0;
|
|
9474
9566
|
try {
|
|
9475
9567
|
const files = readdirSync3(harnessDir);
|
|
9476
9568
|
for (const file of files) {
|
|
9477
9569
|
if (file.startsWith("session-taint-") && file.endsWith(".json")) {
|
|
9478
9570
|
try {
|
|
9479
|
-
unlinkSync2(
|
|
9571
|
+
unlinkSync2(join38(harnessDir, file));
|
|
9480
9572
|
count++;
|
|
9481
9573
|
} catch {
|
|
9482
9574
|
}
|
|
@@ -9487,7 +9579,7 @@ function clearTaint(projectRoot, sessionId) {
|
|
|
9487
9579
|
return count;
|
|
9488
9580
|
}
|
|
9489
9581
|
function listTaintedSessions(projectRoot) {
|
|
9490
|
-
const harnessDir =
|
|
9582
|
+
const harnessDir = join38(projectRoot, ".harness");
|
|
9491
9583
|
const sessions = [];
|
|
9492
9584
|
try {
|
|
9493
9585
|
const files = readdirSync3(harnessDir);
|
|
@@ -9659,13 +9751,13 @@ var EMPTY_SUPPLY_CHAIN = {
|
|
|
9659
9751
|
import { readFileSync as readFileSync23, writeFileSync as writeFileSync15, renameSync as renameSync7, mkdirSync as mkdirSync14, existsSync as existsSync34 } from "fs";
|
|
9660
9752
|
import { execSync as execSync3 } from "child_process";
|
|
9661
9753
|
import { randomBytes } from "crypto";
|
|
9662
|
-
import { isAbsolute as isAbsolute4, join as
|
|
9754
|
+
import { isAbsolute as isAbsolute4, join as join39, relative as relative3, dirname as dirname11 } from "path";
|
|
9663
9755
|
var SecurityTimelineManager = class _SecurityTimelineManager {
|
|
9664
9756
|
rootDir;
|
|
9665
9757
|
timelinePath;
|
|
9666
9758
|
constructor(rootDir) {
|
|
9667
9759
|
this.rootDir = rootDir;
|
|
9668
|
-
this.timelinePath =
|
|
9760
|
+
this.timelinePath = join39(rootDir, ".harness", "security", "timeline.json");
|
|
9669
9761
|
}
|
|
9670
9762
|
/**
|
|
9671
9763
|
* Load timeline from disk.
|
|
@@ -9709,7 +9801,7 @@ var SecurityTimelineManager = class _SecurityTimelineManager {
|
|
|
9709
9801
|
* Save timeline to disk using atomic write (temp file + rename).
|
|
9710
9802
|
*/
|
|
9711
9803
|
save(timeline) {
|
|
9712
|
-
const dir =
|
|
9804
|
+
const dir = dirname11(this.timelinePath);
|
|
9713
9805
|
if (!existsSync34(dir)) {
|
|
9714
9806
|
mkdirSync14(dir, { recursive: true });
|
|
9715
9807
|
}
|
|
@@ -10059,7 +10151,7 @@ var SecurityTimelineManager = class _SecurityTimelineManager {
|
|
|
10059
10151
|
};
|
|
10060
10152
|
|
|
10061
10153
|
// src/ci/check-orchestrator.ts
|
|
10062
|
-
import * as
|
|
10154
|
+
import * as path27 from "path";
|
|
10063
10155
|
import { skipDirGlobs as skipDirGlobs3 } from "@harness-engineering/graph";
|
|
10064
10156
|
import { GraphStore, queryTraceability } from "@harness-engineering/graph";
|
|
10065
10157
|
var ALL_CHECKS = [
|
|
@@ -10075,7 +10167,7 @@ var ALL_CHECKS = [
|
|
|
10075
10167
|
];
|
|
10076
10168
|
async function runValidateCheck(projectRoot, config) {
|
|
10077
10169
|
const issues = [];
|
|
10078
|
-
const agentsPath =
|
|
10170
|
+
const agentsPath = path27.join(projectRoot, config.agentsMapPath ?? "AGENTS.md");
|
|
10079
10171
|
const result = await validateAgentsMap(agentsPath);
|
|
10080
10172
|
if (!result.ok) {
|
|
10081
10173
|
issues.push({ severity: "error", message: result.error.message });
|
|
@@ -10132,7 +10224,7 @@ async function runDepsCheck(projectRoot, config) {
|
|
|
10132
10224
|
}
|
|
10133
10225
|
async function runDocsCheck(projectRoot, config) {
|
|
10134
10226
|
const issues = [];
|
|
10135
|
-
const docsDir =
|
|
10227
|
+
const docsDir = path27.join(projectRoot, config.docsDir ?? "docs");
|
|
10136
10228
|
const entropyConfig = config.entropy || {};
|
|
10137
10229
|
const result = await checkDocCoverage("project", {
|
|
10138
10230
|
docsDir,
|
|
@@ -10311,7 +10403,7 @@ async function runTraceabilityCheck(projectRoot, config) {
|
|
|
10311
10403
|
const issues = [];
|
|
10312
10404
|
const traceConfig = config.traceability || {};
|
|
10313
10405
|
if (traceConfig.enabled === false) return issues;
|
|
10314
|
-
const graphDir =
|
|
10406
|
+
const graphDir = path27.join(projectRoot, ".harness", "graph");
|
|
10315
10407
|
const store = new GraphStore();
|
|
10316
10408
|
const loaded = await store.load(graphDir);
|
|
10317
10409
|
if (!loaded) {
|
|
@@ -10534,7 +10626,7 @@ var CINotifier = class {
|
|
|
10534
10626
|
};
|
|
10535
10627
|
|
|
10536
10628
|
// src/review/mechanical-checks.ts
|
|
10537
|
-
import * as
|
|
10629
|
+
import * as path28 from "path";
|
|
10538
10630
|
async function runMechanicalChecks(options) {
|
|
10539
10631
|
const { projectRoot, config, skip = [], changedFiles } = options;
|
|
10540
10632
|
const findings = [];
|
|
@@ -10546,7 +10638,7 @@ async function runMechanicalChecks(options) {
|
|
|
10546
10638
|
};
|
|
10547
10639
|
if (!skip.includes("validate")) {
|
|
10548
10640
|
try {
|
|
10549
|
-
const agentsPath =
|
|
10641
|
+
const agentsPath = path28.join(projectRoot, config.agentsMapPath ?? "AGENTS.md");
|
|
10550
10642
|
const result = await validateAgentsMap(agentsPath);
|
|
10551
10643
|
if (!result.ok) {
|
|
10552
10644
|
statuses.validate = "fail";
|
|
@@ -10583,7 +10675,7 @@ async function runMechanicalChecks(options) {
|
|
|
10583
10675
|
statuses.validate = "fail";
|
|
10584
10676
|
findings.push({
|
|
10585
10677
|
tool: "validate",
|
|
10586
|
-
file:
|
|
10678
|
+
file: path28.join(projectRoot, "AGENTS.md"),
|
|
10587
10679
|
message: err instanceof Error ? err.message : String(err),
|
|
10588
10680
|
severity: "error"
|
|
10589
10681
|
});
|
|
@@ -10647,7 +10739,7 @@ async function runMechanicalChecks(options) {
|
|
|
10647
10739
|
(async () => {
|
|
10648
10740
|
const localFindings = [];
|
|
10649
10741
|
try {
|
|
10650
|
-
const docsDir =
|
|
10742
|
+
const docsDir = path28.join(projectRoot, config.docsDir ?? "docs");
|
|
10651
10743
|
const result = await checkDocCoverage("project", { docsDir });
|
|
10652
10744
|
if (!result.ok) {
|
|
10653
10745
|
statuses["check-docs"] = "warn";
|
|
@@ -10674,7 +10766,7 @@ async function runMechanicalChecks(options) {
|
|
|
10674
10766
|
statuses["check-docs"] = "warn";
|
|
10675
10767
|
localFindings.push({
|
|
10676
10768
|
tool: "check-docs",
|
|
10677
|
-
file:
|
|
10769
|
+
file: path28.join(projectRoot, "docs"),
|
|
10678
10770
|
message: err instanceof Error ? err.message : String(err),
|
|
10679
10771
|
severity: "warning"
|
|
10680
10772
|
});
|
|
@@ -10822,7 +10914,7 @@ function detectChangeType(commitMessage, diff2) {
|
|
|
10822
10914
|
}
|
|
10823
10915
|
|
|
10824
10916
|
// src/review/context-scoper.ts
|
|
10825
|
-
import * as
|
|
10917
|
+
import * as path29 from "path";
|
|
10826
10918
|
var ALL_DOMAINS = ["compliance", "bug", "security", "architecture", "learnings"];
|
|
10827
10919
|
var SECURITY_PATTERNS = /auth|crypto|password|secret|token|session|cookie|hash|encrypt|decrypt|sql|shell|exec|eval/i;
|
|
10828
10920
|
function computeContextBudget(diffLines) {
|
|
@@ -10830,18 +10922,18 @@ function computeContextBudget(diffLines) {
|
|
|
10830
10922
|
return diffLines;
|
|
10831
10923
|
}
|
|
10832
10924
|
function isWithinProject(absPath, projectRoot) {
|
|
10833
|
-
const resolvedRoot =
|
|
10834
|
-
const resolvedPath =
|
|
10835
|
-
return resolvedPath.startsWith(resolvedRoot) || resolvedPath ===
|
|
10925
|
+
const resolvedRoot = path29.resolve(projectRoot) + path29.sep;
|
|
10926
|
+
const resolvedPath = path29.resolve(absPath);
|
|
10927
|
+
return resolvedPath.startsWith(resolvedRoot) || resolvedPath === path29.resolve(projectRoot);
|
|
10836
10928
|
}
|
|
10837
10929
|
async function readContextFile(projectRoot, filePath, reason) {
|
|
10838
|
-
const absPath =
|
|
10930
|
+
const absPath = path29.isAbsolute(filePath) ? filePath : path29.join(projectRoot, filePath);
|
|
10839
10931
|
if (!isWithinProject(absPath, projectRoot)) return null;
|
|
10840
10932
|
const result = await readFileContent(absPath);
|
|
10841
10933
|
if (!result.ok) return null;
|
|
10842
10934
|
const content = result.value;
|
|
10843
10935
|
const lines = content.split("\n").length;
|
|
10844
|
-
const relPath2 =
|
|
10936
|
+
const relPath2 = path29.isAbsolute(filePath) ? relativePosix(projectRoot, filePath) : filePath;
|
|
10845
10937
|
return { path: relPath2, content, reason, lines };
|
|
10846
10938
|
}
|
|
10847
10939
|
function extractImportSources(content) {
|
|
@@ -10862,24 +10954,24 @@ var JS_EXT_FALLBACKS = {
|
|
|
10862
10954
|
};
|
|
10863
10955
|
async function resolveImportPath(projectRoot, fromFile, importSource) {
|
|
10864
10956
|
if (!importSource.startsWith(".")) return null;
|
|
10865
|
-
const fromDir =
|
|
10866
|
-
const basePath =
|
|
10957
|
+
const fromDir = path29.dirname(path29.join(projectRoot, fromFile));
|
|
10958
|
+
const basePath = path29.resolve(fromDir, importSource);
|
|
10867
10959
|
if (!isWithinProject(basePath, projectRoot)) return null;
|
|
10868
10960
|
const relBase = relativePosix(projectRoot, basePath);
|
|
10869
|
-
const sourceExt =
|
|
10961
|
+
const sourceExt = path29.extname(relBase);
|
|
10870
10962
|
const candidates = [];
|
|
10871
10963
|
const fallbacks = JS_EXT_FALLBACKS[sourceExt];
|
|
10872
10964
|
if (fallbacks) {
|
|
10873
10965
|
const stripped = relBase.slice(0, -sourceExt.length);
|
|
10874
10966
|
for (const ext of fallbacks) candidates.push(stripped + ext);
|
|
10875
|
-
candidates.push(
|
|
10876
|
-
candidates.push(
|
|
10877
|
-
candidates.push(
|
|
10967
|
+
candidates.push(path29.join(stripped, "index.ts"));
|
|
10968
|
+
candidates.push(path29.join(stripped, "index.tsx"));
|
|
10969
|
+
candidates.push(path29.join(stripped, "index.jsx"));
|
|
10878
10970
|
}
|
|
10879
10971
|
candidates.push(relBase + ".ts", relBase + ".tsx", relBase + ".mts");
|
|
10880
|
-
candidates.push(
|
|
10972
|
+
candidates.push(path29.join(relBase, "index.ts"));
|
|
10881
10973
|
for (const candidate of candidates) {
|
|
10882
|
-
const absCandidate =
|
|
10974
|
+
const absCandidate = path29.join(projectRoot, candidate);
|
|
10883
10975
|
if (await fileExists(absCandidate)) {
|
|
10884
10976
|
return candidate;
|
|
10885
10977
|
}
|
|
@@ -10887,7 +10979,7 @@ async function resolveImportPath(projectRoot, fromFile, importSource) {
|
|
|
10887
10979
|
return null;
|
|
10888
10980
|
}
|
|
10889
10981
|
async function findTestFiles(projectRoot, sourceFile) {
|
|
10890
|
-
const baseName =
|
|
10982
|
+
const baseName = path29.basename(sourceFile, path29.extname(sourceFile));
|
|
10891
10983
|
const pattern = `**/${baseName}.{test,spec}.{ts,tsx,mts}`;
|
|
10892
10984
|
const results = await findFiles(pattern, projectRoot);
|
|
10893
10985
|
return results.map((f) => relativePosix(projectRoot, f));
|
|
@@ -11783,7 +11875,7 @@ async function fanOutReview(options) {
|
|
|
11783
11875
|
}
|
|
11784
11876
|
|
|
11785
11877
|
// src/review/validate-findings.ts
|
|
11786
|
-
import * as
|
|
11878
|
+
import * as path30 from "path";
|
|
11787
11879
|
var DOWNGRADE_MAP = {
|
|
11788
11880
|
critical: "important",
|
|
11789
11881
|
important: "suggestion",
|
|
@@ -11804,7 +11896,7 @@ function normalizePath(filePath, projectRoot) {
|
|
|
11804
11896
|
let normalized = filePath;
|
|
11805
11897
|
normalized = normalized.replace(/\\/g, "/");
|
|
11806
11898
|
const normalizedRoot = projectRoot.replace(/\\/g, "/");
|
|
11807
|
-
if (
|
|
11899
|
+
if (path30.isAbsolute(normalized)) {
|
|
11808
11900
|
const root = normalizedRoot.endsWith("/") ? normalizedRoot : normalizedRoot + "/";
|
|
11809
11901
|
if (normalized.startsWith(root)) {
|
|
11810
11902
|
normalized = normalized.slice(root.length);
|
|
@@ -11816,12 +11908,12 @@ function normalizePath(filePath, projectRoot) {
|
|
|
11816
11908
|
return normalized;
|
|
11817
11909
|
}
|
|
11818
11910
|
function resolveImportPath2(currentFile, importPath) {
|
|
11819
|
-
const dir =
|
|
11820
|
-
let resolved =
|
|
11911
|
+
const dir = path30.dirname(currentFile);
|
|
11912
|
+
let resolved = path30.join(dir, importPath).replace(/\\/g, "/");
|
|
11821
11913
|
if (!resolved.match(/\.(ts|tsx|js|jsx)$/)) {
|
|
11822
11914
|
resolved += ".ts";
|
|
11823
11915
|
}
|
|
11824
|
-
return
|
|
11916
|
+
return path30.normalize(resolved).replace(/\\/g, "/");
|
|
11825
11917
|
}
|
|
11826
11918
|
function enqueueImports(content, current, visited, queue, maxDepth) {
|
|
11827
11919
|
const importRegex = /import\s+.*?from\s+['"]([^'"]+)['"]/g;
|
|
@@ -11853,7 +11945,7 @@ function isMechanicallyExcluded(finding, exclusionSet, projectRoot) {
|
|
|
11853
11945
|
const normalizedFile = normalizePath(finding.file, projectRoot);
|
|
11854
11946
|
if (exclusionSet.isExcluded(normalizedFile, finding.lineRange)) return true;
|
|
11855
11947
|
if (exclusionSet.isExcluded(finding.file, finding.lineRange)) return true;
|
|
11856
|
-
const absoluteFile =
|
|
11948
|
+
const absoluteFile = path30.isAbsolute(finding.file) ? finding.file : path30.join(projectRoot, finding.file).replace(/\\/g, "/");
|
|
11857
11949
|
return exclusionSet.isExcluded(absoluteFile, finding.lineRange);
|
|
11858
11950
|
}
|
|
11859
11951
|
async function validateWithGraph(crossFileRefs, graph) {
|
|
@@ -12914,8 +13006,8 @@ function serializeAssignmentHistory(records) {
|
|
|
12914
13006
|
}
|
|
12915
13007
|
|
|
12916
13008
|
// src/roadmap/sync.ts
|
|
12917
|
-
import * as
|
|
12918
|
-
import * as
|
|
13009
|
+
import * as fs31 from "fs";
|
|
13010
|
+
import * as path31 from "path";
|
|
12919
13011
|
import { Ok as Ok3 } from "@harness-engineering/types";
|
|
12920
13012
|
|
|
12921
13013
|
// src/roadmap/status-rank.ts
|
|
@@ -12936,7 +13028,7 @@ function isRegression(from, to) {
|
|
|
12936
13028
|
// src/roadmap/sync.ts
|
|
12937
13029
|
function collectAutopilotStatuses(autopilotPath, featurePlans, allTaskStatuses) {
|
|
12938
13030
|
try {
|
|
12939
|
-
const raw =
|
|
13031
|
+
const raw = fs31.readFileSync(autopilotPath, "utf-8");
|
|
12940
13032
|
const autopilot = JSON.parse(raw);
|
|
12941
13033
|
if (!autopilot.phases) return;
|
|
12942
13034
|
const linkedPhases = autopilot.phases.filter(
|
|
@@ -12968,10 +13060,10 @@ function inferStatus(feature, projectPath, allFeatures) {
|
|
|
12968
13060
|
const featuresWithPlans = allFeatures.filter((f) => f.plans.length > 0);
|
|
12969
13061
|
const useRootState = featuresWithPlans.length <= 1;
|
|
12970
13062
|
if (useRootState) {
|
|
12971
|
-
const rootStatePath =
|
|
12972
|
-
if (
|
|
13063
|
+
const rootStatePath = path31.join(projectPath, ".harness", "state.json");
|
|
13064
|
+
if (fs31.existsSync(rootStatePath)) {
|
|
12973
13065
|
try {
|
|
12974
|
-
const raw =
|
|
13066
|
+
const raw = fs31.readFileSync(rootStatePath, "utf-8");
|
|
12975
13067
|
const state = JSON.parse(raw);
|
|
12976
13068
|
if (state.progress) {
|
|
12977
13069
|
for (const status of Object.values(state.progress)) {
|
|
@@ -12982,14 +13074,14 @@ function inferStatus(feature, projectPath, allFeatures) {
|
|
|
12982
13074
|
}
|
|
12983
13075
|
}
|
|
12984
13076
|
}
|
|
12985
|
-
const sessionsDir =
|
|
12986
|
-
if (
|
|
13077
|
+
const sessionsDir = path31.join(projectPath, ".harness", "sessions");
|
|
13078
|
+
if (fs31.existsSync(sessionsDir)) {
|
|
12987
13079
|
try {
|
|
12988
|
-
const sessionDirs =
|
|
13080
|
+
const sessionDirs = fs31.readdirSync(sessionsDir, { withFileTypes: true });
|
|
12989
13081
|
for (const entry of sessionDirs) {
|
|
12990
13082
|
if (!entry.isDirectory()) continue;
|
|
12991
|
-
const autopilotPath =
|
|
12992
|
-
if (!
|
|
13083
|
+
const autopilotPath = path31.join(sessionsDir, entry.name, "autopilot-state.json");
|
|
13084
|
+
if (!fs31.existsSync(autopilotPath)) continue;
|
|
12993
13085
|
collectAutopilotStatuses(autopilotPath, feature.plans, allTaskStatuses);
|
|
12994
13086
|
}
|
|
12995
13087
|
} catch {
|
|
@@ -13476,8 +13568,8 @@ var GitHubIssuesSyncAdapter = class {
|
|
|
13476
13568
|
};
|
|
13477
13569
|
|
|
13478
13570
|
// src/roadmap/tracker-config.ts
|
|
13479
|
-
import * as
|
|
13480
|
-
import * as
|
|
13571
|
+
import * as fs32 from "fs";
|
|
13572
|
+
import * as path32 from "path";
|
|
13481
13573
|
function isValidTrackerShape(tracker) {
|
|
13482
13574
|
if (!tracker || typeof tracker !== "object") return false;
|
|
13483
13575
|
const t = tracker;
|
|
@@ -13490,9 +13582,9 @@ function isValidTrackerShape(tracker) {
|
|
|
13490
13582
|
}
|
|
13491
13583
|
function loadTrackerSyncConfig(projectRoot) {
|
|
13492
13584
|
try {
|
|
13493
|
-
const configPath =
|
|
13494
|
-
if (!
|
|
13495
|
-
const raw =
|
|
13585
|
+
const configPath = path32.join(projectRoot, "harness.config.json");
|
|
13586
|
+
if (!fs32.existsSync(configPath)) return null;
|
|
13587
|
+
const raw = fs32.readFileSync(configPath, "utf-8");
|
|
13496
13588
|
const config = JSON.parse(raw);
|
|
13497
13589
|
const tracker = config.roadmap?.tracker;
|
|
13498
13590
|
if (!isValidTrackerShape(tracker)) return null;
|
|
@@ -13503,7 +13595,7 @@ function loadTrackerSyncConfig(projectRoot) {
|
|
|
13503
13595
|
}
|
|
13504
13596
|
|
|
13505
13597
|
// src/roadmap/sync-engine.ts
|
|
13506
|
-
import * as
|
|
13598
|
+
import * as fs33 from "fs";
|
|
13507
13599
|
function emptySyncResult() {
|
|
13508
13600
|
return { created: [], updated: [], assignmentChanges: [], errors: [] };
|
|
13509
13601
|
}
|
|
@@ -13616,7 +13708,7 @@ async function fullSync(roadmapPath, adapter2, config, options) {
|
|
|
13616
13708
|
});
|
|
13617
13709
|
await previousSync;
|
|
13618
13710
|
try {
|
|
13619
|
-
const raw =
|
|
13711
|
+
const raw = fs33.readFileSync(roadmapPath, "utf-8");
|
|
13620
13712
|
const parseResult = parseRoadmap(raw);
|
|
13621
13713
|
if (!parseResult.ok) {
|
|
13622
13714
|
return {
|
|
@@ -13629,7 +13721,7 @@ async function fullSync(roadmapPath, adapter2, config, options) {
|
|
|
13629
13721
|
const tickets = fetchResult.ok ? fetchResult.value : void 0;
|
|
13630
13722
|
const pushResult = await syncToExternal(roadmap, adapter2, config, tickets);
|
|
13631
13723
|
const pullResult = await syncFromExternal(roadmap, adapter2, config, options);
|
|
13632
|
-
|
|
13724
|
+
fs33.writeFileSync(roadmapPath, serializeRoadmap(roadmap), "utf-8");
|
|
13633
13725
|
return {
|
|
13634
13726
|
created: pushResult.created,
|
|
13635
13727
|
updated: pushResult.updated,
|
|
@@ -14589,13 +14681,13 @@ function makeTrackerConflictBody(err, opts = {}) {
|
|
|
14589
14681
|
}
|
|
14590
14682
|
|
|
14591
14683
|
// src/roadmap/load-mode.ts
|
|
14592
|
-
import * as
|
|
14593
|
-
import * as
|
|
14684
|
+
import * as fs34 from "fs";
|
|
14685
|
+
import * as path33 from "path";
|
|
14594
14686
|
function loadProjectRoadmapMode(projectRoot) {
|
|
14595
14687
|
try {
|
|
14596
|
-
const configPath =
|
|
14597
|
-
if (!
|
|
14598
|
-
const raw =
|
|
14688
|
+
const configPath = path33.join(projectRoot, "harness.config.json");
|
|
14689
|
+
if (!fs34.existsSync(configPath)) return getRoadmapMode(null);
|
|
14690
|
+
const raw = fs34.readFileSync(configPath, "utf-8");
|
|
14599
14691
|
const parsed = JSON.parse(raw);
|
|
14600
14692
|
return getRoadmapMode(parsed);
|
|
14601
14693
|
} catch {
|
|
@@ -14604,16 +14696,16 @@ function loadProjectRoadmapMode(projectRoot) {
|
|
|
14604
14696
|
}
|
|
14605
14697
|
|
|
14606
14698
|
// src/roadmap/load-tracker-client-config.ts
|
|
14607
|
-
import * as
|
|
14608
|
-
import * as
|
|
14699
|
+
import * as fs35 from "fs";
|
|
14700
|
+
import * as path34 from "path";
|
|
14609
14701
|
import { Ok as Ok7, Err as Err6 } from "@harness-engineering/types";
|
|
14610
14702
|
function loadTrackerClientConfigFromProject(projectRoot) {
|
|
14611
14703
|
try {
|
|
14612
|
-
const configPath =
|
|
14613
|
-
if (!
|
|
14704
|
+
const configPath = path34.join(projectRoot, "harness.config.json");
|
|
14705
|
+
if (!fs35.existsSync(configPath)) {
|
|
14614
14706
|
return Err6(new Error("harness.config.json not found"));
|
|
14615
14707
|
}
|
|
14616
|
-
const cfg = JSON.parse(
|
|
14708
|
+
const cfg = JSON.parse(fs35.readFileSync(configPath, "utf-8"));
|
|
14617
14709
|
const tracker = cfg.roadmap?.tracker;
|
|
14618
14710
|
if (!tracker) {
|
|
14619
14711
|
return Err6(
|
|
@@ -14839,13 +14931,13 @@ function mapAction(action) {
|
|
|
14839
14931
|
|
|
14840
14932
|
// src/roadmap/migrate/run.ts
|
|
14841
14933
|
import { Ok as Ok8 } from "@harness-engineering/types";
|
|
14842
|
-
import * as
|
|
14934
|
+
import * as path35 from "path";
|
|
14843
14935
|
async function runMigrationPlan(plan, deps, opts) {
|
|
14844
14936
|
const projectRoot = opts.projectRoot;
|
|
14845
|
-
const roadmapPath =
|
|
14846
|
-
const archivedPath =
|
|
14847
|
-
const configPath =
|
|
14848
|
-
const configBackupPath =
|
|
14937
|
+
const roadmapPath = path35.join(projectRoot, "docs", "roadmap.md");
|
|
14938
|
+
const archivedPath = path35.join(projectRoot, "docs", "roadmap.md.archived");
|
|
14939
|
+
const configPath = path35.join(projectRoot, "harness.config.json");
|
|
14940
|
+
const configBackupPath = path35.join(projectRoot, "harness.config.json.pre-migration");
|
|
14849
14941
|
const createdSoFar = [];
|
|
14850
14942
|
const report = {
|
|
14851
14943
|
created: 0,
|
|
@@ -14986,18 +15078,18 @@ var EmitInteractionInputSchema = z11.object({
|
|
|
14986
15078
|
});
|
|
14987
15079
|
|
|
14988
15080
|
// src/blueprint/scanner.ts
|
|
14989
|
-
import * as
|
|
14990
|
-
import * as
|
|
15081
|
+
import * as fs36 from "fs/promises";
|
|
15082
|
+
import * as path36 from "path";
|
|
14991
15083
|
var ProjectScanner = class {
|
|
14992
15084
|
constructor(rootDir) {
|
|
14993
15085
|
this.rootDir = rootDir;
|
|
14994
15086
|
}
|
|
14995
15087
|
rootDir;
|
|
14996
15088
|
async scan() {
|
|
14997
|
-
let projectName =
|
|
15089
|
+
let projectName = path36.basename(this.rootDir);
|
|
14998
15090
|
try {
|
|
14999
|
-
const pkgPath =
|
|
15000
|
-
const pkgRaw = await
|
|
15091
|
+
const pkgPath = path36.join(this.rootDir, "package.json");
|
|
15092
|
+
const pkgRaw = await fs36.readFile(pkgPath, "utf-8");
|
|
15001
15093
|
const pkg = JSON.parse(pkgRaw);
|
|
15002
15094
|
if (pkg.name) projectName = pkg.name;
|
|
15003
15095
|
} catch {
|
|
@@ -15038,8 +15130,8 @@ var ProjectScanner = class {
|
|
|
15038
15130
|
};
|
|
15039
15131
|
|
|
15040
15132
|
// src/blueprint/generator.ts
|
|
15041
|
-
import * as
|
|
15042
|
-
import * as
|
|
15133
|
+
import * as fs37 from "fs/promises";
|
|
15134
|
+
import * as path37 from "path";
|
|
15043
15135
|
import * as ejs from "ejs";
|
|
15044
15136
|
|
|
15045
15137
|
// src/blueprint/templates.ts
|
|
@@ -15123,19 +15215,19 @@ var BlueprintGenerator = class {
|
|
|
15123
15215
|
styles: STYLES,
|
|
15124
15216
|
scripts: SCRIPTS
|
|
15125
15217
|
});
|
|
15126
|
-
await
|
|
15127
|
-
await
|
|
15218
|
+
await fs37.mkdir(options.outputDir, { recursive: true });
|
|
15219
|
+
await fs37.writeFile(path37.join(options.outputDir, "index.html"), html);
|
|
15128
15220
|
}
|
|
15129
15221
|
};
|
|
15130
15222
|
|
|
15131
15223
|
// src/update-checker.ts
|
|
15132
|
-
import * as
|
|
15133
|
-
import * as
|
|
15224
|
+
import * as fs38 from "fs";
|
|
15225
|
+
import * as path38 from "path";
|
|
15134
15226
|
import * as os from "os";
|
|
15135
15227
|
import { spawn as spawn2 } from "child_process";
|
|
15136
15228
|
function getStatePath() {
|
|
15137
15229
|
const home = process.env["HOME"] || os.homedir();
|
|
15138
|
-
return
|
|
15230
|
+
return path38.join(home, ".harness", "update-check.json");
|
|
15139
15231
|
}
|
|
15140
15232
|
function isUpdateCheckEnabled(configInterval) {
|
|
15141
15233
|
if (process.env["HARNESS_NO_UPDATE_CHECK"] === "1") return false;
|
|
@@ -15148,13 +15240,13 @@ function shouldRunCheck(state, intervalMs) {
|
|
|
15148
15240
|
}
|
|
15149
15241
|
function invalidateCheckState() {
|
|
15150
15242
|
try {
|
|
15151
|
-
|
|
15243
|
+
fs38.unlinkSync(getStatePath());
|
|
15152
15244
|
} catch {
|
|
15153
15245
|
}
|
|
15154
15246
|
}
|
|
15155
15247
|
function readCheckState() {
|
|
15156
15248
|
try {
|
|
15157
|
-
const raw =
|
|
15249
|
+
const raw = fs38.readFileSync(getStatePath(), "utf-8");
|
|
15158
15250
|
const parsed = JSON.parse(raw);
|
|
15159
15251
|
if (typeof parsed === "object" && parsed !== null && "lastCheckTime" in parsed && typeof parsed.lastCheckTime === "number" && "currentVersion" in parsed && typeof parsed.currentVersion === "string") {
|
|
15160
15252
|
const state = parsed;
|
|
@@ -15171,7 +15263,7 @@ function readCheckState() {
|
|
|
15171
15263
|
}
|
|
15172
15264
|
function spawnBackgroundCheck(currentVersion) {
|
|
15173
15265
|
const statePath = getStatePath();
|
|
15174
|
-
const stateDir =
|
|
15266
|
+
const stateDir = path38.dirname(statePath);
|
|
15175
15267
|
const script = `
|
|
15176
15268
|
const { execSync } = require('child_process');
|
|
15177
15269
|
const fs = require('fs');
|
|
@@ -15422,8 +15514,8 @@ function getModelPrice(model, dataset) {
|
|
|
15422
15514
|
}
|
|
15423
15515
|
|
|
15424
15516
|
// src/pricing/cache.ts
|
|
15425
|
-
import * as
|
|
15426
|
-
import * as
|
|
15517
|
+
import * as fs39 from "fs/promises";
|
|
15518
|
+
import * as path39 from "path";
|
|
15427
15519
|
|
|
15428
15520
|
// src/pricing/fallback.json
|
|
15429
15521
|
var fallback_default = {
|
|
@@ -15476,14 +15568,14 @@ var LITELLM_PRICING_URL = "https://raw.githubusercontent.com/BerriAI/litellm/mai
|
|
|
15476
15568
|
var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
15477
15569
|
var STALENESS_WARNING_DAYS = 7;
|
|
15478
15570
|
function getCachePath(projectRoot) {
|
|
15479
|
-
return
|
|
15571
|
+
return path39.join(projectRoot, ".harness", "cache", "pricing.json");
|
|
15480
15572
|
}
|
|
15481
15573
|
function getStalenessMarkerPath(projectRoot) {
|
|
15482
|
-
return
|
|
15574
|
+
return path39.join(projectRoot, ".harness", "cache", "staleness-marker.json");
|
|
15483
15575
|
}
|
|
15484
15576
|
async function readDiskCache(projectRoot) {
|
|
15485
15577
|
try {
|
|
15486
|
-
const raw = await
|
|
15578
|
+
const raw = await fs39.readFile(getCachePath(projectRoot), "utf-8");
|
|
15487
15579
|
return JSON.parse(raw);
|
|
15488
15580
|
} catch {
|
|
15489
15581
|
return null;
|
|
@@ -15491,8 +15583,8 @@ async function readDiskCache(projectRoot) {
|
|
|
15491
15583
|
}
|
|
15492
15584
|
async function writeDiskCache(projectRoot, data) {
|
|
15493
15585
|
const cachePath = getCachePath(projectRoot);
|
|
15494
|
-
await
|
|
15495
|
-
await
|
|
15586
|
+
await fs39.mkdir(path39.dirname(cachePath), { recursive: true });
|
|
15587
|
+
await fs39.writeFile(cachePath, JSON.stringify(data, null, 2));
|
|
15496
15588
|
}
|
|
15497
15589
|
async function fetchFromNetwork() {
|
|
15498
15590
|
try {
|
|
@@ -15519,7 +15611,7 @@ function loadFallbackDataset() {
|
|
|
15519
15611
|
async function checkAndWarnStaleness(projectRoot) {
|
|
15520
15612
|
const markerPath = getStalenessMarkerPath(projectRoot);
|
|
15521
15613
|
try {
|
|
15522
|
-
const raw = await
|
|
15614
|
+
const raw = await fs39.readFile(markerPath, "utf-8");
|
|
15523
15615
|
const marker = JSON.parse(raw);
|
|
15524
15616
|
const firstUse = new Date(marker.firstFallbackUse).getTime();
|
|
15525
15617
|
const now = Date.now();
|
|
@@ -15531,8 +15623,8 @@ async function checkAndWarnStaleness(projectRoot) {
|
|
|
15531
15623
|
}
|
|
15532
15624
|
} catch {
|
|
15533
15625
|
try {
|
|
15534
|
-
await
|
|
15535
|
-
await
|
|
15626
|
+
await fs39.mkdir(path39.dirname(markerPath), { recursive: true });
|
|
15627
|
+
await fs39.writeFile(
|
|
15536
15628
|
markerPath,
|
|
15537
15629
|
JSON.stringify({ firstFallbackUse: (/* @__PURE__ */ new Date()).toISOString() })
|
|
15538
15630
|
);
|
|
@@ -15542,7 +15634,7 @@ async function checkAndWarnStaleness(projectRoot) {
|
|
|
15542
15634
|
}
|
|
15543
15635
|
async function clearStalenessMarker(projectRoot) {
|
|
15544
15636
|
try {
|
|
15545
|
-
await
|
|
15637
|
+
await fs39.unlink(getStalenessMarkerPath(projectRoot));
|
|
15546
15638
|
} catch {
|
|
15547
15639
|
}
|
|
15548
15640
|
}
|
|
@@ -15741,8 +15833,8 @@ function aggregateByDay(records) {
|
|
|
15741
15833
|
}
|
|
15742
15834
|
|
|
15743
15835
|
// src/usage/jsonl-reader.ts
|
|
15744
|
-
import * as
|
|
15745
|
-
import * as
|
|
15836
|
+
import * as fs40 from "fs";
|
|
15837
|
+
import * as path40 from "path";
|
|
15746
15838
|
function extractTokenUsage(entry, lineNumber) {
|
|
15747
15839
|
const tokenUsage = entry.token_usage;
|
|
15748
15840
|
if (!tokenUsage || typeof tokenUsage !== "object") {
|
|
@@ -15789,10 +15881,10 @@ function parseLine(line, lineNumber) {
|
|
|
15789
15881
|
return record;
|
|
15790
15882
|
}
|
|
15791
15883
|
function readCostRecords(projectRoot) {
|
|
15792
|
-
const costsFile =
|
|
15884
|
+
const costsFile = path40.join(projectRoot, ".harness", "metrics", "costs.jsonl");
|
|
15793
15885
|
let raw;
|
|
15794
15886
|
try {
|
|
15795
|
-
raw =
|
|
15887
|
+
raw = fs40.readFileSync(costsFile, "utf-8");
|
|
15796
15888
|
} catch {
|
|
15797
15889
|
return [];
|
|
15798
15890
|
}
|
|
@@ -15810,8 +15902,8 @@ function readCostRecords(projectRoot) {
|
|
|
15810
15902
|
}
|
|
15811
15903
|
|
|
15812
15904
|
// src/usage/cc-parser.ts
|
|
15813
|
-
import * as
|
|
15814
|
-
import * as
|
|
15905
|
+
import * as fs41 from "fs";
|
|
15906
|
+
import * as path41 from "path";
|
|
15815
15907
|
import * as os2 from "os";
|
|
15816
15908
|
function extractUsage(entry) {
|
|
15817
15909
|
if (entry.type !== "assistant") return null;
|
|
@@ -15847,7 +15939,7 @@ function parseCCLine(line, filePath, lineNumber) {
|
|
|
15847
15939
|
entry = JSON.parse(line);
|
|
15848
15940
|
} catch {
|
|
15849
15941
|
console.warn(
|
|
15850
|
-
`[harness usage] Skipping malformed CC JSONL line ${lineNumber} in ${
|
|
15942
|
+
`[harness usage] Skipping malformed CC JSONL line ${lineNumber} in ${path41.basename(filePath)}`
|
|
15851
15943
|
);
|
|
15852
15944
|
return null;
|
|
15853
15945
|
}
|
|
@@ -15861,7 +15953,7 @@ function parseCCLine(line, filePath, lineNumber) {
|
|
|
15861
15953
|
function readCCFile(filePath) {
|
|
15862
15954
|
let raw;
|
|
15863
15955
|
try {
|
|
15864
|
-
raw =
|
|
15956
|
+
raw = fs41.readFileSync(filePath, "utf-8");
|
|
15865
15957
|
} catch {
|
|
15866
15958
|
return [];
|
|
15867
15959
|
}
|
|
@@ -15883,10 +15975,10 @@ function readCCFile(filePath) {
|
|
|
15883
15975
|
}
|
|
15884
15976
|
function parseCCRecords() {
|
|
15885
15977
|
const homeDir = process.env.HOME ?? os2.homedir();
|
|
15886
|
-
const projectsDir =
|
|
15978
|
+
const projectsDir = path41.join(homeDir, ".claude", "projects");
|
|
15887
15979
|
let projectDirs;
|
|
15888
15980
|
try {
|
|
15889
|
-
projectDirs =
|
|
15981
|
+
projectDirs = fs41.readdirSync(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => path41.join(projectsDir, d.name));
|
|
15890
15982
|
} catch {
|
|
15891
15983
|
return [];
|
|
15892
15984
|
}
|
|
@@ -15894,7 +15986,7 @@ function parseCCRecords() {
|
|
|
15894
15986
|
for (const dir of projectDirs) {
|
|
15895
15987
|
let files;
|
|
15896
15988
|
try {
|
|
15897
|
-
files =
|
|
15989
|
+
files = fs41.readdirSync(dir).filter((f) => f.endsWith(".jsonl")).map((f) => path41.join(dir, f));
|
|
15898
15990
|
} catch {
|
|
15899
15991
|
continue;
|
|
15900
15992
|
}
|
|
@@ -15906,8 +15998,8 @@ function parseCCRecords() {
|
|
|
15906
15998
|
}
|
|
15907
15999
|
|
|
15908
16000
|
// src/adoption/reader.ts
|
|
15909
|
-
import * as
|
|
15910
|
-
import * as
|
|
16001
|
+
import * as fs42 from "fs";
|
|
16002
|
+
import * as path42 from "path";
|
|
15911
16003
|
function parseLine2(line, lineNumber) {
|
|
15912
16004
|
try {
|
|
15913
16005
|
const parsed = JSON.parse(line);
|
|
@@ -15926,10 +16018,10 @@ function parseLine2(line, lineNumber) {
|
|
|
15926
16018
|
}
|
|
15927
16019
|
}
|
|
15928
16020
|
function readAdoptionRecords(projectRoot) {
|
|
15929
|
-
const adoptionFile =
|
|
16021
|
+
const adoptionFile = path42.join(projectRoot, ".harness", "metrics", "adoption.jsonl");
|
|
15930
16022
|
let raw;
|
|
15931
16023
|
try {
|
|
15932
|
-
raw =
|
|
16024
|
+
raw = fs42.readFileSync(adoptionFile, "utf-8");
|
|
15933
16025
|
} catch {
|
|
15934
16026
|
return [];
|
|
15935
16027
|
}
|
|
@@ -16005,17 +16097,17 @@ function topSkills(records, n) {
|
|
|
16005
16097
|
}
|
|
16006
16098
|
|
|
16007
16099
|
// src/notifications/config-loader.ts
|
|
16008
|
-
import * as
|
|
16009
|
-
import * as
|
|
16100
|
+
import * as fs43 from "fs";
|
|
16101
|
+
import * as path43 from "path";
|
|
16010
16102
|
import { NotificationsConfigSchema } from "@harness-engineering/types";
|
|
16011
16103
|
function loadNotificationsConfig(projectRoot) {
|
|
16012
|
-
const configPath =
|
|
16013
|
-
if (!
|
|
16104
|
+
const configPath = path43.join(projectRoot, "harness.config.json");
|
|
16105
|
+
if (!fs43.existsSync(configPath)) {
|
|
16014
16106
|
return Ok({ sinks: [] });
|
|
16015
16107
|
}
|
|
16016
16108
|
let raw;
|
|
16017
16109
|
try {
|
|
16018
|
-
raw =
|
|
16110
|
+
raw = fs43.readFileSync(configPath, "utf-8");
|
|
16019
16111
|
} catch (err) {
|
|
16020
16112
|
return Err(err instanceof Error ? err : new Error(String(err)));
|
|
16021
16113
|
}
|
|
@@ -16300,28 +16392,28 @@ var GeminiCacheAdapter = class {
|
|
|
16300
16392
|
};
|
|
16301
16393
|
|
|
16302
16394
|
// src/telemetry/consent.ts
|
|
16303
|
-
import * as
|
|
16304
|
-
import * as
|
|
16395
|
+
import * as fs45 from "fs";
|
|
16396
|
+
import * as path45 from "path";
|
|
16305
16397
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
16306
16398
|
|
|
16307
16399
|
// src/telemetry/install-id.ts
|
|
16308
|
-
import * as
|
|
16309
|
-
import * as
|
|
16400
|
+
import * as fs44 from "fs";
|
|
16401
|
+
import * as path44 from "path";
|
|
16310
16402
|
import * as crypto4 from "crypto";
|
|
16311
16403
|
function getOrCreateInstallId(projectRoot) {
|
|
16312
|
-
const harnessDir =
|
|
16313
|
-
const installIdFile =
|
|
16404
|
+
const harnessDir = path44.join(projectRoot, ".harness");
|
|
16405
|
+
const installIdFile = path44.join(harnessDir, ".install-id");
|
|
16314
16406
|
const UUID_V4_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
16315
16407
|
try {
|
|
16316
|
-
const existing =
|
|
16408
|
+
const existing = fs44.readFileSync(installIdFile, "utf-8").trim();
|
|
16317
16409
|
if (UUID_V4_RE.test(existing)) {
|
|
16318
16410
|
return existing;
|
|
16319
16411
|
}
|
|
16320
16412
|
} catch {
|
|
16321
16413
|
}
|
|
16322
16414
|
const id = crypto4.randomUUID();
|
|
16323
|
-
|
|
16324
|
-
|
|
16415
|
+
fs44.mkdirSync(harnessDir, { recursive: true });
|
|
16416
|
+
fs44.writeFileSync(installIdFile, id, { encoding: "utf-8", mode: 384 });
|
|
16325
16417
|
return id;
|
|
16326
16418
|
}
|
|
16327
16419
|
|
|
@@ -16329,7 +16421,7 @@ function getOrCreateInstallId(projectRoot) {
|
|
|
16329
16421
|
function parseIdentityFromTelemetryFile(filePath) {
|
|
16330
16422
|
const identity = {};
|
|
16331
16423
|
try {
|
|
16332
|
-
const raw =
|
|
16424
|
+
const raw = fs45.readFileSync(filePath, "utf-8");
|
|
16333
16425
|
const parsed = JSON.parse(raw);
|
|
16334
16426
|
const src = parsed?.identity;
|
|
16335
16427
|
if (!src || typeof src !== "object") return identity;
|
|
@@ -16342,7 +16434,7 @@ function parseIdentityFromTelemetryFile(filePath) {
|
|
|
16342
16434
|
}
|
|
16343
16435
|
function readProjectNameFallback(configPath) {
|
|
16344
16436
|
try {
|
|
16345
|
-
const raw =
|
|
16437
|
+
const raw = fs45.readFileSync(configPath, "utf-8");
|
|
16346
16438
|
const config = JSON.parse(raw);
|
|
16347
16439
|
return typeof config?.name === "string" ? config.name : void 0;
|
|
16348
16440
|
} catch {
|
|
@@ -16363,10 +16455,10 @@ function readGitAliasFallback(cwd) {
|
|
|
16363
16455
|
}
|
|
16364
16456
|
}
|
|
16365
16457
|
function readIdentity(projectRoot) {
|
|
16366
|
-
const filePath =
|
|
16458
|
+
const filePath = path45.join(projectRoot, ".harness", "telemetry.json");
|
|
16367
16459
|
const identity = parseIdentityFromTelemetryFile(filePath);
|
|
16368
16460
|
if (!identity.project) {
|
|
16369
|
-
const fallbackProject = readProjectNameFallback(
|
|
16461
|
+
const fallbackProject = readProjectNameFallback(path45.join(projectRoot, "harness.config.json"));
|
|
16370
16462
|
if (fallbackProject) identity.project = fallbackProject;
|
|
16371
16463
|
}
|
|
16372
16464
|
if (!identity.alias) {
|
|
@@ -16663,8 +16755,8 @@ var SpanKind = /* @__PURE__ */ ((SpanKind2) => {
|
|
|
16663
16755
|
})(SpanKind || {});
|
|
16664
16756
|
|
|
16665
16757
|
// src/locks/compound-lock.ts
|
|
16666
|
-
import * as
|
|
16667
|
-
import * as
|
|
16758
|
+
import * as fs46 from "fs";
|
|
16759
|
+
import * as path46 from "path";
|
|
16668
16760
|
var CompoundLockHeldError = class extends Error {
|
|
16669
16761
|
constructor(category, holderPid, lockPath) {
|
|
16670
16762
|
super(
|
|
@@ -16687,12 +16779,12 @@ function acquireCompoundLock(category, opts = {}) {
|
|
|
16687
16779
|
);
|
|
16688
16780
|
}
|
|
16689
16781
|
const cwd = opts.cwd ?? process.cwd();
|
|
16690
|
-
const lockDir =
|
|
16691
|
-
|
|
16692
|
-
const lockPath =
|
|
16782
|
+
const lockDir = path46.join(cwd, ".harness", "locks");
|
|
16783
|
+
fs46.mkdirSync(lockDir, { recursive: true });
|
|
16784
|
+
const lockPath = path46.join(lockDir, `compound-${category}.lock`);
|
|
16693
16785
|
let fd;
|
|
16694
16786
|
try {
|
|
16695
|
-
fd =
|
|
16787
|
+
fd = fs46.openSync(lockPath, "wx");
|
|
16696
16788
|
} catch (e) {
|
|
16697
16789
|
const err = e;
|
|
16698
16790
|
if (err.code === "EEXIST") {
|
|
@@ -16701,14 +16793,14 @@ function acquireCompoundLock(category, opts = {}) {
|
|
|
16701
16793
|
}
|
|
16702
16794
|
throw err;
|
|
16703
16795
|
}
|
|
16704
|
-
|
|
16705
|
-
|
|
16796
|
+
fs46.writeSync(fd, String(process.pid));
|
|
16797
|
+
fs46.closeSync(fd);
|
|
16706
16798
|
let released = false;
|
|
16707
16799
|
const release = () => {
|
|
16708
16800
|
if (released) return;
|
|
16709
16801
|
released = true;
|
|
16710
16802
|
try {
|
|
16711
|
-
|
|
16803
|
+
fs46.unlinkSync(lockPath);
|
|
16712
16804
|
} catch {
|
|
16713
16805
|
}
|
|
16714
16806
|
process.removeListener("exit", onExit);
|
|
@@ -16734,7 +16826,7 @@ function acquireCompoundLock(category, opts = {}) {
|
|
|
16734
16826
|
}
|
|
16735
16827
|
function readHolderPid(lockPath) {
|
|
16736
16828
|
try {
|
|
16737
|
-
const raw =
|
|
16829
|
+
const raw = fs46.readFileSync(lockPath, "utf-8").trim();
|
|
16738
16830
|
const n = Number.parseInt(raw, 10);
|
|
16739
16831
|
return Number.isFinite(n) ? n : -1;
|
|
16740
16832
|
} catch {
|
|
@@ -16784,13 +16876,13 @@ function assertSanitized(value) {
|
|
|
16784
16876
|
}
|
|
16785
16877
|
|
|
16786
16878
|
// src/pulse/config-writer.ts
|
|
16787
|
-
import * as
|
|
16879
|
+
import * as fs47 from "fs";
|
|
16788
16880
|
function writePulseConfig(config, opts) {
|
|
16789
16881
|
PulseConfigSchema.parse(config);
|
|
16790
|
-
if (!
|
|
16882
|
+
if (!fs47.existsSync(opts.configPath)) {
|
|
16791
16883
|
throw new Error(`harness.config.json not found at ${opts.configPath}`);
|
|
16792
16884
|
}
|
|
16793
|
-
const raw =
|
|
16885
|
+
const raw = fs47.readFileSync(opts.configPath, "utf-8");
|
|
16794
16886
|
let parsed;
|
|
16795
16887
|
try {
|
|
16796
16888
|
parsed = JSON.parse(raw);
|
|
@@ -16798,18 +16890,18 @@ function writePulseConfig(config, opts) {
|
|
|
16798
16890
|
throw new Error(`Invalid JSON in ${opts.configPath}: ${e.message}`, { cause: e });
|
|
16799
16891
|
}
|
|
16800
16892
|
const bakPath = `${opts.configPath}.bak`;
|
|
16801
|
-
if (!opts.skipBackup && !
|
|
16802
|
-
|
|
16893
|
+
if (!opts.skipBackup && !fs47.existsSync(bakPath)) {
|
|
16894
|
+
fs47.writeFileSync(bakPath, raw, "utf-8");
|
|
16803
16895
|
}
|
|
16804
16896
|
parsed.pulse = config;
|
|
16805
16897
|
const serialized = JSON.stringify(parsed, null, 2) + "\n";
|
|
16806
16898
|
const tmpPath = `${opts.configPath}.tmp-${process.pid}`;
|
|
16807
|
-
|
|
16899
|
+
fs47.writeFileSync(tmpPath, serialized, "utf-8");
|
|
16808
16900
|
try {
|
|
16809
|
-
|
|
16901
|
+
fs47.renameSync(tmpPath, opts.configPath);
|
|
16810
16902
|
} catch (e) {
|
|
16811
16903
|
try {
|
|
16812
|
-
|
|
16904
|
+
fs47.unlinkSync(tmpPath);
|
|
16813
16905
|
} catch {
|
|
16814
16906
|
}
|
|
16815
16907
|
throw e;
|
|
@@ -16817,16 +16909,16 @@ function writePulseConfig(config, opts) {
|
|
|
16817
16909
|
}
|
|
16818
16910
|
|
|
16819
16911
|
// src/pulse/strategy-seeder.ts
|
|
16820
|
-
import * as
|
|
16821
|
-
import * as
|
|
16912
|
+
import * as fs48 from "fs";
|
|
16913
|
+
import * as path47 from "path";
|
|
16822
16914
|
function seedFromStrategy(opts = {}) {
|
|
16823
16915
|
const cwd = opts.cwd ?? process.cwd();
|
|
16824
|
-
const strategyPath =
|
|
16916
|
+
const strategyPath = path47.join(cwd, "STRATEGY.md");
|
|
16825
16917
|
const warnings = [];
|
|
16826
|
-
if (!
|
|
16918
|
+
if (!fs48.existsSync(strategyPath)) {
|
|
16827
16919
|
return { name: null, keyMetrics: [], warnings: ["STRATEGY.md not found"] };
|
|
16828
16920
|
}
|
|
16829
|
-
const raw =
|
|
16921
|
+
const raw = fs48.readFileSync(strategyPath, "utf-8");
|
|
16830
16922
|
let name = null;
|
|
16831
16923
|
const fmMatch = /^---\s*\n([\s\S]*?)\n---\s*\n/.exec(raw);
|
|
16832
16924
|
const fm = fmMatch?.[1];
|
|
@@ -17021,7 +17113,7 @@ async function runPulse(config, window) {
|
|
|
17021
17113
|
// src/pulse/run/report.ts
|
|
17022
17114
|
import { readFileSync as readFileSync39 } from "fs";
|
|
17023
17115
|
import { fileURLToPath } from "url";
|
|
17024
|
-
import { dirname as
|
|
17116
|
+
import { dirname as dirname16, join as join61 } from "path";
|
|
17025
17117
|
var MAX_LINES = 40;
|
|
17026
17118
|
var INLINE_TEMPLATE = `# {{productName}} Pulse \u2014 {{windowLabel}}
|
|
17027
17119
|
|
|
@@ -17045,8 +17137,8 @@ function loadTemplate() {
|
|
|
17045
17137
|
try {
|
|
17046
17138
|
const url = import.meta?.url;
|
|
17047
17139
|
if (!url) return INLINE_TEMPLATE;
|
|
17048
|
-
const here =
|
|
17049
|
-
return readFileSync39(
|
|
17140
|
+
const here = dirname16(fileURLToPath(url));
|
|
17141
|
+
return readFileSync39(join61(here, "template.md"), "utf-8");
|
|
17050
17142
|
} catch {
|
|
17051
17143
|
return INLINE_TEMPLATE;
|
|
17052
17144
|
}
|
|
@@ -17250,16 +17342,16 @@ async function computeHotspots(opts) {
|
|
|
17250
17342
|
}
|
|
17251
17343
|
const counts = /* @__PURE__ */ new Map();
|
|
17252
17344
|
for (const line of stdout.split("\n")) {
|
|
17253
|
-
const
|
|
17254
|
-
if (
|
|
17255
|
-
counts.set(
|
|
17345
|
+
const path50 = line.trim();
|
|
17346
|
+
if (path50.length === 0) continue;
|
|
17347
|
+
counts.set(path50, (counts.get(path50) ?? 0) + 1);
|
|
17256
17348
|
}
|
|
17257
|
-
return [...counts.entries()].filter(([, c]) => c > opts.threshold).map(([
|
|
17349
|
+
return [...counts.entries()].filter(([, c]) => c > opts.threshold).map(([path50, churn]) => ({ path: path50, churn })).sort((a, b) => b.churn - a.churn);
|
|
17258
17350
|
}
|
|
17259
17351
|
|
|
17260
17352
|
// src/solutions/scan-candidates/cross-reference.ts
|
|
17261
|
-
import * as
|
|
17262
|
-
import * as
|
|
17353
|
+
import * as fs49 from "fs/promises";
|
|
17354
|
+
import * as path48 from "path";
|
|
17263
17355
|
var TRACK_CATEGORIES2 = [
|
|
17264
17356
|
["bug-track", BUG_TRACK_CATEGORIES],
|
|
17265
17357
|
["knowledge-track", KNOWLEDGE_TRACK_CATEGORIES]
|
|
@@ -17295,12 +17387,12 @@ function tokenize(text) {
|
|
|
17295
17387
|
async function* walk2(dir) {
|
|
17296
17388
|
let entries;
|
|
17297
17389
|
try {
|
|
17298
|
-
entries = await
|
|
17390
|
+
entries = await fs49.readdir(dir, { withFileTypes: true });
|
|
17299
17391
|
} catch {
|
|
17300
17392
|
return;
|
|
17301
17393
|
}
|
|
17302
17394
|
for (const e of entries) {
|
|
17303
|
-
const p =
|
|
17395
|
+
const p = path48.join(dir, e.name);
|
|
17304
17396
|
if (e.isDirectory()) yield* walk2(p);
|
|
17305
17397
|
else if (e.isFile() && e.name.endsWith(".md")) yield p;
|
|
17306
17398
|
}
|
|
@@ -17309,11 +17401,11 @@ async function readDocumentedTokens(solutionsDir) {
|
|
|
17309
17401
|
const docs = [];
|
|
17310
17402
|
for (const [track, categories] of TRACK_CATEGORIES2) {
|
|
17311
17403
|
for (const category of categories) {
|
|
17312
|
-
const dir =
|
|
17404
|
+
const dir = path48.join(solutionsDir, track, category);
|
|
17313
17405
|
for await (const file of walk2(dir)) {
|
|
17314
|
-
const raw = await
|
|
17406
|
+
const raw = await fs49.readFile(file, "utf-8");
|
|
17315
17407
|
const m = /^#\s+(.+)$/m.exec(raw);
|
|
17316
|
-
const title = m?.[1] ??
|
|
17408
|
+
const title = m?.[1] ?? path48.basename(file, ".md");
|
|
17317
17409
|
docs.push(tokenize(title));
|
|
17318
17410
|
}
|
|
17319
17411
|
}
|
|
@@ -17394,6 +17486,135 @@ function assembleCandidateReport(input) {
|
|
|
17394
17486
|
}
|
|
17395
17487
|
return lines.join("\n");
|
|
17396
17488
|
}
|
|
17489
|
+
|
|
17490
|
+
// src/proposals/store.ts
|
|
17491
|
+
import * as fs50 from "fs";
|
|
17492
|
+
import * as path49 from "path";
|
|
17493
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
17494
|
+
import {
|
|
17495
|
+
SkillProposalSchema,
|
|
17496
|
+
EmitSkillProposalInputSchema
|
|
17497
|
+
} from "@harness-engineering/types";
|
|
17498
|
+
function proposalsDir(projectPath) {
|
|
17499
|
+
return path49.join(projectPath, ".harness", "proposals");
|
|
17500
|
+
}
|
|
17501
|
+
function proposalPath(projectPath, id) {
|
|
17502
|
+
return path49.join(proposalsDir(projectPath), `${id}.json`);
|
|
17503
|
+
}
|
|
17504
|
+
function ensureDir(dir) {
|
|
17505
|
+
fs50.mkdirSync(dir, { recursive: true });
|
|
17506
|
+
}
|
|
17507
|
+
function writeAtomic(filePath, content) {
|
|
17508
|
+
const tmpPath = `${filePath}.tmp`;
|
|
17509
|
+
fs50.writeFileSync(tmpPath, content);
|
|
17510
|
+
fs50.renameSync(tmpPath, filePath);
|
|
17511
|
+
}
|
|
17512
|
+
var ProposalNotFoundError = class extends Error {
|
|
17513
|
+
constructor(id) {
|
|
17514
|
+
super(`proposal not found: ${id}`);
|
|
17515
|
+
this.name = "ProposalNotFoundError";
|
|
17516
|
+
}
|
|
17517
|
+
};
|
|
17518
|
+
var ProposalConflictError = class extends Error {
|
|
17519
|
+
constructor(message) {
|
|
17520
|
+
super(message);
|
|
17521
|
+
this.name = "ProposalConflictError";
|
|
17522
|
+
}
|
|
17523
|
+
};
|
|
17524
|
+
async function createProposal(projectPath, input) {
|
|
17525
|
+
const validated = EmitSkillProposalInputSchema.parse(input);
|
|
17526
|
+
const id = `proposal_${randomUUID2().replace(/-/g, "")}`;
|
|
17527
|
+
const proposal = SkillProposalSchema.parse({
|
|
17528
|
+
id,
|
|
17529
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
17530
|
+
kind: validated.kind,
|
|
17531
|
+
targetSkill: validated.targetSkill,
|
|
17532
|
+
proposedBy: validated.proposedBy,
|
|
17533
|
+
source: {
|
|
17534
|
+
sessionId: validated.sessionId,
|
|
17535
|
+
taskId: validated.taskId,
|
|
17536
|
+
justification: validated.justification
|
|
17537
|
+
},
|
|
17538
|
+
content: validated.content,
|
|
17539
|
+
status: "open"
|
|
17540
|
+
});
|
|
17541
|
+
if (proposal.kind === "refinement" && proposal.targetSkill) {
|
|
17542
|
+
const existing = await listProposals(projectPath, { status: "open" });
|
|
17543
|
+
const clash = existing.find(
|
|
17544
|
+
(p) => p.kind === "refinement" && p.targetSkill === proposal.targetSkill
|
|
17545
|
+
);
|
|
17546
|
+
if (clash) {
|
|
17547
|
+
throw new ProposalConflictError(
|
|
17548
|
+
`An open refinement proposal already exists for skill "${proposal.targetSkill}" (id: ${clash.id})`
|
|
17549
|
+
);
|
|
17550
|
+
}
|
|
17551
|
+
}
|
|
17552
|
+
const dir = proposalsDir(projectPath);
|
|
17553
|
+
ensureDir(dir);
|
|
17554
|
+
writeAtomic(proposalPath(projectPath, id), JSON.stringify(proposal, null, 2));
|
|
17555
|
+
return proposal;
|
|
17556
|
+
}
|
|
17557
|
+
async function getProposal(projectPath, id) {
|
|
17558
|
+
const file = proposalPath(projectPath, id);
|
|
17559
|
+
try {
|
|
17560
|
+
const raw = fs50.readFileSync(file, "utf-8");
|
|
17561
|
+
const parsed = SkillProposalSchema.safeParse(JSON.parse(raw));
|
|
17562
|
+
return parsed.success ? parsed.data : null;
|
|
17563
|
+
} catch {
|
|
17564
|
+
return null;
|
|
17565
|
+
}
|
|
17566
|
+
}
|
|
17567
|
+
async function listProposals(projectPath, opts = {}) {
|
|
17568
|
+
const dir = proposalsDir(projectPath);
|
|
17569
|
+
let files;
|
|
17570
|
+
try {
|
|
17571
|
+
files = fs50.readdirSync(dir);
|
|
17572
|
+
} catch {
|
|
17573
|
+
return [];
|
|
17574
|
+
}
|
|
17575
|
+
const out = [];
|
|
17576
|
+
for (const file of files) {
|
|
17577
|
+
if (!file.endsWith(".json")) continue;
|
|
17578
|
+
const id = file.slice(0, -".json".length);
|
|
17579
|
+
const proposal = await getProposal(projectPath, id);
|
|
17580
|
+
if (!proposal) continue;
|
|
17581
|
+
if (opts.status && opts.status !== "all" && proposal.status !== opts.status) continue;
|
|
17582
|
+
out.push(proposal);
|
|
17583
|
+
}
|
|
17584
|
+
out.sort((a, b) => a.createdAt > b.createdAt ? -1 : a.createdAt < b.createdAt ? 1 : 0);
|
|
17585
|
+
return out;
|
|
17586
|
+
}
|
|
17587
|
+
async function updateProposal(projectPath, id, patch) {
|
|
17588
|
+
const current = await getProposal(projectPath, id);
|
|
17589
|
+
if (!current) throw new ProposalNotFoundError(id);
|
|
17590
|
+
const next = SkillProposalSchema.parse({
|
|
17591
|
+
...current,
|
|
17592
|
+
...patch,
|
|
17593
|
+
id: current.id,
|
|
17594
|
+
createdAt: current.createdAt,
|
|
17595
|
+
kind: current.kind
|
|
17596
|
+
});
|
|
17597
|
+
writeAtomic(proposalPath(projectPath, id), JSON.stringify(next, null, 2));
|
|
17598
|
+
return next;
|
|
17599
|
+
}
|
|
17600
|
+
|
|
17601
|
+
// src/proposals/usage.ts
|
|
17602
|
+
function deriveSkillUsage(projectRoot, skillName, windowDays = 30) {
|
|
17603
|
+
const records = readAdoptionRecords(projectRoot);
|
|
17604
|
+
const cutoffMs = Date.now() - windowDays * 24 * 60 * 60 * 1e3;
|
|
17605
|
+
let count = 0;
|
|
17606
|
+
let lastUsed;
|
|
17607
|
+
for (const rec of records) {
|
|
17608
|
+
if (rec.skill !== skillName) continue;
|
|
17609
|
+
const startedMs = Date.parse(rec.startedAt);
|
|
17610
|
+
if (Number.isFinite(startedMs) && startedMs < cutoffMs) continue;
|
|
17611
|
+
count += 1;
|
|
17612
|
+
if (!lastUsed || rec.startedAt > lastUsed) lastUsed = rec.startedAt;
|
|
17613
|
+
}
|
|
17614
|
+
const stats = { count, windowDays };
|
|
17615
|
+
if (lastUsed) stats.lastUsed = lastUsed;
|
|
17616
|
+
return stats;
|
|
17617
|
+
}
|
|
17397
17618
|
export {
|
|
17398
17619
|
AGENT_DESCRIPTORS,
|
|
17399
17620
|
AGREEMENT_LINE_GAP,
|
|
@@ -17496,6 +17717,8 @@ export {
|
|
|
17496
17717
|
PredictionResultSchema,
|
|
17497
17718
|
PredictionWarningSchema,
|
|
17498
17719
|
ProjectScanner,
|
|
17720
|
+
ProposalConflictError,
|
|
17721
|
+
ProposalNotFoundError,
|
|
17499
17722
|
PulseAdapterAlreadyRegisteredError,
|
|
17500
17723
|
PulseConfigSchema,
|
|
17501
17724
|
PulseDbSourceSchema,
|
|
@@ -17619,7 +17842,9 @@ export {
|
|
|
17619
17842
|
createFixes,
|
|
17620
17843
|
createForbiddenImportFixes,
|
|
17621
17844
|
createOrphanedDepFixes,
|
|
17845
|
+
createOsvClient,
|
|
17622
17846
|
createParseError,
|
|
17847
|
+
createProposal,
|
|
17623
17848
|
createRegionMap,
|
|
17624
17849
|
createSelfReview,
|
|
17625
17850
|
createStream,
|
|
@@ -17631,6 +17856,7 @@ export {
|
|
|
17631
17856
|
deepMergeConstraints,
|
|
17632
17857
|
defaultCollectors,
|
|
17633
17858
|
defineLayer,
|
|
17859
|
+
deriveSkillUsage,
|
|
17634
17860
|
deserializationRules,
|
|
17635
17861
|
detectChangeType,
|
|
17636
17862
|
detectCircularDeps,
|
|
@@ -17683,6 +17909,7 @@ export {
|
|
|
17683
17909
|
getOutline,
|
|
17684
17910
|
getParser,
|
|
17685
17911
|
getPhaseCategories,
|
|
17912
|
+
getProposal,
|
|
17686
17913
|
getPulseAdapter,
|
|
17687
17914
|
getRoadmapMode,
|
|
17688
17915
|
getStreamForBranch,
|
|
@@ -17702,6 +17929,7 @@ export {
|
|
|
17702
17929
|
isUpdateCheckEnabled,
|
|
17703
17930
|
isoWeek,
|
|
17704
17931
|
listActiveSessions,
|
|
17932
|
+
listProposals,
|
|
17705
17933
|
listPulseAdapters,
|
|
17706
17934
|
listStreams,
|
|
17707
17935
|
listTaintedSessions,
|
|
@@ -17752,6 +17980,7 @@ export {
|
|
|
17752
17980
|
previewFix,
|
|
17753
17981
|
projectValue,
|
|
17754
17982
|
promoteSessionLearnings,
|
|
17983
|
+
proposalsDir,
|
|
17755
17984
|
pruneLearnings,
|
|
17756
17985
|
reactRules,
|
|
17757
17986
|
readAdoptionRecords,
|
|
@@ -17824,6 +18053,7 @@ export {
|
|
|
17824
18053
|
trackAction,
|
|
17825
18054
|
unfoldRange,
|
|
17826
18055
|
unfoldSymbol,
|
|
18056
|
+
updateProposal,
|
|
17827
18057
|
updateSessionEntryStatus,
|
|
17828
18058
|
updateSessionIndex,
|
|
17829
18059
|
validateAgentConfigs,
|