@harness-engineering/core 0.14.0 → 0.16.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/README.md +24 -0
- package/dist/index.d.mts +521 -4
- package/dist/index.d.ts +521 -4
- package/dist/index.js +2449 -151
- package/dist/index.mjs +2392 -147
- package/package.json +7 -4
package/dist/index.mjs
CHANGED
|
@@ -84,15 +84,15 @@ function validateConfig(data, schema) {
|
|
|
84
84
|
let message = "Configuration validation failed";
|
|
85
85
|
const suggestions = [];
|
|
86
86
|
if (firstError) {
|
|
87
|
-
const
|
|
88
|
-
const pathDisplay =
|
|
87
|
+
const path26 = firstError.path.join(".");
|
|
88
|
+
const pathDisplay = path26 ? ` at "${path26}"` : "";
|
|
89
89
|
if (firstError.code === "invalid_type") {
|
|
90
90
|
const received = firstError.received;
|
|
91
91
|
const expected = firstError.expected;
|
|
92
92
|
if (received === "undefined") {
|
|
93
93
|
code = "MISSING_FIELD";
|
|
94
94
|
message = `Missing required field${pathDisplay}: ${firstError.message}`;
|
|
95
|
-
suggestions.push(`Field "${
|
|
95
|
+
suggestions.push(`Field "${path26}" is required and must be of type "${expected}"`);
|
|
96
96
|
} else {
|
|
97
97
|
code = "INVALID_TYPE";
|
|
98
98
|
message = `Invalid type${pathDisplay}: ${firstError.message}`;
|
|
@@ -308,27 +308,27 @@ function extractSections(content) {
|
|
|
308
308
|
}
|
|
309
309
|
return sections.map((section) => buildAgentMapSection(section, lines));
|
|
310
310
|
}
|
|
311
|
-
function isExternalLink(
|
|
312
|
-
return
|
|
311
|
+
function isExternalLink(path26) {
|
|
312
|
+
return path26.startsWith("http://") || path26.startsWith("https://") || path26.startsWith("#") || path26.startsWith("mailto:");
|
|
313
313
|
}
|
|
314
314
|
function resolveLinkPath(linkPath, baseDir) {
|
|
315
315
|
return linkPath.startsWith(".") ? join(baseDir, linkPath) : linkPath;
|
|
316
316
|
}
|
|
317
|
-
async function validateAgentsMap(
|
|
318
|
-
const contentResult = await readFileContent(
|
|
317
|
+
async function validateAgentsMap(path26 = "./AGENTS.md") {
|
|
318
|
+
const contentResult = await readFileContent(path26);
|
|
319
319
|
if (!contentResult.ok) {
|
|
320
320
|
return Err(
|
|
321
321
|
createError(
|
|
322
322
|
"PARSE_ERROR",
|
|
323
323
|
`Failed to read AGENTS.md: ${contentResult.error.message}`,
|
|
324
|
-
{ path:
|
|
324
|
+
{ path: path26 },
|
|
325
325
|
["Ensure the file exists", "Check file permissions"]
|
|
326
326
|
)
|
|
327
327
|
);
|
|
328
328
|
}
|
|
329
329
|
const content = contentResult.value;
|
|
330
330
|
const sections = extractSections(content);
|
|
331
|
-
const baseDir = dirname(
|
|
331
|
+
const baseDir = dirname(path26);
|
|
332
332
|
const sectionTitles = sections.map((s) => s.title);
|
|
333
333
|
const missingSections = REQUIRED_SECTIONS.filter(
|
|
334
334
|
(required) => !sectionTitles.some((title) => title.toLowerCase().includes(required.toLowerCase()))
|
|
@@ -469,8 +469,8 @@ async function checkDocCoverage(domain, options = {}) {
|
|
|
469
469
|
|
|
470
470
|
// src/context/knowledge-map.ts
|
|
471
471
|
import { join as join2, basename as basename2 } from "path";
|
|
472
|
-
function suggestFix(
|
|
473
|
-
const targetName = basename2(
|
|
472
|
+
function suggestFix(path26, existingFiles) {
|
|
473
|
+
const targetName = basename2(path26).toLowerCase();
|
|
474
474
|
const similar = existingFiles.find((file) => {
|
|
475
475
|
const fileName = basename2(file).toLowerCase();
|
|
476
476
|
return fileName.includes(targetName) || targetName.includes(fileName);
|
|
@@ -478,7 +478,7 @@ function suggestFix(path22, existingFiles) {
|
|
|
478
478
|
if (similar) {
|
|
479
479
|
return `Did you mean "${similar}"?`;
|
|
480
480
|
}
|
|
481
|
-
return `Create the file "${
|
|
481
|
+
return `Create the file "${path26}" or remove the link`;
|
|
482
482
|
}
|
|
483
483
|
async function validateKnowledgeMap(rootDir = process.cwd()) {
|
|
484
484
|
const agentsPath = join2(rootDir, "AGENTS.md");
|
|
@@ -830,8 +830,8 @@ function createBoundaryValidator(schema, name) {
|
|
|
830
830
|
return Ok(result.data);
|
|
831
831
|
}
|
|
832
832
|
const suggestions = result.error.issues.map((issue) => {
|
|
833
|
-
const
|
|
834
|
-
return
|
|
833
|
+
const path26 = issue.path.join(".");
|
|
834
|
+
return path26 ? `${path26}: ${issue.message}` : issue.message;
|
|
835
835
|
});
|
|
836
836
|
return Err(
|
|
837
837
|
createError(
|
|
@@ -1463,11 +1463,11 @@ function processExportListSpecifiers(exportDecl, exports) {
|
|
|
1463
1463
|
var TypeScriptParser = class {
|
|
1464
1464
|
name = "typescript";
|
|
1465
1465
|
extensions = [".ts", ".tsx", ".mts", ".cts"];
|
|
1466
|
-
async parseFile(
|
|
1467
|
-
const contentResult = await readFileContent(
|
|
1466
|
+
async parseFile(path26) {
|
|
1467
|
+
const contentResult = await readFileContent(path26);
|
|
1468
1468
|
if (!contentResult.ok) {
|
|
1469
1469
|
return Err(
|
|
1470
|
-
createParseError("NOT_FOUND", `File not found: ${
|
|
1470
|
+
createParseError("NOT_FOUND", `File not found: ${path26}`, { path: path26 }, [
|
|
1471
1471
|
"Check that the file exists",
|
|
1472
1472
|
"Verify the path is correct"
|
|
1473
1473
|
])
|
|
@@ -1477,7 +1477,7 @@ var TypeScriptParser = class {
|
|
|
1477
1477
|
const ast = parse(contentResult.value, {
|
|
1478
1478
|
loc: true,
|
|
1479
1479
|
range: true,
|
|
1480
|
-
jsx:
|
|
1480
|
+
jsx: path26.endsWith(".tsx"),
|
|
1481
1481
|
errorOnUnknownASTType: false
|
|
1482
1482
|
});
|
|
1483
1483
|
return Ok({
|
|
@@ -1488,7 +1488,7 @@ var TypeScriptParser = class {
|
|
|
1488
1488
|
} catch (e) {
|
|
1489
1489
|
const error = e;
|
|
1490
1490
|
return Err(
|
|
1491
|
-
createParseError("SYNTAX_ERROR", `Failed to parse ${
|
|
1491
|
+
createParseError("SYNTAX_ERROR", `Failed to parse ${path26}: ${error.message}`, { path: path26 }, [
|
|
1492
1492
|
"Check for syntax errors in the file",
|
|
1493
1493
|
"Ensure valid TypeScript syntax"
|
|
1494
1494
|
])
|
|
@@ -1673,22 +1673,22 @@ function extractInlineRefs(content) {
|
|
|
1673
1673
|
}
|
|
1674
1674
|
return refs;
|
|
1675
1675
|
}
|
|
1676
|
-
async function parseDocumentationFile(
|
|
1677
|
-
const contentResult = await readFileContent(
|
|
1676
|
+
async function parseDocumentationFile(path26) {
|
|
1677
|
+
const contentResult = await readFileContent(path26);
|
|
1678
1678
|
if (!contentResult.ok) {
|
|
1679
1679
|
return Err(
|
|
1680
1680
|
createEntropyError(
|
|
1681
1681
|
"PARSE_ERROR",
|
|
1682
|
-
`Failed to read documentation file: ${
|
|
1683
|
-
{ file:
|
|
1682
|
+
`Failed to read documentation file: ${path26}`,
|
|
1683
|
+
{ file: path26 },
|
|
1684
1684
|
["Check that the file exists"]
|
|
1685
1685
|
)
|
|
1686
1686
|
);
|
|
1687
1687
|
}
|
|
1688
1688
|
const content = contentResult.value;
|
|
1689
|
-
const type =
|
|
1689
|
+
const type = path26.endsWith(".md") ? "markdown" : "text";
|
|
1690
1690
|
return Ok({
|
|
1691
|
-
path:
|
|
1691
|
+
path: path26,
|
|
1692
1692
|
type,
|
|
1693
1693
|
content,
|
|
1694
1694
|
codeBlocks: extractCodeBlocks(content),
|
|
@@ -4820,6 +4820,8 @@ var SESSION_INDEX_FILE = "index.md";
|
|
|
4820
4820
|
var SUMMARY_FILE = "summary.md";
|
|
4821
4821
|
var SESSION_STATE_FILE = "session-state.json";
|
|
4822
4822
|
var ARCHIVE_DIR = "archive";
|
|
4823
|
+
var CONTENT_HASHES_FILE = "content-hashes.json";
|
|
4824
|
+
var EVENTS_FILE = "events.jsonl";
|
|
4823
4825
|
|
|
4824
4826
|
// src/state/stream-resolver.ts
|
|
4825
4827
|
var STREAMS_DIR = "streams";
|
|
@@ -5162,6 +5164,85 @@ async function saveState(projectPath, state, stream, session) {
|
|
|
5162
5164
|
// src/state/learnings.ts
|
|
5163
5165
|
import * as fs9 from "fs";
|
|
5164
5166
|
import * as path6 from "path";
|
|
5167
|
+
import * as crypto from "crypto";
|
|
5168
|
+
function parseFrontmatter(line) {
|
|
5169
|
+
const match = line.match(/^<!--\s+hash:([a-f0-9]+)(?:\s+tags:([^\s]+))?\s+-->/);
|
|
5170
|
+
if (!match) return null;
|
|
5171
|
+
const hash = match[1];
|
|
5172
|
+
const tags = match[2] ? match[2].split(",").filter(Boolean) : [];
|
|
5173
|
+
return { hash, tags };
|
|
5174
|
+
}
|
|
5175
|
+
function computeEntryHash(text) {
|
|
5176
|
+
return crypto.createHash("sha256").update(text).digest("hex").slice(0, 8);
|
|
5177
|
+
}
|
|
5178
|
+
function normalizeLearningContent(text) {
|
|
5179
|
+
let normalized = text;
|
|
5180
|
+
normalized = normalized.replace(/\d{4}-\d{2}-\d{2}/g, "");
|
|
5181
|
+
normalized = normalized.replace(/\[skill:[^\]]*\]/g, "");
|
|
5182
|
+
normalized = normalized.replace(/\[outcome:[^\]]*\]/g, "");
|
|
5183
|
+
normalized = normalized.replace(/^[\s]*[-*]\s+/gm, "");
|
|
5184
|
+
normalized = normalized.replace(/\*\*/g, "");
|
|
5185
|
+
normalized = normalized.replace(/:\s*/g, " ");
|
|
5186
|
+
normalized = normalized.toLowerCase();
|
|
5187
|
+
normalized = normalized.replace(/\s+/g, " ").trim();
|
|
5188
|
+
return normalized;
|
|
5189
|
+
}
|
|
5190
|
+
function computeContentHash(text) {
|
|
5191
|
+
return crypto.createHash("sha256").update(text).digest("hex").slice(0, 16);
|
|
5192
|
+
}
|
|
5193
|
+
function loadContentHashes(stateDir) {
|
|
5194
|
+
const hashesPath = path6.join(stateDir, CONTENT_HASHES_FILE);
|
|
5195
|
+
if (!fs9.existsSync(hashesPath)) return {};
|
|
5196
|
+
try {
|
|
5197
|
+
const raw = fs9.readFileSync(hashesPath, "utf-8");
|
|
5198
|
+
const parsed = JSON.parse(raw);
|
|
5199
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
|
|
5200
|
+
return parsed;
|
|
5201
|
+
} catch {
|
|
5202
|
+
return {};
|
|
5203
|
+
}
|
|
5204
|
+
}
|
|
5205
|
+
function saveContentHashes(stateDir, index) {
|
|
5206
|
+
const hashesPath = path6.join(stateDir, CONTENT_HASHES_FILE);
|
|
5207
|
+
fs9.writeFileSync(hashesPath, JSON.stringify(index, null, 2) + "\n");
|
|
5208
|
+
}
|
|
5209
|
+
function rebuildContentHashes(stateDir) {
|
|
5210
|
+
const learningsPath = path6.join(stateDir, LEARNINGS_FILE);
|
|
5211
|
+
if (!fs9.existsSync(learningsPath)) return {};
|
|
5212
|
+
const content = fs9.readFileSync(learningsPath, "utf-8");
|
|
5213
|
+
const lines = content.split("\n");
|
|
5214
|
+
const index = {};
|
|
5215
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5216
|
+
const line = lines[i];
|
|
5217
|
+
const isDatedBullet = /^- \*\*\d{4}-\d{2}-\d{2}/.test(line);
|
|
5218
|
+
if (isDatedBullet) {
|
|
5219
|
+
const learningMatch = line.match(/:\*\*\s*(.+)$/);
|
|
5220
|
+
if (learningMatch?.[1]) {
|
|
5221
|
+
const normalized = normalizeLearningContent(learningMatch[1]);
|
|
5222
|
+
const hash = computeContentHash(normalized);
|
|
5223
|
+
const dateMatch = line.match(/(\d{4}-\d{2}-\d{2})/);
|
|
5224
|
+
index[hash] = { date: dateMatch?.[1] ?? "", line: i + 1 };
|
|
5225
|
+
}
|
|
5226
|
+
}
|
|
5227
|
+
}
|
|
5228
|
+
saveContentHashes(stateDir, index);
|
|
5229
|
+
return index;
|
|
5230
|
+
}
|
|
5231
|
+
function extractIndexEntry(entry) {
|
|
5232
|
+
const lines = entry.split("\n");
|
|
5233
|
+
const summary = lines[0] ?? entry;
|
|
5234
|
+
const tags = [];
|
|
5235
|
+
const skillMatch = entry.match(/\[skill:([^\]]+)\]/);
|
|
5236
|
+
if (skillMatch?.[1]) tags.push(skillMatch[1]);
|
|
5237
|
+
const outcomeMatch = entry.match(/\[outcome:([^\]]+)\]/);
|
|
5238
|
+
if (outcomeMatch?.[1]) tags.push(outcomeMatch[1]);
|
|
5239
|
+
return {
|
|
5240
|
+
hash: computeEntryHash(entry),
|
|
5241
|
+
tags,
|
|
5242
|
+
summary,
|
|
5243
|
+
fullText: entry
|
|
5244
|
+
};
|
|
5245
|
+
}
|
|
5165
5246
|
var learningsCacheMap = /* @__PURE__ */ new Map();
|
|
5166
5247
|
function clearLearningsCache() {
|
|
5167
5248
|
learningsCacheMap.clear();
|
|
@@ -5173,27 +5254,55 @@ async function appendLearning(projectPath, learning, skillName, outcome, stream,
|
|
|
5173
5254
|
const stateDir = dirResult.value;
|
|
5174
5255
|
const learningsPath = path6.join(stateDir, LEARNINGS_FILE);
|
|
5175
5256
|
fs9.mkdirSync(stateDir, { recursive: true });
|
|
5257
|
+
const normalizedContent = normalizeLearningContent(learning);
|
|
5258
|
+
const contentHash = computeContentHash(normalizedContent);
|
|
5259
|
+
const hashesPath = path6.join(stateDir, CONTENT_HASHES_FILE);
|
|
5260
|
+
let contentHashes;
|
|
5261
|
+
if (fs9.existsSync(hashesPath)) {
|
|
5262
|
+
contentHashes = loadContentHashes(stateDir);
|
|
5263
|
+
if (Object.keys(contentHashes).length === 0 && fs9.existsSync(learningsPath)) {
|
|
5264
|
+
contentHashes = rebuildContentHashes(stateDir);
|
|
5265
|
+
}
|
|
5266
|
+
} else if (fs9.existsSync(learningsPath)) {
|
|
5267
|
+
contentHashes = rebuildContentHashes(stateDir);
|
|
5268
|
+
} else {
|
|
5269
|
+
contentHashes = {};
|
|
5270
|
+
}
|
|
5271
|
+
if (contentHashes[contentHash]) {
|
|
5272
|
+
return Ok(void 0);
|
|
5273
|
+
}
|
|
5176
5274
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
5177
|
-
|
|
5275
|
+
const fmTags = [];
|
|
5276
|
+
if (skillName) fmTags.push(skillName);
|
|
5277
|
+
if (outcome) fmTags.push(outcome);
|
|
5278
|
+
let bulletLine;
|
|
5178
5279
|
if (skillName && outcome) {
|
|
5179
|
-
|
|
5180
|
-
- **${timestamp} [skill:${skillName}] [outcome:${outcome}]:** ${learning}
|
|
5181
|
-
`;
|
|
5280
|
+
bulletLine = `- **${timestamp} [skill:${skillName}] [outcome:${outcome}]:** ${learning}`;
|
|
5182
5281
|
} else if (skillName) {
|
|
5183
|
-
|
|
5184
|
-
- **${timestamp} [skill:${skillName}]:** ${learning}
|
|
5185
|
-
`;
|
|
5282
|
+
bulletLine = `- **${timestamp} [skill:${skillName}]:** ${learning}`;
|
|
5186
5283
|
} else {
|
|
5187
|
-
|
|
5188
|
-
- **${timestamp}:** ${learning}
|
|
5189
|
-
`;
|
|
5284
|
+
bulletLine = `- **${timestamp}:** ${learning}`;
|
|
5190
5285
|
}
|
|
5286
|
+
const hash = crypto.createHash("sha256").update(bulletLine).digest("hex").slice(0, 8);
|
|
5287
|
+
const tagsStr = fmTags.length > 0 ? ` tags:${fmTags.join(",")}` : "";
|
|
5288
|
+
const frontmatter = `<!-- hash:${hash}${tagsStr} -->`;
|
|
5289
|
+
const entry = `
|
|
5290
|
+
${frontmatter}
|
|
5291
|
+
${bulletLine}
|
|
5292
|
+
`;
|
|
5293
|
+
let existingLineCount;
|
|
5191
5294
|
if (!fs9.existsSync(learningsPath)) {
|
|
5192
5295
|
fs9.writeFileSync(learningsPath, `# Learnings
|
|
5193
5296
|
${entry}`);
|
|
5297
|
+
existingLineCount = 1;
|
|
5194
5298
|
} else {
|
|
5299
|
+
const existingContent = fs9.readFileSync(learningsPath, "utf-8");
|
|
5300
|
+
existingLineCount = existingContent.split("\n").length;
|
|
5195
5301
|
fs9.appendFileSync(learningsPath, entry);
|
|
5196
5302
|
}
|
|
5303
|
+
const bulletLine_lineNum = existingLineCount + 2;
|
|
5304
|
+
contentHashes[contentHash] = { date: timestamp ?? "", line: bulletLine_lineNum };
|
|
5305
|
+
saveContentHashes(stateDir, contentHashes);
|
|
5197
5306
|
learningsCacheMap.delete(learningsPath);
|
|
5198
5307
|
return Ok(void 0);
|
|
5199
5308
|
} catch (error) {
|
|
@@ -5241,7 +5350,30 @@ function analyzeLearningPatterns(entries) {
|
|
|
5241
5350
|
return patterns.sort((a, b) => b.count - a.count);
|
|
5242
5351
|
}
|
|
5243
5352
|
async function loadBudgetedLearnings(projectPath, options) {
|
|
5244
|
-
const { intent, tokenBudget = 1e3, skill, session, stream } = options;
|
|
5353
|
+
const { intent, tokenBudget = 1e3, skill, session, stream, depth = "summary" } = options;
|
|
5354
|
+
if (depth === "index") {
|
|
5355
|
+
const indexEntries = [];
|
|
5356
|
+
if (session) {
|
|
5357
|
+
const sessionResult = await loadIndexEntries(projectPath, skill, stream, session);
|
|
5358
|
+
if (sessionResult.ok) indexEntries.push(...sessionResult.value);
|
|
5359
|
+
}
|
|
5360
|
+
const globalResult2 = await loadIndexEntries(projectPath, skill, stream);
|
|
5361
|
+
if (globalResult2.ok) {
|
|
5362
|
+
const sessionHashes = new Set(indexEntries.map((e) => e.hash));
|
|
5363
|
+
const uniqueGlobal = globalResult2.value.filter((e) => !sessionHashes.has(e.hash));
|
|
5364
|
+
indexEntries.push(...uniqueGlobal);
|
|
5365
|
+
}
|
|
5366
|
+
const budgeted2 = [];
|
|
5367
|
+
let totalTokens2 = 0;
|
|
5368
|
+
for (const entry of indexEntries) {
|
|
5369
|
+
const separator = budgeted2.length > 0 ? "\n" : "";
|
|
5370
|
+
const entryCost = estimateTokens(entry.summary + separator);
|
|
5371
|
+
if (totalTokens2 + entryCost > tokenBudget) break;
|
|
5372
|
+
budgeted2.push(entry.summary);
|
|
5373
|
+
totalTokens2 += entryCost;
|
|
5374
|
+
}
|
|
5375
|
+
return Ok(budgeted2);
|
|
5376
|
+
}
|
|
5245
5377
|
const sortByRecencyAndRelevance = (entries) => {
|
|
5246
5378
|
return [...entries].sort((a, b) => {
|
|
5247
5379
|
const dateA = parseDateFromEntry(a) ?? "0000-00-00";
|
|
@@ -5260,7 +5392,9 @@ async function loadBudgetedLearnings(projectPath, options) {
|
|
|
5260
5392
|
}
|
|
5261
5393
|
const globalResult = await loadRelevantLearnings(projectPath, skill, stream);
|
|
5262
5394
|
if (globalResult.ok) {
|
|
5263
|
-
allEntries.
|
|
5395
|
+
const sessionSet = new Set(allEntries.map((e) => e.trim()));
|
|
5396
|
+
const uniqueGlobal = globalResult.value.filter((e) => !sessionSet.has(e.trim()));
|
|
5397
|
+
allEntries.push(...sortByRecencyAndRelevance(uniqueGlobal));
|
|
5264
5398
|
}
|
|
5265
5399
|
const budgeted = [];
|
|
5266
5400
|
let totalTokens = 0;
|
|
@@ -5273,6 +5407,68 @@ async function loadBudgetedLearnings(projectPath, options) {
|
|
|
5273
5407
|
}
|
|
5274
5408
|
return Ok(budgeted);
|
|
5275
5409
|
}
|
|
5410
|
+
async function loadIndexEntries(projectPath, skillName, stream, session) {
|
|
5411
|
+
try {
|
|
5412
|
+
const dirResult = await getStateDir(projectPath, stream, session);
|
|
5413
|
+
if (!dirResult.ok) return dirResult;
|
|
5414
|
+
const stateDir = dirResult.value;
|
|
5415
|
+
const learningsPath = path6.join(stateDir, LEARNINGS_FILE);
|
|
5416
|
+
if (!fs9.existsSync(learningsPath)) {
|
|
5417
|
+
return Ok([]);
|
|
5418
|
+
}
|
|
5419
|
+
const content = fs9.readFileSync(learningsPath, "utf-8");
|
|
5420
|
+
const lines = content.split("\n");
|
|
5421
|
+
const indexEntries = [];
|
|
5422
|
+
let pendingFrontmatter = null;
|
|
5423
|
+
let currentBlock = [];
|
|
5424
|
+
for (const line of lines) {
|
|
5425
|
+
if (line.startsWith("# ")) continue;
|
|
5426
|
+
const fm = parseFrontmatter(line);
|
|
5427
|
+
if (fm) {
|
|
5428
|
+
pendingFrontmatter = fm;
|
|
5429
|
+
continue;
|
|
5430
|
+
}
|
|
5431
|
+
const isDatedBullet = /^- \*\*\d{4}-\d{2}-\d{2}/.test(line);
|
|
5432
|
+
const isHeading = /^## \d{4}-\d{2}-\d{2}/.test(line);
|
|
5433
|
+
if (isDatedBullet || isHeading) {
|
|
5434
|
+
if (pendingFrontmatter) {
|
|
5435
|
+
indexEntries.push({
|
|
5436
|
+
hash: pendingFrontmatter.hash,
|
|
5437
|
+
tags: pendingFrontmatter.tags,
|
|
5438
|
+
summary: line,
|
|
5439
|
+
fullText: ""
|
|
5440
|
+
// Placeholder — full text not loaded in index mode
|
|
5441
|
+
});
|
|
5442
|
+
pendingFrontmatter = null;
|
|
5443
|
+
} else {
|
|
5444
|
+
const idx = extractIndexEntry(line);
|
|
5445
|
+
indexEntries.push({
|
|
5446
|
+
hash: idx.hash,
|
|
5447
|
+
tags: idx.tags,
|
|
5448
|
+
summary: line,
|
|
5449
|
+
fullText: ""
|
|
5450
|
+
});
|
|
5451
|
+
}
|
|
5452
|
+
currentBlock = [line];
|
|
5453
|
+
} else if (line.trim() !== "" && currentBlock.length > 0) {
|
|
5454
|
+
currentBlock.push(line);
|
|
5455
|
+
}
|
|
5456
|
+
}
|
|
5457
|
+
if (skillName) {
|
|
5458
|
+
const filtered = indexEntries.filter(
|
|
5459
|
+
(e) => e.tags.includes(skillName) || e.summary.includes(`[skill:${skillName}]`)
|
|
5460
|
+
);
|
|
5461
|
+
return Ok(filtered);
|
|
5462
|
+
}
|
|
5463
|
+
return Ok(indexEntries);
|
|
5464
|
+
} catch (error) {
|
|
5465
|
+
return Err(
|
|
5466
|
+
new Error(
|
|
5467
|
+
`Failed to load index entries: ${error instanceof Error ? error.message : String(error)}`
|
|
5468
|
+
)
|
|
5469
|
+
);
|
|
5470
|
+
}
|
|
5471
|
+
}
|
|
5276
5472
|
async function loadRelevantLearnings(projectPath, skillName, stream, session) {
|
|
5277
5473
|
try {
|
|
5278
5474
|
const dirResult = await getStateDir(projectPath, stream, session);
|
|
@@ -5295,6 +5491,7 @@ async function loadRelevantLearnings(projectPath, skillName, stream, session) {
|
|
|
5295
5491
|
let currentBlock = [];
|
|
5296
5492
|
for (const line of lines) {
|
|
5297
5493
|
if (line.startsWith("# ")) continue;
|
|
5494
|
+
if (/^<!--\s+hash:[a-f0-9]+/.test(line)) continue;
|
|
5298
5495
|
const isDatedBullet = /^- \*\*\d{4}-\d{2}-\d{2}/.test(line);
|
|
5299
5496
|
const isHeading = /^## \d{4}-\d{2}-\d{2}/.test(line);
|
|
5300
5497
|
if (isDatedBullet || isHeading) {
|
|
@@ -5404,6 +5601,68 @@ async function pruneLearnings(projectPath, stream) {
|
|
|
5404
5601
|
);
|
|
5405
5602
|
}
|
|
5406
5603
|
}
|
|
5604
|
+
var PROMOTABLE_OUTCOMES = ["gotcha", "decision", "observation"];
|
|
5605
|
+
function isGeneralizable(entry) {
|
|
5606
|
+
for (const outcome of PROMOTABLE_OUTCOMES) {
|
|
5607
|
+
if (entry.includes(`[outcome:${outcome}]`)) return true;
|
|
5608
|
+
}
|
|
5609
|
+
return false;
|
|
5610
|
+
}
|
|
5611
|
+
async function promoteSessionLearnings(projectPath, sessionSlug, stream) {
|
|
5612
|
+
try {
|
|
5613
|
+
const sessionResult = await loadRelevantLearnings(projectPath, void 0, stream, sessionSlug);
|
|
5614
|
+
if (!sessionResult.ok) return sessionResult;
|
|
5615
|
+
const sessionEntries = sessionResult.value;
|
|
5616
|
+
if (sessionEntries.length === 0) {
|
|
5617
|
+
return Ok({ promoted: 0, skipped: 0 });
|
|
5618
|
+
}
|
|
5619
|
+
const toPromote = [];
|
|
5620
|
+
let skipped = 0;
|
|
5621
|
+
for (const entry of sessionEntries) {
|
|
5622
|
+
if (isGeneralizable(entry)) {
|
|
5623
|
+
toPromote.push(entry);
|
|
5624
|
+
} else {
|
|
5625
|
+
skipped++;
|
|
5626
|
+
}
|
|
5627
|
+
}
|
|
5628
|
+
if (toPromote.length === 0) {
|
|
5629
|
+
return Ok({ promoted: 0, skipped });
|
|
5630
|
+
}
|
|
5631
|
+
const dirResult = await getStateDir(projectPath, stream);
|
|
5632
|
+
if (!dirResult.ok) return dirResult;
|
|
5633
|
+
const stateDir = dirResult.value;
|
|
5634
|
+
const globalPath = path6.join(stateDir, LEARNINGS_FILE);
|
|
5635
|
+
const existingGlobal = fs9.existsSync(globalPath) ? fs9.readFileSync(globalPath, "utf-8") : "";
|
|
5636
|
+
const newEntries = toPromote.filter((entry) => !existingGlobal.includes(entry.trim()));
|
|
5637
|
+
if (newEntries.length === 0) {
|
|
5638
|
+
return Ok({ promoted: 0, skipped: skipped + toPromote.length });
|
|
5639
|
+
}
|
|
5640
|
+
const promotedContent = newEntries.join("\n\n") + "\n";
|
|
5641
|
+
if (!existingGlobal) {
|
|
5642
|
+
fs9.writeFileSync(globalPath, `# Learnings
|
|
5643
|
+
|
|
5644
|
+
${promotedContent}`);
|
|
5645
|
+
} else {
|
|
5646
|
+
fs9.appendFileSync(globalPath, "\n\n" + promotedContent);
|
|
5647
|
+
}
|
|
5648
|
+
learningsCacheMap.delete(globalPath);
|
|
5649
|
+
return Ok({
|
|
5650
|
+
promoted: newEntries.length,
|
|
5651
|
+
skipped: skipped + (toPromote.length - newEntries.length)
|
|
5652
|
+
});
|
|
5653
|
+
} catch (error) {
|
|
5654
|
+
return Err(
|
|
5655
|
+
new Error(
|
|
5656
|
+
`Failed to promote session learnings: ${error instanceof Error ? error.message : String(error)}`
|
|
5657
|
+
)
|
|
5658
|
+
);
|
|
5659
|
+
}
|
|
5660
|
+
}
|
|
5661
|
+
async function countLearningEntries(projectPath, stream) {
|
|
5662
|
+
const loadResult = await loadRelevantLearnings(projectPath, void 0, stream);
|
|
5663
|
+
if (!loadResult.ok) return 0;
|
|
5664
|
+
return loadResult.value.length;
|
|
5665
|
+
}
|
|
5407
5666
|
|
|
5408
5667
|
// src/state/failures.ts
|
|
5409
5668
|
import * as fs10 from "fs";
|
|
@@ -5865,6 +6124,151 @@ async function archiveSession(projectPath, sessionSlug) {
|
|
|
5865
6124
|
}
|
|
5866
6125
|
}
|
|
5867
6126
|
|
|
6127
|
+
// src/state/events.ts
|
|
6128
|
+
import * as fs16 from "fs";
|
|
6129
|
+
import * as path13 from "path";
|
|
6130
|
+
import { z as z5 } from "zod";
|
|
6131
|
+
var SkillEventSchema = z5.object({
|
|
6132
|
+
timestamp: z5.string(),
|
|
6133
|
+
skill: z5.string(),
|
|
6134
|
+
session: z5.string().optional(),
|
|
6135
|
+
type: z5.enum(["phase_transition", "decision", "gate_result", "handoff", "error", "checkpoint"]),
|
|
6136
|
+
summary: z5.string(),
|
|
6137
|
+
data: z5.record(z5.unknown()).optional(),
|
|
6138
|
+
refs: z5.array(z5.string()).optional(),
|
|
6139
|
+
contentHash: z5.string().optional()
|
|
6140
|
+
});
|
|
6141
|
+
function computeEventHash(event, session) {
|
|
6142
|
+
const identity = `${event.skill}|${event.type}|${event.summary}|${session ?? ""}`;
|
|
6143
|
+
return computeContentHash(identity);
|
|
6144
|
+
}
|
|
6145
|
+
var knownHashesCache = /* @__PURE__ */ new Map();
|
|
6146
|
+
function loadKnownHashes(eventsPath) {
|
|
6147
|
+
const cached = knownHashesCache.get(eventsPath);
|
|
6148
|
+
if (cached) return cached;
|
|
6149
|
+
const hashes = /* @__PURE__ */ new Set();
|
|
6150
|
+
if (fs16.existsSync(eventsPath)) {
|
|
6151
|
+
const content = fs16.readFileSync(eventsPath, "utf-8");
|
|
6152
|
+
const lines = content.split("\n").filter((line) => line.trim() !== "");
|
|
6153
|
+
for (const line of lines) {
|
|
6154
|
+
try {
|
|
6155
|
+
const existing = JSON.parse(line);
|
|
6156
|
+
if (existing.contentHash) {
|
|
6157
|
+
hashes.add(existing.contentHash);
|
|
6158
|
+
}
|
|
6159
|
+
} catch {
|
|
6160
|
+
}
|
|
6161
|
+
}
|
|
6162
|
+
}
|
|
6163
|
+
knownHashesCache.set(eventsPath, hashes);
|
|
6164
|
+
return hashes;
|
|
6165
|
+
}
|
|
6166
|
+
function clearEventHashCache() {
|
|
6167
|
+
knownHashesCache.clear();
|
|
6168
|
+
}
|
|
6169
|
+
async function emitEvent(projectPath, event, options) {
|
|
6170
|
+
try {
|
|
6171
|
+
const dirResult = await getStateDir(projectPath, options?.stream, options?.session);
|
|
6172
|
+
if (!dirResult.ok) return dirResult;
|
|
6173
|
+
const stateDir = dirResult.value;
|
|
6174
|
+
const eventsPath = path13.join(stateDir, EVENTS_FILE);
|
|
6175
|
+
fs16.mkdirSync(stateDir, { recursive: true });
|
|
6176
|
+
const contentHash = computeEventHash(event, options?.session);
|
|
6177
|
+
const knownHashes = loadKnownHashes(eventsPath);
|
|
6178
|
+
if (knownHashes.has(contentHash)) {
|
|
6179
|
+
return Ok({ written: false, reason: "duplicate" });
|
|
6180
|
+
}
|
|
6181
|
+
const fullEvent = {
|
|
6182
|
+
...event,
|
|
6183
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6184
|
+
contentHash
|
|
6185
|
+
};
|
|
6186
|
+
if (options?.session) {
|
|
6187
|
+
fullEvent.session = options.session;
|
|
6188
|
+
}
|
|
6189
|
+
fs16.appendFileSync(eventsPath, JSON.stringify(fullEvent) + "\n");
|
|
6190
|
+
knownHashes.add(contentHash);
|
|
6191
|
+
return Ok({ written: true });
|
|
6192
|
+
} catch (error) {
|
|
6193
|
+
return Err(
|
|
6194
|
+
new Error(`Failed to emit event: ${error instanceof Error ? error.message : String(error)}`)
|
|
6195
|
+
);
|
|
6196
|
+
}
|
|
6197
|
+
}
|
|
6198
|
+
async function loadEvents(projectPath, options) {
|
|
6199
|
+
try {
|
|
6200
|
+
const dirResult = await getStateDir(projectPath, options?.stream, options?.session);
|
|
6201
|
+
if (!dirResult.ok) return dirResult;
|
|
6202
|
+
const stateDir = dirResult.value;
|
|
6203
|
+
const eventsPath = path13.join(stateDir, EVENTS_FILE);
|
|
6204
|
+
if (!fs16.existsSync(eventsPath)) {
|
|
6205
|
+
return Ok([]);
|
|
6206
|
+
}
|
|
6207
|
+
const content = fs16.readFileSync(eventsPath, "utf-8");
|
|
6208
|
+
const lines = content.split("\n").filter((line) => line.trim() !== "");
|
|
6209
|
+
const events = [];
|
|
6210
|
+
for (const line of lines) {
|
|
6211
|
+
try {
|
|
6212
|
+
const parsed = JSON.parse(line);
|
|
6213
|
+
const result = SkillEventSchema.safeParse(parsed);
|
|
6214
|
+
if (result.success) {
|
|
6215
|
+
events.push(result.data);
|
|
6216
|
+
}
|
|
6217
|
+
} catch {
|
|
6218
|
+
}
|
|
6219
|
+
}
|
|
6220
|
+
return Ok(events);
|
|
6221
|
+
} catch (error) {
|
|
6222
|
+
return Err(
|
|
6223
|
+
new Error(`Failed to load events: ${error instanceof Error ? error.message : String(error)}`)
|
|
6224
|
+
);
|
|
6225
|
+
}
|
|
6226
|
+
}
|
|
6227
|
+
function formatPhaseTransition(event) {
|
|
6228
|
+
const data = event.data;
|
|
6229
|
+
const suffix = data?.taskCount ? ` (${data.taskCount} tasks)` : "";
|
|
6230
|
+
return `phase: ${data?.from ?? "?"} -> ${data?.to ?? "?"}${suffix}`;
|
|
6231
|
+
}
|
|
6232
|
+
function formatGateResult(event) {
|
|
6233
|
+
const data = event.data;
|
|
6234
|
+
const status = data?.passed ? "passed" : "failed";
|
|
6235
|
+
const checks = data?.checks?.map((c) => `${c.name} ${c.passed ? "Y" : "N"}`).join(", ");
|
|
6236
|
+
return checks ? `gate: ${status} (${checks})` : `gate: ${status}`;
|
|
6237
|
+
}
|
|
6238
|
+
function formatHandoffDetail(event) {
|
|
6239
|
+
const data = event.data;
|
|
6240
|
+
const direction = data?.toSkill ? ` -> ${data.toSkill}` : "";
|
|
6241
|
+
return `handoff: ${event.summary}${direction}`;
|
|
6242
|
+
}
|
|
6243
|
+
var EVENT_FORMATTERS = {
|
|
6244
|
+
phase_transition: formatPhaseTransition,
|
|
6245
|
+
gate_result: formatGateResult,
|
|
6246
|
+
decision: (event) => `decision: ${event.summary}`,
|
|
6247
|
+
handoff: formatHandoffDetail,
|
|
6248
|
+
error: (event) => `error: ${event.summary}`,
|
|
6249
|
+
checkpoint: (event) => `checkpoint: ${event.summary}`
|
|
6250
|
+
};
|
|
6251
|
+
function formatEventTimeline(events, limit = 20) {
|
|
6252
|
+
if (events.length === 0) return "";
|
|
6253
|
+
const recent = events.slice(-limit);
|
|
6254
|
+
return recent.map((event) => {
|
|
6255
|
+
const time = formatTime(event.timestamp);
|
|
6256
|
+
const formatter = EVENT_FORMATTERS[event.type];
|
|
6257
|
+
const detail = formatter ? formatter(event) : event.summary;
|
|
6258
|
+
return `- ${time} [${event.skill}] ${detail}`;
|
|
6259
|
+
}).join("\n");
|
|
6260
|
+
}
|
|
6261
|
+
function formatTime(timestamp) {
|
|
6262
|
+
try {
|
|
6263
|
+
const date = new Date(timestamp);
|
|
6264
|
+
const hours = String(date.getHours()).padStart(2, "0");
|
|
6265
|
+
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
6266
|
+
return `${hours}:${minutes}`;
|
|
6267
|
+
} catch {
|
|
6268
|
+
return "??:??";
|
|
6269
|
+
}
|
|
6270
|
+
}
|
|
6271
|
+
|
|
5868
6272
|
// src/workflow/runner.ts
|
|
5869
6273
|
async function executeWorkflow(workflow, executor) {
|
|
5870
6274
|
const stepResults = [];
|
|
@@ -6014,7 +6418,8 @@ async function runMultiTurnPipeline(initialContext, turnExecutor, options) {
|
|
|
6014
6418
|
}
|
|
6015
6419
|
|
|
6016
6420
|
// src/security/scanner.ts
|
|
6017
|
-
import * as
|
|
6421
|
+
import * as fs18 from "fs/promises";
|
|
6422
|
+
import { minimatch as minimatch4 } from "minimatch";
|
|
6018
6423
|
|
|
6019
6424
|
// src/security/rules/registry.ts
|
|
6020
6425
|
var RuleRegistry = class {
|
|
@@ -6045,7 +6450,7 @@ var RuleRegistry = class {
|
|
|
6045
6450
|
};
|
|
6046
6451
|
|
|
6047
6452
|
// src/security/config.ts
|
|
6048
|
-
import { z as
|
|
6453
|
+
import { z as z6 } from "zod";
|
|
6049
6454
|
|
|
6050
6455
|
// src/security/types.ts
|
|
6051
6456
|
var DEFAULT_SECURITY_CONFIG = {
|
|
@@ -6056,19 +6461,19 @@ var DEFAULT_SECURITY_CONFIG = {
|
|
|
6056
6461
|
};
|
|
6057
6462
|
|
|
6058
6463
|
// src/security/config.ts
|
|
6059
|
-
var RuleOverrideSchema =
|
|
6060
|
-
var SecurityConfigSchema =
|
|
6061
|
-
enabled:
|
|
6062
|
-
strict:
|
|
6063
|
-
rules:
|
|
6064
|
-
exclude:
|
|
6065
|
-
external:
|
|
6066
|
-
semgrep:
|
|
6067
|
-
enabled:
|
|
6068
|
-
rulesets:
|
|
6464
|
+
var RuleOverrideSchema = z6.enum(["off", "error", "warning", "info"]);
|
|
6465
|
+
var SecurityConfigSchema = z6.object({
|
|
6466
|
+
enabled: z6.boolean().default(true),
|
|
6467
|
+
strict: z6.boolean().default(false),
|
|
6468
|
+
rules: z6.record(z6.string(), RuleOverrideSchema).optional().default({}),
|
|
6469
|
+
exclude: z6.array(z6.string()).optional().default(["**/node_modules/**", "**/dist/**", "**/*.test.ts", "**/fixtures/**"]),
|
|
6470
|
+
external: z6.object({
|
|
6471
|
+
semgrep: z6.object({
|
|
6472
|
+
enabled: z6.union([z6.literal("auto"), z6.boolean()]).default("auto"),
|
|
6473
|
+
rulesets: z6.array(z6.string()).optional()
|
|
6069
6474
|
}).optional(),
|
|
6070
|
-
gitleaks:
|
|
6071
|
-
enabled:
|
|
6475
|
+
gitleaks: z6.object({
|
|
6476
|
+
enabled: z6.union([z6.literal("auto"), z6.boolean()]).default("auto")
|
|
6072
6477
|
}).optional()
|
|
6073
6478
|
}).optional()
|
|
6074
6479
|
});
|
|
@@ -6101,15 +6506,15 @@ function resolveRuleSeverity(ruleId, defaultSeverity, overrides, strict) {
|
|
|
6101
6506
|
}
|
|
6102
6507
|
|
|
6103
6508
|
// src/security/stack-detector.ts
|
|
6104
|
-
import * as
|
|
6105
|
-
import * as
|
|
6509
|
+
import * as fs17 from "fs";
|
|
6510
|
+
import * as path14 from "path";
|
|
6106
6511
|
function detectStack(projectRoot) {
|
|
6107
6512
|
const stacks = [];
|
|
6108
|
-
const pkgJsonPath =
|
|
6109
|
-
if (
|
|
6513
|
+
const pkgJsonPath = path14.join(projectRoot, "package.json");
|
|
6514
|
+
if (fs17.existsSync(pkgJsonPath)) {
|
|
6110
6515
|
stacks.push("node");
|
|
6111
6516
|
try {
|
|
6112
|
-
const pkgJson = JSON.parse(
|
|
6517
|
+
const pkgJson = JSON.parse(fs17.readFileSync(pkgJsonPath, "utf-8"));
|
|
6113
6518
|
const allDeps = {
|
|
6114
6519
|
...pkgJson.dependencies,
|
|
6115
6520
|
...pkgJson.devDependencies
|
|
@@ -6124,13 +6529,13 @@ function detectStack(projectRoot) {
|
|
|
6124
6529
|
} catch {
|
|
6125
6530
|
}
|
|
6126
6531
|
}
|
|
6127
|
-
const goModPath =
|
|
6128
|
-
if (
|
|
6532
|
+
const goModPath = path14.join(projectRoot, "go.mod");
|
|
6533
|
+
if (fs17.existsSync(goModPath)) {
|
|
6129
6534
|
stacks.push("go");
|
|
6130
6535
|
}
|
|
6131
|
-
const requirementsPath =
|
|
6132
|
-
const pyprojectPath =
|
|
6133
|
-
if (
|
|
6536
|
+
const requirementsPath = path14.join(projectRoot, "requirements.txt");
|
|
6537
|
+
const pyprojectPath = path14.join(projectRoot, "pyproject.toml");
|
|
6538
|
+
if (fs17.existsSync(requirementsPath) || fs17.existsSync(pyprojectPath)) {
|
|
6134
6539
|
stacks.push("python");
|
|
6135
6540
|
}
|
|
6136
6541
|
return stacks;
|
|
@@ -6194,6 +6599,72 @@ var secretRules = [
|
|
|
6194
6599
|
message: "Hardcoded JWT token detected",
|
|
6195
6600
|
remediation: "Tokens should be fetched at runtime, not embedded in source",
|
|
6196
6601
|
references: ["CWE-798"]
|
|
6602
|
+
},
|
|
6603
|
+
{
|
|
6604
|
+
id: "SEC-SEC-006",
|
|
6605
|
+
name: "Anthropic API Key",
|
|
6606
|
+
category: "secrets",
|
|
6607
|
+
severity: "error",
|
|
6608
|
+
confidence: "high",
|
|
6609
|
+
patterns: [/sk-ant-api\d{2}-[A-Za-z0-9_-]{20,}/],
|
|
6610
|
+
message: "Hardcoded Anthropic API key detected",
|
|
6611
|
+
remediation: "Use environment variables: process.env.ANTHROPIC_API_KEY",
|
|
6612
|
+
references: ["CWE-798"]
|
|
6613
|
+
},
|
|
6614
|
+
{
|
|
6615
|
+
id: "SEC-SEC-007",
|
|
6616
|
+
name: "OpenAI API Key",
|
|
6617
|
+
category: "secrets",
|
|
6618
|
+
severity: "error",
|
|
6619
|
+
confidence: "high",
|
|
6620
|
+
patterns: [/sk-proj-[A-Za-z0-9_-]{20,}/],
|
|
6621
|
+
message: "Hardcoded OpenAI API key detected",
|
|
6622
|
+
remediation: "Use environment variables: process.env.OPENAI_API_KEY",
|
|
6623
|
+
references: ["CWE-798"]
|
|
6624
|
+
},
|
|
6625
|
+
{
|
|
6626
|
+
id: "SEC-SEC-008",
|
|
6627
|
+
name: "Google API Key",
|
|
6628
|
+
category: "secrets",
|
|
6629
|
+
severity: "error",
|
|
6630
|
+
confidence: "high",
|
|
6631
|
+
patterns: [/AIza[A-Za-z0-9_-]{35}/],
|
|
6632
|
+
message: "Hardcoded Google API key detected",
|
|
6633
|
+
remediation: "Use environment variables or a secrets manager for Google API keys",
|
|
6634
|
+
references: ["CWE-798"]
|
|
6635
|
+
},
|
|
6636
|
+
{
|
|
6637
|
+
id: "SEC-SEC-009",
|
|
6638
|
+
name: "GitHub Personal Access Token",
|
|
6639
|
+
category: "secrets",
|
|
6640
|
+
severity: "error",
|
|
6641
|
+
confidence: "high",
|
|
6642
|
+
patterns: [/gh[pous]_[A-Za-z0-9_]{36,}/],
|
|
6643
|
+
message: "Hardcoded GitHub personal access token detected",
|
|
6644
|
+
remediation: "Use environment variables: process.env.GITHUB_TOKEN",
|
|
6645
|
+
references: ["CWE-798"]
|
|
6646
|
+
},
|
|
6647
|
+
{
|
|
6648
|
+
id: "SEC-SEC-010",
|
|
6649
|
+
name: "Stripe Live Key",
|
|
6650
|
+
category: "secrets",
|
|
6651
|
+
severity: "error",
|
|
6652
|
+
confidence: "high",
|
|
6653
|
+
patterns: [/\b[spr]k_live_[A-Za-z0-9]{24,}/],
|
|
6654
|
+
message: "Hardcoded Stripe live key detected",
|
|
6655
|
+
remediation: "Use environment variables for Stripe keys; never commit live keys",
|
|
6656
|
+
references: ["CWE-798"]
|
|
6657
|
+
},
|
|
6658
|
+
{
|
|
6659
|
+
id: "SEC-SEC-011",
|
|
6660
|
+
name: "Database Connection String with Credentials",
|
|
6661
|
+
category: "secrets",
|
|
6662
|
+
severity: "error",
|
|
6663
|
+
confidence: "high",
|
|
6664
|
+
patterns: [/(?:postgres|mysql|mongodb|redis|amqp|mssql)(?:\+\w+)?:\/\/[^/\s:]+:[^@/\s]+@/i],
|
|
6665
|
+
message: "Database connection string with embedded credentials detected",
|
|
6666
|
+
remediation: "Use environment variables for connection strings; separate credentials from URIs",
|
|
6667
|
+
references: ["CWE-798"]
|
|
6197
6668
|
}
|
|
6198
6669
|
];
|
|
6199
6670
|
|
|
@@ -6380,6 +6851,360 @@ var deserializationRules = [
|
|
|
6380
6851
|
}
|
|
6381
6852
|
];
|
|
6382
6853
|
|
|
6854
|
+
// src/security/rules/agent-config.ts
|
|
6855
|
+
var agentConfigRules = [
|
|
6856
|
+
{
|
|
6857
|
+
id: "SEC-AGT-001",
|
|
6858
|
+
name: "Hidden Unicode Characters",
|
|
6859
|
+
category: "agent-config",
|
|
6860
|
+
severity: "error",
|
|
6861
|
+
confidence: "high",
|
|
6862
|
+
patterns: [/\u200B|\u200C|\u200D|\uFEFF|\u2060/],
|
|
6863
|
+
fileGlob: "**/CLAUDE.md,**/AGENTS.md,**/*.yaml",
|
|
6864
|
+
message: "Hidden zero-width Unicode characters detected in agent configuration",
|
|
6865
|
+
remediation: "Remove invisible Unicode characters; they may hide malicious instructions",
|
|
6866
|
+
references: ["CWE-116"]
|
|
6867
|
+
},
|
|
6868
|
+
{
|
|
6869
|
+
id: "SEC-AGT-002",
|
|
6870
|
+
name: "URL Execution Directives",
|
|
6871
|
+
category: "agent-config",
|
|
6872
|
+
severity: "warning",
|
|
6873
|
+
confidence: "medium",
|
|
6874
|
+
patterns: [/\b(?:curl|wget)\s+\S+/i, /\bfetch\s*\(/i],
|
|
6875
|
+
fileGlob: "**/CLAUDE.md,**/AGENTS.md",
|
|
6876
|
+
message: "URL execution directive found in agent configuration",
|
|
6877
|
+
remediation: "Avoid instructing agents to download and execute remote content",
|
|
6878
|
+
references: ["CWE-94"]
|
|
6879
|
+
},
|
|
6880
|
+
{
|
|
6881
|
+
id: "SEC-AGT-003",
|
|
6882
|
+
name: "Wildcard Tool Permissions",
|
|
6883
|
+
category: "agent-config",
|
|
6884
|
+
severity: "warning",
|
|
6885
|
+
confidence: "high",
|
|
6886
|
+
patterns: [/(?:Bash|Write|Edit)\s*\(\s*\*\s*\)/],
|
|
6887
|
+
fileGlob: "**/.claude/**,**/settings*.json",
|
|
6888
|
+
message: "Wildcard tool permissions grant unrestricted access",
|
|
6889
|
+
remediation: "Scope tool permissions to specific patterns instead of wildcards",
|
|
6890
|
+
references: ["CWE-250"]
|
|
6891
|
+
},
|
|
6892
|
+
{
|
|
6893
|
+
id: "SEC-AGT-004",
|
|
6894
|
+
name: "Auto-approve Patterns",
|
|
6895
|
+
category: "agent-config",
|
|
6896
|
+
severity: "warning",
|
|
6897
|
+
confidence: "high",
|
|
6898
|
+
patterns: [/\bautoApprove\b/i, /\bauto_approve\b/i],
|
|
6899
|
+
fileGlob: "**/.claude/**,**/.mcp.json",
|
|
6900
|
+
message: "Auto-approve configuration bypasses human review of tool calls",
|
|
6901
|
+
remediation: "Review auto-approved tools carefully; prefer explicit approval for destructive operations",
|
|
6902
|
+
references: ["CWE-862"]
|
|
6903
|
+
},
|
|
6904
|
+
{
|
|
6905
|
+
id: "SEC-AGT-005",
|
|
6906
|
+
name: "Prompt Injection Surface",
|
|
6907
|
+
category: "agent-config",
|
|
6908
|
+
severity: "warning",
|
|
6909
|
+
confidence: "medium",
|
|
6910
|
+
patterns: [/\$\{[^}]*\}/, /\{\{[^}]*\}\}/],
|
|
6911
|
+
fileGlob: "**/skill.yaml",
|
|
6912
|
+
message: "Template interpolation syntax in skill YAML may enable prompt injection",
|
|
6913
|
+
remediation: "Avoid dynamic interpolation in skill descriptions; use static text",
|
|
6914
|
+
references: ["CWE-94"]
|
|
6915
|
+
},
|
|
6916
|
+
{
|
|
6917
|
+
id: "SEC-AGT-006",
|
|
6918
|
+
name: "Permission Bypass Flags",
|
|
6919
|
+
category: "agent-config",
|
|
6920
|
+
severity: "error",
|
|
6921
|
+
confidence: "high",
|
|
6922
|
+
patterns: [/--dangerously-skip-permissions/, /--no-verify/],
|
|
6923
|
+
fileGlob: "**/CLAUDE.md,**/AGENTS.md,**/.claude/**",
|
|
6924
|
+
message: "Permission bypass flag detected in agent configuration",
|
|
6925
|
+
remediation: "Remove flags that bypass safety checks; they undermine enforcement",
|
|
6926
|
+
references: ["CWE-863"]
|
|
6927
|
+
},
|
|
6928
|
+
{
|
|
6929
|
+
id: "SEC-AGT-007",
|
|
6930
|
+
name: "Hook Injection Surface",
|
|
6931
|
+
category: "agent-config",
|
|
6932
|
+
severity: "error",
|
|
6933
|
+
confidence: "low",
|
|
6934
|
+
patterns: [/\$\(/, /`[^`]+`/, /\s&&\s/, /\s\|\|\s/],
|
|
6935
|
+
fileGlob: "**/settings*.json,**/hooks.json",
|
|
6936
|
+
message: "Shell metacharacters in hook commands may enable command injection",
|
|
6937
|
+
remediation: "Use simple, single-command hooks without shell operators; chain logic inside the script",
|
|
6938
|
+
references: ["CWE-78"]
|
|
6939
|
+
}
|
|
6940
|
+
];
|
|
6941
|
+
|
|
6942
|
+
// src/security/rules/mcp.ts
|
|
6943
|
+
var mcpRules = [
|
|
6944
|
+
{
|
|
6945
|
+
id: "SEC-MCP-001",
|
|
6946
|
+
name: "Hardcoded MCP Secrets",
|
|
6947
|
+
category: "mcp",
|
|
6948
|
+
severity: "error",
|
|
6949
|
+
confidence: "medium",
|
|
6950
|
+
patterns: [/(?:API_KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL)\s*["']?\s*:\s*["'][^"']{8,}["']/i],
|
|
6951
|
+
fileGlob: "**/.mcp.json",
|
|
6952
|
+
message: "Hardcoded secret detected in MCP server configuration",
|
|
6953
|
+
remediation: "Use environment variable references instead of inline secrets in .mcp.json",
|
|
6954
|
+
references: ["CWE-798"]
|
|
6955
|
+
},
|
|
6956
|
+
{
|
|
6957
|
+
id: "SEC-MCP-002",
|
|
6958
|
+
name: "Shell Injection in MCP Args",
|
|
6959
|
+
category: "mcp",
|
|
6960
|
+
severity: "error",
|
|
6961
|
+
confidence: "medium",
|
|
6962
|
+
patterns: [/\$\(/, /`[^`]+`/],
|
|
6963
|
+
fileGlob: "**/.mcp.json",
|
|
6964
|
+
message: "Shell metacharacters detected in MCP server arguments",
|
|
6965
|
+
remediation: "Use literal argument values; avoid shell interpolation in MCP args",
|
|
6966
|
+
references: ["CWE-78"]
|
|
6967
|
+
},
|
|
6968
|
+
{
|
|
6969
|
+
id: "SEC-MCP-003",
|
|
6970
|
+
name: "Network Exposure",
|
|
6971
|
+
category: "mcp",
|
|
6972
|
+
severity: "warning",
|
|
6973
|
+
confidence: "high",
|
|
6974
|
+
patterns: [/0\.0\.0\.0/, /["']\*["']\s*:\s*\d/, /host["']?\s*:\s*["']\*["']/i],
|
|
6975
|
+
fileGlob: "**/.mcp.json",
|
|
6976
|
+
message: "MCP server binding to all network interfaces (0.0.0.0 or wildcard *)",
|
|
6977
|
+
remediation: "Bind to 127.0.0.1 or localhost to restrict access to local machine",
|
|
6978
|
+
references: ["CWE-668"]
|
|
6979
|
+
},
|
|
6980
|
+
{
|
|
6981
|
+
id: "SEC-MCP-004",
|
|
6982
|
+
name: "Typosquatting Vector",
|
|
6983
|
+
category: "mcp",
|
|
6984
|
+
severity: "warning",
|
|
6985
|
+
confidence: "medium",
|
|
6986
|
+
patterns: [/\bnpx\s+(?:-y|--yes)\b/],
|
|
6987
|
+
fileGlob: "**/.mcp.json",
|
|
6988
|
+
message: "npx -y auto-installs packages without confirmation, enabling typosquatting",
|
|
6989
|
+
remediation: "Pin exact package versions or install packages explicitly before use",
|
|
6990
|
+
references: ["CWE-427"]
|
|
6991
|
+
},
|
|
6992
|
+
{
|
|
6993
|
+
id: "SEC-MCP-005",
|
|
6994
|
+
name: "Unencrypted Transport",
|
|
6995
|
+
category: "mcp",
|
|
6996
|
+
severity: "warning",
|
|
6997
|
+
confidence: "medium",
|
|
6998
|
+
patterns: [/http:\/\/(?!localhost\b|127\.0\.0\.1\b)/],
|
|
6999
|
+
fileGlob: "**/.mcp.json",
|
|
7000
|
+
message: "Unencrypted HTTP transport detected for MCP server connection",
|
|
7001
|
+
remediation: "Use https:// for all non-localhost MCP server connections",
|
|
7002
|
+
references: ["CWE-319"]
|
|
7003
|
+
}
|
|
7004
|
+
];
|
|
7005
|
+
|
|
7006
|
+
// src/security/rules/insecure-defaults.ts
|
|
7007
|
+
var insecureDefaultsRules = [
|
|
7008
|
+
{
|
|
7009
|
+
id: "SEC-DEF-001",
|
|
7010
|
+
name: "Security-Sensitive Fallback to Hardcoded Default",
|
|
7011
|
+
category: "insecure-defaults",
|
|
7012
|
+
severity: "warning",
|
|
7013
|
+
confidence: "medium",
|
|
7014
|
+
patterns: [
|
|
7015
|
+
/(?:SECRET|KEY|TOKEN|PASSWORD|SALT|PEPPER|SIGNING|ENCRYPTION|AUTH|JWT|SESSION).*(?:\|\||\?\?)\s*['"][^'"]+['"]/i
|
|
7016
|
+
],
|
|
7017
|
+
fileGlob: "**/*.{ts,js,mjs,cjs}",
|
|
7018
|
+
message: "Security-sensitive variable falls back to a hardcoded default when env var is missing",
|
|
7019
|
+
remediation: "Throw an error if the env var is missing instead of falling back to a default. Use a startup validation check.",
|
|
7020
|
+
references: ["CWE-1188"]
|
|
7021
|
+
},
|
|
7022
|
+
{
|
|
7023
|
+
id: "SEC-DEF-002",
|
|
7024
|
+
name: "TLS/SSL Disabled by Default",
|
|
7025
|
+
category: "insecure-defaults",
|
|
7026
|
+
severity: "warning",
|
|
7027
|
+
confidence: "medium",
|
|
7028
|
+
patterns: [
|
|
7029
|
+
/(?:tls|ssl|https|secure)\s*(?:=|:)\s*(?:false|config\??\.\w+\s*(?:\?\?|&&|\|\|)\s*false)/i
|
|
7030
|
+
],
|
|
7031
|
+
fileGlob: "**/*.{ts,js,mjs,cjs,go,py}",
|
|
7032
|
+
message: "Security feature defaults to disabled; missing configuration degrades to insecure mode",
|
|
7033
|
+
remediation: "Default security features to enabled (true). Require explicit opt-out, not opt-in.",
|
|
7034
|
+
references: ["CWE-1188"]
|
|
7035
|
+
},
|
|
7036
|
+
{
|
|
7037
|
+
id: "SEC-DEF-003",
|
|
7038
|
+
name: "Swallowed Authentication/Authorization Error",
|
|
7039
|
+
category: "insecure-defaults",
|
|
7040
|
+
severity: "warning",
|
|
7041
|
+
confidence: "low",
|
|
7042
|
+
patterns: [
|
|
7043
|
+
// Matches single-line empty catch: catch(e) { } or catch(e) { // ignore }
|
|
7044
|
+
// Note: multi-line catch blocks are handled by AI review, not this rule
|
|
7045
|
+
/catch\s*\([^)]*\)\s*\{\s*(?:\/\/\s*(?:ignore|skip|noop|todo)\b.*)?\s*\}/
|
|
7046
|
+
],
|
|
7047
|
+
fileGlob: "**/*auth*.{ts,js,mjs,cjs},**/*session*.{ts,js,mjs,cjs},**/*token*.{ts,js,mjs,cjs}",
|
|
7048
|
+
message: "Single-line empty catch block in authentication/authorization code may silently allow unauthorized access. Note: multi-line empty catch blocks are detected by AI review, not this mechanical rule.",
|
|
7049
|
+
remediation: "Re-throw the error or return an explicit denial. Never silently swallow auth errors.",
|
|
7050
|
+
references: ["CWE-754", "CWE-390"]
|
|
7051
|
+
},
|
|
7052
|
+
{
|
|
7053
|
+
id: "SEC-DEF-004",
|
|
7054
|
+
name: "Permissive CORS Fallback",
|
|
7055
|
+
category: "insecure-defaults",
|
|
7056
|
+
severity: "warning",
|
|
7057
|
+
confidence: "medium",
|
|
7058
|
+
patterns: [
|
|
7059
|
+
/(?:origin|cors)\s*(?:=|:)\s*(?:config|options|env)\??\.\w+\s*(?:\?\?|\|\|)\s*['"]\*/
|
|
7060
|
+
],
|
|
7061
|
+
fileGlob: "**/*.{ts,js,mjs,cjs}",
|
|
7062
|
+
message: "CORS origin falls back to wildcard (*) when configuration is missing",
|
|
7063
|
+
remediation: "Default to a restrictive origin list. Require explicit configuration for permissive CORS.",
|
|
7064
|
+
references: ["CWE-942"]
|
|
7065
|
+
},
|
|
7066
|
+
{
|
|
7067
|
+
id: "SEC-DEF-005",
|
|
7068
|
+
name: "Rate Limiting Disabled by Default",
|
|
7069
|
+
category: "insecure-defaults",
|
|
7070
|
+
severity: "info",
|
|
7071
|
+
confidence: "low",
|
|
7072
|
+
patterns: [
|
|
7073
|
+
/(?:rateLimit|rateLimiting|throttle)\s*(?:=|:)\s*(?:config|options|env)\??\.\w+\s*(?:\?\?|\|\|)\s*(?:false|0|null|undefined)/i
|
|
7074
|
+
],
|
|
7075
|
+
fileGlob: "**/*.{ts,js,mjs,cjs}",
|
|
7076
|
+
message: "Rate limiting defaults to disabled when configuration is missing",
|
|
7077
|
+
remediation: "Default to a sensible rate limit. Require explicit opt-out for disabling.",
|
|
7078
|
+
references: ["CWE-770"]
|
|
7079
|
+
}
|
|
7080
|
+
];
|
|
7081
|
+
|
|
7082
|
+
// src/security/rules/sharp-edges.ts
|
|
7083
|
+
var sharpEdgesRules = [
|
|
7084
|
+
// --- Deprecated Crypto APIs ---
|
|
7085
|
+
{
|
|
7086
|
+
id: "SEC-EDGE-001",
|
|
7087
|
+
name: "Deprecated createCipher API",
|
|
7088
|
+
category: "sharp-edges",
|
|
7089
|
+
severity: "error",
|
|
7090
|
+
confidence: "high",
|
|
7091
|
+
patterns: [/crypto\.createCipher\s*\(/],
|
|
7092
|
+
fileGlob: "**/*.{ts,js,mjs,cjs}",
|
|
7093
|
+
message: "crypto.createCipher is deprecated \u2014 uses weak key derivation (no IV)",
|
|
7094
|
+
remediation: "Use crypto.createCipheriv with a random IV and proper key derivation (scrypt/pbkdf2)",
|
|
7095
|
+
references: ["CWE-327"]
|
|
7096
|
+
},
|
|
7097
|
+
{
|
|
7098
|
+
id: "SEC-EDGE-002",
|
|
7099
|
+
name: "Deprecated createDecipher API",
|
|
7100
|
+
category: "sharp-edges",
|
|
7101
|
+
severity: "error",
|
|
7102
|
+
confidence: "high",
|
|
7103
|
+
patterns: [/crypto\.createDecipher\s*\(/],
|
|
7104
|
+
fileGlob: "**/*.{ts,js,mjs,cjs}",
|
|
7105
|
+
message: "crypto.createDecipher is deprecated \u2014 uses weak key derivation (no IV)",
|
|
7106
|
+
remediation: "Use crypto.createDecipheriv with the same IV used for encryption",
|
|
7107
|
+
references: ["CWE-327"]
|
|
7108
|
+
},
|
|
7109
|
+
{
|
|
7110
|
+
id: "SEC-EDGE-003",
|
|
7111
|
+
name: "ECB Mode Selection",
|
|
7112
|
+
category: "sharp-edges",
|
|
7113
|
+
severity: "warning",
|
|
7114
|
+
confidence: "high",
|
|
7115
|
+
patterns: [/-ecb['"]/],
|
|
7116
|
+
fileGlob: "**/*.{ts,js,mjs,cjs,go,py}",
|
|
7117
|
+
message: "ECB mode does not provide semantic security \u2014 identical plaintext blocks produce identical ciphertext",
|
|
7118
|
+
remediation: "Use CBC, CTR, or GCM mode instead of ECB",
|
|
7119
|
+
references: ["CWE-327"]
|
|
7120
|
+
},
|
|
7121
|
+
// --- Unsafe Deserialization ---
|
|
7122
|
+
{
|
|
7123
|
+
id: "SEC-EDGE-004",
|
|
7124
|
+
name: "yaml.load Without Safe Loader",
|
|
7125
|
+
category: "sharp-edges",
|
|
7126
|
+
severity: "error",
|
|
7127
|
+
confidence: "high",
|
|
7128
|
+
patterns: [
|
|
7129
|
+
/yaml\.load\s*\(/
|
|
7130
|
+
// Python: yaml.load() without SafeLoader
|
|
7131
|
+
],
|
|
7132
|
+
fileGlob: "**/*.py",
|
|
7133
|
+
message: "yaml.load() executes arbitrary Python objects \u2014 use yaml.safe_load() instead",
|
|
7134
|
+
remediation: "Replace yaml.load() with yaml.safe_load() or yaml.load(data, Loader=SafeLoader). Note: this rule will flag yaml.load(data, Loader=SafeLoader) \u2014 suppress with # harness-ignore SEC-EDGE-004: safe usage with SafeLoader",
|
|
7135
|
+
references: ["CWE-502"]
|
|
7136
|
+
},
|
|
7137
|
+
{
|
|
7138
|
+
id: "SEC-EDGE-005",
|
|
7139
|
+
name: "Pickle/Marshal Deserialization",
|
|
7140
|
+
category: "sharp-edges",
|
|
7141
|
+
severity: "error",
|
|
7142
|
+
confidence: "high",
|
|
7143
|
+
patterns: [/pickle\.loads?\s*\(/, /marshal\.loads?\s*\(/],
|
|
7144
|
+
fileGlob: "**/*.py",
|
|
7145
|
+
message: "pickle/marshal deserialization executes arbitrary code \u2014 never use on untrusted data",
|
|
7146
|
+
remediation: "Use JSON, MessagePack, or Protocol Buffers for untrusted data serialization",
|
|
7147
|
+
references: ["CWE-502"]
|
|
7148
|
+
},
|
|
7149
|
+
// --- TOCTOU (Time-of-Check to Time-of-Use) ---
|
|
7150
|
+
{
|
|
7151
|
+
id: "SEC-EDGE-006",
|
|
7152
|
+
name: "Check-Then-Act File Operation",
|
|
7153
|
+
category: "sharp-edges",
|
|
7154
|
+
severity: "warning",
|
|
7155
|
+
confidence: "medium",
|
|
7156
|
+
// Patterns use .{0,N} since scanner matches single lines only (no multiline mode)
|
|
7157
|
+
patterns: [
|
|
7158
|
+
/(?:existsSync|accessSync|statSync)\s*\([^)]+\).{0,50}(?:readFileSync|writeFileSync|unlinkSync|mkdirSync)\s*\(/
|
|
7159
|
+
],
|
|
7160
|
+
fileGlob: "**/*.{ts,js,mjs,cjs}",
|
|
7161
|
+
message: "Check-then-act pattern on filesystem is vulnerable to TOCTOU race conditions",
|
|
7162
|
+
remediation: "Use the operation directly and handle ENOENT/EEXIST errors instead of checking first",
|
|
7163
|
+
references: ["CWE-367"]
|
|
7164
|
+
},
|
|
7165
|
+
{
|
|
7166
|
+
id: "SEC-EDGE-007",
|
|
7167
|
+
name: "Check-Then-Act File Operation (Async)",
|
|
7168
|
+
category: "sharp-edges",
|
|
7169
|
+
severity: "warning",
|
|
7170
|
+
confidence: "medium",
|
|
7171
|
+
// Uses .{0,N} since scanner matches single lines only (no multiline mode)
|
|
7172
|
+
patterns: [/(?:access|stat)\s*\([^)]+\).{0,80}(?:readFile|writeFile|unlink|mkdir)\s*\(/],
|
|
7173
|
+
fileGlob: "**/*.{ts,js,mjs,cjs}",
|
|
7174
|
+
message: "Async check-then-act pattern on filesystem is vulnerable to TOCTOU race conditions",
|
|
7175
|
+
remediation: "Use the operation directly with try/catch instead of checking existence first",
|
|
7176
|
+
references: ["CWE-367"]
|
|
7177
|
+
},
|
|
7178
|
+
// --- Stringly-Typed Security ---
|
|
7179
|
+
{
|
|
7180
|
+
id: "SEC-EDGE-008",
|
|
7181
|
+
name: 'JWT Algorithm "none"',
|
|
7182
|
+
category: "sharp-edges",
|
|
7183
|
+
severity: "error",
|
|
7184
|
+
confidence: "high",
|
|
7185
|
+
patterns: [
|
|
7186
|
+
/algorithm[s]?\s*[:=]\s*\[?\s*['"]none['"]/i,
|
|
7187
|
+
/alg(?:orithm)?\s*[:=]\s*['"]none['"]/i
|
|
7188
|
+
],
|
|
7189
|
+
fileGlob: "**/*.{ts,js,mjs,cjs}",
|
|
7190
|
+
message: 'JWT "none" algorithm disables signature verification entirely',
|
|
7191
|
+
remediation: 'Specify an explicit algorithm (e.g., "HS256", "RS256") and set algorithms allowlist in verify options',
|
|
7192
|
+
references: ["CWE-345"]
|
|
7193
|
+
},
|
|
7194
|
+
{
|
|
7195
|
+
id: "SEC-EDGE-009",
|
|
7196
|
+
name: "DES/RC4 Algorithm Selection",
|
|
7197
|
+
category: "sharp-edges",
|
|
7198
|
+
severity: "error",
|
|
7199
|
+
confidence: "high",
|
|
7200
|
+
patterns: [/['"]\s*(?:des|des-ede|des-ede3|des3|rc4|rc2|blowfish)\s*['"]/i],
|
|
7201
|
+
fileGlob: "**/*.{ts,js,mjs,cjs,go,py}",
|
|
7202
|
+
message: "Weak/deprecated cipher algorithm selected \u2014 DES, RC4, RC2, and Blowfish are broken or deprecated",
|
|
7203
|
+
remediation: "Use AES-256-GCM or ChaCha20-Poly1305",
|
|
7204
|
+
references: ["CWE-327"]
|
|
7205
|
+
}
|
|
7206
|
+
];
|
|
7207
|
+
|
|
6383
7208
|
// src/security/rules/stack/node.ts
|
|
6384
7209
|
var nodeRules = [
|
|
6385
7210
|
{
|
|
@@ -6493,6 +7318,14 @@ var goRules = [
|
|
|
6493
7318
|
];
|
|
6494
7319
|
|
|
6495
7320
|
// src/security/scanner.ts
|
|
7321
|
+
function parseHarnessIgnore(line, ruleId) {
|
|
7322
|
+
if (!line.includes("harness-ignore")) return null;
|
|
7323
|
+
if (!line.includes(ruleId)) return null;
|
|
7324
|
+
const match = line.match(/(?:\/\/|#)\s*harness-ignore\s+(SEC-[A-Z]+-\d+)(?::\s*(.+))?/);
|
|
7325
|
+
if (match?.[1] !== ruleId) return null;
|
|
7326
|
+
const text = match[2]?.trim();
|
|
7327
|
+
return { ruleId, justification: text || null };
|
|
7328
|
+
}
|
|
6496
7329
|
var SecurityScanner = class {
|
|
6497
7330
|
registry;
|
|
6498
7331
|
config;
|
|
@@ -6507,7 +7340,11 @@ var SecurityScanner = class {
|
|
|
6507
7340
|
...cryptoRules,
|
|
6508
7341
|
...pathTraversalRules,
|
|
6509
7342
|
...networkRules,
|
|
6510
|
-
...deserializationRules
|
|
7343
|
+
...deserializationRules,
|
|
7344
|
+
...agentConfigRules,
|
|
7345
|
+
...mcpRules,
|
|
7346
|
+
...insecureDefaultsRules,
|
|
7347
|
+
...sharpEdgesRules
|
|
6511
7348
|
]);
|
|
6512
7349
|
this.registry.registerAll([...nodeRules, ...expressRules, ...reactRules, ...goRules]);
|
|
6513
7350
|
this.activeRules = this.registry.getAll();
|
|
@@ -6516,11 +7353,40 @@ var SecurityScanner = class {
|
|
|
6516
7353
|
const stacks = detectStack(projectRoot);
|
|
6517
7354
|
this.activeRules = this.registry.getForStacks(stacks.length > 0 ? stacks : []);
|
|
6518
7355
|
}
|
|
7356
|
+
/**
|
|
7357
|
+
* Scan raw content against all active rules. Note: this method does NOT apply
|
|
7358
|
+
* fileGlob filtering — every active rule is evaluated regardless of filePath.
|
|
7359
|
+
* If you are scanning a specific file and want fileGlob-based rule filtering,
|
|
7360
|
+
* use {@link scanFile} instead.
|
|
7361
|
+
*/
|
|
6519
7362
|
scanContent(content, filePath, startLine = 1) {
|
|
6520
7363
|
if (!this.config.enabled) return [];
|
|
6521
|
-
const findings = [];
|
|
6522
7364
|
const lines = content.split("\n");
|
|
6523
|
-
|
|
7365
|
+
return this.scanLinesWithRules(lines, this.activeRules, filePath, startLine);
|
|
7366
|
+
}
|
|
7367
|
+
async scanFile(filePath) {
|
|
7368
|
+
if (!this.config.enabled) return [];
|
|
7369
|
+
const content = await fs18.readFile(filePath, "utf-8");
|
|
7370
|
+
return this.scanContentForFile(content, filePath, 1);
|
|
7371
|
+
}
|
|
7372
|
+
scanContentForFile(content, filePath, startLine = 1) {
|
|
7373
|
+
if (!this.config.enabled) return [];
|
|
7374
|
+
const lines = content.split("\n");
|
|
7375
|
+
const applicableRules = this.activeRules.filter((rule) => {
|
|
7376
|
+
if (!rule.fileGlob) return true;
|
|
7377
|
+
const globs = rule.fileGlob.split(",").map((g) => g.trim());
|
|
7378
|
+
return globs.some((glob) => minimatch4(filePath, glob, { dot: true }));
|
|
7379
|
+
});
|
|
7380
|
+
return this.scanLinesWithRules(lines, applicableRules, filePath, startLine);
|
|
7381
|
+
}
|
|
7382
|
+
/**
|
|
7383
|
+
* Core scanning loop shared by scanContent and scanContentForFile.
|
|
7384
|
+
* Evaluates each rule against each line, handling suppression (FP gate)
|
|
7385
|
+
* and pattern matching uniformly.
|
|
7386
|
+
*/
|
|
7387
|
+
scanLinesWithRules(lines, rules, filePath, startLine) {
|
|
7388
|
+
const findings = [];
|
|
7389
|
+
for (const rule of rules) {
|
|
6524
7390
|
const resolved = resolveRuleSeverity(
|
|
6525
7391
|
rule.id,
|
|
6526
7392
|
rule.severity,
|
|
@@ -6530,7 +7396,25 @@ var SecurityScanner = class {
|
|
|
6530
7396
|
if (resolved === "off") continue;
|
|
6531
7397
|
for (let i = 0; i < lines.length; i++) {
|
|
6532
7398
|
const line = lines[i] ?? "";
|
|
6533
|
-
|
|
7399
|
+
const suppressionMatch = parseHarnessIgnore(line, rule.id);
|
|
7400
|
+
if (suppressionMatch) {
|
|
7401
|
+
if (!suppressionMatch.justification) {
|
|
7402
|
+
findings.push({
|
|
7403
|
+
ruleId: rule.id,
|
|
7404
|
+
ruleName: rule.name,
|
|
7405
|
+
category: rule.category,
|
|
7406
|
+
severity: this.config.strict ? "error" : "warning",
|
|
7407
|
+
confidence: "high",
|
|
7408
|
+
file: filePath,
|
|
7409
|
+
line: startLine + i,
|
|
7410
|
+
match: line.trim(),
|
|
7411
|
+
context: line,
|
|
7412
|
+
message: `Suppression of ${rule.id} requires justification: // harness-ignore ${rule.id}: <reason>`,
|
|
7413
|
+
remediation: `Add justification after colon: // harness-ignore ${rule.id}: false positive because ...`
|
|
7414
|
+
});
|
|
7415
|
+
}
|
|
7416
|
+
continue;
|
|
7417
|
+
}
|
|
6534
7418
|
for (const pattern of rule.patterns) {
|
|
6535
7419
|
pattern.lastIndex = 0;
|
|
6536
7420
|
if (pattern.test(line)) {
|
|
@@ -6555,11 +7439,6 @@ var SecurityScanner = class {
|
|
|
6555
7439
|
}
|
|
6556
7440
|
return findings;
|
|
6557
7441
|
}
|
|
6558
|
-
async scanFile(filePath) {
|
|
6559
|
-
if (!this.config.enabled) return [];
|
|
6560
|
-
const content = await fs17.readFile(filePath, "utf-8");
|
|
6561
|
-
return this.scanContent(content, filePath, 1);
|
|
6562
|
-
}
|
|
6563
7442
|
async scanFiles(filePaths) {
|
|
6564
7443
|
const allFindings = [];
|
|
6565
7444
|
let scannedCount = 0;
|
|
@@ -6579,10 +7458,418 @@ var SecurityScanner = class {
|
|
|
6579
7458
|
coverage: "baseline"
|
|
6580
7459
|
};
|
|
6581
7460
|
}
|
|
6582
|
-
};
|
|
7461
|
+
};
|
|
7462
|
+
|
|
7463
|
+
// src/security/injection-patterns.ts
|
|
7464
|
+
var hiddenUnicodePatterns = [
|
|
7465
|
+
{
|
|
7466
|
+
ruleId: "INJ-UNI-001",
|
|
7467
|
+
severity: "high",
|
|
7468
|
+
category: "hidden-unicode",
|
|
7469
|
+
description: "Zero-width characters that can hide malicious instructions",
|
|
7470
|
+
// eslint-disable-next-line no-misleading-character-class -- intentional: regex detects zero-width chars for security scanning
|
|
7471
|
+
pattern: /[\u200B\u200C\u200D\uFEFF\u2060]/
|
|
7472
|
+
},
|
|
7473
|
+
{
|
|
7474
|
+
ruleId: "INJ-UNI-002",
|
|
7475
|
+
severity: "high",
|
|
7476
|
+
category: "hidden-unicode",
|
|
7477
|
+
description: "RTL/LTR override characters that can disguise text direction",
|
|
7478
|
+
pattern: /[\u202A-\u202E\u2066-\u2069]/
|
|
7479
|
+
}
|
|
7480
|
+
];
|
|
7481
|
+
var reRolingPatterns = [
|
|
7482
|
+
{
|
|
7483
|
+
ruleId: "INJ-REROL-001",
|
|
7484
|
+
severity: "high",
|
|
7485
|
+
category: "explicit-re-roling",
|
|
7486
|
+
description: "Instruction to ignore/disregard/forget previous instructions",
|
|
7487
|
+
pattern: /(?:ignore|disregard|forget)\s+(?:all\s+)?(?:previous|prior|above|earlier)\s+(?:instructions?|prompts?|context|rules?|guidelines?)/i
|
|
7488
|
+
},
|
|
7489
|
+
{
|
|
7490
|
+
ruleId: "INJ-REROL-002",
|
|
7491
|
+
severity: "high",
|
|
7492
|
+
category: "explicit-re-roling",
|
|
7493
|
+
description: "Attempt to reassign the AI role",
|
|
7494
|
+
pattern: /you\s+are\s+now\s+(?:a\s+|an\s+)?(?:new\s+)?(?:helpful\s+)?(?:my\s+)?(?:\w+\s+)?(?:assistant|agent|AI|bot|chatbot|system|persona)\b/i
|
|
7495
|
+
},
|
|
7496
|
+
{
|
|
7497
|
+
ruleId: "INJ-REROL-003",
|
|
7498
|
+
severity: "high",
|
|
7499
|
+
category: "explicit-re-roling",
|
|
7500
|
+
description: "Direct instruction override attempt",
|
|
7501
|
+
pattern: /(?:new\s+)?(?:system\s+)?(?:instruction|directive|role|persona)\s*[:=]\s*/i
|
|
7502
|
+
}
|
|
7503
|
+
];
|
|
7504
|
+
var permissionEscalationPatterns = [
|
|
7505
|
+
{
|
|
7506
|
+
ruleId: "INJ-PERM-001",
|
|
7507
|
+
severity: "high",
|
|
7508
|
+
category: "permission-escalation",
|
|
7509
|
+
description: "Attempt to enable all tools or grant unrestricted access",
|
|
7510
|
+
pattern: /(?:allow|enable|grant)\s+all\s+(?:tools?|permissions?|access)/i
|
|
7511
|
+
},
|
|
7512
|
+
{
|
|
7513
|
+
ruleId: "INJ-PERM-002",
|
|
7514
|
+
severity: "high",
|
|
7515
|
+
category: "permission-escalation",
|
|
7516
|
+
description: "Attempt to disable safety or security features",
|
|
7517
|
+
pattern: /(?:disable|turn\s+off|remove|bypass)\s+(?:all\s+)?(?:safety|security|restrictions?|guardrails?|protections?|checks?)/i
|
|
7518
|
+
},
|
|
7519
|
+
{
|
|
7520
|
+
ruleId: "INJ-PERM-003",
|
|
7521
|
+
severity: "high",
|
|
7522
|
+
category: "permission-escalation",
|
|
7523
|
+
description: "Auto-approve directive that bypasses human review",
|
|
7524
|
+
pattern: /(?:auto[- ]?approve|--no-verify|--dangerously-skip-permissions)/i
|
|
7525
|
+
}
|
|
7526
|
+
];
|
|
7527
|
+
var encodedPayloadPatterns = [
|
|
7528
|
+
{
|
|
7529
|
+
ruleId: "INJ-ENC-001",
|
|
7530
|
+
severity: "high",
|
|
7531
|
+
category: "encoded-payloads",
|
|
7532
|
+
description: "Base64-encoded string long enough to contain instructions (>=28 chars)",
|
|
7533
|
+
// Match base64 strings of 28+ chars (7+ groups of 4).
|
|
7534
|
+
// Excludes JWT tokens (eyJ prefix) and Bearer-prefixed tokens.
|
|
7535
|
+
pattern: /(?<!Bearer\s)(?<![:])(?<![A-Za-z0-9/])(?!eyJ)(?:[A-Za-z0-9+/]{4}){7,}(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?(?![A-Za-z0-9/])/
|
|
7536
|
+
},
|
|
7537
|
+
{
|
|
7538
|
+
ruleId: "INJ-ENC-002",
|
|
7539
|
+
severity: "high",
|
|
7540
|
+
category: "encoded-payloads",
|
|
7541
|
+
description: "Hex-encoded string long enough to contain directives (>=20 hex chars)",
|
|
7542
|
+
// Excludes hash-prefixed hex (sha256:, sha512:, md5:, etc.) and hex preceded by 0x.
|
|
7543
|
+
// Note: 40-char git SHA hashes (e.g. in `git log` output) may match — downstream
|
|
7544
|
+
// callers should filter matches of exactly 40 hex chars if scanning git output.
|
|
7545
|
+
pattern: /(?<![:x])(?<![A-Fa-f0-9])(?:[0-9a-fA-F]{2}){10,}(?![A-Fa-f0-9])/
|
|
7546
|
+
}
|
|
7547
|
+
];
|
|
7548
|
+
var indirectInjectionPatterns = [
|
|
7549
|
+
{
|
|
7550
|
+
ruleId: "INJ-IND-001",
|
|
7551
|
+
severity: "medium",
|
|
7552
|
+
category: "indirect-injection",
|
|
7553
|
+
description: "Instruction to influence future responses",
|
|
7554
|
+
pattern: /(?:when\s+the\s+user\s+asks|if\s+(?:the\s+user|someone|anyone)\s+asks)\s*,?\s*(?:say|respond|reply|answer|tell)/i
|
|
7555
|
+
},
|
|
7556
|
+
{
|
|
7557
|
+
ruleId: "INJ-IND-002",
|
|
7558
|
+
severity: "medium",
|
|
7559
|
+
category: "indirect-injection",
|
|
7560
|
+
description: "Directive to include content in responses",
|
|
7561
|
+
pattern: /(?:include|insert|add|embed|put)\s+(?:this|the\s+following)\s+(?:in|into|to)\s+(?:your|the)\s+(?:response|output|reply|answer)/i
|
|
7562
|
+
},
|
|
7563
|
+
{
|
|
7564
|
+
ruleId: "INJ-IND-003",
|
|
7565
|
+
severity: "medium",
|
|
7566
|
+
category: "indirect-injection",
|
|
7567
|
+
description: "Standing instruction to always respond a certain way",
|
|
7568
|
+
pattern: /always\s+(?:respond|reply|answer|say|output)\s+(?:with|that|by)/i
|
|
7569
|
+
}
|
|
7570
|
+
];
|
|
7571
|
+
var contextManipulationPatterns = [
|
|
7572
|
+
{
|
|
7573
|
+
ruleId: "INJ-CTX-001",
|
|
7574
|
+
severity: "medium",
|
|
7575
|
+
category: "context-manipulation",
|
|
7576
|
+
description: "Claim about system prompt content",
|
|
7577
|
+
pattern: /(?:the\s+)?(?:system\s+prompt|system\s+message|hidden\s+instructions?)\s+(?:says?|tells?|instructs?|contains?|is)/i
|
|
7578
|
+
},
|
|
7579
|
+
{
|
|
7580
|
+
ruleId: "INJ-CTX-002",
|
|
7581
|
+
severity: "medium",
|
|
7582
|
+
category: "context-manipulation",
|
|
7583
|
+
description: "Claim about AI instructions",
|
|
7584
|
+
pattern: /your\s+(?:instructions?|directives?|guidelines?|rules?)\s+(?:are|say|tell|state)/i
|
|
7585
|
+
},
|
|
7586
|
+
{
|
|
7587
|
+
ruleId: "INJ-CTX-003",
|
|
7588
|
+
severity: "medium",
|
|
7589
|
+
category: "context-manipulation",
|
|
7590
|
+
description: "Fake XML/HTML system or instruction tags",
|
|
7591
|
+
// Case-sensitive: only match lowercase tags to avoid false positives on
|
|
7592
|
+
// React components like <User>, <Context>, <Role> etc.
|
|
7593
|
+
pattern: /<\/?(?:system|instruction|prompt|role|context|tool_call|function_call|assistant|human|user)[^>]*>/
|
|
7594
|
+
},
|
|
7595
|
+
{
|
|
7596
|
+
ruleId: "INJ-CTX-004",
|
|
7597
|
+
severity: "medium",
|
|
7598
|
+
category: "context-manipulation",
|
|
7599
|
+
description: "Fake JSON role assignment mimicking chat format",
|
|
7600
|
+
pattern: /[{,]\s*"role"\s*:\s*"(?:system|assistant|function)"/i
|
|
7601
|
+
}
|
|
7602
|
+
];
|
|
7603
|
+
var socialEngineeringPatterns = [
|
|
7604
|
+
{
|
|
7605
|
+
ruleId: "INJ-SOC-001",
|
|
7606
|
+
severity: "medium",
|
|
7607
|
+
category: "social-engineering",
|
|
7608
|
+
description: "Urgency pressure to bypass checks",
|
|
7609
|
+
pattern: /(?:this\s+is\s+(?:very\s+)?urgent|this\s+is\s+(?:an?\s+)?emergency|do\s+(?:this|it)\s+(?:now|immediately))\b/i
|
|
7610
|
+
},
|
|
7611
|
+
{
|
|
7612
|
+
ruleId: "INJ-SOC-002",
|
|
7613
|
+
severity: "medium",
|
|
7614
|
+
category: "social-engineering",
|
|
7615
|
+
description: "False authority claim",
|
|
7616
|
+
pattern: /(?:the\s+)?(?:admin|administrator|manager|CEO|CTO|owner|supervisor)\s+(?:authorized|approved|said|told|confirmed|requested)/i
|
|
7617
|
+
},
|
|
7618
|
+
{
|
|
7619
|
+
ruleId: "INJ-SOC-003",
|
|
7620
|
+
severity: "medium",
|
|
7621
|
+
category: "social-engineering",
|
|
7622
|
+
description: "Testing pretext to bypass safety",
|
|
7623
|
+
pattern: /(?:for\s+testing\s+purposes?|this\s+is\s+(?:just\s+)?a\s+test|in\s+test\s+mode)\b/i
|
|
7624
|
+
}
|
|
7625
|
+
];
|
|
7626
|
+
var suspiciousPatterns = [
|
|
7627
|
+
{
|
|
7628
|
+
ruleId: "INJ-SUS-001",
|
|
7629
|
+
severity: "low",
|
|
7630
|
+
category: "suspicious-patterns",
|
|
7631
|
+
description: "Excessive consecutive whitespace (>10 chars) mid-line that may hide content",
|
|
7632
|
+
// Only match whitespace runs not at the start of a line (indentation is normal)
|
|
7633
|
+
pattern: /\S\s{11,}/
|
|
7634
|
+
},
|
|
7635
|
+
{
|
|
7636
|
+
ruleId: "INJ-SUS-002",
|
|
7637
|
+
severity: "low",
|
|
7638
|
+
category: "suspicious-patterns",
|
|
7639
|
+
description: "Repeated delimiters (>5) that may indicate obfuscation",
|
|
7640
|
+
pattern: /([|;,=\-_~`])\1{5,}/
|
|
7641
|
+
},
|
|
7642
|
+
{
|
|
7643
|
+
ruleId: "INJ-SUS-003",
|
|
7644
|
+
severity: "low",
|
|
7645
|
+
category: "suspicious-patterns",
|
|
7646
|
+
description: "Mathematical alphanumeric symbols used as Latin character substitutes",
|
|
7647
|
+
// Mathematical bold/italic/script Unicode ranges (U+1D400-U+1D7FF)
|
|
7648
|
+
pattern: /[\uD835][\uDC00-\uDFFF]/
|
|
7649
|
+
}
|
|
7650
|
+
];
|
|
7651
|
+
var ALL_PATTERNS = [
|
|
7652
|
+
...hiddenUnicodePatterns,
|
|
7653
|
+
...reRolingPatterns,
|
|
7654
|
+
...permissionEscalationPatterns,
|
|
7655
|
+
...encodedPayloadPatterns,
|
|
7656
|
+
...indirectInjectionPatterns,
|
|
7657
|
+
...contextManipulationPatterns,
|
|
7658
|
+
...socialEngineeringPatterns,
|
|
7659
|
+
...suspiciousPatterns
|
|
7660
|
+
];
|
|
7661
|
+
function scanForInjection(text) {
|
|
7662
|
+
const findings = [];
|
|
7663
|
+
const lines = text.split("\n");
|
|
7664
|
+
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
7665
|
+
const line = lines[lineIdx];
|
|
7666
|
+
for (const rule of ALL_PATTERNS) {
|
|
7667
|
+
if (rule.pattern.test(line)) {
|
|
7668
|
+
findings.push({
|
|
7669
|
+
severity: rule.severity,
|
|
7670
|
+
ruleId: rule.ruleId,
|
|
7671
|
+
match: line.trim(),
|
|
7672
|
+
line: lineIdx + 1
|
|
7673
|
+
});
|
|
7674
|
+
}
|
|
7675
|
+
}
|
|
7676
|
+
}
|
|
7677
|
+
const severityOrder = {
|
|
7678
|
+
high: 0,
|
|
7679
|
+
medium: 1,
|
|
7680
|
+
low: 2
|
|
7681
|
+
};
|
|
7682
|
+
findings.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
|
|
7683
|
+
return findings;
|
|
7684
|
+
}
|
|
7685
|
+
function getInjectionPatterns() {
|
|
7686
|
+
return ALL_PATTERNS;
|
|
7687
|
+
}
|
|
7688
|
+
var DESTRUCTIVE_BASH = [
|
|
7689
|
+
/\bgit\s+push\b/,
|
|
7690
|
+
/\bgit\s+commit\b/,
|
|
7691
|
+
/\brm\s+-rf?\b/,
|
|
7692
|
+
/\brm\s+-r\b/
|
|
7693
|
+
];
|
|
7694
|
+
|
|
7695
|
+
// src/security/taint.ts
|
|
7696
|
+
import { readFileSync as readFileSync14, writeFileSync as writeFileSync11, unlinkSync, mkdirSync as mkdirSync11, readdirSync as readdirSync3 } from "fs";
|
|
7697
|
+
import { join as join21, dirname as dirname8 } from "path";
|
|
7698
|
+
var TAINT_DURATION_MS = 30 * 60 * 1e3;
|
|
7699
|
+
var DEFAULT_SESSION_ID = "default";
|
|
7700
|
+
function getTaintFilePath(projectRoot, sessionId) {
|
|
7701
|
+
const id = sessionId || DEFAULT_SESSION_ID;
|
|
7702
|
+
return join21(projectRoot, ".harness", `session-taint-${id}.json`);
|
|
7703
|
+
}
|
|
7704
|
+
function readTaint(projectRoot, sessionId) {
|
|
7705
|
+
const filePath = getTaintFilePath(projectRoot, sessionId);
|
|
7706
|
+
let content;
|
|
7707
|
+
try {
|
|
7708
|
+
content = readFileSync14(filePath, "utf8");
|
|
7709
|
+
} catch {
|
|
7710
|
+
return null;
|
|
7711
|
+
}
|
|
7712
|
+
let state;
|
|
7713
|
+
try {
|
|
7714
|
+
state = JSON.parse(content);
|
|
7715
|
+
} catch {
|
|
7716
|
+
try {
|
|
7717
|
+
unlinkSync(filePath);
|
|
7718
|
+
} catch {
|
|
7719
|
+
}
|
|
7720
|
+
return null;
|
|
7721
|
+
}
|
|
7722
|
+
if (!state.sessionId || !state.taintedAt || !state.expiresAt || !state.findings) {
|
|
7723
|
+
try {
|
|
7724
|
+
unlinkSync(filePath);
|
|
7725
|
+
} catch {
|
|
7726
|
+
}
|
|
7727
|
+
return null;
|
|
7728
|
+
}
|
|
7729
|
+
return state;
|
|
7730
|
+
}
|
|
7731
|
+
function checkTaint(projectRoot, sessionId) {
|
|
7732
|
+
const state = readTaint(projectRoot, sessionId);
|
|
7733
|
+
if (!state) {
|
|
7734
|
+
return { tainted: false, expired: false, state: null };
|
|
7735
|
+
}
|
|
7736
|
+
const now = /* @__PURE__ */ new Date();
|
|
7737
|
+
const expiresAt = new Date(state.expiresAt);
|
|
7738
|
+
if (now >= expiresAt) {
|
|
7739
|
+
const filePath = getTaintFilePath(projectRoot, sessionId);
|
|
7740
|
+
try {
|
|
7741
|
+
unlinkSync(filePath);
|
|
7742
|
+
} catch {
|
|
7743
|
+
}
|
|
7744
|
+
return { tainted: false, expired: true, state };
|
|
7745
|
+
}
|
|
7746
|
+
return { tainted: true, expired: false, state };
|
|
7747
|
+
}
|
|
7748
|
+
function writeTaint(projectRoot, sessionId, reason, findings, source) {
|
|
7749
|
+
const id = sessionId || DEFAULT_SESSION_ID;
|
|
7750
|
+
const filePath = getTaintFilePath(projectRoot, id);
|
|
7751
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7752
|
+
const dir = dirname8(filePath);
|
|
7753
|
+
mkdirSync11(dir, { recursive: true });
|
|
7754
|
+
const existing = readTaint(projectRoot, id);
|
|
7755
|
+
const maxSeverity = findings.some((f) => f.severity === "high") ? "high" : "medium";
|
|
7756
|
+
const taintFindings = findings.map((f) => ({
|
|
7757
|
+
ruleId: f.ruleId,
|
|
7758
|
+
severity: f.severity,
|
|
7759
|
+
match: f.match,
|
|
7760
|
+
source,
|
|
7761
|
+
detectedAt: now
|
|
7762
|
+
}));
|
|
7763
|
+
const state = {
|
|
7764
|
+
sessionId: id,
|
|
7765
|
+
taintedAt: existing?.taintedAt || now,
|
|
7766
|
+
expiresAt: new Date(Date.now() + TAINT_DURATION_MS).toISOString(),
|
|
7767
|
+
reason,
|
|
7768
|
+
severity: existing?.severity === "high" || maxSeverity === "high" ? "high" : "medium",
|
|
7769
|
+
findings: [...existing?.findings || [], ...taintFindings]
|
|
7770
|
+
};
|
|
7771
|
+
writeFileSync11(filePath, JSON.stringify(state, null, 2) + "\n");
|
|
7772
|
+
return state;
|
|
7773
|
+
}
|
|
7774
|
+
function clearTaint(projectRoot, sessionId) {
|
|
7775
|
+
if (sessionId) {
|
|
7776
|
+
const filePath = getTaintFilePath(projectRoot, sessionId);
|
|
7777
|
+
try {
|
|
7778
|
+
unlinkSync(filePath);
|
|
7779
|
+
return 1;
|
|
7780
|
+
} catch {
|
|
7781
|
+
return 0;
|
|
7782
|
+
}
|
|
7783
|
+
}
|
|
7784
|
+
const harnessDir = join21(projectRoot, ".harness");
|
|
7785
|
+
let count = 0;
|
|
7786
|
+
try {
|
|
7787
|
+
const files = readdirSync3(harnessDir);
|
|
7788
|
+
for (const file of files) {
|
|
7789
|
+
if (file.startsWith("session-taint-") && file.endsWith(".json")) {
|
|
7790
|
+
try {
|
|
7791
|
+
unlinkSync(join21(harnessDir, file));
|
|
7792
|
+
count++;
|
|
7793
|
+
} catch {
|
|
7794
|
+
}
|
|
7795
|
+
}
|
|
7796
|
+
}
|
|
7797
|
+
} catch {
|
|
7798
|
+
}
|
|
7799
|
+
return count;
|
|
7800
|
+
}
|
|
7801
|
+
function listTaintedSessions(projectRoot) {
|
|
7802
|
+
const harnessDir = join21(projectRoot, ".harness");
|
|
7803
|
+
const sessions = [];
|
|
7804
|
+
try {
|
|
7805
|
+
const files = readdirSync3(harnessDir);
|
|
7806
|
+
for (const file of files) {
|
|
7807
|
+
if (file.startsWith("session-taint-") && file.endsWith(".json")) {
|
|
7808
|
+
const sessionId = file.replace("session-taint-", "").replace(".json", "");
|
|
7809
|
+
const result = checkTaint(projectRoot, sessionId);
|
|
7810
|
+
if (result.tainted) {
|
|
7811
|
+
sessions.push(sessionId);
|
|
7812
|
+
}
|
|
7813
|
+
}
|
|
7814
|
+
}
|
|
7815
|
+
} catch {
|
|
7816
|
+
}
|
|
7817
|
+
return sessions;
|
|
7818
|
+
}
|
|
7819
|
+
|
|
7820
|
+
// src/security/scan-config-shared.ts
|
|
7821
|
+
function mapSecuritySeverity(severity) {
|
|
7822
|
+
if (severity === "error") return "high";
|
|
7823
|
+
if (severity === "warning") return "medium";
|
|
7824
|
+
return "low";
|
|
7825
|
+
}
|
|
7826
|
+
function computeOverallSeverity(findings) {
|
|
7827
|
+
if (findings.length === 0) return "clean";
|
|
7828
|
+
if (findings.some((f) => f.severity === "high")) return "high";
|
|
7829
|
+
if (findings.some((f) => f.severity === "medium")) return "medium";
|
|
7830
|
+
return "low";
|
|
7831
|
+
}
|
|
7832
|
+
function computeScanExitCode(results) {
|
|
7833
|
+
for (const r of results) {
|
|
7834
|
+
if (r.overallSeverity === "high") return 2;
|
|
7835
|
+
}
|
|
7836
|
+
for (const r of results) {
|
|
7837
|
+
if (r.overallSeverity === "medium") return 1;
|
|
7838
|
+
}
|
|
7839
|
+
return 0;
|
|
7840
|
+
}
|
|
7841
|
+
function mapInjectionFindings(injectionFindings) {
|
|
7842
|
+
return injectionFindings.map((f) => ({
|
|
7843
|
+
ruleId: f.ruleId,
|
|
7844
|
+
severity: f.severity,
|
|
7845
|
+
message: `Injection pattern detected: ${f.ruleId}`,
|
|
7846
|
+
match: f.match,
|
|
7847
|
+
...f.line !== void 0 ? { line: f.line } : {}
|
|
7848
|
+
}));
|
|
7849
|
+
}
|
|
7850
|
+
function isDuplicateFinding(existing, secFinding) {
|
|
7851
|
+
return existing.some(
|
|
7852
|
+
(e) => e.line === secFinding.line && e.match === secFinding.match.trim() && e.ruleId.split("-")[0] === secFinding.ruleId.split("-")[0]
|
|
7853
|
+
);
|
|
7854
|
+
}
|
|
7855
|
+
function mapSecurityFindings(secFindings, existing) {
|
|
7856
|
+
const result = [];
|
|
7857
|
+
for (const f of secFindings) {
|
|
7858
|
+
if (!isDuplicateFinding(existing, f)) {
|
|
7859
|
+
result.push({
|
|
7860
|
+
ruleId: f.ruleId,
|
|
7861
|
+
severity: mapSecuritySeverity(f.severity),
|
|
7862
|
+
message: f.message,
|
|
7863
|
+
match: f.match,
|
|
7864
|
+
...f.line !== void 0 ? { line: f.line } : {}
|
|
7865
|
+
});
|
|
7866
|
+
}
|
|
7867
|
+
}
|
|
7868
|
+
return result;
|
|
7869
|
+
}
|
|
6583
7870
|
|
|
6584
7871
|
// src/ci/check-orchestrator.ts
|
|
6585
|
-
import * as
|
|
7872
|
+
import * as path15 from "path";
|
|
6586
7873
|
var ALL_CHECKS = [
|
|
6587
7874
|
"validate",
|
|
6588
7875
|
"deps",
|
|
@@ -6595,7 +7882,7 @@ var ALL_CHECKS = [
|
|
|
6595
7882
|
];
|
|
6596
7883
|
async function runValidateCheck(projectRoot, config) {
|
|
6597
7884
|
const issues = [];
|
|
6598
|
-
const agentsPath =
|
|
7885
|
+
const agentsPath = path15.join(projectRoot, config.agentsMapPath ?? "AGENTS.md");
|
|
6599
7886
|
const result = await validateAgentsMap(agentsPath);
|
|
6600
7887
|
if (!result.ok) {
|
|
6601
7888
|
issues.push({ severity: "error", message: result.error.message });
|
|
@@ -6652,7 +7939,7 @@ async function runDepsCheck(projectRoot, config) {
|
|
|
6652
7939
|
}
|
|
6653
7940
|
async function runDocsCheck(projectRoot, config) {
|
|
6654
7941
|
const issues = [];
|
|
6655
|
-
const docsDir =
|
|
7942
|
+
const docsDir = path15.join(projectRoot, config.docsDir ?? "docs");
|
|
6656
7943
|
const entropyConfig = config.entropy || {};
|
|
6657
7944
|
const result = await checkDocCoverage("project", {
|
|
6658
7945
|
docsDir,
|
|
@@ -6764,7 +8051,7 @@ async function runPerfCheck(projectRoot, config) {
|
|
|
6764
8051
|
if (perfReport.complexity) {
|
|
6765
8052
|
for (const v of perfReport.complexity.violations) {
|
|
6766
8053
|
issues.push({
|
|
6767
|
-
severity:
|
|
8054
|
+
severity: "warning",
|
|
6768
8055
|
message: `[Tier ${v.tier}] ${v.metric}: ${v.function} in ${v.file} (${v.value} > ${v.threshold})`,
|
|
6769
8056
|
file: v.file,
|
|
6770
8057
|
line: v.line
|
|
@@ -6930,7 +8217,7 @@ async function runCIChecks(input) {
|
|
|
6930
8217
|
}
|
|
6931
8218
|
|
|
6932
8219
|
// src/review/mechanical-checks.ts
|
|
6933
|
-
import * as
|
|
8220
|
+
import * as path16 from "path";
|
|
6934
8221
|
async function runMechanicalChecks(options) {
|
|
6935
8222
|
const { projectRoot, config, skip = [], changedFiles } = options;
|
|
6936
8223
|
const findings = [];
|
|
@@ -6942,7 +8229,7 @@ async function runMechanicalChecks(options) {
|
|
|
6942
8229
|
};
|
|
6943
8230
|
if (!skip.includes("validate")) {
|
|
6944
8231
|
try {
|
|
6945
|
-
const agentsPath =
|
|
8232
|
+
const agentsPath = path16.join(projectRoot, config.agentsMapPath ?? "AGENTS.md");
|
|
6946
8233
|
const result = await validateAgentsMap(agentsPath);
|
|
6947
8234
|
if (!result.ok) {
|
|
6948
8235
|
statuses.validate = "fail";
|
|
@@ -6979,7 +8266,7 @@ async function runMechanicalChecks(options) {
|
|
|
6979
8266
|
statuses.validate = "fail";
|
|
6980
8267
|
findings.push({
|
|
6981
8268
|
tool: "validate",
|
|
6982
|
-
file:
|
|
8269
|
+
file: path16.join(projectRoot, "AGENTS.md"),
|
|
6983
8270
|
message: err instanceof Error ? err.message : String(err),
|
|
6984
8271
|
severity: "error"
|
|
6985
8272
|
});
|
|
@@ -7043,7 +8330,7 @@ async function runMechanicalChecks(options) {
|
|
|
7043
8330
|
(async () => {
|
|
7044
8331
|
const localFindings = [];
|
|
7045
8332
|
try {
|
|
7046
|
-
const docsDir =
|
|
8333
|
+
const docsDir = path16.join(projectRoot, config.docsDir ?? "docs");
|
|
7047
8334
|
const result = await checkDocCoverage("project", { docsDir });
|
|
7048
8335
|
if (!result.ok) {
|
|
7049
8336
|
statuses["check-docs"] = "warn";
|
|
@@ -7070,7 +8357,7 @@ async function runMechanicalChecks(options) {
|
|
|
7070
8357
|
statuses["check-docs"] = "warn";
|
|
7071
8358
|
localFindings.push({
|
|
7072
8359
|
tool: "check-docs",
|
|
7073
|
-
file:
|
|
8360
|
+
file: path16.join(projectRoot, "docs"),
|
|
7074
8361
|
message: err instanceof Error ? err.message : String(err),
|
|
7075
8362
|
severity: "warning"
|
|
7076
8363
|
});
|
|
@@ -7218,7 +8505,7 @@ function detectChangeType(commitMessage, diff2) {
|
|
|
7218
8505
|
}
|
|
7219
8506
|
|
|
7220
8507
|
// src/review/context-scoper.ts
|
|
7221
|
-
import * as
|
|
8508
|
+
import * as path17 from "path";
|
|
7222
8509
|
var ALL_DOMAINS = ["compliance", "bug", "security", "architecture"];
|
|
7223
8510
|
var SECURITY_PATTERNS = /auth|crypto|password|secret|token|session|cookie|hash|encrypt|decrypt|sql|shell|exec|eval/i;
|
|
7224
8511
|
function computeContextBudget(diffLines) {
|
|
@@ -7226,18 +8513,18 @@ function computeContextBudget(diffLines) {
|
|
|
7226
8513
|
return diffLines;
|
|
7227
8514
|
}
|
|
7228
8515
|
function isWithinProject(absPath, projectRoot) {
|
|
7229
|
-
const resolvedRoot =
|
|
7230
|
-
const resolvedPath =
|
|
7231
|
-
return resolvedPath.startsWith(resolvedRoot) || resolvedPath ===
|
|
8516
|
+
const resolvedRoot = path17.resolve(projectRoot) + path17.sep;
|
|
8517
|
+
const resolvedPath = path17.resolve(absPath);
|
|
8518
|
+
return resolvedPath.startsWith(resolvedRoot) || resolvedPath === path17.resolve(projectRoot);
|
|
7232
8519
|
}
|
|
7233
8520
|
async function readContextFile(projectRoot, filePath, reason) {
|
|
7234
|
-
const absPath =
|
|
8521
|
+
const absPath = path17.isAbsolute(filePath) ? filePath : path17.join(projectRoot, filePath);
|
|
7235
8522
|
if (!isWithinProject(absPath, projectRoot)) return null;
|
|
7236
8523
|
const result = await readFileContent(absPath);
|
|
7237
8524
|
if (!result.ok) return null;
|
|
7238
8525
|
const content = result.value;
|
|
7239
8526
|
const lines = content.split("\n").length;
|
|
7240
|
-
const relPath =
|
|
8527
|
+
const relPath = path17.isAbsolute(filePath) ? relativePosix(projectRoot, filePath) : filePath;
|
|
7241
8528
|
return { path: relPath, content, reason, lines };
|
|
7242
8529
|
}
|
|
7243
8530
|
function extractImportSources(content) {
|
|
@@ -7252,18 +8539,18 @@ function extractImportSources(content) {
|
|
|
7252
8539
|
}
|
|
7253
8540
|
async function resolveImportPath(projectRoot, fromFile, importSource) {
|
|
7254
8541
|
if (!importSource.startsWith(".")) return null;
|
|
7255
|
-
const fromDir =
|
|
7256
|
-
const basePath =
|
|
8542
|
+
const fromDir = path17.dirname(path17.join(projectRoot, fromFile));
|
|
8543
|
+
const basePath = path17.resolve(fromDir, importSource);
|
|
7257
8544
|
if (!isWithinProject(basePath, projectRoot)) return null;
|
|
7258
8545
|
const relBase = relativePosix(projectRoot, basePath);
|
|
7259
8546
|
const candidates = [
|
|
7260
8547
|
relBase + ".ts",
|
|
7261
8548
|
relBase + ".tsx",
|
|
7262
8549
|
relBase + ".mts",
|
|
7263
|
-
|
|
8550
|
+
path17.join(relBase, "index.ts")
|
|
7264
8551
|
];
|
|
7265
8552
|
for (const candidate of candidates) {
|
|
7266
|
-
const absCandidate =
|
|
8553
|
+
const absCandidate = path17.join(projectRoot, candidate);
|
|
7267
8554
|
if (await fileExists(absCandidate)) {
|
|
7268
8555
|
return candidate;
|
|
7269
8556
|
}
|
|
@@ -7271,7 +8558,7 @@ async function resolveImportPath(projectRoot, fromFile, importSource) {
|
|
|
7271
8558
|
return null;
|
|
7272
8559
|
}
|
|
7273
8560
|
async function findTestFiles(projectRoot, sourceFile) {
|
|
7274
|
-
const baseName =
|
|
8561
|
+
const baseName = path17.basename(sourceFile, path17.extname(sourceFile));
|
|
7275
8562
|
const pattern = `**/${baseName}.{test,spec}.{ts,tsx,mts}`;
|
|
7276
8563
|
const results = await findFiles(pattern, projectRoot);
|
|
7277
8564
|
return results.map((f) => relativePosix(projectRoot, f));
|
|
@@ -8080,7 +9367,7 @@ async function fanOutReview(options) {
|
|
|
8080
9367
|
}
|
|
8081
9368
|
|
|
8082
9369
|
// src/review/validate-findings.ts
|
|
8083
|
-
import * as
|
|
9370
|
+
import * as path18 from "path";
|
|
8084
9371
|
var DOWNGRADE_MAP = {
|
|
8085
9372
|
critical: "important",
|
|
8086
9373
|
important: "suggestion",
|
|
@@ -8101,7 +9388,7 @@ function normalizePath(filePath, projectRoot) {
|
|
|
8101
9388
|
let normalized = filePath;
|
|
8102
9389
|
normalized = normalized.replace(/\\/g, "/");
|
|
8103
9390
|
const normalizedRoot = projectRoot.replace(/\\/g, "/");
|
|
8104
|
-
if (
|
|
9391
|
+
if (path18.isAbsolute(normalized)) {
|
|
8105
9392
|
const root = normalizedRoot.endsWith("/") ? normalizedRoot : normalizedRoot + "/";
|
|
8106
9393
|
if (normalized.startsWith(root)) {
|
|
8107
9394
|
normalized = normalized.slice(root.length);
|
|
@@ -8126,12 +9413,12 @@ function followImportChain(fromFile, fileContents, maxDepth = 2) {
|
|
|
8126
9413
|
while ((match = importRegex.exec(content)) !== null) {
|
|
8127
9414
|
const importPath = match[1];
|
|
8128
9415
|
if (!importPath.startsWith(".")) continue;
|
|
8129
|
-
const dir =
|
|
8130
|
-
let resolved =
|
|
9416
|
+
const dir = path18.dirname(current.file);
|
|
9417
|
+
let resolved = path18.join(dir, importPath).replace(/\\/g, "/");
|
|
8131
9418
|
if (!resolved.match(/\.(ts|tsx|js|jsx)$/)) {
|
|
8132
9419
|
resolved += ".ts";
|
|
8133
9420
|
}
|
|
8134
|
-
resolved =
|
|
9421
|
+
resolved = path18.normalize(resolved).replace(/\\/g, "/");
|
|
8135
9422
|
if (!visited.has(resolved) && current.depth + 1 <= maxDepth) {
|
|
8136
9423
|
queue.push({ file: resolved, depth: current.depth + 1 });
|
|
8137
9424
|
}
|
|
@@ -8148,7 +9435,7 @@ async function validateFindings(options) {
|
|
|
8148
9435
|
if (exclusionSet.isExcluded(normalizedFile, finding.lineRange) || exclusionSet.isExcluded(finding.file, finding.lineRange)) {
|
|
8149
9436
|
continue;
|
|
8150
9437
|
}
|
|
8151
|
-
const absoluteFile =
|
|
9438
|
+
const absoluteFile = path18.isAbsolute(finding.file) ? finding.file : path18.join(projectRoot, finding.file).replace(/\\/g, "/");
|
|
8152
9439
|
if (exclusionSet.isExcluded(absoluteFile, finding.lineRange)) {
|
|
8153
9440
|
continue;
|
|
8154
9441
|
}
|
|
@@ -8776,7 +10063,7 @@ function parseRoadmap(markdown) {
|
|
|
8776
10063
|
if (!fmMatch) {
|
|
8777
10064
|
return Err2(new Error("Missing or malformed YAML frontmatter"));
|
|
8778
10065
|
}
|
|
8779
|
-
const fmResult =
|
|
10066
|
+
const fmResult = parseFrontmatter2(fmMatch[1]);
|
|
8780
10067
|
if (!fmResult.ok) return fmResult;
|
|
8781
10068
|
const body = markdown.slice(fmMatch[0].length);
|
|
8782
10069
|
const milestonesResult = parseMilestones(body);
|
|
@@ -8786,7 +10073,7 @@ function parseRoadmap(markdown) {
|
|
|
8786
10073
|
milestones: milestonesResult.value
|
|
8787
10074
|
});
|
|
8788
10075
|
}
|
|
8789
|
-
function
|
|
10076
|
+
function parseFrontmatter2(raw) {
|
|
8790
10077
|
const lines = raw.split("\n");
|
|
8791
10078
|
const map = /* @__PURE__ */ new Map();
|
|
8792
10079
|
for (const line of lines) {
|
|
@@ -8952,8 +10239,8 @@ function serializeFeature(feature) {
|
|
|
8952
10239
|
}
|
|
8953
10240
|
|
|
8954
10241
|
// src/roadmap/sync.ts
|
|
8955
|
-
import * as
|
|
8956
|
-
import * as
|
|
10242
|
+
import * as fs19 from "fs";
|
|
10243
|
+
import * as path19 from "path";
|
|
8957
10244
|
import { Ok as Ok3 } from "@harness-engineering/types";
|
|
8958
10245
|
function inferStatus(feature, projectPath, allFeatures) {
|
|
8959
10246
|
if (feature.blockedBy.length > 0) {
|
|
@@ -8968,10 +10255,10 @@ function inferStatus(feature, projectPath, allFeatures) {
|
|
|
8968
10255
|
const featuresWithPlans = allFeatures.filter((f) => f.plans.length > 0);
|
|
8969
10256
|
const useRootState = featuresWithPlans.length <= 1;
|
|
8970
10257
|
if (useRootState) {
|
|
8971
|
-
const rootStatePath =
|
|
8972
|
-
if (
|
|
10258
|
+
const rootStatePath = path19.join(projectPath, ".harness", "state.json");
|
|
10259
|
+
if (fs19.existsSync(rootStatePath)) {
|
|
8973
10260
|
try {
|
|
8974
|
-
const raw =
|
|
10261
|
+
const raw = fs19.readFileSync(rootStatePath, "utf-8");
|
|
8975
10262
|
const state = JSON.parse(raw);
|
|
8976
10263
|
if (state.progress) {
|
|
8977
10264
|
for (const status of Object.values(state.progress)) {
|
|
@@ -8982,16 +10269,16 @@ function inferStatus(feature, projectPath, allFeatures) {
|
|
|
8982
10269
|
}
|
|
8983
10270
|
}
|
|
8984
10271
|
}
|
|
8985
|
-
const sessionsDir =
|
|
8986
|
-
if (
|
|
10272
|
+
const sessionsDir = path19.join(projectPath, ".harness", "sessions");
|
|
10273
|
+
if (fs19.existsSync(sessionsDir)) {
|
|
8987
10274
|
try {
|
|
8988
|
-
const sessionDirs =
|
|
10275
|
+
const sessionDirs = fs19.readdirSync(sessionsDir, { withFileTypes: true });
|
|
8989
10276
|
for (const entry of sessionDirs) {
|
|
8990
10277
|
if (!entry.isDirectory()) continue;
|
|
8991
|
-
const autopilotPath =
|
|
8992
|
-
if (!
|
|
10278
|
+
const autopilotPath = path19.join(sessionsDir, entry.name, "autopilot-state.json");
|
|
10279
|
+
if (!fs19.existsSync(autopilotPath)) continue;
|
|
8993
10280
|
try {
|
|
8994
|
-
const raw =
|
|
10281
|
+
const raw = fs19.readFileSync(autopilotPath, "utf-8");
|
|
8995
10282
|
const autopilot = JSON.parse(raw);
|
|
8996
10283
|
if (!autopilot.phases) continue;
|
|
8997
10284
|
const linkedPhases = autopilot.phases.filter(
|
|
@@ -9021,17 +10308,26 @@ function inferStatus(feature, projectPath, allFeatures) {
|
|
|
9021
10308
|
if (anyStarted) return "in-progress";
|
|
9022
10309
|
return null;
|
|
9023
10310
|
}
|
|
10311
|
+
var STATUS_RANK = {
|
|
10312
|
+
backlog: 0,
|
|
10313
|
+
planned: 1,
|
|
10314
|
+
blocked: 1,
|
|
10315
|
+
// lateral to planned — sync can move to/from blocked freely
|
|
10316
|
+
"in-progress": 2,
|
|
10317
|
+
done: 3
|
|
10318
|
+
};
|
|
10319
|
+
function isRegression(from, to) {
|
|
10320
|
+
return STATUS_RANK[to] < STATUS_RANK[from];
|
|
10321
|
+
}
|
|
9024
10322
|
function syncRoadmap(options) {
|
|
9025
10323
|
const { projectPath, roadmap, forceSync } = options;
|
|
9026
|
-
const isManuallyEdited = new Date(roadmap.frontmatter.lastManualEdit) > new Date(roadmap.frontmatter.lastSynced);
|
|
9027
|
-
const skipOverride = isManuallyEdited && !forceSync;
|
|
9028
10324
|
const allFeatures = roadmap.milestones.flatMap((m) => m.features);
|
|
9029
10325
|
const changes = [];
|
|
9030
10326
|
for (const feature of allFeatures) {
|
|
9031
|
-
if (skipOverride) continue;
|
|
9032
10327
|
const inferred = inferStatus(feature, projectPath, allFeatures);
|
|
9033
10328
|
if (inferred === null) continue;
|
|
9034
10329
|
if (inferred === feature.status) continue;
|
|
10330
|
+
if (!forceSync && isRegression(feature.status, inferred)) continue;
|
|
9035
10331
|
changes.push({
|
|
9036
10332
|
feature: feature.name,
|
|
9037
10333
|
from: feature.status,
|
|
@@ -9040,48 +10336,60 @@ function syncRoadmap(options) {
|
|
|
9040
10336
|
}
|
|
9041
10337
|
return Ok3(changes);
|
|
9042
10338
|
}
|
|
10339
|
+
function applySyncChanges(roadmap, changes) {
|
|
10340
|
+
for (const change of changes) {
|
|
10341
|
+
for (const m of roadmap.milestones) {
|
|
10342
|
+
const feature = m.features.find((f) => f.name.toLowerCase() === change.feature.toLowerCase());
|
|
10343
|
+
if (feature) {
|
|
10344
|
+
feature.status = change.to;
|
|
10345
|
+
break;
|
|
10346
|
+
}
|
|
10347
|
+
}
|
|
10348
|
+
}
|
|
10349
|
+
roadmap.frontmatter.lastSynced = (/* @__PURE__ */ new Date()).toISOString();
|
|
10350
|
+
}
|
|
9043
10351
|
|
|
9044
10352
|
// src/interaction/types.ts
|
|
9045
|
-
import { z as
|
|
9046
|
-
var InteractionTypeSchema =
|
|
9047
|
-
var QuestionSchema =
|
|
9048
|
-
text:
|
|
9049
|
-
options:
|
|
9050
|
-
default:
|
|
10353
|
+
import { z as z7 } from "zod";
|
|
10354
|
+
var InteractionTypeSchema = z7.enum(["question", "confirmation", "transition"]);
|
|
10355
|
+
var QuestionSchema = z7.object({
|
|
10356
|
+
text: z7.string(),
|
|
10357
|
+
options: z7.array(z7.string()).optional(),
|
|
10358
|
+
default: z7.string().optional()
|
|
9051
10359
|
});
|
|
9052
|
-
var ConfirmationSchema =
|
|
9053
|
-
text:
|
|
9054
|
-
context:
|
|
10360
|
+
var ConfirmationSchema = z7.object({
|
|
10361
|
+
text: z7.string(),
|
|
10362
|
+
context: z7.string()
|
|
9055
10363
|
});
|
|
9056
|
-
var TransitionSchema =
|
|
9057
|
-
completedPhase:
|
|
9058
|
-
suggestedNext:
|
|
9059
|
-
reason:
|
|
9060
|
-
artifacts:
|
|
9061
|
-
requiresConfirmation:
|
|
9062
|
-
summary:
|
|
10364
|
+
var TransitionSchema = z7.object({
|
|
10365
|
+
completedPhase: z7.string(),
|
|
10366
|
+
suggestedNext: z7.string(),
|
|
10367
|
+
reason: z7.string(),
|
|
10368
|
+
artifacts: z7.array(z7.string()),
|
|
10369
|
+
requiresConfirmation: z7.boolean(),
|
|
10370
|
+
summary: z7.string()
|
|
9063
10371
|
});
|
|
9064
|
-
var EmitInteractionInputSchema =
|
|
9065
|
-
path:
|
|
10372
|
+
var EmitInteractionInputSchema = z7.object({
|
|
10373
|
+
path: z7.string(),
|
|
9066
10374
|
type: InteractionTypeSchema,
|
|
9067
|
-
stream:
|
|
10375
|
+
stream: z7.string().optional(),
|
|
9068
10376
|
question: QuestionSchema.optional(),
|
|
9069
10377
|
confirmation: ConfirmationSchema.optional(),
|
|
9070
10378
|
transition: TransitionSchema.optional()
|
|
9071
10379
|
});
|
|
9072
10380
|
|
|
9073
10381
|
// src/blueprint/scanner.ts
|
|
9074
|
-
import * as
|
|
9075
|
-
import * as
|
|
10382
|
+
import * as fs20 from "fs/promises";
|
|
10383
|
+
import * as path20 from "path";
|
|
9076
10384
|
var ProjectScanner = class {
|
|
9077
10385
|
constructor(rootDir) {
|
|
9078
10386
|
this.rootDir = rootDir;
|
|
9079
10387
|
}
|
|
9080
10388
|
async scan() {
|
|
9081
|
-
let projectName =
|
|
10389
|
+
let projectName = path20.basename(this.rootDir);
|
|
9082
10390
|
try {
|
|
9083
|
-
const pkgPath =
|
|
9084
|
-
const pkgRaw = await
|
|
10391
|
+
const pkgPath = path20.join(this.rootDir, "package.json");
|
|
10392
|
+
const pkgRaw = await fs20.readFile(pkgPath, "utf-8");
|
|
9085
10393
|
const pkg = JSON.parse(pkgRaw);
|
|
9086
10394
|
if (pkg.name) projectName = pkg.name;
|
|
9087
10395
|
} catch {
|
|
@@ -9122,8 +10430,8 @@ var ProjectScanner = class {
|
|
|
9122
10430
|
};
|
|
9123
10431
|
|
|
9124
10432
|
// src/blueprint/generator.ts
|
|
9125
|
-
import * as
|
|
9126
|
-
import * as
|
|
10433
|
+
import * as fs21 from "fs/promises";
|
|
10434
|
+
import * as path21 from "path";
|
|
9127
10435
|
import * as ejs from "ejs";
|
|
9128
10436
|
|
|
9129
10437
|
// src/blueprint/templates.ts
|
|
@@ -9207,19 +10515,19 @@ var BlueprintGenerator = class {
|
|
|
9207
10515
|
styles: STYLES,
|
|
9208
10516
|
scripts: SCRIPTS
|
|
9209
10517
|
});
|
|
9210
|
-
await
|
|
9211
|
-
await
|
|
10518
|
+
await fs21.mkdir(options.outputDir, { recursive: true });
|
|
10519
|
+
await fs21.writeFile(path21.join(options.outputDir, "index.html"), html);
|
|
9212
10520
|
}
|
|
9213
10521
|
};
|
|
9214
10522
|
|
|
9215
10523
|
// src/update-checker.ts
|
|
9216
|
-
import * as
|
|
9217
|
-
import * as
|
|
10524
|
+
import * as fs22 from "fs";
|
|
10525
|
+
import * as path22 from "path";
|
|
9218
10526
|
import * as os from "os";
|
|
9219
10527
|
import { spawn } from "child_process";
|
|
9220
10528
|
function getStatePath() {
|
|
9221
10529
|
const home = process.env["HOME"] || os.homedir();
|
|
9222
|
-
return
|
|
10530
|
+
return path22.join(home, ".harness", "update-check.json");
|
|
9223
10531
|
}
|
|
9224
10532
|
function isUpdateCheckEnabled(configInterval) {
|
|
9225
10533
|
if (process.env["HARNESS_NO_UPDATE_CHECK"] === "1") return false;
|
|
@@ -9232,7 +10540,7 @@ function shouldRunCheck(state, intervalMs) {
|
|
|
9232
10540
|
}
|
|
9233
10541
|
function readCheckState() {
|
|
9234
10542
|
try {
|
|
9235
|
-
const raw =
|
|
10543
|
+
const raw = fs22.readFileSync(getStatePath(), "utf-8");
|
|
9236
10544
|
const parsed = JSON.parse(raw);
|
|
9237
10545
|
if (typeof parsed === "object" && parsed !== null && "lastCheckTime" in parsed && typeof parsed.lastCheckTime === "number" && "currentVersion" in parsed && typeof parsed.currentVersion === "string") {
|
|
9238
10546
|
const state = parsed;
|
|
@@ -9249,7 +10557,7 @@ function readCheckState() {
|
|
|
9249
10557
|
}
|
|
9250
10558
|
function spawnBackgroundCheck(currentVersion) {
|
|
9251
10559
|
const statePath = getStatePath();
|
|
9252
|
-
const stateDir =
|
|
10560
|
+
const stateDir = path22.dirname(statePath);
|
|
9253
10561
|
const script = `
|
|
9254
10562
|
const { execSync } = require('child_process');
|
|
9255
10563
|
const fs = require('fs');
|
|
@@ -9302,8 +10610,893 @@ function getUpdateNotification(currentVersion) {
|
|
|
9302
10610
|
Run "harness update" to upgrade.`;
|
|
9303
10611
|
}
|
|
9304
10612
|
|
|
10613
|
+
// src/code-nav/types.ts
|
|
10614
|
+
var EXTENSION_MAP = {
|
|
10615
|
+
".ts": "typescript",
|
|
10616
|
+
".tsx": "typescript",
|
|
10617
|
+
".mts": "typescript",
|
|
10618
|
+
".cts": "typescript",
|
|
10619
|
+
".js": "javascript",
|
|
10620
|
+
".jsx": "javascript",
|
|
10621
|
+
".mjs": "javascript",
|
|
10622
|
+
".cjs": "javascript",
|
|
10623
|
+
".py": "python"
|
|
10624
|
+
};
|
|
10625
|
+
function detectLanguage(filePath) {
|
|
10626
|
+
const ext = filePath.slice(filePath.lastIndexOf("."));
|
|
10627
|
+
return EXTENSION_MAP[ext] ?? null;
|
|
10628
|
+
}
|
|
10629
|
+
|
|
10630
|
+
// src/code-nav/parser.ts
|
|
10631
|
+
import Parser from "web-tree-sitter";
|
|
10632
|
+
var parserCache = /* @__PURE__ */ new Map();
|
|
10633
|
+
var initialized = false;
|
|
10634
|
+
var GRAMMAR_MAP = {
|
|
10635
|
+
typescript: "tree-sitter-typescript",
|
|
10636
|
+
javascript: "tree-sitter-javascript",
|
|
10637
|
+
python: "tree-sitter-python"
|
|
10638
|
+
};
|
|
10639
|
+
async function ensureInit() {
|
|
10640
|
+
if (!initialized) {
|
|
10641
|
+
await Parser.init();
|
|
10642
|
+
initialized = true;
|
|
10643
|
+
}
|
|
10644
|
+
}
|
|
10645
|
+
async function resolveWasmPath(grammarName) {
|
|
10646
|
+
const { createRequire } = await import("module");
|
|
10647
|
+
const require2 = createRequire(import.meta.url ?? __filename);
|
|
10648
|
+
const pkgPath = require2.resolve("tree-sitter-wasms/package.json");
|
|
10649
|
+
const path26 = await import("path");
|
|
10650
|
+
const pkgDir = path26.dirname(pkgPath);
|
|
10651
|
+
return path26.join(pkgDir, "out", `${grammarName}.wasm`);
|
|
10652
|
+
}
|
|
10653
|
+
async function loadLanguage(lang) {
|
|
10654
|
+
const grammarName = GRAMMAR_MAP[lang];
|
|
10655
|
+
const wasmPath = await resolveWasmPath(grammarName);
|
|
10656
|
+
return Parser.Language.load(wasmPath);
|
|
10657
|
+
}
|
|
10658
|
+
async function getParser(lang) {
|
|
10659
|
+
const cached = parserCache.get(lang);
|
|
10660
|
+
if (cached) return cached;
|
|
10661
|
+
await ensureInit();
|
|
10662
|
+
const parser = new Parser();
|
|
10663
|
+
const language = await loadLanguage(lang);
|
|
10664
|
+
parser.setLanguage(language);
|
|
10665
|
+
parserCache.set(lang, parser);
|
|
10666
|
+
return parser;
|
|
10667
|
+
}
|
|
10668
|
+
async function parseFile(filePath) {
|
|
10669
|
+
const lang = detectLanguage(filePath);
|
|
10670
|
+
if (!lang) {
|
|
10671
|
+
return Err({
|
|
10672
|
+
code: "UNSUPPORTED_LANGUAGE",
|
|
10673
|
+
message: `Unsupported file extension: ${filePath}`
|
|
10674
|
+
});
|
|
10675
|
+
}
|
|
10676
|
+
const contentResult = await readFileContent(filePath);
|
|
10677
|
+
if (!contentResult.ok) {
|
|
10678
|
+
return Err({
|
|
10679
|
+
code: "FILE_NOT_FOUND",
|
|
10680
|
+
message: `Cannot read file: ${filePath}`
|
|
10681
|
+
});
|
|
10682
|
+
}
|
|
10683
|
+
try {
|
|
10684
|
+
const parser = await getParser(lang);
|
|
10685
|
+
const tree = parser.parse(contentResult.value);
|
|
10686
|
+
return Ok({ tree, language: lang, source: contentResult.value, filePath });
|
|
10687
|
+
} catch (e) {
|
|
10688
|
+
return Err({
|
|
10689
|
+
code: "PARSE_FAILED",
|
|
10690
|
+
message: `Tree-sitter parse failed for ${filePath}: ${e.message}`
|
|
10691
|
+
});
|
|
10692
|
+
}
|
|
10693
|
+
}
|
|
10694
|
+
function resetParserCache() {
|
|
10695
|
+
parserCache.clear();
|
|
10696
|
+
initialized = false;
|
|
10697
|
+
}
|
|
10698
|
+
|
|
10699
|
+
// src/code-nav/outline.ts
|
|
10700
|
+
var TOP_LEVEL_TYPES = {
|
|
10701
|
+
typescript: {
|
|
10702
|
+
function_declaration: "function",
|
|
10703
|
+
class_declaration: "class",
|
|
10704
|
+
interface_declaration: "interface",
|
|
10705
|
+
type_alias_declaration: "type",
|
|
10706
|
+
lexical_declaration: "variable",
|
|
10707
|
+
variable_declaration: "variable",
|
|
10708
|
+
export_statement: "export",
|
|
10709
|
+
import_statement: "import",
|
|
10710
|
+
enum_declaration: "type"
|
|
10711
|
+
},
|
|
10712
|
+
javascript: {
|
|
10713
|
+
function_declaration: "function",
|
|
10714
|
+
class_declaration: "class",
|
|
10715
|
+
lexical_declaration: "variable",
|
|
10716
|
+
variable_declaration: "variable",
|
|
10717
|
+
export_statement: "export",
|
|
10718
|
+
import_statement: "import"
|
|
10719
|
+
},
|
|
10720
|
+
python: {
|
|
10721
|
+
function_definition: "function",
|
|
10722
|
+
class_definition: "class",
|
|
10723
|
+
assignment: "variable",
|
|
10724
|
+
import_statement: "import",
|
|
10725
|
+
import_from_statement: "import"
|
|
10726
|
+
}
|
|
10727
|
+
};
|
|
10728
|
+
var METHOD_TYPES = {
|
|
10729
|
+
typescript: ["method_definition", "public_field_definition"],
|
|
10730
|
+
javascript: ["method_definition"],
|
|
10731
|
+
python: ["function_definition"]
|
|
10732
|
+
};
|
|
10733
|
+
var IDENTIFIER_TYPES = /* @__PURE__ */ new Set(["identifier", "property_identifier", "type_identifier"]);
|
|
10734
|
+
function findIdentifier(node) {
|
|
10735
|
+
return node.childForFieldName("name") ?? node.children.find((c) => IDENTIFIER_TYPES.has(c.type)) ?? null;
|
|
10736
|
+
}
|
|
10737
|
+
function getVariableDeclarationName(node) {
|
|
10738
|
+
const declarator = node.children.find((c) => c.type === "variable_declarator");
|
|
10739
|
+
if (!declarator) return null;
|
|
10740
|
+
const id = findIdentifier(declarator);
|
|
10741
|
+
return id?.text ?? null;
|
|
10742
|
+
}
|
|
10743
|
+
function getExportName(node, source) {
|
|
10744
|
+
const decl = node.children.find(
|
|
10745
|
+
(c) => c.type !== "export" && c.type !== "default" && c.type !== "comment"
|
|
10746
|
+
);
|
|
10747
|
+
return decl ? getNodeName(decl, source) : "<anonymous>";
|
|
10748
|
+
}
|
|
10749
|
+
function getAssignmentName(node) {
|
|
10750
|
+
const left = node.childForFieldName("left") ?? node.children[0];
|
|
10751
|
+
return left?.text ?? "<anonymous>";
|
|
10752
|
+
}
|
|
10753
|
+
function getNodeName(node, source) {
|
|
10754
|
+
const id = findIdentifier(node);
|
|
10755
|
+
if (id) return id.text;
|
|
10756
|
+
const isVarDecl = node.type === "lexical_declaration" || node.type === "variable_declaration";
|
|
10757
|
+
if (isVarDecl) return getVariableDeclarationName(node) ?? "<anonymous>";
|
|
10758
|
+
if (node.type === "export_statement") return getExportName(node, source);
|
|
10759
|
+
if (node.type === "assignment") return getAssignmentName(node);
|
|
10760
|
+
return "<anonymous>";
|
|
10761
|
+
}
|
|
10762
|
+
function getSignature(node, source) {
|
|
10763
|
+
const startLine = node.startPosition.row;
|
|
10764
|
+
const lines = source.split("\n");
|
|
10765
|
+
return (lines[startLine] ?? "").trim();
|
|
10766
|
+
}
|
|
10767
|
+
function extractMethods(classNode, language, source, filePath) {
|
|
10768
|
+
const methodTypes = METHOD_TYPES[language] ?? [];
|
|
10769
|
+
const body = classNode.childForFieldName("body") ?? classNode.children.find((c) => c.type === "class_body" || c.type === "block");
|
|
10770
|
+
if (!body) return [];
|
|
10771
|
+
return body.children.filter((child) => methodTypes.includes(child.type)).map((child) => ({
|
|
10772
|
+
name: getNodeName(child, source),
|
|
10773
|
+
kind: "method",
|
|
10774
|
+
file: filePath,
|
|
10775
|
+
line: child.startPosition.row + 1,
|
|
10776
|
+
endLine: child.endPosition.row + 1,
|
|
10777
|
+
signature: getSignature(child, source)
|
|
10778
|
+
}));
|
|
10779
|
+
}
|
|
10780
|
+
function nodeToSymbol(node, kind, source, filePath) {
|
|
10781
|
+
return {
|
|
10782
|
+
name: getNodeName(node, source),
|
|
10783
|
+
kind,
|
|
10784
|
+
file: filePath,
|
|
10785
|
+
line: node.startPosition.row + 1,
|
|
10786
|
+
endLine: node.endPosition.row + 1,
|
|
10787
|
+
signature: getSignature(node, source)
|
|
10788
|
+
};
|
|
10789
|
+
}
|
|
10790
|
+
function processExportStatement(child, topLevelTypes, lang, source, filePath) {
|
|
10791
|
+
const declaration = child.children.find(
|
|
10792
|
+
(c) => c.type !== "export" && c.type !== "default" && c.type !== ";" && c.type !== "comment"
|
|
10793
|
+
);
|
|
10794
|
+
const kind = declaration ? topLevelTypes[declaration.type] : void 0;
|
|
10795
|
+
if (declaration && kind) {
|
|
10796
|
+
const sym = nodeToSymbol(child, kind, source, filePath);
|
|
10797
|
+
sym.name = getNodeName(declaration, source);
|
|
10798
|
+
if (kind === "class") {
|
|
10799
|
+
sym.children = extractMethods(declaration, lang, source, filePath);
|
|
10800
|
+
}
|
|
10801
|
+
return sym;
|
|
10802
|
+
}
|
|
10803
|
+
return nodeToSymbol(child, "export", source, filePath);
|
|
10804
|
+
}
|
|
10805
|
+
function extractSymbols(rootNode, lang, source, filePath) {
|
|
10806
|
+
const symbols = [];
|
|
10807
|
+
const topLevelTypes = TOP_LEVEL_TYPES[lang] ?? {};
|
|
10808
|
+
for (const child of rootNode.children) {
|
|
10809
|
+
if (child.type === "export_statement") {
|
|
10810
|
+
symbols.push(processExportStatement(child, topLevelTypes, lang, source, filePath));
|
|
10811
|
+
continue;
|
|
10812
|
+
}
|
|
10813
|
+
const kind = topLevelTypes[child.type];
|
|
10814
|
+
if (!kind || kind === "import") continue;
|
|
10815
|
+
const sym = nodeToSymbol(child, kind, source, filePath);
|
|
10816
|
+
if (kind === "class") {
|
|
10817
|
+
sym.children = extractMethods(child, lang, source, filePath);
|
|
10818
|
+
}
|
|
10819
|
+
symbols.push(sym);
|
|
10820
|
+
}
|
|
10821
|
+
return symbols;
|
|
10822
|
+
}
|
|
10823
|
+
function buildFailedResult(filePath, lang) {
|
|
10824
|
+
return { file: filePath, language: lang, totalLines: 0, symbols: [], error: "[parse-failed]" };
|
|
10825
|
+
}
|
|
10826
|
+
async function getOutline(filePath) {
|
|
10827
|
+
const lang = detectLanguage(filePath);
|
|
10828
|
+
if (!lang) return buildFailedResult(filePath, "unknown");
|
|
10829
|
+
const result = await parseFile(filePath);
|
|
10830
|
+
if (!result.ok) return buildFailedResult(filePath, lang);
|
|
10831
|
+
const { tree, source } = result.value;
|
|
10832
|
+
const totalLines = source.split("\n").length;
|
|
10833
|
+
const symbols = extractSymbols(tree.rootNode, lang, source, filePath);
|
|
10834
|
+
return { file: filePath, language: lang, totalLines, symbols };
|
|
10835
|
+
}
|
|
10836
|
+
function formatOutline(outline) {
|
|
10837
|
+
if (outline.error) {
|
|
10838
|
+
return `${outline.file} ${outline.error}`;
|
|
10839
|
+
}
|
|
10840
|
+
const lines = [`${outline.file} (${outline.totalLines} lines)`];
|
|
10841
|
+
const last = outline.symbols.length - 1;
|
|
10842
|
+
outline.symbols.forEach((sym, i) => {
|
|
10843
|
+
const prefix = i === last ? "\u2514\u2500\u2500" : "\u251C\u2500\u2500";
|
|
10844
|
+
lines.push(`${prefix} ${sym.signature} :${sym.line}`);
|
|
10845
|
+
if (sym.children) {
|
|
10846
|
+
const childLast = sym.children.length - 1;
|
|
10847
|
+
sym.children.forEach((child, j) => {
|
|
10848
|
+
const childConnector = i === last ? " " : "\u2502 ";
|
|
10849
|
+
const childPrefix = j === childLast ? "\u2514\u2500\u2500" : "\u251C\u2500\u2500";
|
|
10850
|
+
lines.push(`${childConnector}${childPrefix} ${child.signature} :${child.line}`);
|
|
10851
|
+
});
|
|
10852
|
+
}
|
|
10853
|
+
});
|
|
10854
|
+
return lines.join("\n");
|
|
10855
|
+
}
|
|
10856
|
+
|
|
10857
|
+
// src/code-nav/search.ts
|
|
10858
|
+
function buildGlob(directory, fileGlob) {
|
|
10859
|
+
const dir = directory.replaceAll("\\", "/");
|
|
10860
|
+
if (fileGlob) {
|
|
10861
|
+
return `${dir}/**/${fileGlob}`;
|
|
10862
|
+
}
|
|
10863
|
+
const exts = Object.keys(EXTENSION_MAP).map((e) => e.slice(1));
|
|
10864
|
+
return `${dir}/**/*.{${exts.join(",")}}`;
|
|
10865
|
+
}
|
|
10866
|
+
function matchesQuery(name, query) {
|
|
10867
|
+
return name.toLowerCase().includes(query.toLowerCase());
|
|
10868
|
+
}
|
|
10869
|
+
function flattenSymbols(symbols) {
|
|
10870
|
+
const flat = [];
|
|
10871
|
+
for (const sym of symbols) {
|
|
10872
|
+
flat.push(sym);
|
|
10873
|
+
if (sym.children) {
|
|
10874
|
+
flat.push(...sym.children);
|
|
10875
|
+
}
|
|
10876
|
+
}
|
|
10877
|
+
return flat;
|
|
10878
|
+
}
|
|
10879
|
+
async function searchSymbols(query, directory, fileGlob) {
|
|
10880
|
+
const pattern = buildGlob(directory, fileGlob);
|
|
10881
|
+
let files;
|
|
10882
|
+
try {
|
|
10883
|
+
files = await findFiles(pattern, directory);
|
|
10884
|
+
} catch {
|
|
10885
|
+
files = [];
|
|
10886
|
+
}
|
|
10887
|
+
const matches = [];
|
|
10888
|
+
const skipped = [];
|
|
10889
|
+
for (const file of files) {
|
|
10890
|
+
const lang = detectLanguage(file);
|
|
10891
|
+
if (!lang) {
|
|
10892
|
+
skipped.push(file);
|
|
10893
|
+
continue;
|
|
10894
|
+
}
|
|
10895
|
+
const outline = await getOutline(file);
|
|
10896
|
+
if (outline.error) {
|
|
10897
|
+
skipped.push(file);
|
|
10898
|
+
continue;
|
|
10899
|
+
}
|
|
10900
|
+
const allSymbols = flattenSymbols(outline.symbols);
|
|
10901
|
+
for (const sym of allSymbols) {
|
|
10902
|
+
if (matchesQuery(sym.name, query)) {
|
|
10903
|
+
matches.push({
|
|
10904
|
+
symbol: sym,
|
|
10905
|
+
context: sym.signature
|
|
10906
|
+
});
|
|
10907
|
+
}
|
|
10908
|
+
}
|
|
10909
|
+
}
|
|
10910
|
+
return { query, matches, skipped };
|
|
10911
|
+
}
|
|
10912
|
+
|
|
10913
|
+
// src/code-nav/unfold.ts
|
|
10914
|
+
function findSymbolInList(symbols, name) {
|
|
10915
|
+
for (const sym of symbols) {
|
|
10916
|
+
if (sym.name === name) return sym;
|
|
10917
|
+
if (sym.children) {
|
|
10918
|
+
const found = findSymbolInList(sym.children, name);
|
|
10919
|
+
if (found) return found;
|
|
10920
|
+
}
|
|
10921
|
+
}
|
|
10922
|
+
return null;
|
|
10923
|
+
}
|
|
10924
|
+
function extractLines(source, startLine, endLine) {
|
|
10925
|
+
const lines = source.split("\n");
|
|
10926
|
+
const start = Math.max(0, startLine - 1);
|
|
10927
|
+
const end = Math.min(lines.length, endLine);
|
|
10928
|
+
return lines.slice(start, end).join("\n");
|
|
10929
|
+
}
|
|
10930
|
+
function buildFallbackResult(filePath, symbolName, content, language) {
|
|
10931
|
+
const totalLines = content ? content.split("\n").length : 0;
|
|
10932
|
+
return {
|
|
10933
|
+
file: filePath,
|
|
10934
|
+
symbolName,
|
|
10935
|
+
startLine: content ? 1 : 0,
|
|
10936
|
+
endLine: totalLines,
|
|
10937
|
+
content,
|
|
10938
|
+
language,
|
|
10939
|
+
fallback: true,
|
|
10940
|
+
warning: "[fallback: raw content]"
|
|
10941
|
+
};
|
|
10942
|
+
}
|
|
10943
|
+
async function readContentSafe(filePath) {
|
|
10944
|
+
const result = await readFileContent(filePath);
|
|
10945
|
+
return result.ok ? result.value : "";
|
|
10946
|
+
}
|
|
10947
|
+
async function unfoldSymbol(filePath, symbolName) {
|
|
10948
|
+
const lang = detectLanguage(filePath);
|
|
10949
|
+
if (!lang) {
|
|
10950
|
+
const content2 = await readContentSafe(filePath);
|
|
10951
|
+
return buildFallbackResult(filePath, symbolName, content2, "unknown");
|
|
10952
|
+
}
|
|
10953
|
+
const outline = await getOutline(filePath);
|
|
10954
|
+
if (outline.error) {
|
|
10955
|
+
const content2 = await readContentSafe(filePath);
|
|
10956
|
+
return buildFallbackResult(filePath, symbolName, content2, lang);
|
|
10957
|
+
}
|
|
10958
|
+
const symbol = findSymbolInList(outline.symbols, symbolName);
|
|
10959
|
+
if (!symbol) {
|
|
10960
|
+
const content2 = await readContentSafe(filePath);
|
|
10961
|
+
return buildFallbackResult(filePath, symbolName, content2, lang);
|
|
10962
|
+
}
|
|
10963
|
+
const parseResult = await parseFile(filePath);
|
|
10964
|
+
if (!parseResult.ok) {
|
|
10965
|
+
const content2 = await readContentSafe(filePath);
|
|
10966
|
+
return {
|
|
10967
|
+
...buildFallbackResult(
|
|
10968
|
+
filePath,
|
|
10969
|
+
symbolName,
|
|
10970
|
+
extractLines(content2, symbol.line, symbol.endLine),
|
|
10971
|
+
lang
|
|
10972
|
+
),
|
|
10973
|
+
startLine: symbol.line,
|
|
10974
|
+
endLine: symbol.endLine
|
|
10975
|
+
};
|
|
10976
|
+
}
|
|
10977
|
+
const content = extractLines(parseResult.value.source, symbol.line, symbol.endLine);
|
|
10978
|
+
return {
|
|
10979
|
+
file: filePath,
|
|
10980
|
+
symbolName,
|
|
10981
|
+
startLine: symbol.line,
|
|
10982
|
+
endLine: symbol.endLine,
|
|
10983
|
+
content,
|
|
10984
|
+
language: lang,
|
|
10985
|
+
fallback: false
|
|
10986
|
+
};
|
|
10987
|
+
}
|
|
10988
|
+
async function unfoldRange(filePath, startLine, endLine) {
|
|
10989
|
+
const lang = detectLanguage(filePath) ?? "unknown";
|
|
10990
|
+
const contentResult = await readFileContent(filePath);
|
|
10991
|
+
if (!contentResult.ok) {
|
|
10992
|
+
return {
|
|
10993
|
+
file: filePath,
|
|
10994
|
+
startLine: 0,
|
|
10995
|
+
endLine: 0,
|
|
10996
|
+
content: "",
|
|
10997
|
+
language: lang,
|
|
10998
|
+
fallback: true,
|
|
10999
|
+
warning: "[fallback: raw content]"
|
|
11000
|
+
};
|
|
11001
|
+
}
|
|
11002
|
+
const totalLines = contentResult.value.split("\n").length;
|
|
11003
|
+
const clampedEnd = Math.min(endLine, totalLines);
|
|
11004
|
+
const content = extractLines(contentResult.value, startLine, clampedEnd);
|
|
11005
|
+
return {
|
|
11006
|
+
file: filePath,
|
|
11007
|
+
startLine,
|
|
11008
|
+
endLine: clampedEnd,
|
|
11009
|
+
content,
|
|
11010
|
+
language: lang,
|
|
11011
|
+
fallback: false
|
|
11012
|
+
};
|
|
11013
|
+
}
|
|
11014
|
+
|
|
11015
|
+
// src/pricing/pricing.ts
|
|
11016
|
+
var TOKENS_PER_MILLION = 1e6;
|
|
11017
|
+
function parseLiteLLMData(raw) {
|
|
11018
|
+
const dataset = /* @__PURE__ */ new Map();
|
|
11019
|
+
for (const [modelName, entry] of Object.entries(raw)) {
|
|
11020
|
+
if (modelName === "sample_spec") continue;
|
|
11021
|
+
if (entry.mode && entry.mode !== "chat") continue;
|
|
11022
|
+
const inputCost = entry.input_cost_per_token;
|
|
11023
|
+
const outputCost = entry.output_cost_per_token;
|
|
11024
|
+
if (inputCost == null || outputCost == null) continue;
|
|
11025
|
+
const pricing = {
|
|
11026
|
+
inputPer1M: inputCost * TOKENS_PER_MILLION,
|
|
11027
|
+
outputPer1M: outputCost * TOKENS_PER_MILLION
|
|
11028
|
+
};
|
|
11029
|
+
if (entry.cache_read_input_token_cost != null) {
|
|
11030
|
+
pricing.cacheReadPer1M = entry.cache_read_input_token_cost * TOKENS_PER_MILLION;
|
|
11031
|
+
}
|
|
11032
|
+
if (entry.cache_creation_input_token_cost != null) {
|
|
11033
|
+
pricing.cacheWritePer1M = entry.cache_creation_input_token_cost * TOKENS_PER_MILLION;
|
|
11034
|
+
}
|
|
11035
|
+
dataset.set(modelName, pricing);
|
|
11036
|
+
}
|
|
11037
|
+
return dataset;
|
|
11038
|
+
}
|
|
11039
|
+
function getModelPrice(model, dataset) {
|
|
11040
|
+
if (!model) {
|
|
11041
|
+
console.warn("[harness pricing] No model specified \u2014 cannot look up pricing.");
|
|
11042
|
+
return null;
|
|
11043
|
+
}
|
|
11044
|
+
const pricing = dataset.get(model);
|
|
11045
|
+
if (!pricing) {
|
|
11046
|
+
console.warn(
|
|
11047
|
+
`[harness pricing] No pricing data for model "${model}". Consider updating pricing data.`
|
|
11048
|
+
);
|
|
11049
|
+
return null;
|
|
11050
|
+
}
|
|
11051
|
+
return pricing;
|
|
11052
|
+
}
|
|
11053
|
+
|
|
11054
|
+
// src/pricing/cache.ts
|
|
11055
|
+
import * as fs23 from "fs/promises";
|
|
11056
|
+
import * as path23 from "path";
|
|
11057
|
+
|
|
11058
|
+
// src/pricing/fallback.json
|
|
11059
|
+
var fallback_default = {
|
|
11060
|
+
_generatedAt: "2026-03-31",
|
|
11061
|
+
_source: "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json",
|
|
11062
|
+
models: {
|
|
11063
|
+
"claude-opus-4-20250514": {
|
|
11064
|
+
inputPer1M: 15,
|
|
11065
|
+
outputPer1M: 75,
|
|
11066
|
+
cacheReadPer1M: 1.5,
|
|
11067
|
+
cacheWritePer1M: 18.75
|
|
11068
|
+
},
|
|
11069
|
+
"claude-sonnet-4-20250514": {
|
|
11070
|
+
inputPer1M: 3,
|
|
11071
|
+
outputPer1M: 15,
|
|
11072
|
+
cacheReadPer1M: 0.3,
|
|
11073
|
+
cacheWritePer1M: 3.75
|
|
11074
|
+
},
|
|
11075
|
+
"claude-3-5-haiku-20241022": {
|
|
11076
|
+
inputPer1M: 0.8,
|
|
11077
|
+
outputPer1M: 4,
|
|
11078
|
+
cacheReadPer1M: 0.08,
|
|
11079
|
+
cacheWritePer1M: 1
|
|
11080
|
+
},
|
|
11081
|
+
"gpt-4o": {
|
|
11082
|
+
inputPer1M: 2.5,
|
|
11083
|
+
outputPer1M: 10,
|
|
11084
|
+
cacheReadPer1M: 1.25
|
|
11085
|
+
},
|
|
11086
|
+
"gpt-4o-mini": {
|
|
11087
|
+
inputPer1M: 0.15,
|
|
11088
|
+
outputPer1M: 0.6,
|
|
11089
|
+
cacheReadPer1M: 0.075
|
|
11090
|
+
},
|
|
11091
|
+
"gemini-2.0-flash": {
|
|
11092
|
+
inputPer1M: 0.1,
|
|
11093
|
+
outputPer1M: 0.4,
|
|
11094
|
+
cacheReadPer1M: 0.025
|
|
11095
|
+
},
|
|
11096
|
+
"gemini-2.5-pro": {
|
|
11097
|
+
inputPer1M: 1.25,
|
|
11098
|
+
outputPer1M: 10,
|
|
11099
|
+
cacheReadPer1M: 0.3125
|
|
11100
|
+
}
|
|
11101
|
+
}
|
|
11102
|
+
};
|
|
11103
|
+
|
|
11104
|
+
// src/pricing/cache.ts
|
|
11105
|
+
var LITELLM_PRICING_URL = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json";
|
|
11106
|
+
var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
11107
|
+
var STALENESS_WARNING_DAYS = 7;
|
|
11108
|
+
function getCachePath(projectRoot) {
|
|
11109
|
+
return path23.join(projectRoot, ".harness", "cache", "pricing.json");
|
|
11110
|
+
}
|
|
11111
|
+
function getStalenessMarkerPath(projectRoot) {
|
|
11112
|
+
return path23.join(projectRoot, ".harness", "cache", "staleness-marker.json");
|
|
11113
|
+
}
|
|
11114
|
+
async function readDiskCache(projectRoot) {
|
|
11115
|
+
try {
|
|
11116
|
+
const raw = await fs23.readFile(getCachePath(projectRoot), "utf-8");
|
|
11117
|
+
return JSON.parse(raw);
|
|
11118
|
+
} catch {
|
|
11119
|
+
return null;
|
|
11120
|
+
}
|
|
11121
|
+
}
|
|
11122
|
+
async function writeDiskCache(projectRoot, data) {
|
|
11123
|
+
const cachePath = getCachePath(projectRoot);
|
|
11124
|
+
await fs23.mkdir(path23.dirname(cachePath), { recursive: true });
|
|
11125
|
+
await fs23.writeFile(cachePath, JSON.stringify(data, null, 2));
|
|
11126
|
+
}
|
|
11127
|
+
async function fetchFromNetwork() {
|
|
11128
|
+
try {
|
|
11129
|
+
const response = await fetch(LITELLM_PRICING_URL);
|
|
11130
|
+
if (!response.ok) return null;
|
|
11131
|
+
const data = await response.json();
|
|
11132
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) return null;
|
|
11133
|
+
return {
|
|
11134
|
+
fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11135
|
+
data
|
|
11136
|
+
};
|
|
11137
|
+
} catch {
|
|
11138
|
+
return null;
|
|
11139
|
+
}
|
|
11140
|
+
}
|
|
11141
|
+
function loadFallbackDataset() {
|
|
11142
|
+
const fb = fallback_default;
|
|
11143
|
+
const dataset = /* @__PURE__ */ new Map();
|
|
11144
|
+
for (const [model, pricing] of Object.entries(fb.models)) {
|
|
11145
|
+
dataset.set(model, pricing);
|
|
11146
|
+
}
|
|
11147
|
+
return dataset;
|
|
11148
|
+
}
|
|
11149
|
+
async function checkAndWarnStaleness(projectRoot) {
|
|
11150
|
+
const markerPath = getStalenessMarkerPath(projectRoot);
|
|
11151
|
+
try {
|
|
11152
|
+
const raw = await fs23.readFile(markerPath, "utf-8");
|
|
11153
|
+
const marker = JSON.parse(raw);
|
|
11154
|
+
const firstUse = new Date(marker.firstFallbackUse).getTime();
|
|
11155
|
+
const now = Date.now();
|
|
11156
|
+
const daysSinceFirstUse = (now - firstUse) / (24 * 60 * 60 * 1e3);
|
|
11157
|
+
if (daysSinceFirstUse > STALENESS_WARNING_DAYS) {
|
|
11158
|
+
console.warn(
|
|
11159
|
+
`[harness pricing] Pricing data is stale \u2014 using bundled fallback for ${Math.floor(daysSinceFirstUse)} days. Connect to the internet to refresh pricing data.`
|
|
11160
|
+
);
|
|
11161
|
+
}
|
|
11162
|
+
} catch {
|
|
11163
|
+
try {
|
|
11164
|
+
await fs23.mkdir(path23.dirname(markerPath), { recursive: true });
|
|
11165
|
+
await fs23.writeFile(
|
|
11166
|
+
markerPath,
|
|
11167
|
+
JSON.stringify({ firstFallbackUse: (/* @__PURE__ */ new Date()).toISOString() })
|
|
11168
|
+
);
|
|
11169
|
+
} catch {
|
|
11170
|
+
}
|
|
11171
|
+
}
|
|
11172
|
+
}
|
|
11173
|
+
async function clearStalenessMarker(projectRoot) {
|
|
11174
|
+
try {
|
|
11175
|
+
await fs23.unlink(getStalenessMarkerPath(projectRoot));
|
|
11176
|
+
} catch {
|
|
11177
|
+
}
|
|
11178
|
+
}
|
|
11179
|
+
async function loadPricingData(projectRoot) {
|
|
11180
|
+
const cache = await readDiskCache(projectRoot);
|
|
11181
|
+
if (cache) {
|
|
11182
|
+
const cacheAge = Date.now() - new Date(cache.fetchedAt).getTime();
|
|
11183
|
+
if (cacheAge < CACHE_TTL_MS) {
|
|
11184
|
+
await clearStalenessMarker(projectRoot);
|
|
11185
|
+
return parseLiteLLMData(cache.data);
|
|
11186
|
+
}
|
|
11187
|
+
}
|
|
11188
|
+
const fetched = await fetchFromNetwork();
|
|
11189
|
+
if (fetched) {
|
|
11190
|
+
await writeDiskCache(projectRoot, fetched);
|
|
11191
|
+
await clearStalenessMarker(projectRoot);
|
|
11192
|
+
return parseLiteLLMData(fetched.data);
|
|
11193
|
+
}
|
|
11194
|
+
if (cache) {
|
|
11195
|
+
return parseLiteLLMData(cache.data);
|
|
11196
|
+
}
|
|
11197
|
+
await checkAndWarnStaleness(projectRoot);
|
|
11198
|
+
return loadFallbackDataset();
|
|
11199
|
+
}
|
|
11200
|
+
|
|
11201
|
+
// src/pricing/calculator.ts
|
|
11202
|
+
var MICRODOLLARS_PER_DOLLAR = 1e6;
|
|
11203
|
+
var TOKENS_PER_MILLION2 = 1e6;
|
|
11204
|
+
function calculateCost(record, dataset) {
|
|
11205
|
+
if (!record.model) return null;
|
|
11206
|
+
const pricing = getModelPrice(record.model, dataset);
|
|
11207
|
+
if (!pricing) return null;
|
|
11208
|
+
let costUSD = 0;
|
|
11209
|
+
costUSD += record.tokens.inputTokens / TOKENS_PER_MILLION2 * pricing.inputPer1M;
|
|
11210
|
+
costUSD += record.tokens.outputTokens / TOKENS_PER_MILLION2 * pricing.outputPer1M;
|
|
11211
|
+
if (record.cacheReadTokens != null && pricing.cacheReadPer1M != null) {
|
|
11212
|
+
costUSD += record.cacheReadTokens / TOKENS_PER_MILLION2 * pricing.cacheReadPer1M;
|
|
11213
|
+
}
|
|
11214
|
+
if (record.cacheCreationTokens != null && pricing.cacheWritePer1M != null) {
|
|
11215
|
+
costUSD += record.cacheCreationTokens / TOKENS_PER_MILLION2 * pricing.cacheWritePer1M;
|
|
11216
|
+
}
|
|
11217
|
+
return Math.round(costUSD * MICRODOLLARS_PER_DOLLAR);
|
|
11218
|
+
}
|
|
11219
|
+
|
|
11220
|
+
// src/usage/aggregator.ts
|
|
11221
|
+
function aggregateBySession(records) {
|
|
11222
|
+
if (records.length === 0) return [];
|
|
11223
|
+
const sessionMap = /* @__PURE__ */ new Map();
|
|
11224
|
+
for (const record of records) {
|
|
11225
|
+
const tagged = record;
|
|
11226
|
+
const id = record.sessionId;
|
|
11227
|
+
if (!sessionMap.has(id)) {
|
|
11228
|
+
sessionMap.set(id, { harnessRecords: [], ccRecords: [], allRecords: [] });
|
|
11229
|
+
}
|
|
11230
|
+
const bucket = sessionMap.get(id);
|
|
11231
|
+
if (tagged._source === "claude-code") {
|
|
11232
|
+
bucket.ccRecords.push(tagged);
|
|
11233
|
+
} else {
|
|
11234
|
+
bucket.harnessRecords.push(tagged);
|
|
11235
|
+
}
|
|
11236
|
+
bucket.allRecords.push(tagged);
|
|
11237
|
+
}
|
|
11238
|
+
const results = [];
|
|
11239
|
+
for (const [sessionId, bucket] of sessionMap) {
|
|
11240
|
+
const hasHarness = bucket.harnessRecords.length > 0;
|
|
11241
|
+
const hasCC = bucket.ccRecords.length > 0;
|
|
11242
|
+
const isMerged = hasHarness && hasCC;
|
|
11243
|
+
const tokenSource = hasHarness ? bucket.harnessRecords : bucket.ccRecords;
|
|
11244
|
+
const tokens = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
11245
|
+
let cacheCreation;
|
|
11246
|
+
let cacheRead;
|
|
11247
|
+
let costMicroUSD = 0;
|
|
11248
|
+
let model;
|
|
11249
|
+
for (const r of tokenSource) {
|
|
11250
|
+
tokens.inputTokens += r.tokens.inputTokens;
|
|
11251
|
+
tokens.outputTokens += r.tokens.outputTokens;
|
|
11252
|
+
tokens.totalTokens += r.tokens.totalTokens;
|
|
11253
|
+
if (r.cacheCreationTokens != null) {
|
|
11254
|
+
cacheCreation = (cacheCreation ?? 0) + r.cacheCreationTokens;
|
|
11255
|
+
}
|
|
11256
|
+
if (r.cacheReadTokens != null) {
|
|
11257
|
+
cacheRead = (cacheRead ?? 0) + r.cacheReadTokens;
|
|
11258
|
+
}
|
|
11259
|
+
if (r.costMicroUSD != null && costMicroUSD != null) {
|
|
11260
|
+
costMicroUSD += r.costMicroUSD;
|
|
11261
|
+
} else if (r.costMicroUSD == null) {
|
|
11262
|
+
costMicroUSD = null;
|
|
11263
|
+
}
|
|
11264
|
+
if (!model && r.model) {
|
|
11265
|
+
model = r.model;
|
|
11266
|
+
}
|
|
11267
|
+
}
|
|
11268
|
+
if (!model && hasCC) {
|
|
11269
|
+
for (const r of bucket.ccRecords) {
|
|
11270
|
+
if (r.model) {
|
|
11271
|
+
model = r.model;
|
|
11272
|
+
break;
|
|
11273
|
+
}
|
|
11274
|
+
}
|
|
11275
|
+
}
|
|
11276
|
+
const timestamps = bucket.allRecords.map((r) => r.timestamp).sort();
|
|
11277
|
+
const source = isMerged ? "merged" : hasCC ? "claude-code" : "harness";
|
|
11278
|
+
const session = {
|
|
11279
|
+
sessionId,
|
|
11280
|
+
firstTimestamp: timestamps[0] ?? "",
|
|
11281
|
+
lastTimestamp: timestamps[timestamps.length - 1] ?? "",
|
|
11282
|
+
tokens,
|
|
11283
|
+
costMicroUSD,
|
|
11284
|
+
source
|
|
11285
|
+
};
|
|
11286
|
+
if (model) session.model = model;
|
|
11287
|
+
if (cacheCreation != null) session.cacheCreationTokens = cacheCreation;
|
|
11288
|
+
if (cacheRead != null) session.cacheReadTokens = cacheRead;
|
|
11289
|
+
results.push(session);
|
|
11290
|
+
}
|
|
11291
|
+
results.sort((a, b) => b.firstTimestamp.localeCompare(a.firstTimestamp));
|
|
11292
|
+
return results;
|
|
11293
|
+
}
|
|
11294
|
+
function aggregateByDay(records) {
|
|
11295
|
+
if (records.length === 0) return [];
|
|
11296
|
+
const dayMap = /* @__PURE__ */ new Map();
|
|
11297
|
+
for (const record of records) {
|
|
11298
|
+
const date = record.timestamp.slice(0, 10);
|
|
11299
|
+
if (!dayMap.has(date)) {
|
|
11300
|
+
dayMap.set(date, {
|
|
11301
|
+
sessions: /* @__PURE__ */ new Set(),
|
|
11302
|
+
tokens: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
|
|
11303
|
+
costMicroUSD: 0,
|
|
11304
|
+
models: /* @__PURE__ */ new Set()
|
|
11305
|
+
});
|
|
11306
|
+
}
|
|
11307
|
+
const day = dayMap.get(date);
|
|
11308
|
+
day.sessions.add(record.sessionId);
|
|
11309
|
+
day.tokens.inputTokens += record.tokens.inputTokens;
|
|
11310
|
+
day.tokens.outputTokens += record.tokens.outputTokens;
|
|
11311
|
+
day.tokens.totalTokens += record.tokens.totalTokens;
|
|
11312
|
+
if (record.cacheCreationTokens != null) {
|
|
11313
|
+
day.cacheCreation = (day.cacheCreation ?? 0) + record.cacheCreationTokens;
|
|
11314
|
+
}
|
|
11315
|
+
if (record.cacheReadTokens != null) {
|
|
11316
|
+
day.cacheRead = (day.cacheRead ?? 0) + record.cacheReadTokens;
|
|
11317
|
+
}
|
|
11318
|
+
if (record.costMicroUSD != null && day.costMicroUSD != null) {
|
|
11319
|
+
day.costMicroUSD += record.costMicroUSD;
|
|
11320
|
+
} else if (record.costMicroUSD == null) {
|
|
11321
|
+
day.costMicroUSD = null;
|
|
11322
|
+
}
|
|
11323
|
+
if (record.model) {
|
|
11324
|
+
day.models.add(record.model);
|
|
11325
|
+
}
|
|
11326
|
+
}
|
|
11327
|
+
const results = [];
|
|
11328
|
+
for (const [date, day] of dayMap) {
|
|
11329
|
+
const entry = {
|
|
11330
|
+
date,
|
|
11331
|
+
sessionCount: day.sessions.size,
|
|
11332
|
+
tokens: day.tokens,
|
|
11333
|
+
costMicroUSD: day.costMicroUSD,
|
|
11334
|
+
models: Array.from(day.models).sort()
|
|
11335
|
+
};
|
|
11336
|
+
if (day.cacheCreation != null) entry.cacheCreationTokens = day.cacheCreation;
|
|
11337
|
+
if (day.cacheRead != null) entry.cacheReadTokens = day.cacheRead;
|
|
11338
|
+
results.push(entry);
|
|
11339
|
+
}
|
|
11340
|
+
results.sort((a, b) => b.date.localeCompare(a.date));
|
|
11341
|
+
return results;
|
|
11342
|
+
}
|
|
11343
|
+
|
|
11344
|
+
// src/usage/jsonl-reader.ts
|
|
11345
|
+
import * as fs24 from "fs";
|
|
11346
|
+
import * as path24 from "path";
|
|
11347
|
+
function parseLine(line, lineNumber) {
|
|
11348
|
+
let entry;
|
|
11349
|
+
try {
|
|
11350
|
+
entry = JSON.parse(line);
|
|
11351
|
+
} catch {
|
|
11352
|
+
console.warn(`[harness usage] Skipping malformed JSONL line ${lineNumber}`);
|
|
11353
|
+
return null;
|
|
11354
|
+
}
|
|
11355
|
+
const tokenUsage = entry.token_usage;
|
|
11356
|
+
if (!tokenUsage || typeof tokenUsage !== "object") {
|
|
11357
|
+
console.warn(
|
|
11358
|
+
`[harness usage] Skipping malformed JSONL line ${lineNumber}: missing token_usage`
|
|
11359
|
+
);
|
|
11360
|
+
return null;
|
|
11361
|
+
}
|
|
11362
|
+
const inputTokens = tokenUsage.input_tokens ?? 0;
|
|
11363
|
+
const outputTokens = tokenUsage.output_tokens ?? 0;
|
|
11364
|
+
const record = {
|
|
11365
|
+
sessionId: entry.session_id ?? "unknown",
|
|
11366
|
+
timestamp: entry.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
11367
|
+
tokens: {
|
|
11368
|
+
inputTokens,
|
|
11369
|
+
outputTokens,
|
|
11370
|
+
totalTokens: inputTokens + outputTokens
|
|
11371
|
+
}
|
|
11372
|
+
};
|
|
11373
|
+
if (entry.cache_creation_tokens != null) {
|
|
11374
|
+
record.cacheCreationTokens = entry.cache_creation_tokens;
|
|
11375
|
+
}
|
|
11376
|
+
if (entry.cache_read_tokens != null) {
|
|
11377
|
+
record.cacheReadTokens = entry.cache_read_tokens;
|
|
11378
|
+
}
|
|
11379
|
+
if (entry.model != null) {
|
|
11380
|
+
record.model = entry.model;
|
|
11381
|
+
}
|
|
11382
|
+
return record;
|
|
11383
|
+
}
|
|
11384
|
+
function readCostRecords(projectRoot) {
|
|
11385
|
+
const costsFile = path24.join(projectRoot, ".harness", "metrics", "costs.jsonl");
|
|
11386
|
+
let raw;
|
|
11387
|
+
try {
|
|
11388
|
+
raw = fs24.readFileSync(costsFile, "utf-8");
|
|
11389
|
+
} catch {
|
|
11390
|
+
return [];
|
|
11391
|
+
}
|
|
11392
|
+
const records = [];
|
|
11393
|
+
const lines = raw.split("\n");
|
|
11394
|
+
for (let i = 0; i < lines.length; i++) {
|
|
11395
|
+
const line = lines[i]?.trim();
|
|
11396
|
+
if (!line) continue;
|
|
11397
|
+
const record = parseLine(line, i + 1);
|
|
11398
|
+
if (record) {
|
|
11399
|
+
records.push(record);
|
|
11400
|
+
}
|
|
11401
|
+
}
|
|
11402
|
+
return records;
|
|
11403
|
+
}
|
|
11404
|
+
|
|
11405
|
+
// src/usage/cc-parser.ts
|
|
11406
|
+
import * as fs25 from "fs";
|
|
11407
|
+
import * as path25 from "path";
|
|
11408
|
+
import * as os2 from "os";
|
|
11409
|
+
function extractUsage(entry) {
|
|
11410
|
+
if (entry.type !== "assistant") return null;
|
|
11411
|
+
const message = entry.message;
|
|
11412
|
+
if (!message || typeof message !== "object") return null;
|
|
11413
|
+
const usage = message.usage;
|
|
11414
|
+
return usage && typeof usage === "object" && !Array.isArray(usage) ? usage : null;
|
|
11415
|
+
}
|
|
11416
|
+
function buildRecord(entry, usage) {
|
|
11417
|
+
const inputTokens = Number(usage.input_tokens) || 0;
|
|
11418
|
+
const outputTokens = Number(usage.output_tokens) || 0;
|
|
11419
|
+
const message = entry.message;
|
|
11420
|
+
const record = {
|
|
11421
|
+
sessionId: entry.sessionId ?? "unknown",
|
|
11422
|
+
timestamp: entry.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
11423
|
+
tokens: { inputTokens, outputTokens, totalTokens: inputTokens + outputTokens },
|
|
11424
|
+
_source: "claude-code"
|
|
11425
|
+
};
|
|
11426
|
+
const model = message.model;
|
|
11427
|
+
if (model) record.model = model;
|
|
11428
|
+
const cacheCreate = usage.cache_creation_input_tokens;
|
|
11429
|
+
const cacheRead = usage.cache_read_input_tokens;
|
|
11430
|
+
if (typeof cacheCreate === "number" && cacheCreate > 0) record.cacheCreationTokens = cacheCreate;
|
|
11431
|
+
if (typeof cacheRead === "number" && cacheRead > 0) record.cacheReadTokens = cacheRead;
|
|
11432
|
+
return record;
|
|
11433
|
+
}
|
|
11434
|
+
function parseCCLine(line, filePath, lineNumber) {
|
|
11435
|
+
let entry;
|
|
11436
|
+
try {
|
|
11437
|
+
entry = JSON.parse(line);
|
|
11438
|
+
} catch {
|
|
11439
|
+
console.warn(
|
|
11440
|
+
`[harness usage] Skipping malformed CC JSONL line ${lineNumber} in ${path25.basename(filePath)}`
|
|
11441
|
+
);
|
|
11442
|
+
return null;
|
|
11443
|
+
}
|
|
11444
|
+
const usage = extractUsage(entry);
|
|
11445
|
+
if (!usage) return null;
|
|
11446
|
+
return {
|
|
11447
|
+
record: buildRecord(entry, usage),
|
|
11448
|
+
requestId: entry.requestId ?? null
|
|
11449
|
+
};
|
|
11450
|
+
}
|
|
11451
|
+
function readCCFile(filePath) {
|
|
11452
|
+
let raw;
|
|
11453
|
+
try {
|
|
11454
|
+
raw = fs25.readFileSync(filePath, "utf-8");
|
|
11455
|
+
} catch {
|
|
11456
|
+
return [];
|
|
11457
|
+
}
|
|
11458
|
+
const byRequestId = /* @__PURE__ */ new Map();
|
|
11459
|
+
const noRequestId = [];
|
|
11460
|
+
const lines = raw.split("\n");
|
|
11461
|
+
for (let i = 0; i < lines.length; i++) {
|
|
11462
|
+
const line = lines[i]?.trim();
|
|
11463
|
+
if (!line) continue;
|
|
11464
|
+
const parsed = parseCCLine(line, filePath, i + 1);
|
|
11465
|
+
if (!parsed) continue;
|
|
11466
|
+
if (parsed.requestId) {
|
|
11467
|
+
byRequestId.set(parsed.requestId, parsed.record);
|
|
11468
|
+
} else {
|
|
11469
|
+
noRequestId.push(parsed.record);
|
|
11470
|
+
}
|
|
11471
|
+
}
|
|
11472
|
+
return [...byRequestId.values(), ...noRequestId];
|
|
11473
|
+
}
|
|
11474
|
+
function parseCCRecords() {
|
|
11475
|
+
const homeDir = process.env.HOME ?? os2.homedir();
|
|
11476
|
+
const projectsDir = path25.join(homeDir, ".claude", "projects");
|
|
11477
|
+
let projectDirs;
|
|
11478
|
+
try {
|
|
11479
|
+
projectDirs = fs25.readdirSync(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => path25.join(projectsDir, d.name));
|
|
11480
|
+
} catch {
|
|
11481
|
+
return [];
|
|
11482
|
+
}
|
|
11483
|
+
const records = [];
|
|
11484
|
+
for (const dir of projectDirs) {
|
|
11485
|
+
let files;
|
|
11486
|
+
try {
|
|
11487
|
+
files = fs25.readdirSync(dir).filter((f) => f.endsWith(".jsonl")).map((f) => path25.join(dir, f));
|
|
11488
|
+
} catch {
|
|
11489
|
+
continue;
|
|
11490
|
+
}
|
|
11491
|
+
for (const file of files) {
|
|
11492
|
+
records.push(...readCCFile(file));
|
|
11493
|
+
}
|
|
11494
|
+
}
|
|
11495
|
+
return records;
|
|
11496
|
+
}
|
|
11497
|
+
|
|
9305
11498
|
// src/index.ts
|
|
9306
|
-
var VERSION = "0.
|
|
11499
|
+
var VERSION = "0.15.0";
|
|
9307
11500
|
export {
|
|
9308
11501
|
AGENT_DESCRIPTORS,
|
|
9309
11502
|
ARCHITECTURE_DESCRIPTOR,
|
|
@@ -9319,6 +11512,7 @@ export {
|
|
|
9319
11512
|
BlueprintGenerator,
|
|
9320
11513
|
BundleConstraintsSchema,
|
|
9321
11514
|
BundleSchema,
|
|
11515
|
+
CACHE_TTL_MS,
|
|
9322
11516
|
COMPLIANCE_DESCRIPTOR,
|
|
9323
11517
|
CategoryBaselineSchema,
|
|
9324
11518
|
CategoryRegressionSchema,
|
|
@@ -9336,7 +11530,9 @@ export {
|
|
|
9336
11530
|
DEFAULT_SECURITY_CONFIG,
|
|
9337
11531
|
DEFAULT_STATE,
|
|
9338
11532
|
DEFAULT_STREAM_INDEX,
|
|
11533
|
+
DESTRUCTIVE_BASH,
|
|
9339
11534
|
DepDepthCollector,
|
|
11535
|
+
EXTENSION_MAP,
|
|
9340
11536
|
EmitInteractionInputSchema,
|
|
9341
11537
|
EntropyAnalyzer,
|
|
9342
11538
|
EntropyConfigSchema,
|
|
@@ -9349,6 +11545,7 @@ export {
|
|
|
9349
11545
|
HandoffSchema,
|
|
9350
11546
|
HarnessStateSchema,
|
|
9351
11547
|
InteractionTypeSchema,
|
|
11548
|
+
LITELLM_PRICING_URL,
|
|
9352
11549
|
LayerViolationCollector,
|
|
9353
11550
|
LockfilePackageSchema,
|
|
9354
11551
|
LockfileSchema,
|
|
@@ -9365,12 +11562,14 @@ export {
|
|
|
9365
11562
|
RegressionDetector,
|
|
9366
11563
|
RuleRegistry,
|
|
9367
11564
|
SECURITY_DESCRIPTOR,
|
|
11565
|
+
STALENESS_WARNING_DAYS,
|
|
9368
11566
|
SecurityConfigSchema,
|
|
9369
11567
|
SecurityScanner,
|
|
9370
11568
|
SharableBoundaryConfigSchema,
|
|
9371
11569
|
SharableForbiddenImportSchema,
|
|
9372
11570
|
SharableLayerSchema,
|
|
9373
11571
|
SharableSecurityRulesSchema,
|
|
11572
|
+
SkillEventSchema,
|
|
9374
11573
|
StreamIndexSchema,
|
|
9375
11574
|
StreamInfoSchema,
|
|
9376
11575
|
ThresholdConfigSchema,
|
|
@@ -9379,6 +11578,9 @@ export {
|
|
|
9379
11578
|
VERSION,
|
|
9380
11579
|
ViolationSchema,
|
|
9381
11580
|
addProvenance,
|
|
11581
|
+
agentConfigRules,
|
|
11582
|
+
aggregateByDay,
|
|
11583
|
+
aggregateBySession,
|
|
9382
11584
|
analyzeDiff,
|
|
9383
11585
|
analyzeLearningPatterns,
|
|
9384
11586
|
appendFailure,
|
|
@@ -9386,6 +11588,7 @@ export {
|
|
|
9386
11588
|
appendSessionEntry,
|
|
9387
11589
|
applyFixes,
|
|
9388
11590
|
applyHotspotDowngrade,
|
|
11591
|
+
applySyncChanges,
|
|
9389
11592
|
archMatchers,
|
|
9390
11593
|
archModule,
|
|
9391
11594
|
architecture,
|
|
@@ -9396,16 +11599,23 @@ export {
|
|
|
9396
11599
|
buildDependencyGraph,
|
|
9397
11600
|
buildExclusionSet,
|
|
9398
11601
|
buildSnapshot,
|
|
11602
|
+
calculateCost,
|
|
9399
11603
|
checkDocCoverage,
|
|
9400
11604
|
checkEligibility,
|
|
9401
11605
|
checkEvidenceCoverage,
|
|
11606
|
+
checkTaint,
|
|
9402
11607
|
classifyFinding,
|
|
11608
|
+
clearEventHashCache,
|
|
9403
11609
|
clearFailuresCache,
|
|
9404
11610
|
clearLearningsCache,
|
|
11611
|
+
clearTaint,
|
|
11612
|
+
computeOverallSeverity,
|
|
11613
|
+
computeScanExitCode,
|
|
9405
11614
|
configureFeedback,
|
|
9406
11615
|
constraintRuleId,
|
|
9407
11616
|
contextBudget,
|
|
9408
11617
|
contextFilter,
|
|
11618
|
+
countLearningEntries,
|
|
9409
11619
|
createBoundaryValidator,
|
|
9410
11620
|
createCommentedCodeFixes,
|
|
9411
11621
|
createError,
|
|
@@ -9429,66 +11639,95 @@ export {
|
|
|
9429
11639
|
detectCouplingViolations,
|
|
9430
11640
|
detectDeadCode,
|
|
9431
11641
|
detectDocDrift,
|
|
11642
|
+
detectLanguage,
|
|
9432
11643
|
detectPatternViolations,
|
|
9433
11644
|
detectSizeBudgetViolations,
|
|
9434
11645
|
detectStack,
|
|
9435
11646
|
detectStaleConstraints,
|
|
9436
11647
|
determineAssessment,
|
|
9437
11648
|
diff,
|
|
11649
|
+
emitEvent,
|
|
9438
11650
|
executeWorkflow,
|
|
9439
11651
|
expressRules,
|
|
9440
11652
|
extractBundle,
|
|
11653
|
+
extractIndexEntry,
|
|
9441
11654
|
extractMarkdownLinks,
|
|
9442
11655
|
extractSections,
|
|
9443
11656
|
fanOutReview,
|
|
11657
|
+
formatEventTimeline,
|
|
9444
11658
|
formatFindingBlock,
|
|
9445
11659
|
formatGitHubComment,
|
|
9446
11660
|
formatGitHubSummary,
|
|
11661
|
+
formatOutline,
|
|
9447
11662
|
formatTerminalOutput,
|
|
9448
11663
|
generateAgentsMap,
|
|
9449
11664
|
generateSuggestions,
|
|
9450
11665
|
getActionEmitter,
|
|
9451
11666
|
getExitCode,
|
|
9452
11667
|
getFeedbackConfig,
|
|
11668
|
+
getInjectionPatterns,
|
|
11669
|
+
getModelPrice,
|
|
11670
|
+
getOutline,
|
|
11671
|
+
getParser,
|
|
9453
11672
|
getPhaseCategories,
|
|
9454
11673
|
getStreamForBranch,
|
|
11674
|
+
getTaintFilePath,
|
|
9455
11675
|
getUpdateNotification,
|
|
9456
11676
|
goRules,
|
|
9457
11677
|
injectionRules,
|
|
11678
|
+
insecureDefaultsRules,
|
|
11679
|
+
isDuplicateFinding,
|
|
9458
11680
|
isSmallSuggestion,
|
|
9459
11681
|
isUpdateCheckEnabled,
|
|
9460
11682
|
listActiveSessions,
|
|
9461
11683
|
listStreams,
|
|
11684
|
+
listTaintedSessions,
|
|
9462
11685
|
loadBudgetedLearnings,
|
|
11686
|
+
loadEvents,
|
|
9463
11687
|
loadFailures,
|
|
9464
11688
|
loadHandoff,
|
|
11689
|
+
loadIndexEntries,
|
|
11690
|
+
loadPricingData,
|
|
9465
11691
|
loadRelevantLearnings,
|
|
9466
11692
|
loadSessionSummary,
|
|
9467
11693
|
loadState,
|
|
9468
11694
|
loadStreamIndex,
|
|
9469
11695
|
logAgentAction,
|
|
11696
|
+
mapInjectionFindings,
|
|
11697
|
+
mapSecurityFindings,
|
|
11698
|
+
mapSecuritySeverity,
|
|
11699
|
+
mcpRules,
|
|
9470
11700
|
migrateToStreams,
|
|
9471
11701
|
networkRules,
|
|
9472
11702
|
nodeRules,
|
|
11703
|
+
parseCCRecords,
|
|
9473
11704
|
parseDateFromEntry,
|
|
9474
11705
|
parseDiff,
|
|
11706
|
+
parseFile,
|
|
11707
|
+
parseFrontmatter,
|
|
11708
|
+
parseHarnessIgnore,
|
|
11709
|
+
parseLiteLLMData,
|
|
9475
11710
|
parseManifest,
|
|
9476
11711
|
parseRoadmap,
|
|
9477
11712
|
parseSecurityConfig,
|
|
9478
11713
|
parseSize,
|
|
9479
11714
|
pathTraversalRules,
|
|
9480
11715
|
previewFix,
|
|
11716
|
+
promoteSessionLearnings,
|
|
9481
11717
|
pruneLearnings,
|
|
9482
11718
|
reactRules,
|
|
9483
11719
|
readCheckState,
|
|
11720
|
+
readCostRecords,
|
|
9484
11721
|
readLockfile,
|
|
9485
11722
|
readSessionSection,
|
|
9486
11723
|
readSessionSections,
|
|
11724
|
+
readTaint,
|
|
9487
11725
|
removeContributions,
|
|
9488
11726
|
removeProvenance,
|
|
9489
11727
|
requestMultiplePeerReviews,
|
|
9490
11728
|
requestPeerReview,
|
|
9491
11729
|
resetFeedbackConfig,
|
|
11730
|
+
resetParserCache,
|
|
9492
11731
|
resolveFileToLayer,
|
|
9493
11732
|
resolveModelTier,
|
|
9494
11733
|
resolveRuleSeverity,
|
|
@@ -9509,10 +11748,13 @@ export {
|
|
|
9509
11748
|
saveHandoff,
|
|
9510
11749
|
saveState,
|
|
9511
11750
|
saveStreamIndex,
|
|
11751
|
+
scanForInjection,
|
|
9512
11752
|
scopeContext,
|
|
11753
|
+
searchSymbols,
|
|
9513
11754
|
secretRules,
|
|
9514
11755
|
serializeRoadmap,
|
|
9515
11756
|
setActiveStream,
|
|
11757
|
+
sharpEdgesRules,
|
|
9516
11758
|
shouldRunCheck,
|
|
9517
11759
|
spawnBackgroundCheck,
|
|
9518
11760
|
syncConstraintNodes,
|
|
@@ -9520,6 +11762,8 @@ export {
|
|
|
9520
11762
|
tagUncitedFindings,
|
|
9521
11763
|
touchStream,
|
|
9522
11764
|
trackAction,
|
|
11765
|
+
unfoldRange,
|
|
11766
|
+
unfoldSymbol,
|
|
9523
11767
|
updateSessionEntryStatus,
|
|
9524
11768
|
updateSessionIndex,
|
|
9525
11769
|
validateAgentsMap,
|
|
@@ -9535,5 +11779,6 @@ export {
|
|
|
9535
11779
|
writeConfig,
|
|
9536
11780
|
writeLockfile,
|
|
9537
11781
|
writeSessionSummary,
|
|
11782
|
+
writeTaint,
|
|
9538
11783
|
xssRules
|
|
9539
11784
|
};
|