@harness-engineering/core 0.15.0 → 0.17.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 +1 -0
- package/dist/architecture/matchers.d.mts +1 -1
- package/dist/architecture/matchers.d.ts +1 -1
- package/dist/index.d.mts +467 -85
- package/dist/index.d.ts +467 -85
- package/dist/index.js +1834 -150
- package/dist/index.mjs +1744 -98
- package/dist/{matchers-Dj1t5vpg.d.mts → matchers-D20x48U9.d.mts} +46 -46
- package/dist/{matchers-Dj1t5vpg.d.ts → matchers-D20x48U9.d.ts} +46 -46
- package/package.json +14 -14
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(path23, 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),
|
|
@@ -7003,6 +7003,208 @@ var mcpRules = [
|
|
|
7003
7003
|
}
|
|
7004
7004
|
];
|
|
7005
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
|
+
|
|
7006
7208
|
// src/security/rules/stack/node.ts
|
|
7007
7209
|
var nodeRules = [
|
|
7008
7210
|
{
|
|
@@ -7116,6 +7318,14 @@ var goRules = [
|
|
|
7116
7318
|
];
|
|
7117
7319
|
|
|
7118
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
|
+
}
|
|
7119
7329
|
var SecurityScanner = class {
|
|
7120
7330
|
registry;
|
|
7121
7331
|
config;
|
|
@@ -7132,7 +7342,9 @@ var SecurityScanner = class {
|
|
|
7132
7342
|
...networkRules,
|
|
7133
7343
|
...deserializationRules,
|
|
7134
7344
|
...agentConfigRules,
|
|
7135
|
-
...mcpRules
|
|
7345
|
+
...mcpRules,
|
|
7346
|
+
...insecureDefaultsRules,
|
|
7347
|
+
...sharpEdgesRules
|
|
7136
7348
|
]);
|
|
7137
7349
|
this.registry.registerAll([...nodeRules, ...expressRules, ...reactRules, ...goRules]);
|
|
7138
7350
|
this.activeRules = this.registry.getAll();
|
|
@@ -7149,42 +7361,8 @@ var SecurityScanner = class {
|
|
|
7149
7361
|
*/
|
|
7150
7362
|
scanContent(content, filePath, startLine = 1) {
|
|
7151
7363
|
if (!this.config.enabled) return [];
|
|
7152
|
-
const findings = [];
|
|
7153
7364
|
const lines = content.split("\n");
|
|
7154
|
-
|
|
7155
|
-
const resolved = resolveRuleSeverity(
|
|
7156
|
-
rule.id,
|
|
7157
|
-
rule.severity,
|
|
7158
|
-
this.config.rules ?? {},
|
|
7159
|
-
this.config.strict
|
|
7160
|
-
);
|
|
7161
|
-
if (resolved === "off") continue;
|
|
7162
|
-
for (let i = 0; i < lines.length; i++) {
|
|
7163
|
-
const line = lines[i] ?? "";
|
|
7164
|
-
if (line.includes("harness-ignore") && line.includes(rule.id)) continue;
|
|
7165
|
-
for (const pattern of rule.patterns) {
|
|
7166
|
-
pattern.lastIndex = 0;
|
|
7167
|
-
if (pattern.test(line)) {
|
|
7168
|
-
findings.push({
|
|
7169
|
-
ruleId: rule.id,
|
|
7170
|
-
ruleName: rule.name,
|
|
7171
|
-
category: rule.category,
|
|
7172
|
-
severity: resolved,
|
|
7173
|
-
confidence: rule.confidence,
|
|
7174
|
-
file: filePath,
|
|
7175
|
-
line: startLine + i,
|
|
7176
|
-
match: line.trim(),
|
|
7177
|
-
context: line,
|
|
7178
|
-
message: rule.message,
|
|
7179
|
-
remediation: rule.remediation,
|
|
7180
|
-
...rule.references ? { references: rule.references } : {}
|
|
7181
|
-
});
|
|
7182
|
-
break;
|
|
7183
|
-
}
|
|
7184
|
-
}
|
|
7185
|
-
}
|
|
7186
|
-
}
|
|
7187
|
-
return findings;
|
|
7365
|
+
return this.scanLinesWithRules(lines, this.activeRules, filePath, startLine);
|
|
7188
7366
|
}
|
|
7189
7367
|
async scanFile(filePath) {
|
|
7190
7368
|
if (!this.config.enabled) return [];
|
|
@@ -7193,14 +7371,22 @@ var SecurityScanner = class {
|
|
|
7193
7371
|
}
|
|
7194
7372
|
scanContentForFile(content, filePath, startLine = 1) {
|
|
7195
7373
|
if (!this.config.enabled) return [];
|
|
7196
|
-
const findings = [];
|
|
7197
7374
|
const lines = content.split("\n");
|
|
7198
7375
|
const applicableRules = this.activeRules.filter((rule) => {
|
|
7199
7376
|
if (!rule.fileGlob) return true;
|
|
7200
7377
|
const globs = rule.fileGlob.split(",").map((g) => g.trim());
|
|
7201
7378
|
return globs.some((glob) => minimatch4(filePath, glob, { dot: true }));
|
|
7202
7379
|
});
|
|
7203
|
-
|
|
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) {
|
|
7204
7390
|
const resolved = resolveRuleSeverity(
|
|
7205
7391
|
rule.id,
|
|
7206
7392
|
rule.severity,
|
|
@@ -7210,7 +7396,25 @@ var SecurityScanner = class {
|
|
|
7210
7396
|
if (resolved === "off") continue;
|
|
7211
7397
|
for (let i = 0; i < lines.length; i++) {
|
|
7212
7398
|
const line = lines[i] ?? "";
|
|
7213
|
-
|
|
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
|
+
}
|
|
7214
7418
|
for (const pattern of rule.patterns) {
|
|
7215
7419
|
pattern.lastIndex = 0;
|
|
7216
7420
|
if (pattern.test(line)) {
|
|
@@ -7256,6 +7460,414 @@ var SecurityScanner = class {
|
|
|
7256
7460
|
}
|
|
7257
7461
|
};
|
|
7258
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
|
+
}
|
|
7870
|
+
|
|
7259
7871
|
// src/ci/check-orchestrator.ts
|
|
7260
7872
|
import * as path15 from "path";
|
|
7261
7873
|
var ALL_CHECKS = [
|
|
@@ -7439,7 +8051,7 @@ async function runPerfCheck(projectRoot, config) {
|
|
|
7439
8051
|
if (perfReport.complexity) {
|
|
7440
8052
|
for (const v of perfReport.complexity.violations) {
|
|
7441
8053
|
issues.push({
|
|
7442
|
-
severity:
|
|
8054
|
+
severity: "warning",
|
|
7443
8055
|
message: `[Tier ${v.tier}] ${v.metric}: ${v.function} in ${v.file} (${v.value} > ${v.threshold})`,
|
|
7444
8056
|
file: v.file,
|
|
7445
8057
|
line: v.line
|
|
@@ -9446,6 +10058,7 @@ var VALID_STATUSES = /* @__PURE__ */ new Set([
|
|
|
9446
10058
|
"blocked"
|
|
9447
10059
|
]);
|
|
9448
10060
|
var EM_DASH = "\u2014";
|
|
10061
|
+
var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
|
|
9449
10062
|
function parseRoadmap(markdown) {
|
|
9450
10063
|
const fmMatch = markdown.match(/^---\n([\s\S]*?)\n---/);
|
|
9451
10064
|
if (!fmMatch) {
|
|
@@ -9456,9 +10069,12 @@ function parseRoadmap(markdown) {
|
|
|
9456
10069
|
const body = markdown.slice(fmMatch[0].length);
|
|
9457
10070
|
const milestonesResult = parseMilestones(body);
|
|
9458
10071
|
if (!milestonesResult.ok) return milestonesResult;
|
|
10072
|
+
const historyResult = parseAssignmentHistory(body);
|
|
10073
|
+
if (!historyResult.ok) return historyResult;
|
|
9459
10074
|
return Ok2({
|
|
9460
10075
|
frontmatter: fmResult.value,
|
|
9461
|
-
milestones: milestonesResult.value
|
|
10076
|
+
milestones: milestonesResult.value,
|
|
10077
|
+
assignmentHistory: historyResult.value
|
|
9462
10078
|
});
|
|
9463
10079
|
}
|
|
9464
10080
|
function parseFrontmatter2(raw) {
|
|
@@ -9498,12 +10114,17 @@ function parseMilestones(body) {
|
|
|
9498
10114
|
const h2Pattern = /^## (.+)$/gm;
|
|
9499
10115
|
const h2Matches = [];
|
|
9500
10116
|
let match;
|
|
10117
|
+
let bodyEnd = body.length;
|
|
9501
10118
|
while ((match = h2Pattern.exec(body)) !== null) {
|
|
10119
|
+
if (match[1] === "Assignment History") {
|
|
10120
|
+
bodyEnd = match.index;
|
|
10121
|
+
break;
|
|
10122
|
+
}
|
|
9502
10123
|
h2Matches.push({ heading: match[1], startIndex: match.index, fullMatch: match[0] });
|
|
9503
10124
|
}
|
|
9504
10125
|
for (let i = 0; i < h2Matches.length; i++) {
|
|
9505
10126
|
const h2 = h2Matches[i];
|
|
9506
|
-
const nextStart = i + 1 < h2Matches.length ? h2Matches[i + 1].startIndex :
|
|
10127
|
+
const nextStart = i + 1 < h2Matches.length ? h2Matches[i + 1].startIndex : bodyEnd;
|
|
9507
10128
|
const sectionBody = body.slice(h2.startIndex + h2.fullMatch.length, nextStart);
|
|
9508
10129
|
const isBacklog = h2.heading === "Backlog";
|
|
9509
10130
|
const milestoneName = isBacklog ? "Backlog" : h2.heading.replace(/^Milestone:\s*/, "");
|
|
@@ -9569,15 +10190,60 @@ function parseFeatureFields(name, body) {
|
|
|
9569
10190
|
const specRaw = fieldMap.get("Spec") ?? EM_DASH;
|
|
9570
10191
|
const plans = parseListField(fieldMap, "Plans", "Plan");
|
|
9571
10192
|
const blockedBy = parseListField(fieldMap, "Blocked by", "Blockers");
|
|
10193
|
+
const assigneeRaw = fieldMap.get("Assignee") ?? EM_DASH;
|
|
10194
|
+
const priorityRaw = fieldMap.get("Priority") ?? EM_DASH;
|
|
10195
|
+
const externalIdRaw = fieldMap.get("External-ID") ?? EM_DASH;
|
|
10196
|
+
if (priorityRaw !== EM_DASH && !VALID_PRIORITIES.has(priorityRaw)) {
|
|
10197
|
+
return Err2(
|
|
10198
|
+
new Error(
|
|
10199
|
+
`Feature "${name}" has invalid priority: "${priorityRaw}". Valid priorities: ${[...VALID_PRIORITIES].join(", ")}`
|
|
10200
|
+
)
|
|
10201
|
+
);
|
|
10202
|
+
}
|
|
9572
10203
|
return Ok2({
|
|
9573
10204
|
name,
|
|
9574
10205
|
status: statusRaw,
|
|
9575
10206
|
spec: specRaw === EM_DASH ? null : specRaw,
|
|
9576
10207
|
plans,
|
|
9577
10208
|
blockedBy,
|
|
9578
|
-
summary: fieldMap.get("Summary") ?? ""
|
|
10209
|
+
summary: fieldMap.get("Summary") ?? "",
|
|
10210
|
+
assignee: assigneeRaw === EM_DASH ? null : assigneeRaw,
|
|
10211
|
+
priority: priorityRaw === EM_DASH ? null : priorityRaw,
|
|
10212
|
+
externalId: externalIdRaw === EM_DASH ? null : externalIdRaw
|
|
9579
10213
|
});
|
|
9580
10214
|
}
|
|
10215
|
+
function parseAssignmentHistory(body) {
|
|
10216
|
+
const historyMatch = body.match(/^## Assignment History\s*\n/m);
|
|
10217
|
+
if (!historyMatch || historyMatch.index === void 0) return Ok2([]);
|
|
10218
|
+
const historyStart = historyMatch.index + historyMatch[0].length;
|
|
10219
|
+
const rawHistoryBody = body.slice(historyStart);
|
|
10220
|
+
const nextH2 = rawHistoryBody.search(/^## /m);
|
|
10221
|
+
const historyBody = nextH2 === -1 ? rawHistoryBody : rawHistoryBody.slice(0, nextH2);
|
|
10222
|
+
const records = [];
|
|
10223
|
+
const lines = historyBody.split("\n");
|
|
10224
|
+
let pastHeader = false;
|
|
10225
|
+
for (const line of lines) {
|
|
10226
|
+
const trimmed = line.trim();
|
|
10227
|
+
if (!trimmed.startsWith("|")) continue;
|
|
10228
|
+
if (!pastHeader) {
|
|
10229
|
+
if (trimmed.match(/^\|[-\s|]+\|$/)) {
|
|
10230
|
+
pastHeader = true;
|
|
10231
|
+
}
|
|
10232
|
+
continue;
|
|
10233
|
+
}
|
|
10234
|
+
const cells = trimmed.split("|").map((c) => c.trim()).filter((c) => c.length > 0);
|
|
10235
|
+
if (cells.length < 4) continue;
|
|
10236
|
+
const action = cells[2];
|
|
10237
|
+
if (!["assigned", "completed", "unassigned"].includes(action)) continue;
|
|
10238
|
+
records.push({
|
|
10239
|
+
feature: cells[0],
|
|
10240
|
+
assignee: cells[1],
|
|
10241
|
+
action,
|
|
10242
|
+
date: cells[3]
|
|
10243
|
+
});
|
|
10244
|
+
}
|
|
10245
|
+
return Ok2(records);
|
|
10246
|
+
}
|
|
9581
10247
|
|
|
9582
10248
|
// src/roadmap/serialize.ts
|
|
9583
10249
|
var EM_DASH2 = "\u2014";
|
|
@@ -9605,6 +10271,10 @@ function serializeRoadmap(roadmap) {
|
|
|
9605
10271
|
lines.push(...serializeFeature(feature));
|
|
9606
10272
|
}
|
|
9607
10273
|
}
|
|
10274
|
+
if (roadmap.assignmentHistory && roadmap.assignmentHistory.length > 0) {
|
|
10275
|
+
lines.push("");
|
|
10276
|
+
lines.push(...serializeAssignmentHistory(roadmap.assignmentHistory));
|
|
10277
|
+
}
|
|
9608
10278
|
lines.push("");
|
|
9609
10279
|
return lines.join("\n");
|
|
9610
10280
|
}
|
|
@@ -9615,7 +10285,7 @@ function serializeFeature(feature) {
|
|
|
9615
10285
|
const spec = feature.spec ?? EM_DASH2;
|
|
9616
10286
|
const plans = feature.plans.length > 0 ? feature.plans.join(", ") : EM_DASH2;
|
|
9617
10287
|
const blockedBy = feature.blockedBy.length > 0 ? feature.blockedBy.join(", ") : EM_DASH2;
|
|
9618
|
-
|
|
10288
|
+
const lines = [
|
|
9619
10289
|
`### ${feature.name}`,
|
|
9620
10290
|
"",
|
|
9621
10291
|
`- **Status:** ${feature.status}`,
|
|
@@ -9624,12 +10294,45 @@ function serializeFeature(feature) {
|
|
|
9624
10294
|
`- **Blockers:** ${blockedBy}`,
|
|
9625
10295
|
`- **Plan:** ${plans}`
|
|
9626
10296
|
];
|
|
10297
|
+
const hasExtended = feature.assignee !== null || feature.priority !== null || feature.externalId !== null;
|
|
10298
|
+
if (hasExtended) {
|
|
10299
|
+
lines.push(`- **Assignee:** ${feature.assignee ?? EM_DASH2}`);
|
|
10300
|
+
lines.push(`- **Priority:** ${feature.priority ?? EM_DASH2}`);
|
|
10301
|
+
lines.push(`- **External-ID:** ${feature.externalId ?? EM_DASH2}`);
|
|
10302
|
+
}
|
|
10303
|
+
return lines;
|
|
10304
|
+
}
|
|
10305
|
+
function serializeAssignmentHistory(records) {
|
|
10306
|
+
const lines = [
|
|
10307
|
+
"## Assignment History",
|
|
10308
|
+
"| Feature | Assignee | Action | Date |",
|
|
10309
|
+
"|---------|----------|--------|------|"
|
|
10310
|
+
];
|
|
10311
|
+
for (const record of records) {
|
|
10312
|
+
lines.push(`| ${record.feature} | ${record.assignee} | ${record.action} | ${record.date} |`);
|
|
10313
|
+
}
|
|
10314
|
+
return lines;
|
|
9627
10315
|
}
|
|
9628
10316
|
|
|
9629
10317
|
// src/roadmap/sync.ts
|
|
9630
10318
|
import * as fs19 from "fs";
|
|
9631
10319
|
import * as path19 from "path";
|
|
9632
10320
|
import { Ok as Ok3 } from "@harness-engineering/types";
|
|
10321
|
+
|
|
10322
|
+
// src/roadmap/status-rank.ts
|
|
10323
|
+
var STATUS_RANK = {
|
|
10324
|
+
backlog: 0,
|
|
10325
|
+
planned: 1,
|
|
10326
|
+
blocked: 1,
|
|
10327
|
+
// lateral to planned — sync can move to/from blocked freely
|
|
10328
|
+
"in-progress": 2,
|
|
10329
|
+
done: 3
|
|
10330
|
+
};
|
|
10331
|
+
function isRegression(from, to) {
|
|
10332
|
+
return STATUS_RANK[to] < STATUS_RANK[from];
|
|
10333
|
+
}
|
|
10334
|
+
|
|
10335
|
+
// src/roadmap/sync.ts
|
|
9633
10336
|
function inferStatus(feature, projectPath, allFeatures) {
|
|
9634
10337
|
if (feature.blockedBy.length > 0) {
|
|
9635
10338
|
const blockerNotDone = feature.blockedBy.some((blockerName) => {
|
|
@@ -9687,25 +10390,14 @@ function inferStatus(feature, projectPath, allFeatures) {
|
|
|
9687
10390
|
}
|
|
9688
10391
|
}
|
|
9689
10392
|
} catch {
|
|
9690
|
-
}
|
|
9691
|
-
}
|
|
9692
|
-
if (allTaskStatuses.length === 0) return null;
|
|
9693
|
-
const allComplete = allTaskStatuses.every((s) => s === "complete");
|
|
9694
|
-
if (allComplete) return "done";
|
|
9695
|
-
const anyStarted = allTaskStatuses.some((s) => s === "in_progress" || s === "complete");
|
|
9696
|
-
if (anyStarted) return "in-progress";
|
|
9697
|
-
return null;
|
|
9698
|
-
}
|
|
9699
|
-
var STATUS_RANK = {
|
|
9700
|
-
backlog: 0,
|
|
9701
|
-
planned: 1,
|
|
9702
|
-
blocked: 1,
|
|
9703
|
-
// lateral to planned — sync can move to/from blocked freely
|
|
9704
|
-
"in-progress": 2,
|
|
9705
|
-
done: 3
|
|
9706
|
-
};
|
|
9707
|
-
function isRegression(from, to) {
|
|
9708
|
-
return STATUS_RANK[to] < STATUS_RANK[from];
|
|
10393
|
+
}
|
|
10394
|
+
}
|
|
10395
|
+
if (allTaskStatuses.length === 0) return null;
|
|
10396
|
+
const allComplete = allTaskStatuses.every((s) => s === "complete");
|
|
10397
|
+
if (allComplete) return "done";
|
|
10398
|
+
const anyStarted = allTaskStatuses.some((s) => s === "in_progress" || s === "complete");
|
|
10399
|
+
if (anyStarted) return "in-progress";
|
|
10400
|
+
return null;
|
|
9709
10401
|
}
|
|
9710
10402
|
function syncRoadmap(options) {
|
|
9711
10403
|
const { projectPath, roadmap, forceSync } = options;
|
|
@@ -9737,6 +10429,438 @@ function applySyncChanges(roadmap, changes) {
|
|
|
9737
10429
|
roadmap.frontmatter.lastSynced = (/* @__PURE__ */ new Date()).toISOString();
|
|
9738
10430
|
}
|
|
9739
10431
|
|
|
10432
|
+
// src/roadmap/tracker-sync.ts
|
|
10433
|
+
function resolveReverseStatus(externalStatus, labels, config) {
|
|
10434
|
+
const reverseMap = config.reverseStatusMap;
|
|
10435
|
+
if (!reverseMap) return null;
|
|
10436
|
+
if (reverseMap[externalStatus]) {
|
|
10437
|
+
return reverseMap[externalStatus];
|
|
10438
|
+
}
|
|
10439
|
+
const statusLabels = ["in-progress", "blocked", "planned"];
|
|
10440
|
+
const matchingLabels = labels.filter((l) => statusLabels.includes(l));
|
|
10441
|
+
if (matchingLabels.length === 1) {
|
|
10442
|
+
const compoundKey = `${externalStatus}:${matchingLabels[0]}`;
|
|
10443
|
+
if (reverseMap[compoundKey]) {
|
|
10444
|
+
return reverseMap[compoundKey];
|
|
10445
|
+
}
|
|
10446
|
+
}
|
|
10447
|
+
return null;
|
|
10448
|
+
}
|
|
10449
|
+
|
|
10450
|
+
// src/roadmap/adapters/github-issues.ts
|
|
10451
|
+
import { Ok as Ok4, Err as Err3 } from "@harness-engineering/types";
|
|
10452
|
+
function parseExternalId(externalId) {
|
|
10453
|
+
const match = externalId.match(/^github:([^/]+)\/([^#]+)#(\d+)$/);
|
|
10454
|
+
if (!match) return null;
|
|
10455
|
+
return { owner: match[1], repo: match[2], number: parseInt(match[3], 10) };
|
|
10456
|
+
}
|
|
10457
|
+
function buildExternalId(owner, repo, number) {
|
|
10458
|
+
return `github:${owner}/${repo}#${number}`;
|
|
10459
|
+
}
|
|
10460
|
+
function labelsForStatus(status, config) {
|
|
10461
|
+
const base = config.labels ?? [];
|
|
10462
|
+
const externalStatus = config.statusMap[status];
|
|
10463
|
+
if (externalStatus === "open" && status !== "backlog") {
|
|
10464
|
+
return [...base, status];
|
|
10465
|
+
}
|
|
10466
|
+
return [...base];
|
|
10467
|
+
}
|
|
10468
|
+
var GitHubIssuesSyncAdapter = class {
|
|
10469
|
+
token;
|
|
10470
|
+
config;
|
|
10471
|
+
fetchFn;
|
|
10472
|
+
apiBase;
|
|
10473
|
+
owner;
|
|
10474
|
+
repo;
|
|
10475
|
+
constructor(options) {
|
|
10476
|
+
this.token = options.token;
|
|
10477
|
+
this.config = options.config;
|
|
10478
|
+
this.fetchFn = options.fetchFn ?? globalThis.fetch;
|
|
10479
|
+
this.apiBase = options.apiBase ?? "https://api.github.com";
|
|
10480
|
+
const repoParts = (options.config.repo ?? "").split("/");
|
|
10481
|
+
if (repoParts.length !== 2 || !repoParts[0] || !repoParts[1]) {
|
|
10482
|
+
throw new Error(`Invalid repo format: "${options.config.repo}". Expected "owner/repo".`);
|
|
10483
|
+
}
|
|
10484
|
+
this.owner = repoParts[0];
|
|
10485
|
+
this.repo = repoParts[1];
|
|
10486
|
+
}
|
|
10487
|
+
headers() {
|
|
10488
|
+
return {
|
|
10489
|
+
Authorization: `Bearer ${this.token}`,
|
|
10490
|
+
Accept: "application/vnd.github+json",
|
|
10491
|
+
"Content-Type": "application/json",
|
|
10492
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
10493
|
+
};
|
|
10494
|
+
}
|
|
10495
|
+
async createTicket(feature, milestone) {
|
|
10496
|
+
try {
|
|
10497
|
+
const labels = labelsForStatus(feature.status, this.config);
|
|
10498
|
+
const body = [
|
|
10499
|
+
feature.summary,
|
|
10500
|
+
"",
|
|
10501
|
+
`**Milestone:** ${milestone}`,
|
|
10502
|
+
feature.spec ? `**Spec:** ${feature.spec}` : ""
|
|
10503
|
+
].filter(Boolean).join("\n");
|
|
10504
|
+
const response = await this.fetchFn(
|
|
10505
|
+
`${this.apiBase}/repos/${this.owner}/${this.repo}/issues`,
|
|
10506
|
+
{
|
|
10507
|
+
method: "POST",
|
|
10508
|
+
headers: this.headers(),
|
|
10509
|
+
body: JSON.stringify({
|
|
10510
|
+
title: feature.name,
|
|
10511
|
+
body,
|
|
10512
|
+
labels
|
|
10513
|
+
})
|
|
10514
|
+
}
|
|
10515
|
+
);
|
|
10516
|
+
if (!response.ok) {
|
|
10517
|
+
const text = await response.text();
|
|
10518
|
+
return Err3(new Error(`GitHub API error ${response.status}: ${text}`));
|
|
10519
|
+
}
|
|
10520
|
+
const data = await response.json();
|
|
10521
|
+
const externalId = buildExternalId(this.owner, this.repo, data.number);
|
|
10522
|
+
return Ok4({ externalId, url: data.html_url });
|
|
10523
|
+
} catch (error) {
|
|
10524
|
+
return Err3(error instanceof Error ? error : new Error(String(error)));
|
|
10525
|
+
}
|
|
10526
|
+
}
|
|
10527
|
+
async updateTicket(externalId, changes) {
|
|
10528
|
+
try {
|
|
10529
|
+
const parsed = parseExternalId(externalId);
|
|
10530
|
+
if (!parsed) return Err3(new Error(`Invalid externalId format: "${externalId}"`));
|
|
10531
|
+
const patch = {};
|
|
10532
|
+
if (changes.name !== void 0) patch.title = changes.name;
|
|
10533
|
+
if (changes.summary !== void 0) {
|
|
10534
|
+
const body = [changes.summary, "", changes.spec ? `**Spec:** ${changes.spec}` : ""].filter(Boolean).join("\n");
|
|
10535
|
+
patch.body = body;
|
|
10536
|
+
}
|
|
10537
|
+
if (changes.status !== void 0) {
|
|
10538
|
+
const externalStatus = this.config.statusMap[changes.status];
|
|
10539
|
+
patch.state = externalStatus;
|
|
10540
|
+
patch.labels = labelsForStatus(changes.status, this.config);
|
|
10541
|
+
}
|
|
10542
|
+
const response = await this.fetchFn(
|
|
10543
|
+
`${this.apiBase}/repos/${parsed.owner}/${parsed.repo}/issues/${parsed.number}`,
|
|
10544
|
+
{
|
|
10545
|
+
method: "PATCH",
|
|
10546
|
+
headers: this.headers(),
|
|
10547
|
+
body: JSON.stringify(patch)
|
|
10548
|
+
}
|
|
10549
|
+
);
|
|
10550
|
+
if (!response.ok) {
|
|
10551
|
+
const text = await response.text();
|
|
10552
|
+
return Err3(new Error(`GitHub API error ${response.status}: ${text}`));
|
|
10553
|
+
}
|
|
10554
|
+
const data = await response.json();
|
|
10555
|
+
return Ok4({ externalId, url: data.html_url });
|
|
10556
|
+
} catch (error) {
|
|
10557
|
+
return Err3(error instanceof Error ? error : new Error(String(error)));
|
|
10558
|
+
}
|
|
10559
|
+
}
|
|
10560
|
+
async fetchTicketState(externalId) {
|
|
10561
|
+
try {
|
|
10562
|
+
const parsed = parseExternalId(externalId);
|
|
10563
|
+
if (!parsed) return Err3(new Error(`Invalid externalId format: "${externalId}"`));
|
|
10564
|
+
const response = await this.fetchFn(
|
|
10565
|
+
`${this.apiBase}/repos/${parsed.owner}/${parsed.repo}/issues/${parsed.number}`,
|
|
10566
|
+
{
|
|
10567
|
+
method: "GET",
|
|
10568
|
+
headers: this.headers()
|
|
10569
|
+
}
|
|
10570
|
+
);
|
|
10571
|
+
if (!response.ok) {
|
|
10572
|
+
const text = await response.text();
|
|
10573
|
+
return Err3(new Error(`GitHub API error ${response.status}: ${text}`));
|
|
10574
|
+
}
|
|
10575
|
+
const data = await response.json();
|
|
10576
|
+
return Ok4({
|
|
10577
|
+
externalId,
|
|
10578
|
+
status: data.state,
|
|
10579
|
+
labels: data.labels.map((l) => l.name),
|
|
10580
|
+
assignee: data.assignee ? `@${data.assignee.login}` : null
|
|
10581
|
+
});
|
|
10582
|
+
} catch (error) {
|
|
10583
|
+
return Err3(error instanceof Error ? error : new Error(String(error)));
|
|
10584
|
+
}
|
|
10585
|
+
}
|
|
10586
|
+
async fetchAllTickets() {
|
|
10587
|
+
try {
|
|
10588
|
+
const filterLabels = this.config.labels ?? [];
|
|
10589
|
+
const labelsParam = filterLabels.length > 0 ? `&labels=${filterLabels.join(",")}` : "";
|
|
10590
|
+
const tickets = [];
|
|
10591
|
+
let page = 1;
|
|
10592
|
+
const perPage = 100;
|
|
10593
|
+
while (true) {
|
|
10594
|
+
const response = await this.fetchFn(
|
|
10595
|
+
`${this.apiBase}/repos/${this.owner}/${this.repo}/issues?state=all&per_page=${perPage}&page=${page}${labelsParam}`,
|
|
10596
|
+
{
|
|
10597
|
+
method: "GET",
|
|
10598
|
+
headers: this.headers()
|
|
10599
|
+
}
|
|
10600
|
+
);
|
|
10601
|
+
if (!response.ok) {
|
|
10602
|
+
const text = await response.text();
|
|
10603
|
+
return Err3(new Error(`GitHub API error ${response.status}: ${text}`));
|
|
10604
|
+
}
|
|
10605
|
+
const data = await response.json();
|
|
10606
|
+
const issues = data.filter((d) => !d.pull_request);
|
|
10607
|
+
for (const issue of issues) {
|
|
10608
|
+
tickets.push({
|
|
10609
|
+
externalId: buildExternalId(this.owner, this.repo, issue.number),
|
|
10610
|
+
status: issue.state,
|
|
10611
|
+
labels: issue.labels.map((l) => l.name),
|
|
10612
|
+
assignee: issue.assignee ? `@${issue.assignee.login}` : null
|
|
10613
|
+
});
|
|
10614
|
+
}
|
|
10615
|
+
if (data.length < perPage) break;
|
|
10616
|
+
page++;
|
|
10617
|
+
}
|
|
10618
|
+
return Ok4(tickets);
|
|
10619
|
+
} catch (error) {
|
|
10620
|
+
return Err3(error instanceof Error ? error : new Error(String(error)));
|
|
10621
|
+
}
|
|
10622
|
+
}
|
|
10623
|
+
async assignTicket(externalId, assignee) {
|
|
10624
|
+
try {
|
|
10625
|
+
const parsed = parseExternalId(externalId);
|
|
10626
|
+
if (!parsed) return Err3(new Error(`Invalid externalId format: "${externalId}"`));
|
|
10627
|
+
const login = assignee.startsWith("@") ? assignee.slice(1) : assignee;
|
|
10628
|
+
const response = await this.fetchFn(
|
|
10629
|
+
`${this.apiBase}/repos/${parsed.owner}/${parsed.repo}/issues/${parsed.number}/assignees`,
|
|
10630
|
+
{
|
|
10631
|
+
method: "POST",
|
|
10632
|
+
headers: this.headers(),
|
|
10633
|
+
body: JSON.stringify({ assignees: [login] })
|
|
10634
|
+
}
|
|
10635
|
+
);
|
|
10636
|
+
if (!response.ok) {
|
|
10637
|
+
const text = await response.text();
|
|
10638
|
+
return Err3(new Error(`GitHub API error ${response.status}: ${text}`));
|
|
10639
|
+
}
|
|
10640
|
+
return Ok4(void 0);
|
|
10641
|
+
} catch (error) {
|
|
10642
|
+
return Err3(error instanceof Error ? error : new Error(String(error)));
|
|
10643
|
+
}
|
|
10644
|
+
}
|
|
10645
|
+
};
|
|
10646
|
+
|
|
10647
|
+
// src/roadmap/sync-engine.ts
|
|
10648
|
+
import * as fs20 from "fs";
|
|
10649
|
+
function emptySyncResult() {
|
|
10650
|
+
return { created: [], updated: [], assignmentChanges: [], errors: [] };
|
|
10651
|
+
}
|
|
10652
|
+
async function syncToExternal(roadmap, adapter, _config) {
|
|
10653
|
+
const result = emptySyncResult();
|
|
10654
|
+
for (const milestone of roadmap.milestones) {
|
|
10655
|
+
for (const feature of milestone.features) {
|
|
10656
|
+
if (!feature.externalId) {
|
|
10657
|
+
const createResult = await adapter.createTicket(feature, milestone.name);
|
|
10658
|
+
if (createResult.ok) {
|
|
10659
|
+
feature.externalId = createResult.value.externalId;
|
|
10660
|
+
result.created.push(createResult.value);
|
|
10661
|
+
} else {
|
|
10662
|
+
result.errors.push({ featureOrId: feature.name, error: createResult.error });
|
|
10663
|
+
}
|
|
10664
|
+
} else {
|
|
10665
|
+
const updateResult = await adapter.updateTicket(feature.externalId, feature);
|
|
10666
|
+
if (updateResult.ok) {
|
|
10667
|
+
result.updated.push(feature.externalId);
|
|
10668
|
+
} else {
|
|
10669
|
+
result.errors.push({ featureOrId: feature.externalId, error: updateResult.error });
|
|
10670
|
+
}
|
|
10671
|
+
}
|
|
10672
|
+
}
|
|
10673
|
+
}
|
|
10674
|
+
return result;
|
|
10675
|
+
}
|
|
10676
|
+
async function syncFromExternal(roadmap, adapter, config, options) {
|
|
10677
|
+
const result = emptySyncResult();
|
|
10678
|
+
const forceSync = options?.forceSync ?? false;
|
|
10679
|
+
const featureByExternalId = /* @__PURE__ */ new Map();
|
|
10680
|
+
for (const milestone of roadmap.milestones) {
|
|
10681
|
+
for (const feature of milestone.features) {
|
|
10682
|
+
if (feature.externalId) {
|
|
10683
|
+
featureByExternalId.set(feature.externalId, feature);
|
|
10684
|
+
}
|
|
10685
|
+
}
|
|
10686
|
+
}
|
|
10687
|
+
if (featureByExternalId.size === 0) return result;
|
|
10688
|
+
const fetchResult = await adapter.fetchAllTickets();
|
|
10689
|
+
if (!fetchResult.ok) {
|
|
10690
|
+
result.errors.push({ featureOrId: "*", error: fetchResult.error });
|
|
10691
|
+
return result;
|
|
10692
|
+
}
|
|
10693
|
+
for (const ticketState of fetchResult.value) {
|
|
10694
|
+
const feature = featureByExternalId.get(ticketState.externalId);
|
|
10695
|
+
if (!feature) continue;
|
|
10696
|
+
if (ticketState.assignee !== feature.assignee) {
|
|
10697
|
+
result.assignmentChanges.push({
|
|
10698
|
+
feature: feature.name,
|
|
10699
|
+
from: feature.assignee,
|
|
10700
|
+
to: ticketState.assignee
|
|
10701
|
+
});
|
|
10702
|
+
feature.assignee = ticketState.assignee;
|
|
10703
|
+
}
|
|
10704
|
+
const resolvedStatus = resolveReverseStatus(ticketState.status, ticketState.labels, config);
|
|
10705
|
+
if (resolvedStatus && resolvedStatus !== feature.status) {
|
|
10706
|
+
const newStatus = resolvedStatus;
|
|
10707
|
+
if (!forceSync && isRegression(feature.status, newStatus)) {
|
|
10708
|
+
continue;
|
|
10709
|
+
}
|
|
10710
|
+
feature.status = newStatus;
|
|
10711
|
+
}
|
|
10712
|
+
}
|
|
10713
|
+
return result;
|
|
10714
|
+
}
|
|
10715
|
+
var syncMutex = Promise.resolve();
|
|
10716
|
+
async function fullSync(roadmapPath, adapter, config, options) {
|
|
10717
|
+
const previousSync = syncMutex;
|
|
10718
|
+
let releaseMutex;
|
|
10719
|
+
syncMutex = new Promise((resolve5) => {
|
|
10720
|
+
releaseMutex = resolve5;
|
|
10721
|
+
});
|
|
10722
|
+
await previousSync;
|
|
10723
|
+
try {
|
|
10724
|
+
const raw = fs20.readFileSync(roadmapPath, "utf-8");
|
|
10725
|
+
const parseResult = parseRoadmap(raw);
|
|
10726
|
+
if (!parseResult.ok) {
|
|
10727
|
+
return {
|
|
10728
|
+
...emptySyncResult(),
|
|
10729
|
+
errors: [{ featureOrId: "*", error: parseResult.error }]
|
|
10730
|
+
};
|
|
10731
|
+
}
|
|
10732
|
+
const roadmap = parseResult.value;
|
|
10733
|
+
const pushResult = await syncToExternal(roadmap, adapter, config);
|
|
10734
|
+
const pullResult = await syncFromExternal(roadmap, adapter, config, options);
|
|
10735
|
+
fs20.writeFileSync(roadmapPath, serializeRoadmap(roadmap), "utf-8");
|
|
10736
|
+
return {
|
|
10737
|
+
created: pushResult.created,
|
|
10738
|
+
updated: pushResult.updated,
|
|
10739
|
+
assignmentChanges: pullResult.assignmentChanges,
|
|
10740
|
+
errors: [...pushResult.errors, ...pullResult.errors]
|
|
10741
|
+
};
|
|
10742
|
+
} finally {
|
|
10743
|
+
releaseMutex();
|
|
10744
|
+
}
|
|
10745
|
+
}
|
|
10746
|
+
|
|
10747
|
+
// src/roadmap/pilot-scoring.ts
|
|
10748
|
+
var PRIORITY_RANK = {
|
|
10749
|
+
P0: 0,
|
|
10750
|
+
P1: 1,
|
|
10751
|
+
P2: 2,
|
|
10752
|
+
P3: 3
|
|
10753
|
+
};
|
|
10754
|
+
var POSITION_WEIGHT = 0.5;
|
|
10755
|
+
var DEPENDENTS_WEIGHT = 0.3;
|
|
10756
|
+
var AFFINITY_WEIGHT = 0.2;
|
|
10757
|
+
function scoreRoadmapCandidates(roadmap, options) {
|
|
10758
|
+
const allFeatures = roadmap.milestones.flatMap((m) => m.features);
|
|
10759
|
+
const allFeatureNames = new Set(allFeatures.map((f) => f.name.toLowerCase()));
|
|
10760
|
+
const doneFeatures = new Set(
|
|
10761
|
+
allFeatures.filter((f) => f.status === "done").map((f) => f.name.toLowerCase())
|
|
10762
|
+
);
|
|
10763
|
+
const dependentsCount = /* @__PURE__ */ new Map();
|
|
10764
|
+
for (const feature of allFeatures) {
|
|
10765
|
+
for (const blocker of feature.blockedBy) {
|
|
10766
|
+
const key = blocker.toLowerCase();
|
|
10767
|
+
dependentsCount.set(key, (dependentsCount.get(key) ?? 0) + 1);
|
|
10768
|
+
}
|
|
10769
|
+
}
|
|
10770
|
+
const maxDependents = Math.max(1, ...dependentsCount.values());
|
|
10771
|
+
const milestoneMap = /* @__PURE__ */ new Map();
|
|
10772
|
+
for (const ms of roadmap.milestones) {
|
|
10773
|
+
milestoneMap.set(
|
|
10774
|
+
ms.name,
|
|
10775
|
+
ms.features.map((f) => f.name.toLowerCase())
|
|
10776
|
+
);
|
|
10777
|
+
}
|
|
10778
|
+
const userCompletedFeatures = /* @__PURE__ */ new Set();
|
|
10779
|
+
if (options?.currentUser) {
|
|
10780
|
+
const user = options.currentUser.toLowerCase();
|
|
10781
|
+
for (const record of roadmap.assignmentHistory) {
|
|
10782
|
+
if (record.action === "completed" && record.assignee.toLowerCase() === user) {
|
|
10783
|
+
userCompletedFeatures.add(record.feature.toLowerCase());
|
|
10784
|
+
}
|
|
10785
|
+
}
|
|
10786
|
+
}
|
|
10787
|
+
let totalPositions = 0;
|
|
10788
|
+
for (const ms of roadmap.milestones) {
|
|
10789
|
+
totalPositions += ms.features.length;
|
|
10790
|
+
}
|
|
10791
|
+
totalPositions = Math.max(1, totalPositions);
|
|
10792
|
+
const candidates = [];
|
|
10793
|
+
let globalPosition = 0;
|
|
10794
|
+
for (const ms of roadmap.milestones) {
|
|
10795
|
+
for (let featureIdx = 0; featureIdx < ms.features.length; featureIdx++) {
|
|
10796
|
+
const feature = ms.features[featureIdx];
|
|
10797
|
+
globalPosition++;
|
|
10798
|
+
if (feature.status !== "planned" && feature.status !== "backlog") continue;
|
|
10799
|
+
const isBlocked = feature.blockedBy.some((blocker) => {
|
|
10800
|
+
const key = blocker.toLowerCase();
|
|
10801
|
+
return allFeatureNames.has(key) && !doneFeatures.has(key);
|
|
10802
|
+
});
|
|
10803
|
+
if (isBlocked) continue;
|
|
10804
|
+
const positionScore = 1 - (globalPosition - 1) / totalPositions;
|
|
10805
|
+
const deps = dependentsCount.get(feature.name.toLowerCase()) ?? 0;
|
|
10806
|
+
const dependentsScore = deps / maxDependents;
|
|
10807
|
+
let affinityScore = 0;
|
|
10808
|
+
if (userCompletedFeatures.size > 0) {
|
|
10809
|
+
const completedBlockers = feature.blockedBy.filter(
|
|
10810
|
+
(b) => userCompletedFeatures.has(b.toLowerCase())
|
|
10811
|
+
);
|
|
10812
|
+
if (completedBlockers.length > 0) {
|
|
10813
|
+
affinityScore = 1;
|
|
10814
|
+
} else {
|
|
10815
|
+
const siblings = milestoneMap.get(ms.name) ?? [];
|
|
10816
|
+
const completedSiblings = siblings.filter((s) => userCompletedFeatures.has(s));
|
|
10817
|
+
if (completedSiblings.length > 0) {
|
|
10818
|
+
affinityScore = 0.5;
|
|
10819
|
+
}
|
|
10820
|
+
}
|
|
10821
|
+
}
|
|
10822
|
+
const weightedScore = POSITION_WEIGHT * positionScore + DEPENDENTS_WEIGHT * dependentsScore + AFFINITY_WEIGHT * affinityScore;
|
|
10823
|
+
const priorityTier = feature.priority ? PRIORITY_RANK[feature.priority] : null;
|
|
10824
|
+
candidates.push({
|
|
10825
|
+
feature,
|
|
10826
|
+
milestone: ms.name,
|
|
10827
|
+
positionScore,
|
|
10828
|
+
dependentsScore,
|
|
10829
|
+
affinityScore,
|
|
10830
|
+
weightedScore,
|
|
10831
|
+
priorityTier
|
|
10832
|
+
});
|
|
10833
|
+
}
|
|
10834
|
+
}
|
|
10835
|
+
candidates.sort((a, b) => {
|
|
10836
|
+
if (a.priorityTier !== null && b.priorityTier === null) return -1;
|
|
10837
|
+
if (a.priorityTier === null && b.priorityTier !== null) return 1;
|
|
10838
|
+
if (a.priorityTier !== null && b.priorityTier !== null) {
|
|
10839
|
+
if (a.priorityTier !== b.priorityTier) return a.priorityTier - b.priorityTier;
|
|
10840
|
+
}
|
|
10841
|
+
return b.weightedScore - a.weightedScore;
|
|
10842
|
+
});
|
|
10843
|
+
return candidates;
|
|
10844
|
+
}
|
|
10845
|
+
function assignFeature(roadmap, feature, assignee, date) {
|
|
10846
|
+
if (feature.assignee === assignee) return;
|
|
10847
|
+
if (feature.assignee !== null) {
|
|
10848
|
+
roadmap.assignmentHistory.push({
|
|
10849
|
+
feature: feature.name,
|
|
10850
|
+
assignee: feature.assignee,
|
|
10851
|
+
action: "unassigned",
|
|
10852
|
+
date
|
|
10853
|
+
});
|
|
10854
|
+
}
|
|
10855
|
+
feature.assignee = assignee;
|
|
10856
|
+
roadmap.assignmentHistory.push({
|
|
10857
|
+
feature: feature.name,
|
|
10858
|
+
assignee,
|
|
10859
|
+
action: "assigned",
|
|
10860
|
+
date
|
|
10861
|
+
});
|
|
10862
|
+
}
|
|
10863
|
+
|
|
9740
10864
|
// src/interaction/types.ts
|
|
9741
10865
|
import { z as z7 } from "zod";
|
|
9742
10866
|
var InteractionTypeSchema = z7.enum(["question", "confirmation", "transition"]);
|
|
@@ -9767,17 +10891,18 @@ var EmitInteractionInputSchema = z7.object({
|
|
|
9767
10891
|
});
|
|
9768
10892
|
|
|
9769
10893
|
// src/blueprint/scanner.ts
|
|
9770
|
-
import * as
|
|
10894
|
+
import * as fs21 from "fs/promises";
|
|
9771
10895
|
import * as path20 from "path";
|
|
9772
10896
|
var ProjectScanner = class {
|
|
9773
10897
|
constructor(rootDir) {
|
|
9774
10898
|
this.rootDir = rootDir;
|
|
9775
10899
|
}
|
|
10900
|
+
rootDir;
|
|
9776
10901
|
async scan() {
|
|
9777
10902
|
let projectName = path20.basename(this.rootDir);
|
|
9778
10903
|
try {
|
|
9779
10904
|
const pkgPath = path20.join(this.rootDir, "package.json");
|
|
9780
|
-
const pkgRaw = await
|
|
10905
|
+
const pkgRaw = await fs21.readFile(pkgPath, "utf-8");
|
|
9781
10906
|
const pkg = JSON.parse(pkgRaw);
|
|
9782
10907
|
if (pkg.name) projectName = pkg.name;
|
|
9783
10908
|
} catch {
|
|
@@ -9818,7 +10943,7 @@ var ProjectScanner = class {
|
|
|
9818
10943
|
};
|
|
9819
10944
|
|
|
9820
10945
|
// src/blueprint/generator.ts
|
|
9821
|
-
import * as
|
|
10946
|
+
import * as fs22 from "fs/promises";
|
|
9822
10947
|
import * as path21 from "path";
|
|
9823
10948
|
import * as ejs from "ejs";
|
|
9824
10949
|
|
|
@@ -9903,13 +11028,13 @@ var BlueprintGenerator = class {
|
|
|
9903
11028
|
styles: STYLES,
|
|
9904
11029
|
scripts: SCRIPTS
|
|
9905
11030
|
});
|
|
9906
|
-
await
|
|
9907
|
-
await
|
|
11031
|
+
await fs22.mkdir(options.outputDir, { recursive: true });
|
|
11032
|
+
await fs22.writeFile(path21.join(options.outputDir, "index.html"), html);
|
|
9908
11033
|
}
|
|
9909
11034
|
};
|
|
9910
11035
|
|
|
9911
11036
|
// src/update-checker.ts
|
|
9912
|
-
import * as
|
|
11037
|
+
import * as fs23 from "fs";
|
|
9913
11038
|
import * as path22 from "path";
|
|
9914
11039
|
import * as os from "os";
|
|
9915
11040
|
import { spawn } from "child_process";
|
|
@@ -9928,7 +11053,7 @@ function shouldRunCheck(state, intervalMs) {
|
|
|
9928
11053
|
}
|
|
9929
11054
|
function readCheckState() {
|
|
9930
11055
|
try {
|
|
9931
|
-
const raw =
|
|
11056
|
+
const raw = fs23.readFileSync(getStatePath(), "utf-8");
|
|
9932
11057
|
const parsed = JSON.parse(raw);
|
|
9933
11058
|
if (typeof parsed === "object" && parsed !== null && "lastCheckTime" in parsed && typeof parsed.lastCheckTime === "number" && "currentVersion" in parsed && typeof parsed.currentVersion === "string") {
|
|
9934
11059
|
const state = parsed;
|
|
@@ -10034,9 +11159,9 @@ async function resolveWasmPath(grammarName) {
|
|
|
10034
11159
|
const { createRequire } = await import("module");
|
|
10035
11160
|
const require2 = createRequire(import.meta.url ?? __filename);
|
|
10036
11161
|
const pkgPath = require2.resolve("tree-sitter-wasms/package.json");
|
|
10037
|
-
const
|
|
10038
|
-
const pkgDir =
|
|
10039
|
-
return
|
|
11162
|
+
const path26 = await import("path");
|
|
11163
|
+
const pkgDir = path26.dirname(pkgPath);
|
|
11164
|
+
return path26.join(pkgDir, "out", `${grammarName}.wasm`);
|
|
10040
11165
|
}
|
|
10041
11166
|
async function loadLanguage(lang) {
|
|
10042
11167
|
const grammarName = GRAMMAR_MAP[lang];
|
|
@@ -10400,6 +11525,489 @@ async function unfoldRange(filePath, startLine, endLine) {
|
|
|
10400
11525
|
};
|
|
10401
11526
|
}
|
|
10402
11527
|
|
|
11528
|
+
// src/pricing/pricing.ts
|
|
11529
|
+
var TOKENS_PER_MILLION = 1e6;
|
|
11530
|
+
function parseLiteLLMData(raw) {
|
|
11531
|
+
const dataset = /* @__PURE__ */ new Map();
|
|
11532
|
+
for (const [modelName, entry] of Object.entries(raw)) {
|
|
11533
|
+
if (modelName === "sample_spec") continue;
|
|
11534
|
+
if (entry.mode && entry.mode !== "chat") continue;
|
|
11535
|
+
const inputCost = entry.input_cost_per_token;
|
|
11536
|
+
const outputCost = entry.output_cost_per_token;
|
|
11537
|
+
if (inputCost == null || outputCost == null) continue;
|
|
11538
|
+
const pricing = {
|
|
11539
|
+
inputPer1M: inputCost * TOKENS_PER_MILLION,
|
|
11540
|
+
outputPer1M: outputCost * TOKENS_PER_MILLION
|
|
11541
|
+
};
|
|
11542
|
+
if (entry.cache_read_input_token_cost != null) {
|
|
11543
|
+
pricing.cacheReadPer1M = entry.cache_read_input_token_cost * TOKENS_PER_MILLION;
|
|
11544
|
+
}
|
|
11545
|
+
if (entry.cache_creation_input_token_cost != null) {
|
|
11546
|
+
pricing.cacheWritePer1M = entry.cache_creation_input_token_cost * TOKENS_PER_MILLION;
|
|
11547
|
+
}
|
|
11548
|
+
dataset.set(modelName, pricing);
|
|
11549
|
+
}
|
|
11550
|
+
return dataset;
|
|
11551
|
+
}
|
|
11552
|
+
function getModelPrice(model, dataset) {
|
|
11553
|
+
if (!model) {
|
|
11554
|
+
console.warn("[harness pricing] No model specified \u2014 cannot look up pricing.");
|
|
11555
|
+
return null;
|
|
11556
|
+
}
|
|
11557
|
+
const pricing = dataset.get(model);
|
|
11558
|
+
if (!pricing) {
|
|
11559
|
+
console.warn(
|
|
11560
|
+
`[harness pricing] No pricing data for model "${model}". Consider updating pricing data.`
|
|
11561
|
+
);
|
|
11562
|
+
return null;
|
|
11563
|
+
}
|
|
11564
|
+
return pricing;
|
|
11565
|
+
}
|
|
11566
|
+
|
|
11567
|
+
// src/pricing/cache.ts
|
|
11568
|
+
import * as fs24 from "fs/promises";
|
|
11569
|
+
import * as path23 from "path";
|
|
11570
|
+
|
|
11571
|
+
// src/pricing/fallback.json
|
|
11572
|
+
var fallback_default = {
|
|
11573
|
+
_generatedAt: "2026-03-31",
|
|
11574
|
+
_source: "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json",
|
|
11575
|
+
models: {
|
|
11576
|
+
"claude-opus-4-20250514": {
|
|
11577
|
+
inputPer1M: 15,
|
|
11578
|
+
outputPer1M: 75,
|
|
11579
|
+
cacheReadPer1M: 1.5,
|
|
11580
|
+
cacheWritePer1M: 18.75
|
|
11581
|
+
},
|
|
11582
|
+
"claude-sonnet-4-20250514": {
|
|
11583
|
+
inputPer1M: 3,
|
|
11584
|
+
outputPer1M: 15,
|
|
11585
|
+
cacheReadPer1M: 0.3,
|
|
11586
|
+
cacheWritePer1M: 3.75
|
|
11587
|
+
},
|
|
11588
|
+
"claude-3-5-haiku-20241022": {
|
|
11589
|
+
inputPer1M: 0.8,
|
|
11590
|
+
outputPer1M: 4,
|
|
11591
|
+
cacheReadPer1M: 0.08,
|
|
11592
|
+
cacheWritePer1M: 1
|
|
11593
|
+
},
|
|
11594
|
+
"gpt-4o": {
|
|
11595
|
+
inputPer1M: 2.5,
|
|
11596
|
+
outputPer1M: 10,
|
|
11597
|
+
cacheReadPer1M: 1.25
|
|
11598
|
+
},
|
|
11599
|
+
"gpt-4o-mini": {
|
|
11600
|
+
inputPer1M: 0.15,
|
|
11601
|
+
outputPer1M: 0.6,
|
|
11602
|
+
cacheReadPer1M: 0.075
|
|
11603
|
+
},
|
|
11604
|
+
"gemini-2.0-flash": {
|
|
11605
|
+
inputPer1M: 0.1,
|
|
11606
|
+
outputPer1M: 0.4,
|
|
11607
|
+
cacheReadPer1M: 0.025
|
|
11608
|
+
},
|
|
11609
|
+
"gemini-2.5-pro": {
|
|
11610
|
+
inputPer1M: 1.25,
|
|
11611
|
+
outputPer1M: 10,
|
|
11612
|
+
cacheReadPer1M: 0.3125
|
|
11613
|
+
}
|
|
11614
|
+
}
|
|
11615
|
+
};
|
|
11616
|
+
|
|
11617
|
+
// src/pricing/cache.ts
|
|
11618
|
+
var LITELLM_PRICING_URL = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json";
|
|
11619
|
+
var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
11620
|
+
var STALENESS_WARNING_DAYS = 7;
|
|
11621
|
+
function getCachePath(projectRoot) {
|
|
11622
|
+
return path23.join(projectRoot, ".harness", "cache", "pricing.json");
|
|
11623
|
+
}
|
|
11624
|
+
function getStalenessMarkerPath(projectRoot) {
|
|
11625
|
+
return path23.join(projectRoot, ".harness", "cache", "staleness-marker.json");
|
|
11626
|
+
}
|
|
11627
|
+
async function readDiskCache(projectRoot) {
|
|
11628
|
+
try {
|
|
11629
|
+
const raw = await fs24.readFile(getCachePath(projectRoot), "utf-8");
|
|
11630
|
+
return JSON.parse(raw);
|
|
11631
|
+
} catch {
|
|
11632
|
+
return null;
|
|
11633
|
+
}
|
|
11634
|
+
}
|
|
11635
|
+
async function writeDiskCache(projectRoot, data) {
|
|
11636
|
+
const cachePath = getCachePath(projectRoot);
|
|
11637
|
+
await fs24.mkdir(path23.dirname(cachePath), { recursive: true });
|
|
11638
|
+
await fs24.writeFile(cachePath, JSON.stringify(data, null, 2));
|
|
11639
|
+
}
|
|
11640
|
+
async function fetchFromNetwork() {
|
|
11641
|
+
try {
|
|
11642
|
+
const response = await fetch(LITELLM_PRICING_URL);
|
|
11643
|
+
if (!response.ok) return null;
|
|
11644
|
+
const data = await response.json();
|
|
11645
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) return null;
|
|
11646
|
+
return {
|
|
11647
|
+
fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11648
|
+
data
|
|
11649
|
+
};
|
|
11650
|
+
} catch {
|
|
11651
|
+
return null;
|
|
11652
|
+
}
|
|
11653
|
+
}
|
|
11654
|
+
function loadFallbackDataset() {
|
|
11655
|
+
const fb = fallback_default;
|
|
11656
|
+
const dataset = /* @__PURE__ */ new Map();
|
|
11657
|
+
for (const [model, pricing] of Object.entries(fb.models)) {
|
|
11658
|
+
dataset.set(model, pricing);
|
|
11659
|
+
}
|
|
11660
|
+
return dataset;
|
|
11661
|
+
}
|
|
11662
|
+
async function checkAndWarnStaleness(projectRoot) {
|
|
11663
|
+
const markerPath = getStalenessMarkerPath(projectRoot);
|
|
11664
|
+
try {
|
|
11665
|
+
const raw = await fs24.readFile(markerPath, "utf-8");
|
|
11666
|
+
const marker = JSON.parse(raw);
|
|
11667
|
+
const firstUse = new Date(marker.firstFallbackUse).getTime();
|
|
11668
|
+
const now = Date.now();
|
|
11669
|
+
const daysSinceFirstUse = (now - firstUse) / (24 * 60 * 60 * 1e3);
|
|
11670
|
+
if (daysSinceFirstUse > STALENESS_WARNING_DAYS) {
|
|
11671
|
+
console.warn(
|
|
11672
|
+
`[harness pricing] Pricing data is stale \u2014 using bundled fallback for ${Math.floor(daysSinceFirstUse)} days. Connect to the internet to refresh pricing data.`
|
|
11673
|
+
);
|
|
11674
|
+
}
|
|
11675
|
+
} catch {
|
|
11676
|
+
try {
|
|
11677
|
+
await fs24.mkdir(path23.dirname(markerPath), { recursive: true });
|
|
11678
|
+
await fs24.writeFile(
|
|
11679
|
+
markerPath,
|
|
11680
|
+
JSON.stringify({ firstFallbackUse: (/* @__PURE__ */ new Date()).toISOString() })
|
|
11681
|
+
);
|
|
11682
|
+
} catch {
|
|
11683
|
+
}
|
|
11684
|
+
}
|
|
11685
|
+
}
|
|
11686
|
+
async function clearStalenessMarker(projectRoot) {
|
|
11687
|
+
try {
|
|
11688
|
+
await fs24.unlink(getStalenessMarkerPath(projectRoot));
|
|
11689
|
+
} catch {
|
|
11690
|
+
}
|
|
11691
|
+
}
|
|
11692
|
+
async function loadPricingData(projectRoot) {
|
|
11693
|
+
const cache = await readDiskCache(projectRoot);
|
|
11694
|
+
if (cache) {
|
|
11695
|
+
const cacheAge = Date.now() - new Date(cache.fetchedAt).getTime();
|
|
11696
|
+
if (cacheAge < CACHE_TTL_MS) {
|
|
11697
|
+
await clearStalenessMarker(projectRoot);
|
|
11698
|
+
return parseLiteLLMData(cache.data);
|
|
11699
|
+
}
|
|
11700
|
+
}
|
|
11701
|
+
const fetched = await fetchFromNetwork();
|
|
11702
|
+
if (fetched) {
|
|
11703
|
+
await writeDiskCache(projectRoot, fetched);
|
|
11704
|
+
await clearStalenessMarker(projectRoot);
|
|
11705
|
+
return parseLiteLLMData(fetched.data);
|
|
11706
|
+
}
|
|
11707
|
+
if (cache) {
|
|
11708
|
+
return parseLiteLLMData(cache.data);
|
|
11709
|
+
}
|
|
11710
|
+
await checkAndWarnStaleness(projectRoot);
|
|
11711
|
+
return loadFallbackDataset();
|
|
11712
|
+
}
|
|
11713
|
+
|
|
11714
|
+
// src/pricing/calculator.ts
|
|
11715
|
+
var MICRODOLLARS_PER_DOLLAR = 1e6;
|
|
11716
|
+
var TOKENS_PER_MILLION2 = 1e6;
|
|
11717
|
+
function calculateCost(record, dataset) {
|
|
11718
|
+
if (!record.model) return null;
|
|
11719
|
+
const pricing = getModelPrice(record.model, dataset);
|
|
11720
|
+
if (!pricing) return null;
|
|
11721
|
+
let costUSD = 0;
|
|
11722
|
+
costUSD += record.tokens.inputTokens / TOKENS_PER_MILLION2 * pricing.inputPer1M;
|
|
11723
|
+
costUSD += record.tokens.outputTokens / TOKENS_PER_MILLION2 * pricing.outputPer1M;
|
|
11724
|
+
if (record.cacheReadTokens != null && pricing.cacheReadPer1M != null) {
|
|
11725
|
+
costUSD += record.cacheReadTokens / TOKENS_PER_MILLION2 * pricing.cacheReadPer1M;
|
|
11726
|
+
}
|
|
11727
|
+
if (record.cacheCreationTokens != null && pricing.cacheWritePer1M != null) {
|
|
11728
|
+
costUSD += record.cacheCreationTokens / TOKENS_PER_MILLION2 * pricing.cacheWritePer1M;
|
|
11729
|
+
}
|
|
11730
|
+
return Math.round(costUSD * MICRODOLLARS_PER_DOLLAR);
|
|
11731
|
+
}
|
|
11732
|
+
|
|
11733
|
+
// src/usage/aggregator.ts
|
|
11734
|
+
function aggregateBySession(records) {
|
|
11735
|
+
if (records.length === 0) return [];
|
|
11736
|
+
const sessionMap = /* @__PURE__ */ new Map();
|
|
11737
|
+
for (const record of records) {
|
|
11738
|
+
const tagged = record;
|
|
11739
|
+
const id = record.sessionId;
|
|
11740
|
+
if (!sessionMap.has(id)) {
|
|
11741
|
+
sessionMap.set(id, { harnessRecords: [], ccRecords: [], allRecords: [] });
|
|
11742
|
+
}
|
|
11743
|
+
const bucket = sessionMap.get(id);
|
|
11744
|
+
if (tagged._source === "claude-code") {
|
|
11745
|
+
bucket.ccRecords.push(tagged);
|
|
11746
|
+
} else {
|
|
11747
|
+
bucket.harnessRecords.push(tagged);
|
|
11748
|
+
}
|
|
11749
|
+
bucket.allRecords.push(tagged);
|
|
11750
|
+
}
|
|
11751
|
+
const results = [];
|
|
11752
|
+
for (const [sessionId, bucket] of sessionMap) {
|
|
11753
|
+
const hasHarness = bucket.harnessRecords.length > 0;
|
|
11754
|
+
const hasCC = bucket.ccRecords.length > 0;
|
|
11755
|
+
const isMerged = hasHarness && hasCC;
|
|
11756
|
+
const tokenSource = hasHarness ? bucket.harnessRecords : bucket.ccRecords;
|
|
11757
|
+
const tokens = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
11758
|
+
let cacheCreation;
|
|
11759
|
+
let cacheRead;
|
|
11760
|
+
let costMicroUSD = 0;
|
|
11761
|
+
let model;
|
|
11762
|
+
for (const r of tokenSource) {
|
|
11763
|
+
tokens.inputTokens += r.tokens.inputTokens;
|
|
11764
|
+
tokens.outputTokens += r.tokens.outputTokens;
|
|
11765
|
+
tokens.totalTokens += r.tokens.totalTokens;
|
|
11766
|
+
if (r.cacheCreationTokens != null) {
|
|
11767
|
+
cacheCreation = (cacheCreation ?? 0) + r.cacheCreationTokens;
|
|
11768
|
+
}
|
|
11769
|
+
if (r.cacheReadTokens != null) {
|
|
11770
|
+
cacheRead = (cacheRead ?? 0) + r.cacheReadTokens;
|
|
11771
|
+
}
|
|
11772
|
+
if (r.costMicroUSD != null && costMicroUSD != null) {
|
|
11773
|
+
costMicroUSD += r.costMicroUSD;
|
|
11774
|
+
} else if (r.costMicroUSD == null) {
|
|
11775
|
+
costMicroUSD = null;
|
|
11776
|
+
}
|
|
11777
|
+
if (!model && r.model) {
|
|
11778
|
+
model = r.model;
|
|
11779
|
+
}
|
|
11780
|
+
}
|
|
11781
|
+
if (!model && hasCC) {
|
|
11782
|
+
for (const r of bucket.ccRecords) {
|
|
11783
|
+
if (r.model) {
|
|
11784
|
+
model = r.model;
|
|
11785
|
+
break;
|
|
11786
|
+
}
|
|
11787
|
+
}
|
|
11788
|
+
}
|
|
11789
|
+
const timestamps = bucket.allRecords.map((r) => r.timestamp).sort();
|
|
11790
|
+
const source = isMerged ? "merged" : hasCC ? "claude-code" : "harness";
|
|
11791
|
+
const session = {
|
|
11792
|
+
sessionId,
|
|
11793
|
+
firstTimestamp: timestamps[0] ?? "",
|
|
11794
|
+
lastTimestamp: timestamps[timestamps.length - 1] ?? "",
|
|
11795
|
+
tokens,
|
|
11796
|
+
costMicroUSD,
|
|
11797
|
+
source
|
|
11798
|
+
};
|
|
11799
|
+
if (model) session.model = model;
|
|
11800
|
+
if (cacheCreation != null) session.cacheCreationTokens = cacheCreation;
|
|
11801
|
+
if (cacheRead != null) session.cacheReadTokens = cacheRead;
|
|
11802
|
+
results.push(session);
|
|
11803
|
+
}
|
|
11804
|
+
results.sort((a, b) => b.firstTimestamp.localeCompare(a.firstTimestamp));
|
|
11805
|
+
return results;
|
|
11806
|
+
}
|
|
11807
|
+
function aggregateByDay(records) {
|
|
11808
|
+
if (records.length === 0) return [];
|
|
11809
|
+
const dayMap = /* @__PURE__ */ new Map();
|
|
11810
|
+
for (const record of records) {
|
|
11811
|
+
const date = record.timestamp.slice(0, 10);
|
|
11812
|
+
if (!dayMap.has(date)) {
|
|
11813
|
+
dayMap.set(date, {
|
|
11814
|
+
sessions: /* @__PURE__ */ new Set(),
|
|
11815
|
+
tokens: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
|
|
11816
|
+
costMicroUSD: 0,
|
|
11817
|
+
models: /* @__PURE__ */ new Set()
|
|
11818
|
+
});
|
|
11819
|
+
}
|
|
11820
|
+
const day = dayMap.get(date);
|
|
11821
|
+
day.sessions.add(record.sessionId);
|
|
11822
|
+
day.tokens.inputTokens += record.tokens.inputTokens;
|
|
11823
|
+
day.tokens.outputTokens += record.tokens.outputTokens;
|
|
11824
|
+
day.tokens.totalTokens += record.tokens.totalTokens;
|
|
11825
|
+
if (record.cacheCreationTokens != null) {
|
|
11826
|
+
day.cacheCreation = (day.cacheCreation ?? 0) + record.cacheCreationTokens;
|
|
11827
|
+
}
|
|
11828
|
+
if (record.cacheReadTokens != null) {
|
|
11829
|
+
day.cacheRead = (day.cacheRead ?? 0) + record.cacheReadTokens;
|
|
11830
|
+
}
|
|
11831
|
+
if (record.costMicroUSD != null && day.costMicroUSD != null) {
|
|
11832
|
+
day.costMicroUSD += record.costMicroUSD;
|
|
11833
|
+
} else if (record.costMicroUSD == null) {
|
|
11834
|
+
day.costMicroUSD = null;
|
|
11835
|
+
}
|
|
11836
|
+
if (record.model) {
|
|
11837
|
+
day.models.add(record.model);
|
|
11838
|
+
}
|
|
11839
|
+
}
|
|
11840
|
+
const results = [];
|
|
11841
|
+
for (const [date, day] of dayMap) {
|
|
11842
|
+
const entry = {
|
|
11843
|
+
date,
|
|
11844
|
+
sessionCount: day.sessions.size,
|
|
11845
|
+
tokens: day.tokens,
|
|
11846
|
+
costMicroUSD: day.costMicroUSD,
|
|
11847
|
+
models: Array.from(day.models).sort()
|
|
11848
|
+
};
|
|
11849
|
+
if (day.cacheCreation != null) entry.cacheCreationTokens = day.cacheCreation;
|
|
11850
|
+
if (day.cacheRead != null) entry.cacheReadTokens = day.cacheRead;
|
|
11851
|
+
results.push(entry);
|
|
11852
|
+
}
|
|
11853
|
+
results.sort((a, b) => b.date.localeCompare(a.date));
|
|
11854
|
+
return results;
|
|
11855
|
+
}
|
|
11856
|
+
|
|
11857
|
+
// src/usage/jsonl-reader.ts
|
|
11858
|
+
import * as fs25 from "fs";
|
|
11859
|
+
import * as path24 from "path";
|
|
11860
|
+
function parseLine(line, lineNumber) {
|
|
11861
|
+
let entry;
|
|
11862
|
+
try {
|
|
11863
|
+
entry = JSON.parse(line);
|
|
11864
|
+
} catch {
|
|
11865
|
+
console.warn(`[harness usage] Skipping malformed JSONL line ${lineNumber}`);
|
|
11866
|
+
return null;
|
|
11867
|
+
}
|
|
11868
|
+
const tokenUsage = entry.token_usage;
|
|
11869
|
+
if (!tokenUsage || typeof tokenUsage !== "object") {
|
|
11870
|
+
console.warn(
|
|
11871
|
+
`[harness usage] Skipping malformed JSONL line ${lineNumber}: missing token_usage`
|
|
11872
|
+
);
|
|
11873
|
+
return null;
|
|
11874
|
+
}
|
|
11875
|
+
const inputTokens = tokenUsage.input_tokens ?? 0;
|
|
11876
|
+
const outputTokens = tokenUsage.output_tokens ?? 0;
|
|
11877
|
+
const record = {
|
|
11878
|
+
sessionId: entry.session_id ?? "unknown",
|
|
11879
|
+
timestamp: entry.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
11880
|
+
tokens: {
|
|
11881
|
+
inputTokens,
|
|
11882
|
+
outputTokens,
|
|
11883
|
+
totalTokens: inputTokens + outputTokens
|
|
11884
|
+
}
|
|
11885
|
+
};
|
|
11886
|
+
if (entry.cache_creation_tokens != null) {
|
|
11887
|
+
record.cacheCreationTokens = entry.cache_creation_tokens;
|
|
11888
|
+
}
|
|
11889
|
+
if (entry.cache_read_tokens != null) {
|
|
11890
|
+
record.cacheReadTokens = entry.cache_read_tokens;
|
|
11891
|
+
}
|
|
11892
|
+
if (entry.model != null) {
|
|
11893
|
+
record.model = entry.model;
|
|
11894
|
+
}
|
|
11895
|
+
return record;
|
|
11896
|
+
}
|
|
11897
|
+
function readCostRecords(projectRoot) {
|
|
11898
|
+
const costsFile = path24.join(projectRoot, ".harness", "metrics", "costs.jsonl");
|
|
11899
|
+
let raw;
|
|
11900
|
+
try {
|
|
11901
|
+
raw = fs25.readFileSync(costsFile, "utf-8");
|
|
11902
|
+
} catch {
|
|
11903
|
+
return [];
|
|
11904
|
+
}
|
|
11905
|
+
const records = [];
|
|
11906
|
+
const lines = raw.split("\n");
|
|
11907
|
+
for (let i = 0; i < lines.length; i++) {
|
|
11908
|
+
const line = lines[i]?.trim();
|
|
11909
|
+
if (!line) continue;
|
|
11910
|
+
const record = parseLine(line, i + 1);
|
|
11911
|
+
if (record) {
|
|
11912
|
+
records.push(record);
|
|
11913
|
+
}
|
|
11914
|
+
}
|
|
11915
|
+
return records;
|
|
11916
|
+
}
|
|
11917
|
+
|
|
11918
|
+
// src/usage/cc-parser.ts
|
|
11919
|
+
import * as fs26 from "fs";
|
|
11920
|
+
import * as path25 from "path";
|
|
11921
|
+
import * as os2 from "os";
|
|
11922
|
+
function extractUsage(entry) {
|
|
11923
|
+
if (entry.type !== "assistant") return null;
|
|
11924
|
+
const message = entry.message;
|
|
11925
|
+
if (!message || typeof message !== "object") return null;
|
|
11926
|
+
const usage = message.usage;
|
|
11927
|
+
return usage && typeof usage === "object" && !Array.isArray(usage) ? usage : null;
|
|
11928
|
+
}
|
|
11929
|
+
function buildRecord(entry, usage) {
|
|
11930
|
+
const inputTokens = Number(usage.input_tokens) || 0;
|
|
11931
|
+
const outputTokens = Number(usage.output_tokens) || 0;
|
|
11932
|
+
const message = entry.message;
|
|
11933
|
+
const record = {
|
|
11934
|
+
sessionId: entry.sessionId ?? "unknown",
|
|
11935
|
+
timestamp: entry.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
11936
|
+
tokens: { inputTokens, outputTokens, totalTokens: inputTokens + outputTokens },
|
|
11937
|
+
_source: "claude-code"
|
|
11938
|
+
};
|
|
11939
|
+
const model = message.model;
|
|
11940
|
+
if (model) record.model = model;
|
|
11941
|
+
const cacheCreate = usage.cache_creation_input_tokens;
|
|
11942
|
+
const cacheRead = usage.cache_read_input_tokens;
|
|
11943
|
+
if (typeof cacheCreate === "number" && cacheCreate > 0) record.cacheCreationTokens = cacheCreate;
|
|
11944
|
+
if (typeof cacheRead === "number" && cacheRead > 0) record.cacheReadTokens = cacheRead;
|
|
11945
|
+
return record;
|
|
11946
|
+
}
|
|
11947
|
+
function parseCCLine(line, filePath, lineNumber) {
|
|
11948
|
+
let entry;
|
|
11949
|
+
try {
|
|
11950
|
+
entry = JSON.parse(line);
|
|
11951
|
+
} catch {
|
|
11952
|
+
console.warn(
|
|
11953
|
+
`[harness usage] Skipping malformed CC JSONL line ${lineNumber} in ${path25.basename(filePath)}`
|
|
11954
|
+
);
|
|
11955
|
+
return null;
|
|
11956
|
+
}
|
|
11957
|
+
const usage = extractUsage(entry);
|
|
11958
|
+
if (!usage) return null;
|
|
11959
|
+
return {
|
|
11960
|
+
record: buildRecord(entry, usage),
|
|
11961
|
+
requestId: entry.requestId ?? null
|
|
11962
|
+
};
|
|
11963
|
+
}
|
|
11964
|
+
function readCCFile(filePath) {
|
|
11965
|
+
let raw;
|
|
11966
|
+
try {
|
|
11967
|
+
raw = fs26.readFileSync(filePath, "utf-8");
|
|
11968
|
+
} catch {
|
|
11969
|
+
return [];
|
|
11970
|
+
}
|
|
11971
|
+
const byRequestId = /* @__PURE__ */ new Map();
|
|
11972
|
+
const noRequestId = [];
|
|
11973
|
+
const lines = raw.split("\n");
|
|
11974
|
+
for (let i = 0; i < lines.length; i++) {
|
|
11975
|
+
const line = lines[i]?.trim();
|
|
11976
|
+
if (!line) continue;
|
|
11977
|
+
const parsed = parseCCLine(line, filePath, i + 1);
|
|
11978
|
+
if (!parsed) continue;
|
|
11979
|
+
if (parsed.requestId) {
|
|
11980
|
+
byRequestId.set(parsed.requestId, parsed.record);
|
|
11981
|
+
} else {
|
|
11982
|
+
noRequestId.push(parsed.record);
|
|
11983
|
+
}
|
|
11984
|
+
}
|
|
11985
|
+
return [...byRequestId.values(), ...noRequestId];
|
|
11986
|
+
}
|
|
11987
|
+
function parseCCRecords() {
|
|
11988
|
+
const homeDir = process.env.HOME ?? os2.homedir();
|
|
11989
|
+
const projectsDir = path25.join(homeDir, ".claude", "projects");
|
|
11990
|
+
let projectDirs;
|
|
11991
|
+
try {
|
|
11992
|
+
projectDirs = fs26.readdirSync(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => path25.join(projectsDir, d.name));
|
|
11993
|
+
} catch {
|
|
11994
|
+
return [];
|
|
11995
|
+
}
|
|
11996
|
+
const records = [];
|
|
11997
|
+
for (const dir of projectDirs) {
|
|
11998
|
+
let files;
|
|
11999
|
+
try {
|
|
12000
|
+
files = fs26.readdirSync(dir).filter((f) => f.endsWith(".jsonl")).map((f) => path25.join(dir, f));
|
|
12001
|
+
} catch {
|
|
12002
|
+
continue;
|
|
12003
|
+
}
|
|
12004
|
+
for (const file of files) {
|
|
12005
|
+
records.push(...readCCFile(file));
|
|
12006
|
+
}
|
|
12007
|
+
}
|
|
12008
|
+
return records;
|
|
12009
|
+
}
|
|
12010
|
+
|
|
10403
12011
|
// src/index.ts
|
|
10404
12012
|
var VERSION = "0.15.0";
|
|
10405
12013
|
export {
|
|
@@ -10417,6 +12025,7 @@ export {
|
|
|
10417
12025
|
BlueprintGenerator,
|
|
10418
12026
|
BundleConstraintsSchema,
|
|
10419
12027
|
BundleSchema,
|
|
12028
|
+
CACHE_TTL_MS,
|
|
10420
12029
|
COMPLIANCE_DESCRIPTOR,
|
|
10421
12030
|
CategoryBaselineSchema,
|
|
10422
12031
|
CategoryRegressionSchema,
|
|
@@ -10434,6 +12043,7 @@ export {
|
|
|
10434
12043
|
DEFAULT_SECURITY_CONFIG,
|
|
10435
12044
|
DEFAULT_STATE,
|
|
10436
12045
|
DEFAULT_STREAM_INDEX,
|
|
12046
|
+
DESTRUCTIVE_BASH,
|
|
10437
12047
|
DepDepthCollector,
|
|
10438
12048
|
EXTENSION_MAP,
|
|
10439
12049
|
EmitInteractionInputSchema,
|
|
@@ -10445,9 +12055,11 @@ export {
|
|
|
10445
12055
|
ForbiddenImportCollector,
|
|
10446
12056
|
GateConfigSchema,
|
|
10447
12057
|
GateResultSchema,
|
|
12058
|
+
GitHubIssuesSyncAdapter,
|
|
10448
12059
|
HandoffSchema,
|
|
10449
12060
|
HarnessStateSchema,
|
|
10450
12061
|
InteractionTypeSchema,
|
|
12062
|
+
LITELLM_PRICING_URL,
|
|
10451
12063
|
LayerViolationCollector,
|
|
10452
12064
|
LockfilePackageSchema,
|
|
10453
12065
|
LockfileSchema,
|
|
@@ -10464,6 +12076,8 @@ export {
|
|
|
10464
12076
|
RegressionDetector,
|
|
10465
12077
|
RuleRegistry,
|
|
10466
12078
|
SECURITY_DESCRIPTOR,
|
|
12079
|
+
STALENESS_WARNING_DAYS,
|
|
12080
|
+
STATUS_RANK,
|
|
10467
12081
|
SecurityConfigSchema,
|
|
10468
12082
|
SecurityScanner,
|
|
10469
12083
|
SharableBoundaryConfigSchema,
|
|
@@ -10480,6 +12094,8 @@ export {
|
|
|
10480
12094
|
ViolationSchema,
|
|
10481
12095
|
addProvenance,
|
|
10482
12096
|
agentConfigRules,
|
|
12097
|
+
aggregateByDay,
|
|
12098
|
+
aggregateBySession,
|
|
10483
12099
|
analyzeDiff,
|
|
10484
12100
|
analyzeLearningPatterns,
|
|
10485
12101
|
appendFailure,
|
|
@@ -10495,16 +12111,22 @@ export {
|
|
|
10495
12111
|
archiveLearnings,
|
|
10496
12112
|
archiveSession,
|
|
10497
12113
|
archiveStream,
|
|
12114
|
+
assignFeature,
|
|
10498
12115
|
buildDependencyGraph,
|
|
10499
12116
|
buildExclusionSet,
|
|
10500
12117
|
buildSnapshot,
|
|
12118
|
+
calculateCost,
|
|
10501
12119
|
checkDocCoverage,
|
|
10502
12120
|
checkEligibility,
|
|
10503
12121
|
checkEvidenceCoverage,
|
|
12122
|
+
checkTaint,
|
|
10504
12123
|
classifyFinding,
|
|
10505
12124
|
clearEventHashCache,
|
|
10506
12125
|
clearFailuresCache,
|
|
10507
12126
|
clearLearningsCache,
|
|
12127
|
+
clearTaint,
|
|
12128
|
+
computeOverallSeverity,
|
|
12129
|
+
computeScanExitCode,
|
|
10508
12130
|
configureFeedback,
|
|
10509
12131
|
constraintRuleId,
|
|
10510
12132
|
contextBudget,
|
|
@@ -10554,40 +12176,55 @@ export {
|
|
|
10554
12176
|
formatGitHubSummary,
|
|
10555
12177
|
formatOutline,
|
|
10556
12178
|
formatTerminalOutput,
|
|
12179
|
+
fullSync,
|
|
10557
12180
|
generateAgentsMap,
|
|
10558
12181
|
generateSuggestions,
|
|
10559
12182
|
getActionEmitter,
|
|
10560
12183
|
getExitCode,
|
|
10561
12184
|
getFeedbackConfig,
|
|
12185
|
+
getInjectionPatterns,
|
|
12186
|
+
getModelPrice,
|
|
10562
12187
|
getOutline,
|
|
10563
12188
|
getParser,
|
|
10564
12189
|
getPhaseCategories,
|
|
10565
12190
|
getStreamForBranch,
|
|
12191
|
+
getTaintFilePath,
|
|
10566
12192
|
getUpdateNotification,
|
|
10567
12193
|
goRules,
|
|
10568
12194
|
injectionRules,
|
|
12195
|
+
insecureDefaultsRules,
|
|
12196
|
+
isDuplicateFinding,
|
|
12197
|
+
isRegression,
|
|
10569
12198
|
isSmallSuggestion,
|
|
10570
12199
|
isUpdateCheckEnabled,
|
|
10571
12200
|
listActiveSessions,
|
|
10572
12201
|
listStreams,
|
|
12202
|
+
listTaintedSessions,
|
|
10573
12203
|
loadBudgetedLearnings,
|
|
10574
12204
|
loadEvents,
|
|
10575
12205
|
loadFailures,
|
|
10576
12206
|
loadHandoff,
|
|
10577
12207
|
loadIndexEntries,
|
|
12208
|
+
loadPricingData,
|
|
10578
12209
|
loadRelevantLearnings,
|
|
10579
12210
|
loadSessionSummary,
|
|
10580
12211
|
loadState,
|
|
10581
12212
|
loadStreamIndex,
|
|
10582
12213
|
logAgentAction,
|
|
12214
|
+
mapInjectionFindings,
|
|
12215
|
+
mapSecurityFindings,
|
|
12216
|
+
mapSecuritySeverity,
|
|
10583
12217
|
mcpRules,
|
|
10584
12218
|
migrateToStreams,
|
|
10585
12219
|
networkRules,
|
|
10586
12220
|
nodeRules,
|
|
12221
|
+
parseCCRecords,
|
|
10587
12222
|
parseDateFromEntry,
|
|
10588
12223
|
parseDiff,
|
|
10589
12224
|
parseFile,
|
|
10590
12225
|
parseFrontmatter,
|
|
12226
|
+
parseHarnessIgnore,
|
|
12227
|
+
parseLiteLLMData,
|
|
10591
12228
|
parseManifest,
|
|
10592
12229
|
parseRoadmap,
|
|
10593
12230
|
parseSecurityConfig,
|
|
@@ -10598,9 +12235,11 @@ export {
|
|
|
10598
12235
|
pruneLearnings,
|
|
10599
12236
|
reactRules,
|
|
10600
12237
|
readCheckState,
|
|
12238
|
+
readCostRecords,
|
|
10601
12239
|
readLockfile,
|
|
10602
12240
|
readSessionSection,
|
|
10603
12241
|
readSessionSections,
|
|
12242
|
+
readTaint,
|
|
10604
12243
|
removeContributions,
|
|
10605
12244
|
removeProvenance,
|
|
10606
12245
|
requestMultiplePeerReviews,
|
|
@@ -10609,6 +12248,7 @@ export {
|
|
|
10609
12248
|
resetParserCache,
|
|
10610
12249
|
resolveFileToLayer,
|
|
10611
12250
|
resolveModelTier,
|
|
12251
|
+
resolveReverseStatus,
|
|
10612
12252
|
resolveRuleSeverity,
|
|
10613
12253
|
resolveSessionDir,
|
|
10614
12254
|
resolveStreamPath,
|
|
@@ -10627,15 +12267,20 @@ export {
|
|
|
10627
12267
|
saveHandoff,
|
|
10628
12268
|
saveState,
|
|
10629
12269
|
saveStreamIndex,
|
|
12270
|
+
scanForInjection,
|
|
10630
12271
|
scopeContext,
|
|
12272
|
+
scoreRoadmapCandidates,
|
|
10631
12273
|
searchSymbols,
|
|
10632
12274
|
secretRules,
|
|
10633
12275
|
serializeRoadmap,
|
|
10634
12276
|
setActiveStream,
|
|
12277
|
+
sharpEdgesRules,
|
|
10635
12278
|
shouldRunCheck,
|
|
10636
12279
|
spawnBackgroundCheck,
|
|
10637
12280
|
syncConstraintNodes,
|
|
12281
|
+
syncFromExternal,
|
|
10638
12282
|
syncRoadmap,
|
|
12283
|
+
syncToExternal,
|
|
10639
12284
|
tagUncitedFindings,
|
|
10640
12285
|
touchStream,
|
|
10641
12286
|
trackAction,
|
|
@@ -10656,5 +12301,6 @@ export {
|
|
|
10656
12301
|
writeConfig,
|
|
10657
12302
|
writeLockfile,
|
|
10658
12303
|
writeSessionSummary,
|
|
12304
|
+
writeTaint,
|
|
10659
12305
|
xssRules
|
|
10660
12306
|
};
|