@harness-engineering/core 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -45,6 +45,7 @@ __export(index_exports, {
45
45
  BlueprintGenerator: () => BlueprintGenerator,
46
46
  BundleConstraintsSchema: () => BundleConstraintsSchema,
47
47
  BundleSchema: () => BundleSchema,
48
+ CACHE_TTL_MS: () => CACHE_TTL_MS,
48
49
  COMPLIANCE_DESCRIPTOR: () => COMPLIANCE_DESCRIPTOR,
49
50
  CategoryBaselineSchema: () => CategoryBaselineSchema,
50
51
  CategoryRegressionSchema: () => CategoryRegressionSchema,
@@ -62,7 +63,9 @@ __export(index_exports, {
62
63
  DEFAULT_SECURITY_CONFIG: () => DEFAULT_SECURITY_CONFIG,
63
64
  DEFAULT_STATE: () => DEFAULT_STATE,
64
65
  DEFAULT_STREAM_INDEX: () => DEFAULT_STREAM_INDEX,
66
+ DESTRUCTIVE_BASH: () => DESTRUCTIVE_BASH,
65
67
  DepDepthCollector: () => DepDepthCollector,
68
+ EXTENSION_MAP: () => EXTENSION_MAP,
66
69
  EmitInteractionInputSchema: () => EmitInteractionInputSchema,
67
70
  EntropyAnalyzer: () => EntropyAnalyzer,
68
71
  EntropyConfigSchema: () => EntropyConfigSchema,
@@ -75,6 +78,7 @@ __export(index_exports, {
75
78
  HandoffSchema: () => HandoffSchema,
76
79
  HarnessStateSchema: () => HarnessStateSchema,
77
80
  InteractionTypeSchema: () => InteractionTypeSchema,
81
+ LITELLM_PRICING_URL: () => LITELLM_PRICING_URL,
78
82
  LayerViolationCollector: () => LayerViolationCollector,
79
83
  LockfilePackageSchema: () => LockfilePackageSchema,
80
84
  LockfileSchema: () => LockfileSchema,
@@ -91,12 +95,14 @@ __export(index_exports, {
91
95
  RegressionDetector: () => RegressionDetector,
92
96
  RuleRegistry: () => RuleRegistry,
93
97
  SECURITY_DESCRIPTOR: () => SECURITY_DESCRIPTOR,
98
+ STALENESS_WARNING_DAYS: () => STALENESS_WARNING_DAYS,
94
99
  SecurityConfigSchema: () => SecurityConfigSchema,
95
100
  SecurityScanner: () => SecurityScanner,
96
101
  SharableBoundaryConfigSchema: () => SharableBoundaryConfigSchema,
97
102
  SharableForbiddenImportSchema: () => SharableForbiddenImportSchema,
98
103
  SharableLayerSchema: () => SharableLayerSchema,
99
104
  SharableSecurityRulesSchema: () => SharableSecurityRulesSchema,
105
+ SkillEventSchema: () => SkillEventSchema,
100
106
  StreamIndexSchema: () => StreamIndexSchema,
101
107
  StreamInfoSchema: () => StreamInfoSchema,
102
108
  ThresholdConfigSchema: () => ThresholdConfigSchema,
@@ -105,6 +111,9 @@ __export(index_exports, {
105
111
  VERSION: () => VERSION,
106
112
  ViolationSchema: () => ViolationSchema,
107
113
  addProvenance: () => addProvenance,
114
+ agentConfigRules: () => agentConfigRules,
115
+ aggregateByDay: () => aggregateByDay,
116
+ aggregateBySession: () => aggregateBySession,
108
117
  analyzeDiff: () => analyzeDiff,
109
118
  analyzeLearningPatterns: () => analyzeLearningPatterns,
110
119
  appendFailure: () => appendFailure,
@@ -112,6 +121,7 @@ __export(index_exports, {
112
121
  appendSessionEntry: () => appendSessionEntry,
113
122
  applyFixes: () => applyFixes,
114
123
  applyHotspotDowngrade: () => applyHotspotDowngrade,
124
+ applySyncChanges: () => applySyncChanges,
115
125
  archMatchers: () => archMatchers,
116
126
  archModule: () => archModule,
117
127
  architecture: () => architecture,
@@ -122,16 +132,23 @@ __export(index_exports, {
122
132
  buildDependencyGraph: () => buildDependencyGraph,
123
133
  buildExclusionSet: () => buildExclusionSet,
124
134
  buildSnapshot: () => buildSnapshot,
135
+ calculateCost: () => calculateCost,
125
136
  checkDocCoverage: () => checkDocCoverage,
126
137
  checkEligibility: () => checkEligibility,
127
138
  checkEvidenceCoverage: () => checkEvidenceCoverage,
139
+ checkTaint: () => checkTaint,
128
140
  classifyFinding: () => classifyFinding,
141
+ clearEventHashCache: () => clearEventHashCache,
129
142
  clearFailuresCache: () => clearFailuresCache,
130
143
  clearLearningsCache: () => clearLearningsCache,
144
+ clearTaint: () => clearTaint,
145
+ computeOverallSeverity: () => computeOverallSeverity,
146
+ computeScanExitCode: () => computeScanExitCode,
131
147
  configureFeedback: () => configureFeedback,
132
148
  constraintRuleId: () => constraintRuleId,
133
149
  contextBudget: () => contextBudget,
134
150
  contextFilter: () => contextFilter,
151
+ countLearningEntries: () => countLearningEntries,
135
152
  createBoundaryValidator: () => createBoundaryValidator,
136
153
  createCommentedCodeFixes: () => createCommentedCodeFixes,
137
154
  createError: () => createError,
@@ -155,66 +172,95 @@ __export(index_exports, {
155
172
  detectCouplingViolations: () => detectCouplingViolations,
156
173
  detectDeadCode: () => detectDeadCode,
157
174
  detectDocDrift: () => detectDocDrift,
175
+ detectLanguage: () => detectLanguage,
158
176
  detectPatternViolations: () => detectPatternViolations,
159
177
  detectSizeBudgetViolations: () => detectSizeBudgetViolations,
160
178
  detectStack: () => detectStack,
161
179
  detectStaleConstraints: () => detectStaleConstraints,
162
180
  determineAssessment: () => determineAssessment,
163
181
  diff: () => diff,
182
+ emitEvent: () => emitEvent,
164
183
  executeWorkflow: () => executeWorkflow,
165
184
  expressRules: () => expressRules,
166
185
  extractBundle: () => extractBundle,
186
+ extractIndexEntry: () => extractIndexEntry,
167
187
  extractMarkdownLinks: () => extractMarkdownLinks,
168
188
  extractSections: () => extractSections,
169
189
  fanOutReview: () => fanOutReview,
190
+ formatEventTimeline: () => formatEventTimeline,
170
191
  formatFindingBlock: () => formatFindingBlock,
171
192
  formatGitHubComment: () => formatGitHubComment,
172
193
  formatGitHubSummary: () => formatGitHubSummary,
194
+ formatOutline: () => formatOutline,
173
195
  formatTerminalOutput: () => formatTerminalOutput,
174
196
  generateAgentsMap: () => generateAgentsMap,
175
197
  generateSuggestions: () => generateSuggestions,
176
198
  getActionEmitter: () => getActionEmitter,
177
199
  getExitCode: () => getExitCode,
178
200
  getFeedbackConfig: () => getFeedbackConfig,
201
+ getInjectionPatterns: () => getInjectionPatterns,
202
+ getModelPrice: () => getModelPrice,
203
+ getOutline: () => getOutline,
204
+ getParser: () => getParser,
179
205
  getPhaseCategories: () => getPhaseCategories,
180
206
  getStreamForBranch: () => getStreamForBranch,
207
+ getTaintFilePath: () => getTaintFilePath,
181
208
  getUpdateNotification: () => getUpdateNotification,
182
209
  goRules: () => goRules,
183
210
  injectionRules: () => injectionRules,
211
+ insecureDefaultsRules: () => insecureDefaultsRules,
212
+ isDuplicateFinding: () => isDuplicateFinding,
184
213
  isSmallSuggestion: () => isSmallSuggestion,
185
214
  isUpdateCheckEnabled: () => isUpdateCheckEnabled,
186
215
  listActiveSessions: () => listActiveSessions,
187
216
  listStreams: () => listStreams,
217
+ listTaintedSessions: () => listTaintedSessions,
188
218
  loadBudgetedLearnings: () => loadBudgetedLearnings,
219
+ loadEvents: () => loadEvents,
189
220
  loadFailures: () => loadFailures,
190
221
  loadHandoff: () => loadHandoff,
222
+ loadIndexEntries: () => loadIndexEntries,
223
+ loadPricingData: () => loadPricingData,
191
224
  loadRelevantLearnings: () => loadRelevantLearnings,
192
225
  loadSessionSummary: () => loadSessionSummary,
193
226
  loadState: () => loadState,
194
227
  loadStreamIndex: () => loadStreamIndex,
195
228
  logAgentAction: () => logAgentAction,
229
+ mapInjectionFindings: () => mapInjectionFindings,
230
+ mapSecurityFindings: () => mapSecurityFindings,
231
+ mapSecuritySeverity: () => mapSecuritySeverity,
232
+ mcpRules: () => mcpRules,
196
233
  migrateToStreams: () => migrateToStreams,
197
234
  networkRules: () => networkRules,
198
235
  nodeRules: () => nodeRules,
236
+ parseCCRecords: () => parseCCRecords,
199
237
  parseDateFromEntry: () => parseDateFromEntry,
200
238
  parseDiff: () => parseDiff,
239
+ parseFile: () => parseFile,
240
+ parseFrontmatter: () => parseFrontmatter,
241
+ parseHarnessIgnore: () => parseHarnessIgnore,
242
+ parseLiteLLMData: () => parseLiteLLMData,
201
243
  parseManifest: () => parseManifest,
202
244
  parseRoadmap: () => parseRoadmap,
203
245
  parseSecurityConfig: () => parseSecurityConfig,
204
246
  parseSize: () => parseSize,
205
247
  pathTraversalRules: () => pathTraversalRules,
206
248
  previewFix: () => previewFix,
249
+ promoteSessionLearnings: () => promoteSessionLearnings,
207
250
  pruneLearnings: () => pruneLearnings,
208
251
  reactRules: () => reactRules,
209
252
  readCheckState: () => readCheckState,
253
+ readCostRecords: () => readCostRecords,
210
254
  readLockfile: () => readLockfile,
211
255
  readSessionSection: () => readSessionSection,
212
256
  readSessionSections: () => readSessionSections,
257
+ readTaint: () => readTaint,
213
258
  removeContributions: () => removeContributions,
214
259
  removeProvenance: () => removeProvenance,
215
260
  requestMultiplePeerReviews: () => requestMultiplePeerReviews,
216
261
  requestPeerReview: () => requestPeerReview,
217
262
  resetFeedbackConfig: () => resetFeedbackConfig,
263
+ resetParserCache: () => resetParserCache,
218
264
  resolveFileToLayer: () => resolveFileToLayer,
219
265
  resolveModelTier: () => resolveModelTier,
220
266
  resolveRuleSeverity: () => resolveRuleSeverity,
@@ -235,10 +281,13 @@ __export(index_exports, {
235
281
  saveHandoff: () => saveHandoff,
236
282
  saveState: () => saveState,
237
283
  saveStreamIndex: () => saveStreamIndex,
284
+ scanForInjection: () => scanForInjection,
238
285
  scopeContext: () => scopeContext,
286
+ searchSymbols: () => searchSymbols,
239
287
  secretRules: () => secretRules,
240
288
  serializeRoadmap: () => serializeRoadmap,
241
289
  setActiveStream: () => setActiveStream,
290
+ sharpEdgesRules: () => sharpEdgesRules,
242
291
  shouldRunCheck: () => shouldRunCheck,
243
292
  spawnBackgroundCheck: () => spawnBackgroundCheck,
244
293
  syncConstraintNodes: () => syncConstraintNodes,
@@ -246,6 +295,8 @@ __export(index_exports, {
246
295
  tagUncitedFindings: () => tagUncitedFindings,
247
296
  touchStream: () => touchStream,
248
297
  trackAction: () => trackAction,
298
+ unfoldRange: () => unfoldRange,
299
+ unfoldSymbol: () => unfoldSymbol,
249
300
  updateSessionEntryStatus: () => updateSessionEntryStatus,
250
301
  updateSessionIndex: () => updateSessionIndex,
251
302
  validateAgentsMap: () => validateAgentsMap,
@@ -261,6 +312,7 @@ __export(index_exports, {
261
312
  writeConfig: () => writeConfig,
262
313
  writeLockfile: () => writeLockfile,
263
314
  writeSessionSummary: () => writeSessionSummary,
315
+ writeTaint: () => writeTaint,
264
316
  xssRules: () => xssRules
265
317
  });
266
318
  module.exports = __toCommonJS(index_exports);
@@ -284,17 +336,17 @@ var import_node_path = require("path");
284
336
  var import_glob = require("glob");
285
337
  var accessAsync = (0, import_util.promisify)(import_fs.access);
286
338
  var readFileAsync = (0, import_util.promisify)(import_fs.readFile);
287
- async function fileExists(path22) {
339
+ async function fileExists(path26) {
288
340
  try {
289
- await accessAsync(path22, import_fs.constants.F_OK);
341
+ await accessAsync(path26, import_fs.constants.F_OK);
290
342
  return true;
291
343
  } catch {
292
344
  return false;
293
345
  }
294
346
  }
295
- async function readFileContent(path22) {
347
+ async function readFileContent(path26) {
296
348
  try {
297
- const content = await readFileAsync(path22, "utf-8");
349
+ const content = await readFileAsync(path26, "utf-8");
298
350
  return (0, import_types.Ok)(content);
299
351
  } catch (error) {
300
352
  return (0, import_types.Err)(error);
@@ -345,15 +397,15 @@ function validateConfig(data, schema) {
345
397
  let message = "Configuration validation failed";
346
398
  const suggestions = [];
347
399
  if (firstError) {
348
- const path22 = firstError.path.join(".");
349
- const pathDisplay = path22 ? ` at "${path22}"` : "";
400
+ const path26 = firstError.path.join(".");
401
+ const pathDisplay = path26 ? ` at "${path26}"` : "";
350
402
  if (firstError.code === "invalid_type") {
351
403
  const received = firstError.received;
352
404
  const expected = firstError.expected;
353
405
  if (received === "undefined") {
354
406
  code = "MISSING_FIELD";
355
407
  message = `Missing required field${pathDisplay}: ${firstError.message}`;
356
- suggestions.push(`Field "${path22}" is required and must be of type "${expected}"`);
408
+ suggestions.push(`Field "${path26}" is required and must be of type "${expected}"`);
357
409
  } else {
358
410
  code = "INVALID_TYPE";
359
411
  message = `Invalid type${pathDisplay}: ${firstError.message}`;
@@ -569,27 +621,27 @@ function extractSections(content) {
569
621
  }
570
622
  return sections.map((section) => buildAgentMapSection(section, lines));
571
623
  }
572
- function isExternalLink(path22) {
573
- return path22.startsWith("http://") || path22.startsWith("https://") || path22.startsWith("#") || path22.startsWith("mailto:");
624
+ function isExternalLink(path26) {
625
+ return path26.startsWith("http://") || path26.startsWith("https://") || path26.startsWith("#") || path26.startsWith("mailto:");
574
626
  }
575
627
  function resolveLinkPath(linkPath, baseDir) {
576
628
  return linkPath.startsWith(".") ? (0, import_path.join)(baseDir, linkPath) : linkPath;
577
629
  }
578
- async function validateAgentsMap(path22 = "./AGENTS.md") {
579
- const contentResult = await readFileContent(path22);
630
+ async function validateAgentsMap(path26 = "./AGENTS.md") {
631
+ const contentResult = await readFileContent(path26);
580
632
  if (!contentResult.ok) {
581
633
  return (0, import_types.Err)(
582
634
  createError(
583
635
  "PARSE_ERROR",
584
636
  `Failed to read AGENTS.md: ${contentResult.error.message}`,
585
- { path: path22 },
637
+ { path: path26 },
586
638
  ["Ensure the file exists", "Check file permissions"]
587
639
  )
588
640
  );
589
641
  }
590
642
  const content = contentResult.value;
591
643
  const sections = extractSections(content);
592
- const baseDir = (0, import_path.dirname)(path22);
644
+ const baseDir = (0, import_path.dirname)(path26);
593
645
  const sectionTitles = sections.map((s) => s.title);
594
646
  const missingSections = REQUIRED_SECTIONS.filter(
595
647
  (required) => !sectionTitles.some((title) => title.toLowerCase().includes(required.toLowerCase()))
@@ -730,8 +782,8 @@ async function checkDocCoverage(domain, options = {}) {
730
782
 
731
783
  // src/context/knowledge-map.ts
732
784
  var import_path3 = require("path");
733
- function suggestFix(path22, existingFiles) {
734
- const targetName = (0, import_path3.basename)(path22).toLowerCase();
785
+ function suggestFix(path26, existingFiles) {
786
+ const targetName = (0, import_path3.basename)(path26).toLowerCase();
735
787
  const similar = existingFiles.find((file) => {
736
788
  const fileName = (0, import_path3.basename)(file).toLowerCase();
737
789
  return fileName.includes(targetName) || targetName.includes(fileName);
@@ -739,7 +791,7 @@ function suggestFix(path22, existingFiles) {
739
791
  if (similar) {
740
792
  return `Did you mean "${similar}"?`;
741
793
  }
742
- return `Create the file "${path22}" or remove the link`;
794
+ return `Create the file "${path26}" or remove the link`;
743
795
  }
744
796
  async function validateKnowledgeMap(rootDir = process.cwd()) {
745
797
  const agentsPath = (0, import_path3.join)(rootDir, "AGENTS.md");
@@ -1345,8 +1397,8 @@ function createBoundaryValidator(schema, name) {
1345
1397
  return (0, import_types.Ok)(result.data);
1346
1398
  }
1347
1399
  const suggestions = result.error.issues.map((issue) => {
1348
- const path22 = issue.path.join(".");
1349
- return path22 ? `${path22}: ${issue.message}` : issue.message;
1400
+ const path26 = issue.path.join(".");
1401
+ return path26 ? `${path26}: ${issue.message}` : issue.message;
1350
1402
  });
1351
1403
  return (0, import_types.Err)(
1352
1404
  createError(
@@ -1978,11 +2030,11 @@ function processExportListSpecifiers(exportDecl, exports2) {
1978
2030
  var TypeScriptParser = class {
1979
2031
  name = "typescript";
1980
2032
  extensions = [".ts", ".tsx", ".mts", ".cts"];
1981
- async parseFile(path22) {
1982
- const contentResult = await readFileContent(path22);
2033
+ async parseFile(path26) {
2034
+ const contentResult = await readFileContent(path26);
1983
2035
  if (!contentResult.ok) {
1984
2036
  return (0, import_types.Err)(
1985
- createParseError("NOT_FOUND", `File not found: ${path22}`, { path: path22 }, [
2037
+ createParseError("NOT_FOUND", `File not found: ${path26}`, { path: path26 }, [
1986
2038
  "Check that the file exists",
1987
2039
  "Verify the path is correct"
1988
2040
  ])
@@ -1992,7 +2044,7 @@ var TypeScriptParser = class {
1992
2044
  const ast = (0, import_typescript_estree.parse)(contentResult.value, {
1993
2045
  loc: true,
1994
2046
  range: true,
1995
- jsx: path22.endsWith(".tsx"),
2047
+ jsx: path26.endsWith(".tsx"),
1996
2048
  errorOnUnknownASTType: false
1997
2049
  });
1998
2050
  return (0, import_types.Ok)({
@@ -2003,7 +2055,7 @@ var TypeScriptParser = class {
2003
2055
  } catch (e) {
2004
2056
  const error = e;
2005
2057
  return (0, import_types.Err)(
2006
- createParseError("SYNTAX_ERROR", `Failed to parse ${path22}: ${error.message}`, { path: path22 }, [
2058
+ createParseError("SYNTAX_ERROR", `Failed to parse ${path26}: ${error.message}`, { path: path26 }, [
2007
2059
  "Check for syntax errors in the file",
2008
2060
  "Ensure valid TypeScript syntax"
2009
2061
  ])
@@ -2188,22 +2240,22 @@ function extractInlineRefs(content) {
2188
2240
  }
2189
2241
  return refs;
2190
2242
  }
2191
- async function parseDocumentationFile(path22) {
2192
- const contentResult = await readFileContent(path22);
2243
+ async function parseDocumentationFile(path26) {
2244
+ const contentResult = await readFileContent(path26);
2193
2245
  if (!contentResult.ok) {
2194
2246
  return (0, import_types.Err)(
2195
2247
  createEntropyError(
2196
2248
  "PARSE_ERROR",
2197
- `Failed to read documentation file: ${path22}`,
2198
- { file: path22 },
2249
+ `Failed to read documentation file: ${path26}`,
2250
+ { file: path26 },
2199
2251
  ["Check that the file exists"]
2200
2252
  )
2201
2253
  );
2202
2254
  }
2203
2255
  const content = contentResult.value;
2204
- const type = path22.endsWith(".md") ? "markdown" : "text";
2256
+ const type = path26.endsWith(".md") ? "markdown" : "text";
2205
2257
  return (0, import_types.Ok)({
2206
- path: path22,
2258
+ path: path26,
2207
2259
  type,
2208
2260
  content,
2209
2261
  codeBlocks: extractCodeBlocks(content),
@@ -6868,6 +6920,8 @@ var SESSION_INDEX_FILE = "index.md";
6868
6920
  var SUMMARY_FILE = "summary.md";
6869
6921
  var SESSION_STATE_FILE = "session-state.json";
6870
6922
  var ARCHIVE_DIR = "archive";
6923
+ var CONTENT_HASHES_FILE = "content-hashes.json";
6924
+ var EVENTS_FILE = "events.jsonl";
6871
6925
 
6872
6926
  // src/state/stream-resolver.ts
6873
6927
  var STREAMS_DIR = "streams";
@@ -7210,6 +7264,85 @@ async function saveState(projectPath, state, stream, session) {
7210
7264
  // src/state/learnings.ts
7211
7265
  var fs9 = __toESM(require("fs"));
7212
7266
  var path6 = __toESM(require("path"));
7267
+ var crypto = __toESM(require("crypto"));
7268
+ function parseFrontmatter(line) {
7269
+ const match = line.match(/^<!--\s+hash:([a-f0-9]+)(?:\s+tags:([^\s]+))?\s+-->/);
7270
+ if (!match) return null;
7271
+ const hash = match[1];
7272
+ const tags = match[2] ? match[2].split(",").filter(Boolean) : [];
7273
+ return { hash, tags };
7274
+ }
7275
+ function computeEntryHash(text) {
7276
+ return crypto.createHash("sha256").update(text).digest("hex").slice(0, 8);
7277
+ }
7278
+ function normalizeLearningContent(text) {
7279
+ let normalized = text;
7280
+ normalized = normalized.replace(/\d{4}-\d{2}-\d{2}/g, "");
7281
+ normalized = normalized.replace(/\[skill:[^\]]*\]/g, "");
7282
+ normalized = normalized.replace(/\[outcome:[^\]]*\]/g, "");
7283
+ normalized = normalized.replace(/^[\s]*[-*]\s+/gm, "");
7284
+ normalized = normalized.replace(/\*\*/g, "");
7285
+ normalized = normalized.replace(/:\s*/g, " ");
7286
+ normalized = normalized.toLowerCase();
7287
+ normalized = normalized.replace(/\s+/g, " ").trim();
7288
+ return normalized;
7289
+ }
7290
+ function computeContentHash(text) {
7291
+ return crypto.createHash("sha256").update(text).digest("hex").slice(0, 16);
7292
+ }
7293
+ function loadContentHashes(stateDir) {
7294
+ const hashesPath = path6.join(stateDir, CONTENT_HASHES_FILE);
7295
+ if (!fs9.existsSync(hashesPath)) return {};
7296
+ try {
7297
+ const raw = fs9.readFileSync(hashesPath, "utf-8");
7298
+ const parsed = JSON.parse(raw);
7299
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
7300
+ return parsed;
7301
+ } catch {
7302
+ return {};
7303
+ }
7304
+ }
7305
+ function saveContentHashes(stateDir, index) {
7306
+ const hashesPath = path6.join(stateDir, CONTENT_HASHES_FILE);
7307
+ fs9.writeFileSync(hashesPath, JSON.stringify(index, null, 2) + "\n");
7308
+ }
7309
+ function rebuildContentHashes(stateDir) {
7310
+ const learningsPath = path6.join(stateDir, LEARNINGS_FILE);
7311
+ if (!fs9.existsSync(learningsPath)) return {};
7312
+ const content = fs9.readFileSync(learningsPath, "utf-8");
7313
+ const lines = content.split("\n");
7314
+ const index = {};
7315
+ for (let i = 0; i < lines.length; i++) {
7316
+ const line = lines[i];
7317
+ const isDatedBullet = /^- \*\*\d{4}-\d{2}-\d{2}/.test(line);
7318
+ if (isDatedBullet) {
7319
+ const learningMatch = line.match(/:\*\*\s*(.+)$/);
7320
+ if (learningMatch?.[1]) {
7321
+ const normalized = normalizeLearningContent(learningMatch[1]);
7322
+ const hash = computeContentHash(normalized);
7323
+ const dateMatch = line.match(/(\d{4}-\d{2}-\d{2})/);
7324
+ index[hash] = { date: dateMatch?.[1] ?? "", line: i + 1 };
7325
+ }
7326
+ }
7327
+ }
7328
+ saveContentHashes(stateDir, index);
7329
+ return index;
7330
+ }
7331
+ function extractIndexEntry(entry) {
7332
+ const lines = entry.split("\n");
7333
+ const summary = lines[0] ?? entry;
7334
+ const tags = [];
7335
+ const skillMatch = entry.match(/\[skill:([^\]]+)\]/);
7336
+ if (skillMatch?.[1]) tags.push(skillMatch[1]);
7337
+ const outcomeMatch = entry.match(/\[outcome:([^\]]+)\]/);
7338
+ if (outcomeMatch?.[1]) tags.push(outcomeMatch[1]);
7339
+ return {
7340
+ hash: computeEntryHash(entry),
7341
+ tags,
7342
+ summary,
7343
+ fullText: entry
7344
+ };
7345
+ }
7213
7346
  var learningsCacheMap = /* @__PURE__ */ new Map();
7214
7347
  function clearLearningsCache() {
7215
7348
  learningsCacheMap.clear();
@@ -7221,27 +7354,55 @@ async function appendLearning(projectPath, learning, skillName, outcome, stream,
7221
7354
  const stateDir = dirResult.value;
7222
7355
  const learningsPath = path6.join(stateDir, LEARNINGS_FILE);
7223
7356
  fs9.mkdirSync(stateDir, { recursive: true });
7357
+ const normalizedContent = normalizeLearningContent(learning);
7358
+ const contentHash = computeContentHash(normalizedContent);
7359
+ const hashesPath = path6.join(stateDir, CONTENT_HASHES_FILE);
7360
+ let contentHashes;
7361
+ if (fs9.existsSync(hashesPath)) {
7362
+ contentHashes = loadContentHashes(stateDir);
7363
+ if (Object.keys(contentHashes).length === 0 && fs9.existsSync(learningsPath)) {
7364
+ contentHashes = rebuildContentHashes(stateDir);
7365
+ }
7366
+ } else if (fs9.existsSync(learningsPath)) {
7367
+ contentHashes = rebuildContentHashes(stateDir);
7368
+ } else {
7369
+ contentHashes = {};
7370
+ }
7371
+ if (contentHashes[contentHash]) {
7372
+ return (0, import_types.Ok)(void 0);
7373
+ }
7224
7374
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
7225
- let entry;
7375
+ const fmTags = [];
7376
+ if (skillName) fmTags.push(skillName);
7377
+ if (outcome) fmTags.push(outcome);
7378
+ let bulletLine;
7226
7379
  if (skillName && outcome) {
7227
- entry = `
7228
- - **${timestamp} [skill:${skillName}] [outcome:${outcome}]:** ${learning}
7229
- `;
7380
+ bulletLine = `- **${timestamp} [skill:${skillName}] [outcome:${outcome}]:** ${learning}`;
7230
7381
  } else if (skillName) {
7231
- entry = `
7232
- - **${timestamp} [skill:${skillName}]:** ${learning}
7233
- `;
7382
+ bulletLine = `- **${timestamp} [skill:${skillName}]:** ${learning}`;
7234
7383
  } else {
7235
- entry = `
7236
- - **${timestamp}:** ${learning}
7237
- `;
7384
+ bulletLine = `- **${timestamp}:** ${learning}`;
7238
7385
  }
7386
+ const hash = crypto.createHash("sha256").update(bulletLine).digest("hex").slice(0, 8);
7387
+ const tagsStr = fmTags.length > 0 ? ` tags:${fmTags.join(",")}` : "";
7388
+ const frontmatter = `<!-- hash:${hash}${tagsStr} -->`;
7389
+ const entry = `
7390
+ ${frontmatter}
7391
+ ${bulletLine}
7392
+ `;
7393
+ let existingLineCount;
7239
7394
  if (!fs9.existsSync(learningsPath)) {
7240
7395
  fs9.writeFileSync(learningsPath, `# Learnings
7241
7396
  ${entry}`);
7397
+ existingLineCount = 1;
7242
7398
  } else {
7399
+ const existingContent = fs9.readFileSync(learningsPath, "utf-8");
7400
+ existingLineCount = existingContent.split("\n").length;
7243
7401
  fs9.appendFileSync(learningsPath, entry);
7244
7402
  }
7403
+ const bulletLine_lineNum = existingLineCount + 2;
7404
+ contentHashes[contentHash] = { date: timestamp ?? "", line: bulletLine_lineNum };
7405
+ saveContentHashes(stateDir, contentHashes);
7245
7406
  learningsCacheMap.delete(learningsPath);
7246
7407
  return (0, import_types.Ok)(void 0);
7247
7408
  } catch (error) {
@@ -7289,7 +7450,30 @@ function analyzeLearningPatterns(entries) {
7289
7450
  return patterns.sort((a, b) => b.count - a.count);
7290
7451
  }
7291
7452
  async function loadBudgetedLearnings(projectPath, options) {
7292
- const { intent, tokenBudget = 1e3, skill, session, stream } = options;
7453
+ const { intent, tokenBudget = 1e3, skill, session, stream, depth = "summary" } = options;
7454
+ if (depth === "index") {
7455
+ const indexEntries = [];
7456
+ if (session) {
7457
+ const sessionResult = await loadIndexEntries(projectPath, skill, stream, session);
7458
+ if (sessionResult.ok) indexEntries.push(...sessionResult.value);
7459
+ }
7460
+ const globalResult2 = await loadIndexEntries(projectPath, skill, stream);
7461
+ if (globalResult2.ok) {
7462
+ const sessionHashes = new Set(indexEntries.map((e) => e.hash));
7463
+ const uniqueGlobal = globalResult2.value.filter((e) => !sessionHashes.has(e.hash));
7464
+ indexEntries.push(...uniqueGlobal);
7465
+ }
7466
+ const budgeted2 = [];
7467
+ let totalTokens2 = 0;
7468
+ for (const entry of indexEntries) {
7469
+ const separator = budgeted2.length > 0 ? "\n" : "";
7470
+ const entryCost = estimateTokens(entry.summary + separator);
7471
+ if (totalTokens2 + entryCost > tokenBudget) break;
7472
+ budgeted2.push(entry.summary);
7473
+ totalTokens2 += entryCost;
7474
+ }
7475
+ return (0, import_types.Ok)(budgeted2);
7476
+ }
7293
7477
  const sortByRecencyAndRelevance = (entries) => {
7294
7478
  return [...entries].sort((a, b) => {
7295
7479
  const dateA = parseDateFromEntry(a) ?? "0000-00-00";
@@ -7308,7 +7492,9 @@ async function loadBudgetedLearnings(projectPath, options) {
7308
7492
  }
7309
7493
  const globalResult = await loadRelevantLearnings(projectPath, skill, stream);
7310
7494
  if (globalResult.ok) {
7311
- allEntries.push(...sortByRecencyAndRelevance(globalResult.value));
7495
+ const sessionSet = new Set(allEntries.map((e) => e.trim()));
7496
+ const uniqueGlobal = globalResult.value.filter((e) => !sessionSet.has(e.trim()));
7497
+ allEntries.push(...sortByRecencyAndRelevance(uniqueGlobal));
7312
7498
  }
7313
7499
  const budgeted = [];
7314
7500
  let totalTokens = 0;
@@ -7321,6 +7507,68 @@ async function loadBudgetedLearnings(projectPath, options) {
7321
7507
  }
7322
7508
  return (0, import_types.Ok)(budgeted);
7323
7509
  }
7510
+ async function loadIndexEntries(projectPath, skillName, stream, session) {
7511
+ try {
7512
+ const dirResult = await getStateDir(projectPath, stream, session);
7513
+ if (!dirResult.ok) return dirResult;
7514
+ const stateDir = dirResult.value;
7515
+ const learningsPath = path6.join(stateDir, LEARNINGS_FILE);
7516
+ if (!fs9.existsSync(learningsPath)) {
7517
+ return (0, import_types.Ok)([]);
7518
+ }
7519
+ const content = fs9.readFileSync(learningsPath, "utf-8");
7520
+ const lines = content.split("\n");
7521
+ const indexEntries = [];
7522
+ let pendingFrontmatter = null;
7523
+ let currentBlock = [];
7524
+ for (const line of lines) {
7525
+ if (line.startsWith("# ")) continue;
7526
+ const fm = parseFrontmatter(line);
7527
+ if (fm) {
7528
+ pendingFrontmatter = fm;
7529
+ continue;
7530
+ }
7531
+ const isDatedBullet = /^- \*\*\d{4}-\d{2}-\d{2}/.test(line);
7532
+ const isHeading = /^## \d{4}-\d{2}-\d{2}/.test(line);
7533
+ if (isDatedBullet || isHeading) {
7534
+ if (pendingFrontmatter) {
7535
+ indexEntries.push({
7536
+ hash: pendingFrontmatter.hash,
7537
+ tags: pendingFrontmatter.tags,
7538
+ summary: line,
7539
+ fullText: ""
7540
+ // Placeholder — full text not loaded in index mode
7541
+ });
7542
+ pendingFrontmatter = null;
7543
+ } else {
7544
+ const idx = extractIndexEntry(line);
7545
+ indexEntries.push({
7546
+ hash: idx.hash,
7547
+ tags: idx.tags,
7548
+ summary: line,
7549
+ fullText: ""
7550
+ });
7551
+ }
7552
+ currentBlock = [line];
7553
+ } else if (line.trim() !== "" && currentBlock.length > 0) {
7554
+ currentBlock.push(line);
7555
+ }
7556
+ }
7557
+ if (skillName) {
7558
+ const filtered = indexEntries.filter(
7559
+ (e) => e.tags.includes(skillName) || e.summary.includes(`[skill:${skillName}]`)
7560
+ );
7561
+ return (0, import_types.Ok)(filtered);
7562
+ }
7563
+ return (0, import_types.Ok)(indexEntries);
7564
+ } catch (error) {
7565
+ return (0, import_types.Err)(
7566
+ new Error(
7567
+ `Failed to load index entries: ${error instanceof Error ? error.message : String(error)}`
7568
+ )
7569
+ );
7570
+ }
7571
+ }
7324
7572
  async function loadRelevantLearnings(projectPath, skillName, stream, session) {
7325
7573
  try {
7326
7574
  const dirResult = await getStateDir(projectPath, stream, session);
@@ -7343,6 +7591,7 @@ async function loadRelevantLearnings(projectPath, skillName, stream, session) {
7343
7591
  let currentBlock = [];
7344
7592
  for (const line of lines) {
7345
7593
  if (line.startsWith("# ")) continue;
7594
+ if (/^<!--\s+hash:[a-f0-9]+/.test(line)) continue;
7346
7595
  const isDatedBullet = /^- \*\*\d{4}-\d{2}-\d{2}/.test(line);
7347
7596
  const isHeading = /^## \d{4}-\d{2}-\d{2}/.test(line);
7348
7597
  if (isDatedBullet || isHeading) {
@@ -7452,6 +7701,68 @@ async function pruneLearnings(projectPath, stream) {
7452
7701
  );
7453
7702
  }
7454
7703
  }
7704
+ var PROMOTABLE_OUTCOMES = ["gotcha", "decision", "observation"];
7705
+ function isGeneralizable(entry) {
7706
+ for (const outcome of PROMOTABLE_OUTCOMES) {
7707
+ if (entry.includes(`[outcome:${outcome}]`)) return true;
7708
+ }
7709
+ return false;
7710
+ }
7711
+ async function promoteSessionLearnings(projectPath, sessionSlug, stream) {
7712
+ try {
7713
+ const sessionResult = await loadRelevantLearnings(projectPath, void 0, stream, sessionSlug);
7714
+ if (!sessionResult.ok) return sessionResult;
7715
+ const sessionEntries = sessionResult.value;
7716
+ if (sessionEntries.length === 0) {
7717
+ return (0, import_types.Ok)({ promoted: 0, skipped: 0 });
7718
+ }
7719
+ const toPromote = [];
7720
+ let skipped = 0;
7721
+ for (const entry of sessionEntries) {
7722
+ if (isGeneralizable(entry)) {
7723
+ toPromote.push(entry);
7724
+ } else {
7725
+ skipped++;
7726
+ }
7727
+ }
7728
+ if (toPromote.length === 0) {
7729
+ return (0, import_types.Ok)({ promoted: 0, skipped });
7730
+ }
7731
+ const dirResult = await getStateDir(projectPath, stream);
7732
+ if (!dirResult.ok) return dirResult;
7733
+ const stateDir = dirResult.value;
7734
+ const globalPath = path6.join(stateDir, LEARNINGS_FILE);
7735
+ const existingGlobal = fs9.existsSync(globalPath) ? fs9.readFileSync(globalPath, "utf-8") : "";
7736
+ const newEntries = toPromote.filter((entry) => !existingGlobal.includes(entry.trim()));
7737
+ if (newEntries.length === 0) {
7738
+ return (0, import_types.Ok)({ promoted: 0, skipped: skipped + toPromote.length });
7739
+ }
7740
+ const promotedContent = newEntries.join("\n\n") + "\n";
7741
+ if (!existingGlobal) {
7742
+ fs9.writeFileSync(globalPath, `# Learnings
7743
+
7744
+ ${promotedContent}`);
7745
+ } else {
7746
+ fs9.appendFileSync(globalPath, "\n\n" + promotedContent);
7747
+ }
7748
+ learningsCacheMap.delete(globalPath);
7749
+ return (0, import_types.Ok)({
7750
+ promoted: newEntries.length,
7751
+ skipped: skipped + (toPromote.length - newEntries.length)
7752
+ });
7753
+ } catch (error) {
7754
+ return (0, import_types.Err)(
7755
+ new Error(
7756
+ `Failed to promote session learnings: ${error instanceof Error ? error.message : String(error)}`
7757
+ )
7758
+ );
7759
+ }
7760
+ }
7761
+ async function countLearningEntries(projectPath, stream) {
7762
+ const loadResult = await loadRelevantLearnings(projectPath, void 0, stream);
7763
+ if (!loadResult.ok) return 0;
7764
+ return loadResult.value.length;
7765
+ }
7455
7766
 
7456
7767
  // src/state/failures.ts
7457
7768
  var fs10 = __toESM(require("fs"));
@@ -7913,6 +8224,151 @@ async function archiveSession(projectPath, sessionSlug) {
7913
8224
  }
7914
8225
  }
7915
8226
 
8227
+ // src/state/events.ts
8228
+ var fs16 = __toESM(require("fs"));
8229
+ var path13 = __toESM(require("path"));
8230
+ var import_zod6 = require("zod");
8231
+ var SkillEventSchema = import_zod6.z.object({
8232
+ timestamp: import_zod6.z.string(),
8233
+ skill: import_zod6.z.string(),
8234
+ session: import_zod6.z.string().optional(),
8235
+ type: import_zod6.z.enum(["phase_transition", "decision", "gate_result", "handoff", "error", "checkpoint"]),
8236
+ summary: import_zod6.z.string(),
8237
+ data: import_zod6.z.record(import_zod6.z.unknown()).optional(),
8238
+ refs: import_zod6.z.array(import_zod6.z.string()).optional(),
8239
+ contentHash: import_zod6.z.string().optional()
8240
+ });
8241
+ function computeEventHash(event, session) {
8242
+ const identity = `${event.skill}|${event.type}|${event.summary}|${session ?? ""}`;
8243
+ return computeContentHash(identity);
8244
+ }
8245
+ var knownHashesCache = /* @__PURE__ */ new Map();
8246
+ function loadKnownHashes(eventsPath) {
8247
+ const cached = knownHashesCache.get(eventsPath);
8248
+ if (cached) return cached;
8249
+ const hashes = /* @__PURE__ */ new Set();
8250
+ if (fs16.existsSync(eventsPath)) {
8251
+ const content = fs16.readFileSync(eventsPath, "utf-8");
8252
+ const lines = content.split("\n").filter((line) => line.trim() !== "");
8253
+ for (const line of lines) {
8254
+ try {
8255
+ const existing = JSON.parse(line);
8256
+ if (existing.contentHash) {
8257
+ hashes.add(existing.contentHash);
8258
+ }
8259
+ } catch {
8260
+ }
8261
+ }
8262
+ }
8263
+ knownHashesCache.set(eventsPath, hashes);
8264
+ return hashes;
8265
+ }
8266
+ function clearEventHashCache() {
8267
+ knownHashesCache.clear();
8268
+ }
8269
+ async function emitEvent(projectPath, event, options) {
8270
+ try {
8271
+ const dirResult = await getStateDir(projectPath, options?.stream, options?.session);
8272
+ if (!dirResult.ok) return dirResult;
8273
+ const stateDir = dirResult.value;
8274
+ const eventsPath = path13.join(stateDir, EVENTS_FILE);
8275
+ fs16.mkdirSync(stateDir, { recursive: true });
8276
+ const contentHash = computeEventHash(event, options?.session);
8277
+ const knownHashes = loadKnownHashes(eventsPath);
8278
+ if (knownHashes.has(contentHash)) {
8279
+ return (0, import_types.Ok)({ written: false, reason: "duplicate" });
8280
+ }
8281
+ const fullEvent = {
8282
+ ...event,
8283
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
8284
+ contentHash
8285
+ };
8286
+ if (options?.session) {
8287
+ fullEvent.session = options.session;
8288
+ }
8289
+ fs16.appendFileSync(eventsPath, JSON.stringify(fullEvent) + "\n");
8290
+ knownHashes.add(contentHash);
8291
+ return (0, import_types.Ok)({ written: true });
8292
+ } catch (error) {
8293
+ return (0, import_types.Err)(
8294
+ new Error(`Failed to emit event: ${error instanceof Error ? error.message : String(error)}`)
8295
+ );
8296
+ }
8297
+ }
8298
+ async function loadEvents(projectPath, options) {
8299
+ try {
8300
+ const dirResult = await getStateDir(projectPath, options?.stream, options?.session);
8301
+ if (!dirResult.ok) return dirResult;
8302
+ const stateDir = dirResult.value;
8303
+ const eventsPath = path13.join(stateDir, EVENTS_FILE);
8304
+ if (!fs16.existsSync(eventsPath)) {
8305
+ return (0, import_types.Ok)([]);
8306
+ }
8307
+ const content = fs16.readFileSync(eventsPath, "utf-8");
8308
+ const lines = content.split("\n").filter((line) => line.trim() !== "");
8309
+ const events = [];
8310
+ for (const line of lines) {
8311
+ try {
8312
+ const parsed = JSON.parse(line);
8313
+ const result = SkillEventSchema.safeParse(parsed);
8314
+ if (result.success) {
8315
+ events.push(result.data);
8316
+ }
8317
+ } catch {
8318
+ }
8319
+ }
8320
+ return (0, import_types.Ok)(events);
8321
+ } catch (error) {
8322
+ return (0, import_types.Err)(
8323
+ new Error(`Failed to load events: ${error instanceof Error ? error.message : String(error)}`)
8324
+ );
8325
+ }
8326
+ }
8327
+ function formatPhaseTransition(event) {
8328
+ const data = event.data;
8329
+ const suffix = data?.taskCount ? ` (${data.taskCount} tasks)` : "";
8330
+ return `phase: ${data?.from ?? "?"} -> ${data?.to ?? "?"}${suffix}`;
8331
+ }
8332
+ function formatGateResult(event) {
8333
+ const data = event.data;
8334
+ const status = data?.passed ? "passed" : "failed";
8335
+ const checks = data?.checks?.map((c) => `${c.name} ${c.passed ? "Y" : "N"}`).join(", ");
8336
+ return checks ? `gate: ${status} (${checks})` : `gate: ${status}`;
8337
+ }
8338
+ function formatHandoffDetail(event) {
8339
+ const data = event.data;
8340
+ const direction = data?.toSkill ? ` -> ${data.toSkill}` : "";
8341
+ return `handoff: ${event.summary}${direction}`;
8342
+ }
8343
+ var EVENT_FORMATTERS = {
8344
+ phase_transition: formatPhaseTransition,
8345
+ gate_result: formatGateResult,
8346
+ decision: (event) => `decision: ${event.summary}`,
8347
+ handoff: formatHandoffDetail,
8348
+ error: (event) => `error: ${event.summary}`,
8349
+ checkpoint: (event) => `checkpoint: ${event.summary}`
8350
+ };
8351
+ function formatEventTimeline(events, limit = 20) {
8352
+ if (events.length === 0) return "";
8353
+ const recent = events.slice(-limit);
8354
+ return recent.map((event) => {
8355
+ const time = formatTime(event.timestamp);
8356
+ const formatter = EVENT_FORMATTERS[event.type];
8357
+ const detail = formatter ? formatter(event) : event.summary;
8358
+ return `- ${time} [${event.skill}] ${detail}`;
8359
+ }).join("\n");
8360
+ }
8361
+ function formatTime(timestamp) {
8362
+ try {
8363
+ const date = new Date(timestamp);
8364
+ const hours = String(date.getHours()).padStart(2, "0");
8365
+ const minutes = String(date.getMinutes()).padStart(2, "0");
8366
+ return `${hours}:${minutes}`;
8367
+ } catch {
8368
+ return "??:??";
8369
+ }
8370
+ }
8371
+
7916
8372
  // src/workflow/runner.ts
7917
8373
  async function executeWorkflow(workflow, executor) {
7918
8374
  const stepResults = [];
@@ -8062,7 +8518,8 @@ async function runMultiTurnPipeline(initialContext, turnExecutor, options) {
8062
8518
  }
8063
8519
 
8064
8520
  // src/security/scanner.ts
8065
- var fs17 = __toESM(require("fs/promises"));
8521
+ var fs18 = __toESM(require("fs/promises"));
8522
+ var import_minimatch5 = require("minimatch");
8066
8523
 
8067
8524
  // src/security/rules/registry.ts
8068
8525
  var RuleRegistry = class {
@@ -8093,7 +8550,7 @@ var RuleRegistry = class {
8093
8550
  };
8094
8551
 
8095
8552
  // src/security/config.ts
8096
- var import_zod6 = require("zod");
8553
+ var import_zod7 = require("zod");
8097
8554
 
8098
8555
  // src/security/types.ts
8099
8556
  var DEFAULT_SECURITY_CONFIG = {
@@ -8104,19 +8561,19 @@ var DEFAULT_SECURITY_CONFIG = {
8104
8561
  };
8105
8562
 
8106
8563
  // src/security/config.ts
8107
- var RuleOverrideSchema = import_zod6.z.enum(["off", "error", "warning", "info"]);
8108
- var SecurityConfigSchema = import_zod6.z.object({
8109
- enabled: import_zod6.z.boolean().default(true),
8110
- strict: import_zod6.z.boolean().default(false),
8111
- rules: import_zod6.z.record(import_zod6.z.string(), RuleOverrideSchema).optional().default({}),
8112
- exclude: import_zod6.z.array(import_zod6.z.string()).optional().default(["**/node_modules/**", "**/dist/**", "**/*.test.ts", "**/fixtures/**"]),
8113
- external: import_zod6.z.object({
8114
- semgrep: import_zod6.z.object({
8115
- enabled: import_zod6.z.union([import_zod6.z.literal("auto"), import_zod6.z.boolean()]).default("auto"),
8116
- rulesets: import_zod6.z.array(import_zod6.z.string()).optional()
8564
+ var RuleOverrideSchema = import_zod7.z.enum(["off", "error", "warning", "info"]);
8565
+ var SecurityConfigSchema = import_zod7.z.object({
8566
+ enabled: import_zod7.z.boolean().default(true),
8567
+ strict: import_zod7.z.boolean().default(false),
8568
+ rules: import_zod7.z.record(import_zod7.z.string(), RuleOverrideSchema).optional().default({}),
8569
+ exclude: import_zod7.z.array(import_zod7.z.string()).optional().default(["**/node_modules/**", "**/dist/**", "**/*.test.ts", "**/fixtures/**"]),
8570
+ external: import_zod7.z.object({
8571
+ semgrep: import_zod7.z.object({
8572
+ enabled: import_zod7.z.union([import_zod7.z.literal("auto"), import_zod7.z.boolean()]).default("auto"),
8573
+ rulesets: import_zod7.z.array(import_zod7.z.string()).optional()
8117
8574
  }).optional(),
8118
- gitleaks: import_zod6.z.object({
8119
- enabled: import_zod6.z.union([import_zod6.z.literal("auto"), import_zod6.z.boolean()]).default("auto")
8575
+ gitleaks: import_zod7.z.object({
8576
+ enabled: import_zod7.z.union([import_zod7.z.literal("auto"), import_zod7.z.boolean()]).default("auto")
8120
8577
  }).optional()
8121
8578
  }).optional()
8122
8579
  });
@@ -8149,15 +8606,15 @@ function resolveRuleSeverity(ruleId, defaultSeverity, overrides, strict) {
8149
8606
  }
8150
8607
 
8151
8608
  // src/security/stack-detector.ts
8152
- var fs16 = __toESM(require("fs"));
8153
- var path13 = __toESM(require("path"));
8609
+ var fs17 = __toESM(require("fs"));
8610
+ var path14 = __toESM(require("path"));
8154
8611
  function detectStack(projectRoot) {
8155
8612
  const stacks = [];
8156
- const pkgJsonPath = path13.join(projectRoot, "package.json");
8157
- if (fs16.existsSync(pkgJsonPath)) {
8613
+ const pkgJsonPath = path14.join(projectRoot, "package.json");
8614
+ if (fs17.existsSync(pkgJsonPath)) {
8158
8615
  stacks.push("node");
8159
8616
  try {
8160
- const pkgJson = JSON.parse(fs16.readFileSync(pkgJsonPath, "utf-8"));
8617
+ const pkgJson = JSON.parse(fs17.readFileSync(pkgJsonPath, "utf-8"));
8161
8618
  const allDeps = {
8162
8619
  ...pkgJson.dependencies,
8163
8620
  ...pkgJson.devDependencies
@@ -8172,13 +8629,13 @@ function detectStack(projectRoot) {
8172
8629
  } catch {
8173
8630
  }
8174
8631
  }
8175
- const goModPath = path13.join(projectRoot, "go.mod");
8176
- if (fs16.existsSync(goModPath)) {
8632
+ const goModPath = path14.join(projectRoot, "go.mod");
8633
+ if (fs17.existsSync(goModPath)) {
8177
8634
  stacks.push("go");
8178
8635
  }
8179
- const requirementsPath = path13.join(projectRoot, "requirements.txt");
8180
- const pyprojectPath = path13.join(projectRoot, "pyproject.toml");
8181
- if (fs16.existsSync(requirementsPath) || fs16.existsSync(pyprojectPath)) {
8636
+ const requirementsPath = path14.join(projectRoot, "requirements.txt");
8637
+ const pyprojectPath = path14.join(projectRoot, "pyproject.toml");
8638
+ if (fs17.existsSync(requirementsPath) || fs17.existsSync(pyprojectPath)) {
8182
8639
  stacks.push("python");
8183
8640
  }
8184
8641
  return stacks;
@@ -8242,6 +8699,72 @@ var secretRules = [
8242
8699
  message: "Hardcoded JWT token detected",
8243
8700
  remediation: "Tokens should be fetched at runtime, not embedded in source",
8244
8701
  references: ["CWE-798"]
8702
+ },
8703
+ {
8704
+ id: "SEC-SEC-006",
8705
+ name: "Anthropic API Key",
8706
+ category: "secrets",
8707
+ severity: "error",
8708
+ confidence: "high",
8709
+ patterns: [/sk-ant-api\d{2}-[A-Za-z0-9_-]{20,}/],
8710
+ message: "Hardcoded Anthropic API key detected",
8711
+ remediation: "Use environment variables: process.env.ANTHROPIC_API_KEY",
8712
+ references: ["CWE-798"]
8713
+ },
8714
+ {
8715
+ id: "SEC-SEC-007",
8716
+ name: "OpenAI API Key",
8717
+ category: "secrets",
8718
+ severity: "error",
8719
+ confidence: "high",
8720
+ patterns: [/sk-proj-[A-Za-z0-9_-]{20,}/],
8721
+ message: "Hardcoded OpenAI API key detected",
8722
+ remediation: "Use environment variables: process.env.OPENAI_API_KEY",
8723
+ references: ["CWE-798"]
8724
+ },
8725
+ {
8726
+ id: "SEC-SEC-008",
8727
+ name: "Google API Key",
8728
+ category: "secrets",
8729
+ severity: "error",
8730
+ confidence: "high",
8731
+ patterns: [/AIza[A-Za-z0-9_-]{35}/],
8732
+ message: "Hardcoded Google API key detected",
8733
+ remediation: "Use environment variables or a secrets manager for Google API keys",
8734
+ references: ["CWE-798"]
8735
+ },
8736
+ {
8737
+ id: "SEC-SEC-009",
8738
+ name: "GitHub Personal Access Token",
8739
+ category: "secrets",
8740
+ severity: "error",
8741
+ confidence: "high",
8742
+ patterns: [/gh[pous]_[A-Za-z0-9_]{36,}/],
8743
+ message: "Hardcoded GitHub personal access token detected",
8744
+ remediation: "Use environment variables: process.env.GITHUB_TOKEN",
8745
+ references: ["CWE-798"]
8746
+ },
8747
+ {
8748
+ id: "SEC-SEC-010",
8749
+ name: "Stripe Live Key",
8750
+ category: "secrets",
8751
+ severity: "error",
8752
+ confidence: "high",
8753
+ patterns: [/\b[spr]k_live_[A-Za-z0-9]{24,}/],
8754
+ message: "Hardcoded Stripe live key detected",
8755
+ remediation: "Use environment variables for Stripe keys; never commit live keys",
8756
+ references: ["CWE-798"]
8757
+ },
8758
+ {
8759
+ id: "SEC-SEC-011",
8760
+ name: "Database Connection String with Credentials",
8761
+ category: "secrets",
8762
+ severity: "error",
8763
+ confidence: "high",
8764
+ patterns: [/(?:postgres|mysql|mongodb|redis|amqp|mssql)(?:\+\w+)?:\/\/[^/\s:]+:[^@/\s]+@/i],
8765
+ message: "Database connection string with embedded credentials detected",
8766
+ remediation: "Use environment variables for connection strings; separate credentials from URIs",
8767
+ references: ["CWE-798"]
8245
8768
  }
8246
8769
  ];
8247
8770
 
@@ -8428,6 +8951,360 @@ var deserializationRules = [
8428
8951
  }
8429
8952
  ];
8430
8953
 
8954
+ // src/security/rules/agent-config.ts
8955
+ var agentConfigRules = [
8956
+ {
8957
+ id: "SEC-AGT-001",
8958
+ name: "Hidden Unicode Characters",
8959
+ category: "agent-config",
8960
+ severity: "error",
8961
+ confidence: "high",
8962
+ patterns: [/\u200B|\u200C|\u200D|\uFEFF|\u2060/],
8963
+ fileGlob: "**/CLAUDE.md,**/AGENTS.md,**/*.yaml",
8964
+ message: "Hidden zero-width Unicode characters detected in agent configuration",
8965
+ remediation: "Remove invisible Unicode characters; they may hide malicious instructions",
8966
+ references: ["CWE-116"]
8967
+ },
8968
+ {
8969
+ id: "SEC-AGT-002",
8970
+ name: "URL Execution Directives",
8971
+ category: "agent-config",
8972
+ severity: "warning",
8973
+ confidence: "medium",
8974
+ patterns: [/\b(?:curl|wget)\s+\S+/i, /\bfetch\s*\(/i],
8975
+ fileGlob: "**/CLAUDE.md,**/AGENTS.md",
8976
+ message: "URL execution directive found in agent configuration",
8977
+ remediation: "Avoid instructing agents to download and execute remote content",
8978
+ references: ["CWE-94"]
8979
+ },
8980
+ {
8981
+ id: "SEC-AGT-003",
8982
+ name: "Wildcard Tool Permissions",
8983
+ category: "agent-config",
8984
+ severity: "warning",
8985
+ confidence: "high",
8986
+ patterns: [/(?:Bash|Write|Edit)\s*\(\s*\*\s*\)/],
8987
+ fileGlob: "**/.claude/**,**/settings*.json",
8988
+ message: "Wildcard tool permissions grant unrestricted access",
8989
+ remediation: "Scope tool permissions to specific patterns instead of wildcards",
8990
+ references: ["CWE-250"]
8991
+ },
8992
+ {
8993
+ id: "SEC-AGT-004",
8994
+ name: "Auto-approve Patterns",
8995
+ category: "agent-config",
8996
+ severity: "warning",
8997
+ confidence: "high",
8998
+ patterns: [/\bautoApprove\b/i, /\bauto_approve\b/i],
8999
+ fileGlob: "**/.claude/**,**/.mcp.json",
9000
+ message: "Auto-approve configuration bypasses human review of tool calls",
9001
+ remediation: "Review auto-approved tools carefully; prefer explicit approval for destructive operations",
9002
+ references: ["CWE-862"]
9003
+ },
9004
+ {
9005
+ id: "SEC-AGT-005",
9006
+ name: "Prompt Injection Surface",
9007
+ category: "agent-config",
9008
+ severity: "warning",
9009
+ confidence: "medium",
9010
+ patterns: [/\$\{[^}]*\}/, /\{\{[^}]*\}\}/],
9011
+ fileGlob: "**/skill.yaml",
9012
+ message: "Template interpolation syntax in skill YAML may enable prompt injection",
9013
+ remediation: "Avoid dynamic interpolation in skill descriptions; use static text",
9014
+ references: ["CWE-94"]
9015
+ },
9016
+ {
9017
+ id: "SEC-AGT-006",
9018
+ name: "Permission Bypass Flags",
9019
+ category: "agent-config",
9020
+ severity: "error",
9021
+ confidence: "high",
9022
+ patterns: [/--dangerously-skip-permissions/, /--no-verify/],
9023
+ fileGlob: "**/CLAUDE.md,**/AGENTS.md,**/.claude/**",
9024
+ message: "Permission bypass flag detected in agent configuration",
9025
+ remediation: "Remove flags that bypass safety checks; they undermine enforcement",
9026
+ references: ["CWE-863"]
9027
+ },
9028
+ {
9029
+ id: "SEC-AGT-007",
9030
+ name: "Hook Injection Surface",
9031
+ category: "agent-config",
9032
+ severity: "error",
9033
+ confidence: "low",
9034
+ patterns: [/\$\(/, /`[^`]+`/, /\s&&\s/, /\s\|\|\s/],
9035
+ fileGlob: "**/settings*.json,**/hooks.json",
9036
+ message: "Shell metacharacters in hook commands may enable command injection",
9037
+ remediation: "Use simple, single-command hooks without shell operators; chain logic inside the script",
9038
+ references: ["CWE-78"]
9039
+ }
9040
+ ];
9041
+
9042
+ // src/security/rules/mcp.ts
9043
+ var mcpRules = [
9044
+ {
9045
+ id: "SEC-MCP-001",
9046
+ name: "Hardcoded MCP Secrets",
9047
+ category: "mcp",
9048
+ severity: "error",
9049
+ confidence: "medium",
9050
+ patterns: [/(?:API_KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL)\s*["']?\s*:\s*["'][^"']{8,}["']/i],
9051
+ fileGlob: "**/.mcp.json",
9052
+ message: "Hardcoded secret detected in MCP server configuration",
9053
+ remediation: "Use environment variable references instead of inline secrets in .mcp.json",
9054
+ references: ["CWE-798"]
9055
+ },
9056
+ {
9057
+ id: "SEC-MCP-002",
9058
+ name: "Shell Injection in MCP Args",
9059
+ category: "mcp",
9060
+ severity: "error",
9061
+ confidence: "medium",
9062
+ patterns: [/\$\(/, /`[^`]+`/],
9063
+ fileGlob: "**/.mcp.json",
9064
+ message: "Shell metacharacters detected in MCP server arguments",
9065
+ remediation: "Use literal argument values; avoid shell interpolation in MCP args",
9066
+ references: ["CWE-78"]
9067
+ },
9068
+ {
9069
+ id: "SEC-MCP-003",
9070
+ name: "Network Exposure",
9071
+ category: "mcp",
9072
+ severity: "warning",
9073
+ confidence: "high",
9074
+ patterns: [/0\.0\.0\.0/, /["']\*["']\s*:\s*\d/, /host["']?\s*:\s*["']\*["']/i],
9075
+ fileGlob: "**/.mcp.json",
9076
+ message: "MCP server binding to all network interfaces (0.0.0.0 or wildcard *)",
9077
+ remediation: "Bind to 127.0.0.1 or localhost to restrict access to local machine",
9078
+ references: ["CWE-668"]
9079
+ },
9080
+ {
9081
+ id: "SEC-MCP-004",
9082
+ name: "Typosquatting Vector",
9083
+ category: "mcp",
9084
+ severity: "warning",
9085
+ confidence: "medium",
9086
+ patterns: [/\bnpx\s+(?:-y|--yes)\b/],
9087
+ fileGlob: "**/.mcp.json",
9088
+ message: "npx -y auto-installs packages without confirmation, enabling typosquatting",
9089
+ remediation: "Pin exact package versions or install packages explicitly before use",
9090
+ references: ["CWE-427"]
9091
+ },
9092
+ {
9093
+ id: "SEC-MCP-005",
9094
+ name: "Unencrypted Transport",
9095
+ category: "mcp",
9096
+ severity: "warning",
9097
+ confidence: "medium",
9098
+ patterns: [/http:\/\/(?!localhost\b|127\.0\.0\.1\b)/],
9099
+ fileGlob: "**/.mcp.json",
9100
+ message: "Unencrypted HTTP transport detected for MCP server connection",
9101
+ remediation: "Use https:// for all non-localhost MCP server connections",
9102
+ references: ["CWE-319"]
9103
+ }
9104
+ ];
9105
+
9106
+ // src/security/rules/insecure-defaults.ts
9107
+ var insecureDefaultsRules = [
9108
+ {
9109
+ id: "SEC-DEF-001",
9110
+ name: "Security-Sensitive Fallback to Hardcoded Default",
9111
+ category: "insecure-defaults",
9112
+ severity: "warning",
9113
+ confidence: "medium",
9114
+ patterns: [
9115
+ /(?:SECRET|KEY|TOKEN|PASSWORD|SALT|PEPPER|SIGNING|ENCRYPTION|AUTH|JWT|SESSION).*(?:\|\||\?\?)\s*['"][^'"]+['"]/i
9116
+ ],
9117
+ fileGlob: "**/*.{ts,js,mjs,cjs}",
9118
+ message: "Security-sensitive variable falls back to a hardcoded default when env var is missing",
9119
+ remediation: "Throw an error if the env var is missing instead of falling back to a default. Use a startup validation check.",
9120
+ references: ["CWE-1188"]
9121
+ },
9122
+ {
9123
+ id: "SEC-DEF-002",
9124
+ name: "TLS/SSL Disabled by Default",
9125
+ category: "insecure-defaults",
9126
+ severity: "warning",
9127
+ confidence: "medium",
9128
+ patterns: [
9129
+ /(?:tls|ssl|https|secure)\s*(?:=|:)\s*(?:false|config\??\.\w+\s*(?:\?\?|&&|\|\|)\s*false)/i
9130
+ ],
9131
+ fileGlob: "**/*.{ts,js,mjs,cjs,go,py}",
9132
+ message: "Security feature defaults to disabled; missing configuration degrades to insecure mode",
9133
+ remediation: "Default security features to enabled (true). Require explicit opt-out, not opt-in.",
9134
+ references: ["CWE-1188"]
9135
+ },
9136
+ {
9137
+ id: "SEC-DEF-003",
9138
+ name: "Swallowed Authentication/Authorization Error",
9139
+ category: "insecure-defaults",
9140
+ severity: "warning",
9141
+ confidence: "low",
9142
+ patterns: [
9143
+ // Matches single-line empty catch: catch(e) { } or catch(e) { // ignore }
9144
+ // Note: multi-line catch blocks are handled by AI review, not this rule
9145
+ /catch\s*\([^)]*\)\s*\{\s*(?:\/\/\s*(?:ignore|skip|noop|todo)\b.*)?\s*\}/
9146
+ ],
9147
+ fileGlob: "**/*auth*.{ts,js,mjs,cjs},**/*session*.{ts,js,mjs,cjs},**/*token*.{ts,js,mjs,cjs}",
9148
+ 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.",
9149
+ remediation: "Re-throw the error or return an explicit denial. Never silently swallow auth errors.",
9150
+ references: ["CWE-754", "CWE-390"]
9151
+ },
9152
+ {
9153
+ id: "SEC-DEF-004",
9154
+ name: "Permissive CORS Fallback",
9155
+ category: "insecure-defaults",
9156
+ severity: "warning",
9157
+ confidence: "medium",
9158
+ patterns: [
9159
+ /(?:origin|cors)\s*(?:=|:)\s*(?:config|options|env)\??\.\w+\s*(?:\?\?|\|\|)\s*['"]\*/
9160
+ ],
9161
+ fileGlob: "**/*.{ts,js,mjs,cjs}",
9162
+ message: "CORS origin falls back to wildcard (*) when configuration is missing",
9163
+ remediation: "Default to a restrictive origin list. Require explicit configuration for permissive CORS.",
9164
+ references: ["CWE-942"]
9165
+ },
9166
+ {
9167
+ id: "SEC-DEF-005",
9168
+ name: "Rate Limiting Disabled by Default",
9169
+ category: "insecure-defaults",
9170
+ severity: "info",
9171
+ confidence: "low",
9172
+ patterns: [
9173
+ /(?:rateLimit|rateLimiting|throttle)\s*(?:=|:)\s*(?:config|options|env)\??\.\w+\s*(?:\?\?|\|\|)\s*(?:false|0|null|undefined)/i
9174
+ ],
9175
+ fileGlob: "**/*.{ts,js,mjs,cjs}",
9176
+ message: "Rate limiting defaults to disabled when configuration is missing",
9177
+ remediation: "Default to a sensible rate limit. Require explicit opt-out for disabling.",
9178
+ references: ["CWE-770"]
9179
+ }
9180
+ ];
9181
+
9182
+ // src/security/rules/sharp-edges.ts
9183
+ var sharpEdgesRules = [
9184
+ // --- Deprecated Crypto APIs ---
9185
+ {
9186
+ id: "SEC-EDGE-001",
9187
+ name: "Deprecated createCipher API",
9188
+ category: "sharp-edges",
9189
+ severity: "error",
9190
+ confidence: "high",
9191
+ patterns: [/crypto\.createCipher\s*\(/],
9192
+ fileGlob: "**/*.{ts,js,mjs,cjs}",
9193
+ message: "crypto.createCipher is deprecated \u2014 uses weak key derivation (no IV)",
9194
+ remediation: "Use crypto.createCipheriv with a random IV and proper key derivation (scrypt/pbkdf2)",
9195
+ references: ["CWE-327"]
9196
+ },
9197
+ {
9198
+ id: "SEC-EDGE-002",
9199
+ name: "Deprecated createDecipher API",
9200
+ category: "sharp-edges",
9201
+ severity: "error",
9202
+ confidence: "high",
9203
+ patterns: [/crypto\.createDecipher\s*\(/],
9204
+ fileGlob: "**/*.{ts,js,mjs,cjs}",
9205
+ message: "crypto.createDecipher is deprecated \u2014 uses weak key derivation (no IV)",
9206
+ remediation: "Use crypto.createDecipheriv with the same IV used for encryption",
9207
+ references: ["CWE-327"]
9208
+ },
9209
+ {
9210
+ id: "SEC-EDGE-003",
9211
+ name: "ECB Mode Selection",
9212
+ category: "sharp-edges",
9213
+ severity: "warning",
9214
+ confidence: "high",
9215
+ patterns: [/-ecb['"]/],
9216
+ fileGlob: "**/*.{ts,js,mjs,cjs,go,py}",
9217
+ message: "ECB mode does not provide semantic security \u2014 identical plaintext blocks produce identical ciphertext",
9218
+ remediation: "Use CBC, CTR, or GCM mode instead of ECB",
9219
+ references: ["CWE-327"]
9220
+ },
9221
+ // --- Unsafe Deserialization ---
9222
+ {
9223
+ id: "SEC-EDGE-004",
9224
+ name: "yaml.load Without Safe Loader",
9225
+ category: "sharp-edges",
9226
+ severity: "error",
9227
+ confidence: "high",
9228
+ patterns: [
9229
+ /yaml\.load\s*\(/
9230
+ // Python: yaml.load() without SafeLoader
9231
+ ],
9232
+ fileGlob: "**/*.py",
9233
+ message: "yaml.load() executes arbitrary Python objects \u2014 use yaml.safe_load() instead",
9234
+ 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",
9235
+ references: ["CWE-502"]
9236
+ },
9237
+ {
9238
+ id: "SEC-EDGE-005",
9239
+ name: "Pickle/Marshal Deserialization",
9240
+ category: "sharp-edges",
9241
+ severity: "error",
9242
+ confidence: "high",
9243
+ patterns: [/pickle\.loads?\s*\(/, /marshal\.loads?\s*\(/],
9244
+ fileGlob: "**/*.py",
9245
+ message: "pickle/marshal deserialization executes arbitrary code \u2014 never use on untrusted data",
9246
+ remediation: "Use JSON, MessagePack, or Protocol Buffers for untrusted data serialization",
9247
+ references: ["CWE-502"]
9248
+ },
9249
+ // --- TOCTOU (Time-of-Check to Time-of-Use) ---
9250
+ {
9251
+ id: "SEC-EDGE-006",
9252
+ name: "Check-Then-Act File Operation",
9253
+ category: "sharp-edges",
9254
+ severity: "warning",
9255
+ confidence: "medium",
9256
+ // Patterns use .{0,N} since scanner matches single lines only (no multiline mode)
9257
+ patterns: [
9258
+ /(?:existsSync|accessSync|statSync)\s*\([^)]+\).{0,50}(?:readFileSync|writeFileSync|unlinkSync|mkdirSync)\s*\(/
9259
+ ],
9260
+ fileGlob: "**/*.{ts,js,mjs,cjs}",
9261
+ message: "Check-then-act pattern on filesystem is vulnerable to TOCTOU race conditions",
9262
+ remediation: "Use the operation directly and handle ENOENT/EEXIST errors instead of checking first",
9263
+ references: ["CWE-367"]
9264
+ },
9265
+ {
9266
+ id: "SEC-EDGE-007",
9267
+ name: "Check-Then-Act File Operation (Async)",
9268
+ category: "sharp-edges",
9269
+ severity: "warning",
9270
+ confidence: "medium",
9271
+ // Uses .{0,N} since scanner matches single lines only (no multiline mode)
9272
+ patterns: [/(?:access|stat)\s*\([^)]+\).{0,80}(?:readFile|writeFile|unlink|mkdir)\s*\(/],
9273
+ fileGlob: "**/*.{ts,js,mjs,cjs}",
9274
+ message: "Async check-then-act pattern on filesystem is vulnerable to TOCTOU race conditions",
9275
+ remediation: "Use the operation directly with try/catch instead of checking existence first",
9276
+ references: ["CWE-367"]
9277
+ },
9278
+ // --- Stringly-Typed Security ---
9279
+ {
9280
+ id: "SEC-EDGE-008",
9281
+ name: 'JWT Algorithm "none"',
9282
+ category: "sharp-edges",
9283
+ severity: "error",
9284
+ confidence: "high",
9285
+ patterns: [
9286
+ /algorithm[s]?\s*[:=]\s*\[?\s*['"]none['"]/i,
9287
+ /alg(?:orithm)?\s*[:=]\s*['"]none['"]/i
9288
+ ],
9289
+ fileGlob: "**/*.{ts,js,mjs,cjs}",
9290
+ message: 'JWT "none" algorithm disables signature verification entirely',
9291
+ remediation: 'Specify an explicit algorithm (e.g., "HS256", "RS256") and set algorithms allowlist in verify options',
9292
+ references: ["CWE-345"]
9293
+ },
9294
+ {
9295
+ id: "SEC-EDGE-009",
9296
+ name: "DES/RC4 Algorithm Selection",
9297
+ category: "sharp-edges",
9298
+ severity: "error",
9299
+ confidence: "high",
9300
+ patterns: [/['"]\s*(?:des|des-ede|des-ede3|des3|rc4|rc2|blowfish)\s*['"]/i],
9301
+ fileGlob: "**/*.{ts,js,mjs,cjs,go,py}",
9302
+ message: "Weak/deprecated cipher algorithm selected \u2014 DES, RC4, RC2, and Blowfish are broken or deprecated",
9303
+ remediation: "Use AES-256-GCM or ChaCha20-Poly1305",
9304
+ references: ["CWE-327"]
9305
+ }
9306
+ ];
9307
+
8431
9308
  // src/security/rules/stack/node.ts
8432
9309
  var nodeRules = [
8433
9310
  {
@@ -8541,6 +9418,14 @@ var goRules = [
8541
9418
  ];
8542
9419
 
8543
9420
  // src/security/scanner.ts
9421
+ function parseHarnessIgnore(line, ruleId) {
9422
+ if (!line.includes("harness-ignore")) return null;
9423
+ if (!line.includes(ruleId)) return null;
9424
+ const match = line.match(/(?:\/\/|#)\s*harness-ignore\s+(SEC-[A-Z]+-\d+)(?::\s*(.+))?/);
9425
+ if (match?.[1] !== ruleId) return null;
9426
+ const text = match[2]?.trim();
9427
+ return { ruleId, justification: text || null };
9428
+ }
8544
9429
  var SecurityScanner = class {
8545
9430
  registry;
8546
9431
  config;
@@ -8555,7 +9440,11 @@ var SecurityScanner = class {
8555
9440
  ...cryptoRules,
8556
9441
  ...pathTraversalRules,
8557
9442
  ...networkRules,
8558
- ...deserializationRules
9443
+ ...deserializationRules,
9444
+ ...agentConfigRules,
9445
+ ...mcpRules,
9446
+ ...insecureDefaultsRules,
9447
+ ...sharpEdgesRules
8559
9448
  ]);
8560
9449
  this.registry.registerAll([...nodeRules, ...expressRules, ...reactRules, ...goRules]);
8561
9450
  this.activeRules = this.registry.getAll();
@@ -8564,11 +9453,40 @@ var SecurityScanner = class {
8564
9453
  const stacks = detectStack(projectRoot);
8565
9454
  this.activeRules = this.registry.getForStacks(stacks.length > 0 ? stacks : []);
8566
9455
  }
9456
+ /**
9457
+ * Scan raw content against all active rules. Note: this method does NOT apply
9458
+ * fileGlob filtering — every active rule is evaluated regardless of filePath.
9459
+ * If you are scanning a specific file and want fileGlob-based rule filtering,
9460
+ * use {@link scanFile} instead.
9461
+ */
8567
9462
  scanContent(content, filePath, startLine = 1) {
8568
9463
  if (!this.config.enabled) return [];
8569
- const findings = [];
8570
9464
  const lines = content.split("\n");
8571
- for (const rule of this.activeRules) {
9465
+ return this.scanLinesWithRules(lines, this.activeRules, filePath, startLine);
9466
+ }
9467
+ async scanFile(filePath) {
9468
+ if (!this.config.enabled) return [];
9469
+ const content = await fs18.readFile(filePath, "utf-8");
9470
+ return this.scanContentForFile(content, filePath, 1);
9471
+ }
9472
+ scanContentForFile(content, filePath, startLine = 1) {
9473
+ if (!this.config.enabled) return [];
9474
+ const lines = content.split("\n");
9475
+ const applicableRules = this.activeRules.filter((rule) => {
9476
+ if (!rule.fileGlob) return true;
9477
+ const globs = rule.fileGlob.split(",").map((g) => g.trim());
9478
+ return globs.some((glob2) => (0, import_minimatch5.minimatch)(filePath, glob2, { dot: true }));
9479
+ });
9480
+ return this.scanLinesWithRules(lines, applicableRules, filePath, startLine);
9481
+ }
9482
+ /**
9483
+ * Core scanning loop shared by scanContent and scanContentForFile.
9484
+ * Evaluates each rule against each line, handling suppression (FP gate)
9485
+ * and pattern matching uniformly.
9486
+ */
9487
+ scanLinesWithRules(lines, rules, filePath, startLine) {
9488
+ const findings = [];
9489
+ for (const rule of rules) {
8572
9490
  const resolved = resolveRuleSeverity(
8573
9491
  rule.id,
8574
9492
  rule.severity,
@@ -8578,7 +9496,25 @@ var SecurityScanner = class {
8578
9496
  if (resolved === "off") continue;
8579
9497
  for (let i = 0; i < lines.length; i++) {
8580
9498
  const line = lines[i] ?? "";
8581
- if (line.includes("harness-ignore") && line.includes(rule.id)) continue;
9499
+ const suppressionMatch = parseHarnessIgnore(line, rule.id);
9500
+ if (suppressionMatch) {
9501
+ if (!suppressionMatch.justification) {
9502
+ findings.push({
9503
+ ruleId: rule.id,
9504
+ ruleName: rule.name,
9505
+ category: rule.category,
9506
+ severity: this.config.strict ? "error" : "warning",
9507
+ confidence: "high",
9508
+ file: filePath,
9509
+ line: startLine + i,
9510
+ match: line.trim(),
9511
+ context: line,
9512
+ message: `Suppression of ${rule.id} requires justification: // harness-ignore ${rule.id}: <reason>`,
9513
+ remediation: `Add justification after colon: // harness-ignore ${rule.id}: false positive because ...`
9514
+ });
9515
+ }
9516
+ continue;
9517
+ }
8582
9518
  for (const pattern of rule.patterns) {
8583
9519
  pattern.lastIndex = 0;
8584
9520
  if (pattern.test(line)) {
@@ -8603,11 +9539,6 @@ var SecurityScanner = class {
8603
9539
  }
8604
9540
  return findings;
8605
9541
  }
8606
- async scanFile(filePath) {
8607
- if (!this.config.enabled) return [];
8608
- const content = await fs17.readFile(filePath, "utf-8");
8609
- return this.scanContent(content, filePath, 1);
8610
- }
8611
9542
  async scanFiles(filePaths) {
8612
9543
  const allFindings = [];
8613
9544
  let scannedCount = 0;
@@ -8627,10 +9558,418 @@ var SecurityScanner = class {
8627
9558
  coverage: "baseline"
8628
9559
  };
8629
9560
  }
8630
- };
9561
+ };
9562
+
9563
+ // src/security/injection-patterns.ts
9564
+ var hiddenUnicodePatterns = [
9565
+ {
9566
+ ruleId: "INJ-UNI-001",
9567
+ severity: "high",
9568
+ category: "hidden-unicode",
9569
+ description: "Zero-width characters that can hide malicious instructions",
9570
+ // eslint-disable-next-line no-misleading-character-class -- intentional: regex detects zero-width chars for security scanning
9571
+ pattern: /[\u200B\u200C\u200D\uFEFF\u2060]/
9572
+ },
9573
+ {
9574
+ ruleId: "INJ-UNI-002",
9575
+ severity: "high",
9576
+ category: "hidden-unicode",
9577
+ description: "RTL/LTR override characters that can disguise text direction",
9578
+ pattern: /[\u202A-\u202E\u2066-\u2069]/
9579
+ }
9580
+ ];
9581
+ var reRolingPatterns = [
9582
+ {
9583
+ ruleId: "INJ-REROL-001",
9584
+ severity: "high",
9585
+ category: "explicit-re-roling",
9586
+ description: "Instruction to ignore/disregard/forget previous instructions",
9587
+ pattern: /(?:ignore|disregard|forget)\s+(?:all\s+)?(?:previous|prior|above|earlier)\s+(?:instructions?|prompts?|context|rules?|guidelines?)/i
9588
+ },
9589
+ {
9590
+ ruleId: "INJ-REROL-002",
9591
+ severity: "high",
9592
+ category: "explicit-re-roling",
9593
+ description: "Attempt to reassign the AI role",
9594
+ 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
9595
+ },
9596
+ {
9597
+ ruleId: "INJ-REROL-003",
9598
+ severity: "high",
9599
+ category: "explicit-re-roling",
9600
+ description: "Direct instruction override attempt",
9601
+ pattern: /(?:new\s+)?(?:system\s+)?(?:instruction|directive|role|persona)\s*[:=]\s*/i
9602
+ }
9603
+ ];
9604
+ var permissionEscalationPatterns = [
9605
+ {
9606
+ ruleId: "INJ-PERM-001",
9607
+ severity: "high",
9608
+ category: "permission-escalation",
9609
+ description: "Attempt to enable all tools or grant unrestricted access",
9610
+ pattern: /(?:allow|enable|grant)\s+all\s+(?:tools?|permissions?|access)/i
9611
+ },
9612
+ {
9613
+ ruleId: "INJ-PERM-002",
9614
+ severity: "high",
9615
+ category: "permission-escalation",
9616
+ description: "Attempt to disable safety or security features",
9617
+ pattern: /(?:disable|turn\s+off|remove|bypass)\s+(?:all\s+)?(?:safety|security|restrictions?|guardrails?|protections?|checks?)/i
9618
+ },
9619
+ {
9620
+ ruleId: "INJ-PERM-003",
9621
+ severity: "high",
9622
+ category: "permission-escalation",
9623
+ description: "Auto-approve directive that bypasses human review",
9624
+ pattern: /(?:auto[- ]?approve|--no-verify|--dangerously-skip-permissions)/i
9625
+ }
9626
+ ];
9627
+ var encodedPayloadPatterns = [
9628
+ {
9629
+ ruleId: "INJ-ENC-001",
9630
+ severity: "high",
9631
+ category: "encoded-payloads",
9632
+ description: "Base64-encoded string long enough to contain instructions (>=28 chars)",
9633
+ // Match base64 strings of 28+ chars (7+ groups of 4).
9634
+ // Excludes JWT tokens (eyJ prefix) and Bearer-prefixed tokens.
9635
+ 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/])/
9636
+ },
9637
+ {
9638
+ ruleId: "INJ-ENC-002",
9639
+ severity: "high",
9640
+ category: "encoded-payloads",
9641
+ description: "Hex-encoded string long enough to contain directives (>=20 hex chars)",
9642
+ // Excludes hash-prefixed hex (sha256:, sha512:, md5:, etc.) and hex preceded by 0x.
9643
+ // Note: 40-char git SHA hashes (e.g. in `git log` output) may match — downstream
9644
+ // callers should filter matches of exactly 40 hex chars if scanning git output.
9645
+ pattern: /(?<![:x])(?<![A-Fa-f0-9])(?:[0-9a-fA-F]{2}){10,}(?![A-Fa-f0-9])/
9646
+ }
9647
+ ];
9648
+ var indirectInjectionPatterns = [
9649
+ {
9650
+ ruleId: "INJ-IND-001",
9651
+ severity: "medium",
9652
+ category: "indirect-injection",
9653
+ description: "Instruction to influence future responses",
9654
+ 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
9655
+ },
9656
+ {
9657
+ ruleId: "INJ-IND-002",
9658
+ severity: "medium",
9659
+ category: "indirect-injection",
9660
+ description: "Directive to include content in responses",
9661
+ pattern: /(?:include|insert|add|embed|put)\s+(?:this|the\s+following)\s+(?:in|into|to)\s+(?:your|the)\s+(?:response|output|reply|answer)/i
9662
+ },
9663
+ {
9664
+ ruleId: "INJ-IND-003",
9665
+ severity: "medium",
9666
+ category: "indirect-injection",
9667
+ description: "Standing instruction to always respond a certain way",
9668
+ pattern: /always\s+(?:respond|reply|answer|say|output)\s+(?:with|that|by)/i
9669
+ }
9670
+ ];
9671
+ var contextManipulationPatterns = [
9672
+ {
9673
+ ruleId: "INJ-CTX-001",
9674
+ severity: "medium",
9675
+ category: "context-manipulation",
9676
+ description: "Claim about system prompt content",
9677
+ pattern: /(?:the\s+)?(?:system\s+prompt|system\s+message|hidden\s+instructions?)\s+(?:says?|tells?|instructs?|contains?|is)/i
9678
+ },
9679
+ {
9680
+ ruleId: "INJ-CTX-002",
9681
+ severity: "medium",
9682
+ category: "context-manipulation",
9683
+ description: "Claim about AI instructions",
9684
+ pattern: /your\s+(?:instructions?|directives?|guidelines?|rules?)\s+(?:are|say|tell|state)/i
9685
+ },
9686
+ {
9687
+ ruleId: "INJ-CTX-003",
9688
+ severity: "medium",
9689
+ category: "context-manipulation",
9690
+ description: "Fake XML/HTML system or instruction tags",
9691
+ // Case-sensitive: only match lowercase tags to avoid false positives on
9692
+ // React components like <User>, <Context>, <Role> etc.
9693
+ pattern: /<\/?(?:system|instruction|prompt|role|context|tool_call|function_call|assistant|human|user)[^>]*>/
9694
+ },
9695
+ {
9696
+ ruleId: "INJ-CTX-004",
9697
+ severity: "medium",
9698
+ category: "context-manipulation",
9699
+ description: "Fake JSON role assignment mimicking chat format",
9700
+ pattern: /[{,]\s*"role"\s*:\s*"(?:system|assistant|function)"/i
9701
+ }
9702
+ ];
9703
+ var socialEngineeringPatterns = [
9704
+ {
9705
+ ruleId: "INJ-SOC-001",
9706
+ severity: "medium",
9707
+ category: "social-engineering",
9708
+ description: "Urgency pressure to bypass checks",
9709
+ pattern: /(?:this\s+is\s+(?:very\s+)?urgent|this\s+is\s+(?:an?\s+)?emergency|do\s+(?:this|it)\s+(?:now|immediately))\b/i
9710
+ },
9711
+ {
9712
+ ruleId: "INJ-SOC-002",
9713
+ severity: "medium",
9714
+ category: "social-engineering",
9715
+ description: "False authority claim",
9716
+ pattern: /(?:the\s+)?(?:admin|administrator|manager|CEO|CTO|owner|supervisor)\s+(?:authorized|approved|said|told|confirmed|requested)/i
9717
+ },
9718
+ {
9719
+ ruleId: "INJ-SOC-003",
9720
+ severity: "medium",
9721
+ category: "social-engineering",
9722
+ description: "Testing pretext to bypass safety",
9723
+ pattern: /(?:for\s+testing\s+purposes?|this\s+is\s+(?:just\s+)?a\s+test|in\s+test\s+mode)\b/i
9724
+ }
9725
+ ];
9726
+ var suspiciousPatterns = [
9727
+ {
9728
+ ruleId: "INJ-SUS-001",
9729
+ severity: "low",
9730
+ category: "suspicious-patterns",
9731
+ description: "Excessive consecutive whitespace (>10 chars) mid-line that may hide content",
9732
+ // Only match whitespace runs not at the start of a line (indentation is normal)
9733
+ pattern: /\S\s{11,}/
9734
+ },
9735
+ {
9736
+ ruleId: "INJ-SUS-002",
9737
+ severity: "low",
9738
+ category: "suspicious-patterns",
9739
+ description: "Repeated delimiters (>5) that may indicate obfuscation",
9740
+ pattern: /([|;,=\-_~`])\1{5,}/
9741
+ },
9742
+ {
9743
+ ruleId: "INJ-SUS-003",
9744
+ severity: "low",
9745
+ category: "suspicious-patterns",
9746
+ description: "Mathematical alphanumeric symbols used as Latin character substitutes",
9747
+ // Mathematical bold/italic/script Unicode ranges (U+1D400-U+1D7FF)
9748
+ pattern: /[\uD835][\uDC00-\uDFFF]/
9749
+ }
9750
+ ];
9751
+ var ALL_PATTERNS = [
9752
+ ...hiddenUnicodePatterns,
9753
+ ...reRolingPatterns,
9754
+ ...permissionEscalationPatterns,
9755
+ ...encodedPayloadPatterns,
9756
+ ...indirectInjectionPatterns,
9757
+ ...contextManipulationPatterns,
9758
+ ...socialEngineeringPatterns,
9759
+ ...suspiciousPatterns
9760
+ ];
9761
+ function scanForInjection(text) {
9762
+ const findings = [];
9763
+ const lines = text.split("\n");
9764
+ for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
9765
+ const line = lines[lineIdx];
9766
+ for (const rule of ALL_PATTERNS) {
9767
+ if (rule.pattern.test(line)) {
9768
+ findings.push({
9769
+ severity: rule.severity,
9770
+ ruleId: rule.ruleId,
9771
+ match: line.trim(),
9772
+ line: lineIdx + 1
9773
+ });
9774
+ }
9775
+ }
9776
+ }
9777
+ const severityOrder = {
9778
+ high: 0,
9779
+ medium: 1,
9780
+ low: 2
9781
+ };
9782
+ findings.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
9783
+ return findings;
9784
+ }
9785
+ function getInjectionPatterns() {
9786
+ return ALL_PATTERNS;
9787
+ }
9788
+ var DESTRUCTIVE_BASH = [
9789
+ /\bgit\s+push\b/,
9790
+ /\bgit\s+commit\b/,
9791
+ /\brm\s+-rf?\b/,
9792
+ /\brm\s+-r\b/
9793
+ ];
9794
+
9795
+ // src/security/taint.ts
9796
+ var import_node_fs4 = require("fs");
9797
+ var import_node_path7 = require("path");
9798
+ var TAINT_DURATION_MS = 30 * 60 * 1e3;
9799
+ var DEFAULT_SESSION_ID = "default";
9800
+ function getTaintFilePath(projectRoot, sessionId) {
9801
+ const id = sessionId || DEFAULT_SESSION_ID;
9802
+ return (0, import_node_path7.join)(projectRoot, ".harness", `session-taint-${id}.json`);
9803
+ }
9804
+ function readTaint(projectRoot, sessionId) {
9805
+ const filePath = getTaintFilePath(projectRoot, sessionId);
9806
+ let content;
9807
+ try {
9808
+ content = (0, import_node_fs4.readFileSync)(filePath, "utf8");
9809
+ } catch {
9810
+ return null;
9811
+ }
9812
+ let state;
9813
+ try {
9814
+ state = JSON.parse(content);
9815
+ } catch {
9816
+ try {
9817
+ (0, import_node_fs4.unlinkSync)(filePath);
9818
+ } catch {
9819
+ }
9820
+ return null;
9821
+ }
9822
+ if (!state.sessionId || !state.taintedAt || !state.expiresAt || !state.findings) {
9823
+ try {
9824
+ (0, import_node_fs4.unlinkSync)(filePath);
9825
+ } catch {
9826
+ }
9827
+ return null;
9828
+ }
9829
+ return state;
9830
+ }
9831
+ function checkTaint(projectRoot, sessionId) {
9832
+ const state = readTaint(projectRoot, sessionId);
9833
+ if (!state) {
9834
+ return { tainted: false, expired: false, state: null };
9835
+ }
9836
+ const now = /* @__PURE__ */ new Date();
9837
+ const expiresAt = new Date(state.expiresAt);
9838
+ if (now >= expiresAt) {
9839
+ const filePath = getTaintFilePath(projectRoot, sessionId);
9840
+ try {
9841
+ (0, import_node_fs4.unlinkSync)(filePath);
9842
+ } catch {
9843
+ }
9844
+ return { tainted: false, expired: true, state };
9845
+ }
9846
+ return { tainted: true, expired: false, state };
9847
+ }
9848
+ function writeTaint(projectRoot, sessionId, reason, findings, source) {
9849
+ const id = sessionId || DEFAULT_SESSION_ID;
9850
+ const filePath = getTaintFilePath(projectRoot, id);
9851
+ const now = (/* @__PURE__ */ new Date()).toISOString();
9852
+ const dir = (0, import_node_path7.dirname)(filePath);
9853
+ (0, import_node_fs4.mkdirSync)(dir, { recursive: true });
9854
+ const existing = readTaint(projectRoot, id);
9855
+ const maxSeverity = findings.some((f) => f.severity === "high") ? "high" : "medium";
9856
+ const taintFindings = findings.map((f) => ({
9857
+ ruleId: f.ruleId,
9858
+ severity: f.severity,
9859
+ match: f.match,
9860
+ source,
9861
+ detectedAt: now
9862
+ }));
9863
+ const state = {
9864
+ sessionId: id,
9865
+ taintedAt: existing?.taintedAt || now,
9866
+ expiresAt: new Date(Date.now() + TAINT_DURATION_MS).toISOString(),
9867
+ reason,
9868
+ severity: existing?.severity === "high" || maxSeverity === "high" ? "high" : "medium",
9869
+ findings: [...existing?.findings || [], ...taintFindings]
9870
+ };
9871
+ (0, import_node_fs4.writeFileSync)(filePath, JSON.stringify(state, null, 2) + "\n");
9872
+ return state;
9873
+ }
9874
+ function clearTaint(projectRoot, sessionId) {
9875
+ if (sessionId) {
9876
+ const filePath = getTaintFilePath(projectRoot, sessionId);
9877
+ try {
9878
+ (0, import_node_fs4.unlinkSync)(filePath);
9879
+ return 1;
9880
+ } catch {
9881
+ return 0;
9882
+ }
9883
+ }
9884
+ const harnessDir = (0, import_node_path7.join)(projectRoot, ".harness");
9885
+ let count = 0;
9886
+ try {
9887
+ const files = (0, import_node_fs4.readdirSync)(harnessDir);
9888
+ for (const file of files) {
9889
+ if (file.startsWith("session-taint-") && file.endsWith(".json")) {
9890
+ try {
9891
+ (0, import_node_fs4.unlinkSync)((0, import_node_path7.join)(harnessDir, file));
9892
+ count++;
9893
+ } catch {
9894
+ }
9895
+ }
9896
+ }
9897
+ } catch {
9898
+ }
9899
+ return count;
9900
+ }
9901
+ function listTaintedSessions(projectRoot) {
9902
+ const harnessDir = (0, import_node_path7.join)(projectRoot, ".harness");
9903
+ const sessions = [];
9904
+ try {
9905
+ const files = (0, import_node_fs4.readdirSync)(harnessDir);
9906
+ for (const file of files) {
9907
+ if (file.startsWith("session-taint-") && file.endsWith(".json")) {
9908
+ const sessionId = file.replace("session-taint-", "").replace(".json", "");
9909
+ const result = checkTaint(projectRoot, sessionId);
9910
+ if (result.tainted) {
9911
+ sessions.push(sessionId);
9912
+ }
9913
+ }
9914
+ }
9915
+ } catch {
9916
+ }
9917
+ return sessions;
9918
+ }
9919
+
9920
+ // src/security/scan-config-shared.ts
9921
+ function mapSecuritySeverity(severity) {
9922
+ if (severity === "error") return "high";
9923
+ if (severity === "warning") return "medium";
9924
+ return "low";
9925
+ }
9926
+ function computeOverallSeverity(findings) {
9927
+ if (findings.length === 0) return "clean";
9928
+ if (findings.some((f) => f.severity === "high")) return "high";
9929
+ if (findings.some((f) => f.severity === "medium")) return "medium";
9930
+ return "low";
9931
+ }
9932
+ function computeScanExitCode(results) {
9933
+ for (const r of results) {
9934
+ if (r.overallSeverity === "high") return 2;
9935
+ }
9936
+ for (const r of results) {
9937
+ if (r.overallSeverity === "medium") return 1;
9938
+ }
9939
+ return 0;
9940
+ }
9941
+ function mapInjectionFindings(injectionFindings) {
9942
+ return injectionFindings.map((f) => ({
9943
+ ruleId: f.ruleId,
9944
+ severity: f.severity,
9945
+ message: `Injection pattern detected: ${f.ruleId}`,
9946
+ match: f.match,
9947
+ ...f.line !== void 0 ? { line: f.line } : {}
9948
+ }));
9949
+ }
9950
+ function isDuplicateFinding(existing, secFinding) {
9951
+ return existing.some(
9952
+ (e) => e.line === secFinding.line && e.match === secFinding.match.trim() && e.ruleId.split("-")[0] === secFinding.ruleId.split("-")[0]
9953
+ );
9954
+ }
9955
+ function mapSecurityFindings(secFindings, existing) {
9956
+ const result = [];
9957
+ for (const f of secFindings) {
9958
+ if (!isDuplicateFinding(existing, f)) {
9959
+ result.push({
9960
+ ruleId: f.ruleId,
9961
+ severity: mapSecuritySeverity(f.severity),
9962
+ message: f.message,
9963
+ match: f.match,
9964
+ ...f.line !== void 0 ? { line: f.line } : {}
9965
+ });
9966
+ }
9967
+ }
9968
+ return result;
9969
+ }
8631
9970
 
8632
9971
  // src/ci/check-orchestrator.ts
8633
- var path14 = __toESM(require("path"));
9972
+ var path15 = __toESM(require("path"));
8634
9973
  var ALL_CHECKS = [
8635
9974
  "validate",
8636
9975
  "deps",
@@ -8643,7 +9982,7 @@ var ALL_CHECKS = [
8643
9982
  ];
8644
9983
  async function runValidateCheck(projectRoot, config) {
8645
9984
  const issues = [];
8646
- const agentsPath = path14.join(projectRoot, config.agentsMapPath ?? "AGENTS.md");
9985
+ const agentsPath = path15.join(projectRoot, config.agentsMapPath ?? "AGENTS.md");
8647
9986
  const result = await validateAgentsMap(agentsPath);
8648
9987
  if (!result.ok) {
8649
9988
  issues.push({ severity: "error", message: result.error.message });
@@ -8700,7 +10039,7 @@ async function runDepsCheck(projectRoot, config) {
8700
10039
  }
8701
10040
  async function runDocsCheck(projectRoot, config) {
8702
10041
  const issues = [];
8703
- const docsDir = path14.join(projectRoot, config.docsDir ?? "docs");
10042
+ const docsDir = path15.join(projectRoot, config.docsDir ?? "docs");
8704
10043
  const entropyConfig = config.entropy || {};
8705
10044
  const result = await checkDocCoverage("project", {
8706
10045
  docsDir,
@@ -8812,7 +10151,7 @@ async function runPerfCheck(projectRoot, config) {
8812
10151
  if (perfReport.complexity) {
8813
10152
  for (const v of perfReport.complexity.violations) {
8814
10153
  issues.push({
8815
- severity: v.severity === "info" ? "warning" : v.severity,
10154
+ severity: "warning",
8816
10155
  message: `[Tier ${v.tier}] ${v.metric}: ${v.function} in ${v.file} (${v.value} > ${v.threshold})`,
8817
10156
  file: v.file,
8818
10157
  line: v.line
@@ -8978,7 +10317,7 @@ async function runCIChecks(input) {
8978
10317
  }
8979
10318
 
8980
10319
  // src/review/mechanical-checks.ts
8981
- var path15 = __toESM(require("path"));
10320
+ var path16 = __toESM(require("path"));
8982
10321
  async function runMechanicalChecks(options) {
8983
10322
  const { projectRoot, config, skip = [], changedFiles } = options;
8984
10323
  const findings = [];
@@ -8990,7 +10329,7 @@ async function runMechanicalChecks(options) {
8990
10329
  };
8991
10330
  if (!skip.includes("validate")) {
8992
10331
  try {
8993
- const agentsPath = path15.join(projectRoot, config.agentsMapPath ?? "AGENTS.md");
10332
+ const agentsPath = path16.join(projectRoot, config.agentsMapPath ?? "AGENTS.md");
8994
10333
  const result = await validateAgentsMap(agentsPath);
8995
10334
  if (!result.ok) {
8996
10335
  statuses.validate = "fail";
@@ -9027,7 +10366,7 @@ async function runMechanicalChecks(options) {
9027
10366
  statuses.validate = "fail";
9028
10367
  findings.push({
9029
10368
  tool: "validate",
9030
- file: path15.join(projectRoot, "AGENTS.md"),
10369
+ file: path16.join(projectRoot, "AGENTS.md"),
9031
10370
  message: err instanceof Error ? err.message : String(err),
9032
10371
  severity: "error"
9033
10372
  });
@@ -9091,7 +10430,7 @@ async function runMechanicalChecks(options) {
9091
10430
  (async () => {
9092
10431
  const localFindings = [];
9093
10432
  try {
9094
- const docsDir = path15.join(projectRoot, config.docsDir ?? "docs");
10433
+ const docsDir = path16.join(projectRoot, config.docsDir ?? "docs");
9095
10434
  const result = await checkDocCoverage("project", { docsDir });
9096
10435
  if (!result.ok) {
9097
10436
  statuses["check-docs"] = "warn";
@@ -9118,7 +10457,7 @@ async function runMechanicalChecks(options) {
9118
10457
  statuses["check-docs"] = "warn";
9119
10458
  localFindings.push({
9120
10459
  tool: "check-docs",
9121
- file: path15.join(projectRoot, "docs"),
10460
+ file: path16.join(projectRoot, "docs"),
9122
10461
  message: err instanceof Error ? err.message : String(err),
9123
10462
  severity: "warning"
9124
10463
  });
@@ -9266,7 +10605,7 @@ function detectChangeType(commitMessage, diff2) {
9266
10605
  }
9267
10606
 
9268
10607
  // src/review/context-scoper.ts
9269
- var path16 = __toESM(require("path"));
10608
+ var path17 = __toESM(require("path"));
9270
10609
  var ALL_DOMAINS = ["compliance", "bug", "security", "architecture"];
9271
10610
  var SECURITY_PATTERNS = /auth|crypto|password|secret|token|session|cookie|hash|encrypt|decrypt|sql|shell|exec|eval/i;
9272
10611
  function computeContextBudget(diffLines) {
@@ -9274,18 +10613,18 @@ function computeContextBudget(diffLines) {
9274
10613
  return diffLines;
9275
10614
  }
9276
10615
  function isWithinProject(absPath, projectRoot) {
9277
- const resolvedRoot = path16.resolve(projectRoot) + path16.sep;
9278
- const resolvedPath = path16.resolve(absPath);
9279
- return resolvedPath.startsWith(resolvedRoot) || resolvedPath === path16.resolve(projectRoot);
10616
+ const resolvedRoot = path17.resolve(projectRoot) + path17.sep;
10617
+ const resolvedPath = path17.resolve(absPath);
10618
+ return resolvedPath.startsWith(resolvedRoot) || resolvedPath === path17.resolve(projectRoot);
9280
10619
  }
9281
10620
  async function readContextFile(projectRoot, filePath, reason) {
9282
- const absPath = path16.isAbsolute(filePath) ? filePath : path16.join(projectRoot, filePath);
10621
+ const absPath = path17.isAbsolute(filePath) ? filePath : path17.join(projectRoot, filePath);
9283
10622
  if (!isWithinProject(absPath, projectRoot)) return null;
9284
10623
  const result = await readFileContent(absPath);
9285
10624
  if (!result.ok) return null;
9286
10625
  const content = result.value;
9287
10626
  const lines = content.split("\n").length;
9288
- const relPath = path16.isAbsolute(filePath) ? relativePosix(projectRoot, filePath) : filePath;
10627
+ const relPath = path17.isAbsolute(filePath) ? relativePosix(projectRoot, filePath) : filePath;
9289
10628
  return { path: relPath, content, reason, lines };
9290
10629
  }
9291
10630
  function extractImportSources2(content) {
@@ -9300,18 +10639,18 @@ function extractImportSources2(content) {
9300
10639
  }
9301
10640
  async function resolveImportPath2(projectRoot, fromFile, importSource) {
9302
10641
  if (!importSource.startsWith(".")) return null;
9303
- const fromDir = path16.dirname(path16.join(projectRoot, fromFile));
9304
- const basePath = path16.resolve(fromDir, importSource);
10642
+ const fromDir = path17.dirname(path17.join(projectRoot, fromFile));
10643
+ const basePath = path17.resolve(fromDir, importSource);
9305
10644
  if (!isWithinProject(basePath, projectRoot)) return null;
9306
10645
  const relBase = relativePosix(projectRoot, basePath);
9307
10646
  const candidates = [
9308
10647
  relBase + ".ts",
9309
10648
  relBase + ".tsx",
9310
10649
  relBase + ".mts",
9311
- path16.join(relBase, "index.ts")
10650
+ path17.join(relBase, "index.ts")
9312
10651
  ];
9313
10652
  for (const candidate of candidates) {
9314
- const absCandidate = path16.join(projectRoot, candidate);
10653
+ const absCandidate = path17.join(projectRoot, candidate);
9315
10654
  if (await fileExists(absCandidate)) {
9316
10655
  return candidate;
9317
10656
  }
@@ -9319,7 +10658,7 @@ async function resolveImportPath2(projectRoot, fromFile, importSource) {
9319
10658
  return null;
9320
10659
  }
9321
10660
  async function findTestFiles(projectRoot, sourceFile) {
9322
- const baseName = path16.basename(sourceFile, path16.extname(sourceFile));
10661
+ const baseName = path17.basename(sourceFile, path17.extname(sourceFile));
9323
10662
  const pattern = `**/${baseName}.{test,spec}.{ts,tsx,mts}`;
9324
10663
  const results = await findFiles(pattern, projectRoot);
9325
10664
  return results.map((f) => relativePosix(projectRoot, f));
@@ -10128,7 +11467,7 @@ async function fanOutReview(options) {
10128
11467
  }
10129
11468
 
10130
11469
  // src/review/validate-findings.ts
10131
- var path17 = __toESM(require("path"));
11470
+ var path18 = __toESM(require("path"));
10132
11471
  var DOWNGRADE_MAP = {
10133
11472
  critical: "important",
10134
11473
  important: "suggestion",
@@ -10149,7 +11488,7 @@ function normalizePath(filePath, projectRoot) {
10149
11488
  let normalized = filePath;
10150
11489
  normalized = normalized.replace(/\\/g, "/");
10151
11490
  const normalizedRoot = projectRoot.replace(/\\/g, "/");
10152
- if (path17.isAbsolute(normalized)) {
11491
+ if (path18.isAbsolute(normalized)) {
10153
11492
  const root = normalizedRoot.endsWith("/") ? normalizedRoot : normalizedRoot + "/";
10154
11493
  if (normalized.startsWith(root)) {
10155
11494
  normalized = normalized.slice(root.length);
@@ -10174,12 +11513,12 @@ function followImportChain(fromFile, fileContents, maxDepth = 2) {
10174
11513
  while ((match = importRegex.exec(content)) !== null) {
10175
11514
  const importPath = match[1];
10176
11515
  if (!importPath.startsWith(".")) continue;
10177
- const dir = path17.dirname(current.file);
10178
- let resolved = path17.join(dir, importPath).replace(/\\/g, "/");
11516
+ const dir = path18.dirname(current.file);
11517
+ let resolved = path18.join(dir, importPath).replace(/\\/g, "/");
10179
11518
  if (!resolved.match(/\.(ts|tsx|js|jsx)$/)) {
10180
11519
  resolved += ".ts";
10181
11520
  }
10182
- resolved = path17.normalize(resolved).replace(/\\/g, "/");
11521
+ resolved = path18.normalize(resolved).replace(/\\/g, "/");
10183
11522
  if (!visited.has(resolved) && current.depth + 1 <= maxDepth) {
10184
11523
  queue.push({ file: resolved, depth: current.depth + 1 });
10185
11524
  }
@@ -10196,7 +11535,7 @@ async function validateFindings(options) {
10196
11535
  if (exclusionSet.isExcluded(normalizedFile, finding.lineRange) || exclusionSet.isExcluded(finding.file, finding.lineRange)) {
10197
11536
  continue;
10198
11537
  }
10199
- const absoluteFile = path17.isAbsolute(finding.file) ? finding.file : path17.join(projectRoot, finding.file).replace(/\\/g, "/");
11538
+ const absoluteFile = path18.isAbsolute(finding.file) ? finding.file : path18.join(projectRoot, finding.file).replace(/\\/g, "/");
10200
11539
  if (exclusionSet.isExcluded(absoluteFile, finding.lineRange)) {
10201
11540
  continue;
10202
11541
  }
@@ -10824,7 +12163,7 @@ function parseRoadmap(markdown) {
10824
12163
  if (!fmMatch) {
10825
12164
  return (0, import_types19.Err)(new Error("Missing or malformed YAML frontmatter"));
10826
12165
  }
10827
- const fmResult = parseFrontmatter(fmMatch[1]);
12166
+ const fmResult = parseFrontmatter2(fmMatch[1]);
10828
12167
  if (!fmResult.ok) return fmResult;
10829
12168
  const body = markdown.slice(fmMatch[0].length);
10830
12169
  const milestonesResult = parseMilestones(body);
@@ -10834,7 +12173,7 @@ function parseRoadmap(markdown) {
10834
12173
  milestones: milestonesResult.value
10835
12174
  });
10836
12175
  }
10837
- function parseFrontmatter(raw) {
12176
+ function parseFrontmatter2(raw) {
10838
12177
  const lines = raw.split("\n");
10839
12178
  const map = /* @__PURE__ */ new Map();
10840
12179
  for (const line of lines) {
@@ -11000,8 +12339,8 @@ function serializeFeature(feature) {
11000
12339
  }
11001
12340
 
11002
12341
  // src/roadmap/sync.ts
11003
- var fs18 = __toESM(require("fs"));
11004
- var path18 = __toESM(require("path"));
12342
+ var fs19 = __toESM(require("fs"));
12343
+ var path19 = __toESM(require("path"));
11005
12344
  var import_types20 = require("@harness-engineering/types");
11006
12345
  function inferStatus(feature, projectPath, allFeatures) {
11007
12346
  if (feature.blockedBy.length > 0) {
@@ -11016,10 +12355,10 @@ function inferStatus(feature, projectPath, allFeatures) {
11016
12355
  const featuresWithPlans = allFeatures.filter((f) => f.plans.length > 0);
11017
12356
  const useRootState = featuresWithPlans.length <= 1;
11018
12357
  if (useRootState) {
11019
- const rootStatePath = path18.join(projectPath, ".harness", "state.json");
11020
- if (fs18.existsSync(rootStatePath)) {
12358
+ const rootStatePath = path19.join(projectPath, ".harness", "state.json");
12359
+ if (fs19.existsSync(rootStatePath)) {
11021
12360
  try {
11022
- const raw = fs18.readFileSync(rootStatePath, "utf-8");
12361
+ const raw = fs19.readFileSync(rootStatePath, "utf-8");
11023
12362
  const state = JSON.parse(raw);
11024
12363
  if (state.progress) {
11025
12364
  for (const status of Object.values(state.progress)) {
@@ -11030,16 +12369,16 @@ function inferStatus(feature, projectPath, allFeatures) {
11030
12369
  }
11031
12370
  }
11032
12371
  }
11033
- const sessionsDir = path18.join(projectPath, ".harness", "sessions");
11034
- if (fs18.existsSync(sessionsDir)) {
12372
+ const sessionsDir = path19.join(projectPath, ".harness", "sessions");
12373
+ if (fs19.existsSync(sessionsDir)) {
11035
12374
  try {
11036
- const sessionDirs = fs18.readdirSync(sessionsDir, { withFileTypes: true });
12375
+ const sessionDirs = fs19.readdirSync(sessionsDir, { withFileTypes: true });
11037
12376
  for (const entry of sessionDirs) {
11038
12377
  if (!entry.isDirectory()) continue;
11039
- const autopilotPath = path18.join(sessionsDir, entry.name, "autopilot-state.json");
11040
- if (!fs18.existsSync(autopilotPath)) continue;
12378
+ const autopilotPath = path19.join(sessionsDir, entry.name, "autopilot-state.json");
12379
+ if (!fs19.existsSync(autopilotPath)) continue;
11041
12380
  try {
11042
- const raw = fs18.readFileSync(autopilotPath, "utf-8");
12381
+ const raw = fs19.readFileSync(autopilotPath, "utf-8");
11043
12382
  const autopilot = JSON.parse(raw);
11044
12383
  if (!autopilot.phases) continue;
11045
12384
  const linkedPhases = autopilot.phases.filter(
@@ -11069,17 +12408,26 @@ function inferStatus(feature, projectPath, allFeatures) {
11069
12408
  if (anyStarted) return "in-progress";
11070
12409
  return null;
11071
12410
  }
12411
+ var STATUS_RANK = {
12412
+ backlog: 0,
12413
+ planned: 1,
12414
+ blocked: 1,
12415
+ // lateral to planned — sync can move to/from blocked freely
12416
+ "in-progress": 2,
12417
+ done: 3
12418
+ };
12419
+ function isRegression(from, to) {
12420
+ return STATUS_RANK[to] < STATUS_RANK[from];
12421
+ }
11072
12422
  function syncRoadmap(options) {
11073
12423
  const { projectPath, roadmap, forceSync } = options;
11074
- const isManuallyEdited = new Date(roadmap.frontmatter.lastManualEdit) > new Date(roadmap.frontmatter.lastSynced);
11075
- const skipOverride = isManuallyEdited && !forceSync;
11076
12424
  const allFeatures = roadmap.milestones.flatMap((m) => m.features);
11077
12425
  const changes = [];
11078
12426
  for (const feature of allFeatures) {
11079
- if (skipOverride) continue;
11080
12427
  const inferred = inferStatus(feature, projectPath, allFeatures);
11081
12428
  if (inferred === null) continue;
11082
12429
  if (inferred === feature.status) continue;
12430
+ if (!forceSync && isRegression(feature.status, inferred)) continue;
11083
12431
  changes.push({
11084
12432
  feature: feature.name,
11085
12433
  from: feature.status,
@@ -11088,48 +12436,60 @@ function syncRoadmap(options) {
11088
12436
  }
11089
12437
  return (0, import_types20.Ok)(changes);
11090
12438
  }
12439
+ function applySyncChanges(roadmap, changes) {
12440
+ for (const change of changes) {
12441
+ for (const m of roadmap.milestones) {
12442
+ const feature = m.features.find((f) => f.name.toLowerCase() === change.feature.toLowerCase());
12443
+ if (feature) {
12444
+ feature.status = change.to;
12445
+ break;
12446
+ }
12447
+ }
12448
+ }
12449
+ roadmap.frontmatter.lastSynced = (/* @__PURE__ */ new Date()).toISOString();
12450
+ }
11091
12451
 
11092
12452
  // src/interaction/types.ts
11093
- var import_zod7 = require("zod");
11094
- var InteractionTypeSchema = import_zod7.z.enum(["question", "confirmation", "transition"]);
11095
- var QuestionSchema = import_zod7.z.object({
11096
- text: import_zod7.z.string(),
11097
- options: import_zod7.z.array(import_zod7.z.string()).optional(),
11098
- default: import_zod7.z.string().optional()
12453
+ var import_zod8 = require("zod");
12454
+ var InteractionTypeSchema = import_zod8.z.enum(["question", "confirmation", "transition"]);
12455
+ var QuestionSchema = import_zod8.z.object({
12456
+ text: import_zod8.z.string(),
12457
+ options: import_zod8.z.array(import_zod8.z.string()).optional(),
12458
+ default: import_zod8.z.string().optional()
11099
12459
  });
11100
- var ConfirmationSchema = import_zod7.z.object({
11101
- text: import_zod7.z.string(),
11102
- context: import_zod7.z.string()
12460
+ var ConfirmationSchema = import_zod8.z.object({
12461
+ text: import_zod8.z.string(),
12462
+ context: import_zod8.z.string()
11103
12463
  });
11104
- var TransitionSchema = import_zod7.z.object({
11105
- completedPhase: import_zod7.z.string(),
11106
- suggestedNext: import_zod7.z.string(),
11107
- reason: import_zod7.z.string(),
11108
- artifacts: import_zod7.z.array(import_zod7.z.string()),
11109
- requiresConfirmation: import_zod7.z.boolean(),
11110
- summary: import_zod7.z.string()
12464
+ var TransitionSchema = import_zod8.z.object({
12465
+ completedPhase: import_zod8.z.string(),
12466
+ suggestedNext: import_zod8.z.string(),
12467
+ reason: import_zod8.z.string(),
12468
+ artifacts: import_zod8.z.array(import_zod8.z.string()),
12469
+ requiresConfirmation: import_zod8.z.boolean(),
12470
+ summary: import_zod8.z.string()
11111
12471
  });
11112
- var EmitInteractionInputSchema = import_zod7.z.object({
11113
- path: import_zod7.z.string(),
12472
+ var EmitInteractionInputSchema = import_zod8.z.object({
12473
+ path: import_zod8.z.string(),
11114
12474
  type: InteractionTypeSchema,
11115
- stream: import_zod7.z.string().optional(),
12475
+ stream: import_zod8.z.string().optional(),
11116
12476
  question: QuestionSchema.optional(),
11117
12477
  confirmation: ConfirmationSchema.optional(),
11118
12478
  transition: TransitionSchema.optional()
11119
12479
  });
11120
12480
 
11121
12481
  // src/blueprint/scanner.ts
11122
- var fs19 = __toESM(require("fs/promises"));
11123
- var path19 = __toESM(require("path"));
12482
+ var fs20 = __toESM(require("fs/promises"));
12483
+ var path20 = __toESM(require("path"));
11124
12484
  var ProjectScanner = class {
11125
12485
  constructor(rootDir) {
11126
12486
  this.rootDir = rootDir;
11127
12487
  }
11128
12488
  async scan() {
11129
- let projectName = path19.basename(this.rootDir);
12489
+ let projectName = path20.basename(this.rootDir);
11130
12490
  try {
11131
- const pkgPath = path19.join(this.rootDir, "package.json");
11132
- const pkgRaw = await fs19.readFile(pkgPath, "utf-8");
12491
+ const pkgPath = path20.join(this.rootDir, "package.json");
12492
+ const pkgRaw = await fs20.readFile(pkgPath, "utf-8");
11133
12493
  const pkg = JSON.parse(pkgRaw);
11134
12494
  if (pkg.name) projectName = pkg.name;
11135
12495
  } catch {
@@ -11170,8 +12530,8 @@ var ProjectScanner = class {
11170
12530
  };
11171
12531
 
11172
12532
  // src/blueprint/generator.ts
11173
- var fs20 = __toESM(require("fs/promises"));
11174
- var path20 = __toESM(require("path"));
12533
+ var fs21 = __toESM(require("fs/promises"));
12534
+ var path21 = __toESM(require("path"));
11175
12535
  var ejs = __toESM(require("ejs"));
11176
12536
 
11177
12537
  // src/blueprint/templates.ts
@@ -11255,19 +12615,19 @@ var BlueprintGenerator = class {
11255
12615
  styles: STYLES,
11256
12616
  scripts: SCRIPTS
11257
12617
  });
11258
- await fs20.mkdir(options.outputDir, { recursive: true });
11259
- await fs20.writeFile(path20.join(options.outputDir, "index.html"), html);
12618
+ await fs21.mkdir(options.outputDir, { recursive: true });
12619
+ await fs21.writeFile(path21.join(options.outputDir, "index.html"), html);
11260
12620
  }
11261
12621
  };
11262
12622
 
11263
12623
  // src/update-checker.ts
11264
- var fs21 = __toESM(require("fs"));
11265
- var path21 = __toESM(require("path"));
12624
+ var fs22 = __toESM(require("fs"));
12625
+ var path22 = __toESM(require("path"));
11266
12626
  var os = __toESM(require("os"));
11267
12627
  var import_child_process3 = require("child_process");
11268
12628
  function getStatePath() {
11269
12629
  const home = process.env["HOME"] || os.homedir();
11270
- return path21.join(home, ".harness", "update-check.json");
12630
+ return path22.join(home, ".harness", "update-check.json");
11271
12631
  }
11272
12632
  function isUpdateCheckEnabled(configInterval) {
11273
12633
  if (process.env["HARNESS_NO_UPDATE_CHECK"] === "1") return false;
@@ -11280,7 +12640,7 @@ function shouldRunCheck(state, intervalMs) {
11280
12640
  }
11281
12641
  function readCheckState() {
11282
12642
  try {
11283
- const raw = fs21.readFileSync(getStatePath(), "utf-8");
12643
+ const raw = fs22.readFileSync(getStatePath(), "utf-8");
11284
12644
  const parsed = JSON.parse(raw);
11285
12645
  if (typeof parsed === "object" && parsed !== null && "lastCheckTime" in parsed && typeof parsed.lastCheckTime === "number" && "currentVersion" in parsed && typeof parsed.currentVersion === "string") {
11286
12646
  const state = parsed;
@@ -11297,7 +12657,7 @@ function readCheckState() {
11297
12657
  }
11298
12658
  function spawnBackgroundCheck(currentVersion) {
11299
12659
  const statePath = getStatePath();
11300
- const stateDir = path21.dirname(statePath);
12660
+ const stateDir = path22.dirname(statePath);
11301
12661
  const script = `
11302
12662
  const { execSync } = require('child_process');
11303
12663
  const fs = require('fs');
@@ -11350,8 +12710,894 @@ function getUpdateNotification(currentVersion) {
11350
12710
  Run "harness update" to upgrade.`;
11351
12711
  }
11352
12712
 
12713
+ // src/code-nav/types.ts
12714
+ var EXTENSION_MAP = {
12715
+ ".ts": "typescript",
12716
+ ".tsx": "typescript",
12717
+ ".mts": "typescript",
12718
+ ".cts": "typescript",
12719
+ ".js": "javascript",
12720
+ ".jsx": "javascript",
12721
+ ".mjs": "javascript",
12722
+ ".cjs": "javascript",
12723
+ ".py": "python"
12724
+ };
12725
+ function detectLanguage(filePath) {
12726
+ const ext = filePath.slice(filePath.lastIndexOf("."));
12727
+ return EXTENSION_MAP[ext] ?? null;
12728
+ }
12729
+
12730
+ // src/code-nav/parser.ts
12731
+ var import_web_tree_sitter = __toESM(require("web-tree-sitter"));
12732
+ var import_meta = {};
12733
+ var parserCache = /* @__PURE__ */ new Map();
12734
+ var initialized = false;
12735
+ var GRAMMAR_MAP = {
12736
+ typescript: "tree-sitter-typescript",
12737
+ javascript: "tree-sitter-javascript",
12738
+ python: "tree-sitter-python"
12739
+ };
12740
+ async function ensureInit() {
12741
+ if (!initialized) {
12742
+ await import_web_tree_sitter.default.init();
12743
+ initialized = true;
12744
+ }
12745
+ }
12746
+ async function resolveWasmPath(grammarName) {
12747
+ const { createRequire } = await import("module");
12748
+ const require2 = createRequire(import_meta.url ?? __filename);
12749
+ const pkgPath = require2.resolve("tree-sitter-wasms/package.json");
12750
+ const path26 = await import("path");
12751
+ const pkgDir = path26.dirname(pkgPath);
12752
+ return path26.join(pkgDir, "out", `${grammarName}.wasm`);
12753
+ }
12754
+ async function loadLanguage(lang) {
12755
+ const grammarName = GRAMMAR_MAP[lang];
12756
+ const wasmPath = await resolveWasmPath(grammarName);
12757
+ return import_web_tree_sitter.default.Language.load(wasmPath);
12758
+ }
12759
+ async function getParser(lang) {
12760
+ const cached = parserCache.get(lang);
12761
+ if (cached) return cached;
12762
+ await ensureInit();
12763
+ const parser = new import_web_tree_sitter.default();
12764
+ const language = await loadLanguage(lang);
12765
+ parser.setLanguage(language);
12766
+ parserCache.set(lang, parser);
12767
+ return parser;
12768
+ }
12769
+ async function parseFile(filePath) {
12770
+ const lang = detectLanguage(filePath);
12771
+ if (!lang) {
12772
+ return (0, import_types.Err)({
12773
+ code: "UNSUPPORTED_LANGUAGE",
12774
+ message: `Unsupported file extension: ${filePath}`
12775
+ });
12776
+ }
12777
+ const contentResult = await readFileContent(filePath);
12778
+ if (!contentResult.ok) {
12779
+ return (0, import_types.Err)({
12780
+ code: "FILE_NOT_FOUND",
12781
+ message: `Cannot read file: ${filePath}`
12782
+ });
12783
+ }
12784
+ try {
12785
+ const parser = await getParser(lang);
12786
+ const tree = parser.parse(contentResult.value);
12787
+ return (0, import_types.Ok)({ tree, language: lang, source: contentResult.value, filePath });
12788
+ } catch (e) {
12789
+ return (0, import_types.Err)({
12790
+ code: "PARSE_FAILED",
12791
+ message: `Tree-sitter parse failed for ${filePath}: ${e.message}`
12792
+ });
12793
+ }
12794
+ }
12795
+ function resetParserCache() {
12796
+ parserCache.clear();
12797
+ initialized = false;
12798
+ }
12799
+
12800
+ // src/code-nav/outline.ts
12801
+ var TOP_LEVEL_TYPES = {
12802
+ typescript: {
12803
+ function_declaration: "function",
12804
+ class_declaration: "class",
12805
+ interface_declaration: "interface",
12806
+ type_alias_declaration: "type",
12807
+ lexical_declaration: "variable",
12808
+ variable_declaration: "variable",
12809
+ export_statement: "export",
12810
+ import_statement: "import",
12811
+ enum_declaration: "type"
12812
+ },
12813
+ javascript: {
12814
+ function_declaration: "function",
12815
+ class_declaration: "class",
12816
+ lexical_declaration: "variable",
12817
+ variable_declaration: "variable",
12818
+ export_statement: "export",
12819
+ import_statement: "import"
12820
+ },
12821
+ python: {
12822
+ function_definition: "function",
12823
+ class_definition: "class",
12824
+ assignment: "variable",
12825
+ import_statement: "import",
12826
+ import_from_statement: "import"
12827
+ }
12828
+ };
12829
+ var METHOD_TYPES = {
12830
+ typescript: ["method_definition", "public_field_definition"],
12831
+ javascript: ["method_definition"],
12832
+ python: ["function_definition"]
12833
+ };
12834
+ var IDENTIFIER_TYPES = /* @__PURE__ */ new Set(["identifier", "property_identifier", "type_identifier"]);
12835
+ function findIdentifier(node) {
12836
+ return node.childForFieldName("name") ?? node.children.find((c) => IDENTIFIER_TYPES.has(c.type)) ?? null;
12837
+ }
12838
+ function getVariableDeclarationName(node) {
12839
+ const declarator = node.children.find((c) => c.type === "variable_declarator");
12840
+ if (!declarator) return null;
12841
+ const id = findIdentifier(declarator);
12842
+ return id?.text ?? null;
12843
+ }
12844
+ function getExportName(node, source) {
12845
+ const decl = node.children.find(
12846
+ (c) => c.type !== "export" && c.type !== "default" && c.type !== "comment"
12847
+ );
12848
+ return decl ? getNodeName(decl, source) : "<anonymous>";
12849
+ }
12850
+ function getAssignmentName(node) {
12851
+ const left = node.childForFieldName("left") ?? node.children[0];
12852
+ return left?.text ?? "<anonymous>";
12853
+ }
12854
+ function getNodeName(node, source) {
12855
+ const id = findIdentifier(node);
12856
+ if (id) return id.text;
12857
+ const isVarDecl = node.type === "lexical_declaration" || node.type === "variable_declaration";
12858
+ if (isVarDecl) return getVariableDeclarationName(node) ?? "<anonymous>";
12859
+ if (node.type === "export_statement") return getExportName(node, source);
12860
+ if (node.type === "assignment") return getAssignmentName(node);
12861
+ return "<anonymous>";
12862
+ }
12863
+ function getSignature(node, source) {
12864
+ const startLine = node.startPosition.row;
12865
+ const lines = source.split("\n");
12866
+ return (lines[startLine] ?? "").trim();
12867
+ }
12868
+ function extractMethods(classNode, language, source, filePath) {
12869
+ const methodTypes = METHOD_TYPES[language] ?? [];
12870
+ const body = classNode.childForFieldName("body") ?? classNode.children.find((c) => c.type === "class_body" || c.type === "block");
12871
+ if (!body) return [];
12872
+ return body.children.filter((child) => methodTypes.includes(child.type)).map((child) => ({
12873
+ name: getNodeName(child, source),
12874
+ kind: "method",
12875
+ file: filePath,
12876
+ line: child.startPosition.row + 1,
12877
+ endLine: child.endPosition.row + 1,
12878
+ signature: getSignature(child, source)
12879
+ }));
12880
+ }
12881
+ function nodeToSymbol(node, kind, source, filePath) {
12882
+ return {
12883
+ name: getNodeName(node, source),
12884
+ kind,
12885
+ file: filePath,
12886
+ line: node.startPosition.row + 1,
12887
+ endLine: node.endPosition.row + 1,
12888
+ signature: getSignature(node, source)
12889
+ };
12890
+ }
12891
+ function processExportStatement(child, topLevelTypes, lang, source, filePath) {
12892
+ const declaration = child.children.find(
12893
+ (c) => c.type !== "export" && c.type !== "default" && c.type !== ";" && c.type !== "comment"
12894
+ );
12895
+ const kind = declaration ? topLevelTypes[declaration.type] : void 0;
12896
+ if (declaration && kind) {
12897
+ const sym = nodeToSymbol(child, kind, source, filePath);
12898
+ sym.name = getNodeName(declaration, source);
12899
+ if (kind === "class") {
12900
+ sym.children = extractMethods(declaration, lang, source, filePath);
12901
+ }
12902
+ return sym;
12903
+ }
12904
+ return nodeToSymbol(child, "export", source, filePath);
12905
+ }
12906
+ function extractSymbols(rootNode, lang, source, filePath) {
12907
+ const symbols = [];
12908
+ const topLevelTypes = TOP_LEVEL_TYPES[lang] ?? {};
12909
+ for (const child of rootNode.children) {
12910
+ if (child.type === "export_statement") {
12911
+ symbols.push(processExportStatement(child, topLevelTypes, lang, source, filePath));
12912
+ continue;
12913
+ }
12914
+ const kind = topLevelTypes[child.type];
12915
+ if (!kind || kind === "import") continue;
12916
+ const sym = nodeToSymbol(child, kind, source, filePath);
12917
+ if (kind === "class") {
12918
+ sym.children = extractMethods(child, lang, source, filePath);
12919
+ }
12920
+ symbols.push(sym);
12921
+ }
12922
+ return symbols;
12923
+ }
12924
+ function buildFailedResult(filePath, lang) {
12925
+ return { file: filePath, language: lang, totalLines: 0, symbols: [], error: "[parse-failed]" };
12926
+ }
12927
+ async function getOutline(filePath) {
12928
+ const lang = detectLanguage(filePath);
12929
+ if (!lang) return buildFailedResult(filePath, "unknown");
12930
+ const result = await parseFile(filePath);
12931
+ if (!result.ok) return buildFailedResult(filePath, lang);
12932
+ const { tree, source } = result.value;
12933
+ const totalLines = source.split("\n").length;
12934
+ const symbols = extractSymbols(tree.rootNode, lang, source, filePath);
12935
+ return { file: filePath, language: lang, totalLines, symbols };
12936
+ }
12937
+ function formatOutline(outline) {
12938
+ if (outline.error) {
12939
+ return `${outline.file} ${outline.error}`;
12940
+ }
12941
+ const lines = [`${outline.file} (${outline.totalLines} lines)`];
12942
+ const last = outline.symbols.length - 1;
12943
+ outline.symbols.forEach((sym, i) => {
12944
+ const prefix = i === last ? "\u2514\u2500\u2500" : "\u251C\u2500\u2500";
12945
+ lines.push(`${prefix} ${sym.signature} :${sym.line}`);
12946
+ if (sym.children) {
12947
+ const childLast = sym.children.length - 1;
12948
+ sym.children.forEach((child, j) => {
12949
+ const childConnector = i === last ? " " : "\u2502 ";
12950
+ const childPrefix = j === childLast ? "\u2514\u2500\u2500" : "\u251C\u2500\u2500";
12951
+ lines.push(`${childConnector}${childPrefix} ${child.signature} :${child.line}`);
12952
+ });
12953
+ }
12954
+ });
12955
+ return lines.join("\n");
12956
+ }
12957
+
12958
+ // src/code-nav/search.ts
12959
+ function buildGlob(directory, fileGlob) {
12960
+ const dir = directory.replaceAll("\\", "/");
12961
+ if (fileGlob) {
12962
+ return `${dir}/**/${fileGlob}`;
12963
+ }
12964
+ const exts = Object.keys(EXTENSION_MAP).map((e) => e.slice(1));
12965
+ return `${dir}/**/*.{${exts.join(",")}}`;
12966
+ }
12967
+ function matchesQuery(name, query) {
12968
+ return name.toLowerCase().includes(query.toLowerCase());
12969
+ }
12970
+ function flattenSymbols(symbols) {
12971
+ const flat = [];
12972
+ for (const sym of symbols) {
12973
+ flat.push(sym);
12974
+ if (sym.children) {
12975
+ flat.push(...sym.children);
12976
+ }
12977
+ }
12978
+ return flat;
12979
+ }
12980
+ async function searchSymbols(query, directory, fileGlob) {
12981
+ const pattern = buildGlob(directory, fileGlob);
12982
+ let files;
12983
+ try {
12984
+ files = await findFiles(pattern, directory);
12985
+ } catch {
12986
+ files = [];
12987
+ }
12988
+ const matches = [];
12989
+ const skipped = [];
12990
+ for (const file of files) {
12991
+ const lang = detectLanguage(file);
12992
+ if (!lang) {
12993
+ skipped.push(file);
12994
+ continue;
12995
+ }
12996
+ const outline = await getOutline(file);
12997
+ if (outline.error) {
12998
+ skipped.push(file);
12999
+ continue;
13000
+ }
13001
+ const allSymbols = flattenSymbols(outline.symbols);
13002
+ for (const sym of allSymbols) {
13003
+ if (matchesQuery(sym.name, query)) {
13004
+ matches.push({
13005
+ symbol: sym,
13006
+ context: sym.signature
13007
+ });
13008
+ }
13009
+ }
13010
+ }
13011
+ return { query, matches, skipped };
13012
+ }
13013
+
13014
+ // src/code-nav/unfold.ts
13015
+ function findSymbolInList(symbols, name) {
13016
+ for (const sym of symbols) {
13017
+ if (sym.name === name) return sym;
13018
+ if (sym.children) {
13019
+ const found = findSymbolInList(sym.children, name);
13020
+ if (found) return found;
13021
+ }
13022
+ }
13023
+ return null;
13024
+ }
13025
+ function extractLines(source, startLine, endLine) {
13026
+ const lines = source.split("\n");
13027
+ const start = Math.max(0, startLine - 1);
13028
+ const end = Math.min(lines.length, endLine);
13029
+ return lines.slice(start, end).join("\n");
13030
+ }
13031
+ function buildFallbackResult(filePath, symbolName, content, language) {
13032
+ const totalLines = content ? content.split("\n").length : 0;
13033
+ return {
13034
+ file: filePath,
13035
+ symbolName,
13036
+ startLine: content ? 1 : 0,
13037
+ endLine: totalLines,
13038
+ content,
13039
+ language,
13040
+ fallback: true,
13041
+ warning: "[fallback: raw content]"
13042
+ };
13043
+ }
13044
+ async function readContentSafe(filePath) {
13045
+ const result = await readFileContent(filePath);
13046
+ return result.ok ? result.value : "";
13047
+ }
13048
+ async function unfoldSymbol(filePath, symbolName) {
13049
+ const lang = detectLanguage(filePath);
13050
+ if (!lang) {
13051
+ const content2 = await readContentSafe(filePath);
13052
+ return buildFallbackResult(filePath, symbolName, content2, "unknown");
13053
+ }
13054
+ const outline = await getOutline(filePath);
13055
+ if (outline.error) {
13056
+ const content2 = await readContentSafe(filePath);
13057
+ return buildFallbackResult(filePath, symbolName, content2, lang);
13058
+ }
13059
+ const symbol = findSymbolInList(outline.symbols, symbolName);
13060
+ if (!symbol) {
13061
+ const content2 = await readContentSafe(filePath);
13062
+ return buildFallbackResult(filePath, symbolName, content2, lang);
13063
+ }
13064
+ const parseResult = await parseFile(filePath);
13065
+ if (!parseResult.ok) {
13066
+ const content2 = await readContentSafe(filePath);
13067
+ return {
13068
+ ...buildFallbackResult(
13069
+ filePath,
13070
+ symbolName,
13071
+ extractLines(content2, symbol.line, symbol.endLine),
13072
+ lang
13073
+ ),
13074
+ startLine: symbol.line,
13075
+ endLine: symbol.endLine
13076
+ };
13077
+ }
13078
+ const content = extractLines(parseResult.value.source, symbol.line, symbol.endLine);
13079
+ return {
13080
+ file: filePath,
13081
+ symbolName,
13082
+ startLine: symbol.line,
13083
+ endLine: symbol.endLine,
13084
+ content,
13085
+ language: lang,
13086
+ fallback: false
13087
+ };
13088
+ }
13089
+ async function unfoldRange(filePath, startLine, endLine) {
13090
+ const lang = detectLanguage(filePath) ?? "unknown";
13091
+ const contentResult = await readFileContent(filePath);
13092
+ if (!contentResult.ok) {
13093
+ return {
13094
+ file: filePath,
13095
+ startLine: 0,
13096
+ endLine: 0,
13097
+ content: "",
13098
+ language: lang,
13099
+ fallback: true,
13100
+ warning: "[fallback: raw content]"
13101
+ };
13102
+ }
13103
+ const totalLines = contentResult.value.split("\n").length;
13104
+ const clampedEnd = Math.min(endLine, totalLines);
13105
+ const content = extractLines(contentResult.value, startLine, clampedEnd);
13106
+ return {
13107
+ file: filePath,
13108
+ startLine,
13109
+ endLine: clampedEnd,
13110
+ content,
13111
+ language: lang,
13112
+ fallback: false
13113
+ };
13114
+ }
13115
+
13116
+ // src/pricing/pricing.ts
13117
+ var TOKENS_PER_MILLION = 1e6;
13118
+ function parseLiteLLMData(raw) {
13119
+ const dataset = /* @__PURE__ */ new Map();
13120
+ for (const [modelName, entry] of Object.entries(raw)) {
13121
+ if (modelName === "sample_spec") continue;
13122
+ if (entry.mode && entry.mode !== "chat") continue;
13123
+ const inputCost = entry.input_cost_per_token;
13124
+ const outputCost = entry.output_cost_per_token;
13125
+ if (inputCost == null || outputCost == null) continue;
13126
+ const pricing = {
13127
+ inputPer1M: inputCost * TOKENS_PER_MILLION,
13128
+ outputPer1M: outputCost * TOKENS_PER_MILLION
13129
+ };
13130
+ if (entry.cache_read_input_token_cost != null) {
13131
+ pricing.cacheReadPer1M = entry.cache_read_input_token_cost * TOKENS_PER_MILLION;
13132
+ }
13133
+ if (entry.cache_creation_input_token_cost != null) {
13134
+ pricing.cacheWritePer1M = entry.cache_creation_input_token_cost * TOKENS_PER_MILLION;
13135
+ }
13136
+ dataset.set(modelName, pricing);
13137
+ }
13138
+ return dataset;
13139
+ }
13140
+ function getModelPrice(model, dataset) {
13141
+ if (!model) {
13142
+ console.warn("[harness pricing] No model specified \u2014 cannot look up pricing.");
13143
+ return null;
13144
+ }
13145
+ const pricing = dataset.get(model);
13146
+ if (!pricing) {
13147
+ console.warn(
13148
+ `[harness pricing] No pricing data for model "${model}". Consider updating pricing data.`
13149
+ );
13150
+ return null;
13151
+ }
13152
+ return pricing;
13153
+ }
13154
+
13155
+ // src/pricing/cache.ts
13156
+ var fs23 = __toESM(require("fs/promises"));
13157
+ var path23 = __toESM(require("path"));
13158
+
13159
+ // src/pricing/fallback.json
13160
+ var fallback_default = {
13161
+ _generatedAt: "2026-03-31",
13162
+ _source: "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json",
13163
+ models: {
13164
+ "claude-opus-4-20250514": {
13165
+ inputPer1M: 15,
13166
+ outputPer1M: 75,
13167
+ cacheReadPer1M: 1.5,
13168
+ cacheWritePer1M: 18.75
13169
+ },
13170
+ "claude-sonnet-4-20250514": {
13171
+ inputPer1M: 3,
13172
+ outputPer1M: 15,
13173
+ cacheReadPer1M: 0.3,
13174
+ cacheWritePer1M: 3.75
13175
+ },
13176
+ "claude-3-5-haiku-20241022": {
13177
+ inputPer1M: 0.8,
13178
+ outputPer1M: 4,
13179
+ cacheReadPer1M: 0.08,
13180
+ cacheWritePer1M: 1
13181
+ },
13182
+ "gpt-4o": {
13183
+ inputPer1M: 2.5,
13184
+ outputPer1M: 10,
13185
+ cacheReadPer1M: 1.25
13186
+ },
13187
+ "gpt-4o-mini": {
13188
+ inputPer1M: 0.15,
13189
+ outputPer1M: 0.6,
13190
+ cacheReadPer1M: 0.075
13191
+ },
13192
+ "gemini-2.0-flash": {
13193
+ inputPer1M: 0.1,
13194
+ outputPer1M: 0.4,
13195
+ cacheReadPer1M: 0.025
13196
+ },
13197
+ "gemini-2.5-pro": {
13198
+ inputPer1M: 1.25,
13199
+ outputPer1M: 10,
13200
+ cacheReadPer1M: 0.3125
13201
+ }
13202
+ }
13203
+ };
13204
+
13205
+ // src/pricing/cache.ts
13206
+ var LITELLM_PRICING_URL = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json";
13207
+ var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
13208
+ var STALENESS_WARNING_DAYS = 7;
13209
+ function getCachePath(projectRoot) {
13210
+ return path23.join(projectRoot, ".harness", "cache", "pricing.json");
13211
+ }
13212
+ function getStalenessMarkerPath(projectRoot) {
13213
+ return path23.join(projectRoot, ".harness", "cache", "staleness-marker.json");
13214
+ }
13215
+ async function readDiskCache(projectRoot) {
13216
+ try {
13217
+ const raw = await fs23.readFile(getCachePath(projectRoot), "utf-8");
13218
+ return JSON.parse(raw);
13219
+ } catch {
13220
+ return null;
13221
+ }
13222
+ }
13223
+ async function writeDiskCache(projectRoot, data) {
13224
+ const cachePath = getCachePath(projectRoot);
13225
+ await fs23.mkdir(path23.dirname(cachePath), { recursive: true });
13226
+ await fs23.writeFile(cachePath, JSON.stringify(data, null, 2));
13227
+ }
13228
+ async function fetchFromNetwork() {
13229
+ try {
13230
+ const response = await fetch(LITELLM_PRICING_URL);
13231
+ if (!response.ok) return null;
13232
+ const data = await response.json();
13233
+ if (typeof data !== "object" || data === null || Array.isArray(data)) return null;
13234
+ return {
13235
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
13236
+ data
13237
+ };
13238
+ } catch {
13239
+ return null;
13240
+ }
13241
+ }
13242
+ function loadFallbackDataset() {
13243
+ const fb = fallback_default;
13244
+ const dataset = /* @__PURE__ */ new Map();
13245
+ for (const [model, pricing] of Object.entries(fb.models)) {
13246
+ dataset.set(model, pricing);
13247
+ }
13248
+ return dataset;
13249
+ }
13250
+ async function checkAndWarnStaleness(projectRoot) {
13251
+ const markerPath = getStalenessMarkerPath(projectRoot);
13252
+ try {
13253
+ const raw = await fs23.readFile(markerPath, "utf-8");
13254
+ const marker = JSON.parse(raw);
13255
+ const firstUse = new Date(marker.firstFallbackUse).getTime();
13256
+ const now = Date.now();
13257
+ const daysSinceFirstUse = (now - firstUse) / (24 * 60 * 60 * 1e3);
13258
+ if (daysSinceFirstUse > STALENESS_WARNING_DAYS) {
13259
+ console.warn(
13260
+ `[harness pricing] Pricing data is stale \u2014 using bundled fallback for ${Math.floor(daysSinceFirstUse)} days. Connect to the internet to refresh pricing data.`
13261
+ );
13262
+ }
13263
+ } catch {
13264
+ try {
13265
+ await fs23.mkdir(path23.dirname(markerPath), { recursive: true });
13266
+ await fs23.writeFile(
13267
+ markerPath,
13268
+ JSON.stringify({ firstFallbackUse: (/* @__PURE__ */ new Date()).toISOString() })
13269
+ );
13270
+ } catch {
13271
+ }
13272
+ }
13273
+ }
13274
+ async function clearStalenessMarker(projectRoot) {
13275
+ try {
13276
+ await fs23.unlink(getStalenessMarkerPath(projectRoot));
13277
+ } catch {
13278
+ }
13279
+ }
13280
+ async function loadPricingData(projectRoot) {
13281
+ const cache = await readDiskCache(projectRoot);
13282
+ if (cache) {
13283
+ const cacheAge = Date.now() - new Date(cache.fetchedAt).getTime();
13284
+ if (cacheAge < CACHE_TTL_MS) {
13285
+ await clearStalenessMarker(projectRoot);
13286
+ return parseLiteLLMData(cache.data);
13287
+ }
13288
+ }
13289
+ const fetched = await fetchFromNetwork();
13290
+ if (fetched) {
13291
+ await writeDiskCache(projectRoot, fetched);
13292
+ await clearStalenessMarker(projectRoot);
13293
+ return parseLiteLLMData(fetched.data);
13294
+ }
13295
+ if (cache) {
13296
+ return parseLiteLLMData(cache.data);
13297
+ }
13298
+ await checkAndWarnStaleness(projectRoot);
13299
+ return loadFallbackDataset();
13300
+ }
13301
+
13302
+ // src/pricing/calculator.ts
13303
+ var MICRODOLLARS_PER_DOLLAR = 1e6;
13304
+ var TOKENS_PER_MILLION2 = 1e6;
13305
+ function calculateCost(record, dataset) {
13306
+ if (!record.model) return null;
13307
+ const pricing = getModelPrice(record.model, dataset);
13308
+ if (!pricing) return null;
13309
+ let costUSD = 0;
13310
+ costUSD += record.tokens.inputTokens / TOKENS_PER_MILLION2 * pricing.inputPer1M;
13311
+ costUSD += record.tokens.outputTokens / TOKENS_PER_MILLION2 * pricing.outputPer1M;
13312
+ if (record.cacheReadTokens != null && pricing.cacheReadPer1M != null) {
13313
+ costUSD += record.cacheReadTokens / TOKENS_PER_MILLION2 * pricing.cacheReadPer1M;
13314
+ }
13315
+ if (record.cacheCreationTokens != null && pricing.cacheWritePer1M != null) {
13316
+ costUSD += record.cacheCreationTokens / TOKENS_PER_MILLION2 * pricing.cacheWritePer1M;
13317
+ }
13318
+ return Math.round(costUSD * MICRODOLLARS_PER_DOLLAR);
13319
+ }
13320
+
13321
+ // src/usage/aggregator.ts
13322
+ function aggregateBySession(records) {
13323
+ if (records.length === 0) return [];
13324
+ const sessionMap = /* @__PURE__ */ new Map();
13325
+ for (const record of records) {
13326
+ const tagged = record;
13327
+ const id = record.sessionId;
13328
+ if (!sessionMap.has(id)) {
13329
+ sessionMap.set(id, { harnessRecords: [], ccRecords: [], allRecords: [] });
13330
+ }
13331
+ const bucket = sessionMap.get(id);
13332
+ if (tagged._source === "claude-code") {
13333
+ bucket.ccRecords.push(tagged);
13334
+ } else {
13335
+ bucket.harnessRecords.push(tagged);
13336
+ }
13337
+ bucket.allRecords.push(tagged);
13338
+ }
13339
+ const results = [];
13340
+ for (const [sessionId, bucket] of sessionMap) {
13341
+ const hasHarness = bucket.harnessRecords.length > 0;
13342
+ const hasCC = bucket.ccRecords.length > 0;
13343
+ const isMerged = hasHarness && hasCC;
13344
+ const tokenSource = hasHarness ? bucket.harnessRecords : bucket.ccRecords;
13345
+ const tokens = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
13346
+ let cacheCreation;
13347
+ let cacheRead;
13348
+ let costMicroUSD = 0;
13349
+ let model;
13350
+ for (const r of tokenSource) {
13351
+ tokens.inputTokens += r.tokens.inputTokens;
13352
+ tokens.outputTokens += r.tokens.outputTokens;
13353
+ tokens.totalTokens += r.tokens.totalTokens;
13354
+ if (r.cacheCreationTokens != null) {
13355
+ cacheCreation = (cacheCreation ?? 0) + r.cacheCreationTokens;
13356
+ }
13357
+ if (r.cacheReadTokens != null) {
13358
+ cacheRead = (cacheRead ?? 0) + r.cacheReadTokens;
13359
+ }
13360
+ if (r.costMicroUSD != null && costMicroUSD != null) {
13361
+ costMicroUSD += r.costMicroUSD;
13362
+ } else if (r.costMicroUSD == null) {
13363
+ costMicroUSD = null;
13364
+ }
13365
+ if (!model && r.model) {
13366
+ model = r.model;
13367
+ }
13368
+ }
13369
+ if (!model && hasCC) {
13370
+ for (const r of bucket.ccRecords) {
13371
+ if (r.model) {
13372
+ model = r.model;
13373
+ break;
13374
+ }
13375
+ }
13376
+ }
13377
+ const timestamps = bucket.allRecords.map((r) => r.timestamp).sort();
13378
+ const source = isMerged ? "merged" : hasCC ? "claude-code" : "harness";
13379
+ const session = {
13380
+ sessionId,
13381
+ firstTimestamp: timestamps[0] ?? "",
13382
+ lastTimestamp: timestamps[timestamps.length - 1] ?? "",
13383
+ tokens,
13384
+ costMicroUSD,
13385
+ source
13386
+ };
13387
+ if (model) session.model = model;
13388
+ if (cacheCreation != null) session.cacheCreationTokens = cacheCreation;
13389
+ if (cacheRead != null) session.cacheReadTokens = cacheRead;
13390
+ results.push(session);
13391
+ }
13392
+ results.sort((a, b) => b.firstTimestamp.localeCompare(a.firstTimestamp));
13393
+ return results;
13394
+ }
13395
+ function aggregateByDay(records) {
13396
+ if (records.length === 0) return [];
13397
+ const dayMap = /* @__PURE__ */ new Map();
13398
+ for (const record of records) {
13399
+ const date = record.timestamp.slice(0, 10);
13400
+ if (!dayMap.has(date)) {
13401
+ dayMap.set(date, {
13402
+ sessions: /* @__PURE__ */ new Set(),
13403
+ tokens: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
13404
+ costMicroUSD: 0,
13405
+ models: /* @__PURE__ */ new Set()
13406
+ });
13407
+ }
13408
+ const day = dayMap.get(date);
13409
+ day.sessions.add(record.sessionId);
13410
+ day.tokens.inputTokens += record.tokens.inputTokens;
13411
+ day.tokens.outputTokens += record.tokens.outputTokens;
13412
+ day.tokens.totalTokens += record.tokens.totalTokens;
13413
+ if (record.cacheCreationTokens != null) {
13414
+ day.cacheCreation = (day.cacheCreation ?? 0) + record.cacheCreationTokens;
13415
+ }
13416
+ if (record.cacheReadTokens != null) {
13417
+ day.cacheRead = (day.cacheRead ?? 0) + record.cacheReadTokens;
13418
+ }
13419
+ if (record.costMicroUSD != null && day.costMicroUSD != null) {
13420
+ day.costMicroUSD += record.costMicroUSD;
13421
+ } else if (record.costMicroUSD == null) {
13422
+ day.costMicroUSD = null;
13423
+ }
13424
+ if (record.model) {
13425
+ day.models.add(record.model);
13426
+ }
13427
+ }
13428
+ const results = [];
13429
+ for (const [date, day] of dayMap) {
13430
+ const entry = {
13431
+ date,
13432
+ sessionCount: day.sessions.size,
13433
+ tokens: day.tokens,
13434
+ costMicroUSD: day.costMicroUSD,
13435
+ models: Array.from(day.models).sort()
13436
+ };
13437
+ if (day.cacheCreation != null) entry.cacheCreationTokens = day.cacheCreation;
13438
+ if (day.cacheRead != null) entry.cacheReadTokens = day.cacheRead;
13439
+ results.push(entry);
13440
+ }
13441
+ results.sort((a, b) => b.date.localeCompare(a.date));
13442
+ return results;
13443
+ }
13444
+
13445
+ // src/usage/jsonl-reader.ts
13446
+ var fs24 = __toESM(require("fs"));
13447
+ var path24 = __toESM(require("path"));
13448
+ function parseLine(line, lineNumber) {
13449
+ let entry;
13450
+ try {
13451
+ entry = JSON.parse(line);
13452
+ } catch {
13453
+ console.warn(`[harness usage] Skipping malformed JSONL line ${lineNumber}`);
13454
+ return null;
13455
+ }
13456
+ const tokenUsage = entry.token_usage;
13457
+ if (!tokenUsage || typeof tokenUsage !== "object") {
13458
+ console.warn(
13459
+ `[harness usage] Skipping malformed JSONL line ${lineNumber}: missing token_usage`
13460
+ );
13461
+ return null;
13462
+ }
13463
+ const inputTokens = tokenUsage.input_tokens ?? 0;
13464
+ const outputTokens = tokenUsage.output_tokens ?? 0;
13465
+ const record = {
13466
+ sessionId: entry.session_id ?? "unknown",
13467
+ timestamp: entry.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
13468
+ tokens: {
13469
+ inputTokens,
13470
+ outputTokens,
13471
+ totalTokens: inputTokens + outputTokens
13472
+ }
13473
+ };
13474
+ if (entry.cache_creation_tokens != null) {
13475
+ record.cacheCreationTokens = entry.cache_creation_tokens;
13476
+ }
13477
+ if (entry.cache_read_tokens != null) {
13478
+ record.cacheReadTokens = entry.cache_read_tokens;
13479
+ }
13480
+ if (entry.model != null) {
13481
+ record.model = entry.model;
13482
+ }
13483
+ return record;
13484
+ }
13485
+ function readCostRecords(projectRoot) {
13486
+ const costsFile = path24.join(projectRoot, ".harness", "metrics", "costs.jsonl");
13487
+ let raw;
13488
+ try {
13489
+ raw = fs24.readFileSync(costsFile, "utf-8");
13490
+ } catch {
13491
+ return [];
13492
+ }
13493
+ const records = [];
13494
+ const lines = raw.split("\n");
13495
+ for (let i = 0; i < lines.length; i++) {
13496
+ const line = lines[i]?.trim();
13497
+ if (!line) continue;
13498
+ const record = parseLine(line, i + 1);
13499
+ if (record) {
13500
+ records.push(record);
13501
+ }
13502
+ }
13503
+ return records;
13504
+ }
13505
+
13506
+ // src/usage/cc-parser.ts
13507
+ var fs25 = __toESM(require("fs"));
13508
+ var path25 = __toESM(require("path"));
13509
+ var os2 = __toESM(require("os"));
13510
+ function extractUsage(entry) {
13511
+ if (entry.type !== "assistant") return null;
13512
+ const message = entry.message;
13513
+ if (!message || typeof message !== "object") return null;
13514
+ const usage = message.usage;
13515
+ return usage && typeof usage === "object" && !Array.isArray(usage) ? usage : null;
13516
+ }
13517
+ function buildRecord(entry, usage) {
13518
+ const inputTokens = Number(usage.input_tokens) || 0;
13519
+ const outputTokens = Number(usage.output_tokens) || 0;
13520
+ const message = entry.message;
13521
+ const record = {
13522
+ sessionId: entry.sessionId ?? "unknown",
13523
+ timestamp: entry.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
13524
+ tokens: { inputTokens, outputTokens, totalTokens: inputTokens + outputTokens },
13525
+ _source: "claude-code"
13526
+ };
13527
+ const model = message.model;
13528
+ if (model) record.model = model;
13529
+ const cacheCreate = usage.cache_creation_input_tokens;
13530
+ const cacheRead = usage.cache_read_input_tokens;
13531
+ if (typeof cacheCreate === "number" && cacheCreate > 0) record.cacheCreationTokens = cacheCreate;
13532
+ if (typeof cacheRead === "number" && cacheRead > 0) record.cacheReadTokens = cacheRead;
13533
+ return record;
13534
+ }
13535
+ function parseCCLine(line, filePath, lineNumber) {
13536
+ let entry;
13537
+ try {
13538
+ entry = JSON.parse(line);
13539
+ } catch {
13540
+ console.warn(
13541
+ `[harness usage] Skipping malformed CC JSONL line ${lineNumber} in ${path25.basename(filePath)}`
13542
+ );
13543
+ return null;
13544
+ }
13545
+ const usage = extractUsage(entry);
13546
+ if (!usage) return null;
13547
+ return {
13548
+ record: buildRecord(entry, usage),
13549
+ requestId: entry.requestId ?? null
13550
+ };
13551
+ }
13552
+ function readCCFile(filePath) {
13553
+ let raw;
13554
+ try {
13555
+ raw = fs25.readFileSync(filePath, "utf-8");
13556
+ } catch {
13557
+ return [];
13558
+ }
13559
+ const byRequestId = /* @__PURE__ */ new Map();
13560
+ const noRequestId = [];
13561
+ const lines = raw.split("\n");
13562
+ for (let i = 0; i < lines.length; i++) {
13563
+ const line = lines[i]?.trim();
13564
+ if (!line) continue;
13565
+ const parsed = parseCCLine(line, filePath, i + 1);
13566
+ if (!parsed) continue;
13567
+ if (parsed.requestId) {
13568
+ byRequestId.set(parsed.requestId, parsed.record);
13569
+ } else {
13570
+ noRequestId.push(parsed.record);
13571
+ }
13572
+ }
13573
+ return [...byRequestId.values(), ...noRequestId];
13574
+ }
13575
+ function parseCCRecords() {
13576
+ const homeDir = process.env.HOME ?? os2.homedir();
13577
+ const projectsDir = path25.join(homeDir, ".claude", "projects");
13578
+ let projectDirs;
13579
+ try {
13580
+ projectDirs = fs25.readdirSync(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => path25.join(projectsDir, d.name));
13581
+ } catch {
13582
+ return [];
13583
+ }
13584
+ const records = [];
13585
+ for (const dir of projectDirs) {
13586
+ let files;
13587
+ try {
13588
+ files = fs25.readdirSync(dir).filter((f) => f.endsWith(".jsonl")).map((f) => path25.join(dir, f));
13589
+ } catch {
13590
+ continue;
13591
+ }
13592
+ for (const file of files) {
13593
+ records.push(...readCCFile(file));
13594
+ }
13595
+ }
13596
+ return records;
13597
+ }
13598
+
11353
13599
  // src/index.ts
11354
- var VERSION = "0.14.0";
13600
+ var VERSION = "0.15.0";
11355
13601
  // Annotate the CommonJS export names for ESM import in node:
11356
13602
  0 && (module.exports = {
11357
13603
  AGENT_DESCRIPTORS,
@@ -11368,6 +13614,7 @@ var VERSION = "0.14.0";
11368
13614
  BlueprintGenerator,
11369
13615
  BundleConstraintsSchema,
11370
13616
  BundleSchema,
13617
+ CACHE_TTL_MS,
11371
13618
  COMPLIANCE_DESCRIPTOR,
11372
13619
  CategoryBaselineSchema,
11373
13620
  CategoryRegressionSchema,
@@ -11385,7 +13632,9 @@ var VERSION = "0.14.0";
11385
13632
  DEFAULT_SECURITY_CONFIG,
11386
13633
  DEFAULT_STATE,
11387
13634
  DEFAULT_STREAM_INDEX,
13635
+ DESTRUCTIVE_BASH,
11388
13636
  DepDepthCollector,
13637
+ EXTENSION_MAP,
11389
13638
  EmitInteractionInputSchema,
11390
13639
  EntropyAnalyzer,
11391
13640
  EntropyConfigSchema,
@@ -11398,6 +13647,7 @@ var VERSION = "0.14.0";
11398
13647
  HandoffSchema,
11399
13648
  HarnessStateSchema,
11400
13649
  InteractionTypeSchema,
13650
+ LITELLM_PRICING_URL,
11401
13651
  LayerViolationCollector,
11402
13652
  LockfilePackageSchema,
11403
13653
  LockfileSchema,
@@ -11414,12 +13664,14 @@ var VERSION = "0.14.0";
11414
13664
  RegressionDetector,
11415
13665
  RuleRegistry,
11416
13666
  SECURITY_DESCRIPTOR,
13667
+ STALENESS_WARNING_DAYS,
11417
13668
  SecurityConfigSchema,
11418
13669
  SecurityScanner,
11419
13670
  SharableBoundaryConfigSchema,
11420
13671
  SharableForbiddenImportSchema,
11421
13672
  SharableLayerSchema,
11422
13673
  SharableSecurityRulesSchema,
13674
+ SkillEventSchema,
11423
13675
  StreamIndexSchema,
11424
13676
  StreamInfoSchema,
11425
13677
  ThresholdConfigSchema,
@@ -11428,6 +13680,9 @@ var VERSION = "0.14.0";
11428
13680
  VERSION,
11429
13681
  ViolationSchema,
11430
13682
  addProvenance,
13683
+ agentConfigRules,
13684
+ aggregateByDay,
13685
+ aggregateBySession,
11431
13686
  analyzeDiff,
11432
13687
  analyzeLearningPatterns,
11433
13688
  appendFailure,
@@ -11435,6 +13690,7 @@ var VERSION = "0.14.0";
11435
13690
  appendSessionEntry,
11436
13691
  applyFixes,
11437
13692
  applyHotspotDowngrade,
13693
+ applySyncChanges,
11438
13694
  archMatchers,
11439
13695
  archModule,
11440
13696
  architecture,
@@ -11445,16 +13701,23 @@ var VERSION = "0.14.0";
11445
13701
  buildDependencyGraph,
11446
13702
  buildExclusionSet,
11447
13703
  buildSnapshot,
13704
+ calculateCost,
11448
13705
  checkDocCoverage,
11449
13706
  checkEligibility,
11450
13707
  checkEvidenceCoverage,
13708
+ checkTaint,
11451
13709
  classifyFinding,
13710
+ clearEventHashCache,
11452
13711
  clearFailuresCache,
11453
13712
  clearLearningsCache,
13713
+ clearTaint,
13714
+ computeOverallSeverity,
13715
+ computeScanExitCode,
11454
13716
  configureFeedback,
11455
13717
  constraintRuleId,
11456
13718
  contextBudget,
11457
13719
  contextFilter,
13720
+ countLearningEntries,
11458
13721
  createBoundaryValidator,
11459
13722
  createCommentedCodeFixes,
11460
13723
  createError,
@@ -11478,66 +13741,95 @@ var VERSION = "0.14.0";
11478
13741
  detectCouplingViolations,
11479
13742
  detectDeadCode,
11480
13743
  detectDocDrift,
13744
+ detectLanguage,
11481
13745
  detectPatternViolations,
11482
13746
  detectSizeBudgetViolations,
11483
13747
  detectStack,
11484
13748
  detectStaleConstraints,
11485
13749
  determineAssessment,
11486
13750
  diff,
13751
+ emitEvent,
11487
13752
  executeWorkflow,
11488
13753
  expressRules,
11489
13754
  extractBundle,
13755
+ extractIndexEntry,
11490
13756
  extractMarkdownLinks,
11491
13757
  extractSections,
11492
13758
  fanOutReview,
13759
+ formatEventTimeline,
11493
13760
  formatFindingBlock,
11494
13761
  formatGitHubComment,
11495
13762
  formatGitHubSummary,
13763
+ formatOutline,
11496
13764
  formatTerminalOutput,
11497
13765
  generateAgentsMap,
11498
13766
  generateSuggestions,
11499
13767
  getActionEmitter,
11500
13768
  getExitCode,
11501
13769
  getFeedbackConfig,
13770
+ getInjectionPatterns,
13771
+ getModelPrice,
13772
+ getOutline,
13773
+ getParser,
11502
13774
  getPhaseCategories,
11503
13775
  getStreamForBranch,
13776
+ getTaintFilePath,
11504
13777
  getUpdateNotification,
11505
13778
  goRules,
11506
13779
  injectionRules,
13780
+ insecureDefaultsRules,
13781
+ isDuplicateFinding,
11507
13782
  isSmallSuggestion,
11508
13783
  isUpdateCheckEnabled,
11509
13784
  listActiveSessions,
11510
13785
  listStreams,
13786
+ listTaintedSessions,
11511
13787
  loadBudgetedLearnings,
13788
+ loadEvents,
11512
13789
  loadFailures,
11513
13790
  loadHandoff,
13791
+ loadIndexEntries,
13792
+ loadPricingData,
11514
13793
  loadRelevantLearnings,
11515
13794
  loadSessionSummary,
11516
13795
  loadState,
11517
13796
  loadStreamIndex,
11518
13797
  logAgentAction,
13798
+ mapInjectionFindings,
13799
+ mapSecurityFindings,
13800
+ mapSecuritySeverity,
13801
+ mcpRules,
11519
13802
  migrateToStreams,
11520
13803
  networkRules,
11521
13804
  nodeRules,
13805
+ parseCCRecords,
11522
13806
  parseDateFromEntry,
11523
13807
  parseDiff,
13808
+ parseFile,
13809
+ parseFrontmatter,
13810
+ parseHarnessIgnore,
13811
+ parseLiteLLMData,
11524
13812
  parseManifest,
11525
13813
  parseRoadmap,
11526
13814
  parseSecurityConfig,
11527
13815
  parseSize,
11528
13816
  pathTraversalRules,
11529
13817
  previewFix,
13818
+ promoteSessionLearnings,
11530
13819
  pruneLearnings,
11531
13820
  reactRules,
11532
13821
  readCheckState,
13822
+ readCostRecords,
11533
13823
  readLockfile,
11534
13824
  readSessionSection,
11535
13825
  readSessionSections,
13826
+ readTaint,
11536
13827
  removeContributions,
11537
13828
  removeProvenance,
11538
13829
  requestMultiplePeerReviews,
11539
13830
  requestPeerReview,
11540
13831
  resetFeedbackConfig,
13832
+ resetParserCache,
11541
13833
  resolveFileToLayer,
11542
13834
  resolveModelTier,
11543
13835
  resolveRuleSeverity,
@@ -11558,10 +13850,13 @@ var VERSION = "0.14.0";
11558
13850
  saveHandoff,
11559
13851
  saveState,
11560
13852
  saveStreamIndex,
13853
+ scanForInjection,
11561
13854
  scopeContext,
13855
+ searchSymbols,
11562
13856
  secretRules,
11563
13857
  serializeRoadmap,
11564
13858
  setActiveStream,
13859
+ sharpEdgesRules,
11565
13860
  shouldRunCheck,
11566
13861
  spawnBackgroundCheck,
11567
13862
  syncConstraintNodes,
@@ -11569,6 +13864,8 @@ var VERSION = "0.14.0";
11569
13864
  tagUncitedFindings,
11570
13865
  touchStream,
11571
13866
  trackAction,
13867
+ unfoldRange,
13868
+ unfoldSymbol,
11572
13869
  updateSessionEntryStatus,
11573
13870
  updateSessionIndex,
11574
13871
  validateAgentsMap,
@@ -11584,6 +13881,7 @@ var VERSION = "0.14.0";
11584
13881
  writeConfig,
11585
13882
  writeLockfile,
11586
13883
  writeSessionSummary,
13884
+ writeTaint,
11587
13885
  xssRules,
11588
13886
  ...require("@harness-engineering/types")
11589
13887
  });