@harness-engineering/core 0.19.0 → 0.20.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.mjs CHANGED
@@ -84,15 +84,15 @@ function validateConfig(data, schema) {
84
84
  let message = "Configuration validation failed";
85
85
  const suggestions = [];
86
86
  if (firstError) {
87
- const path26 = firstError.path.join(".");
88
- const pathDisplay = path26 ? ` at "${path26}"` : "";
87
+ const path28 = firstError.path.join(".");
88
+ const pathDisplay = path28 ? ` at "${path28}"` : "";
89
89
  if (firstError.code === "invalid_type") {
90
90
  const received = firstError.received;
91
91
  const expected = firstError.expected;
92
92
  if (received === "undefined") {
93
93
  code = "MISSING_FIELD";
94
94
  message = `Missing required field${pathDisplay}: ${firstError.message}`;
95
- suggestions.push(`Field "${path26}" is required and must be of type "${expected}"`);
95
+ suggestions.push(`Field "${path28}" is required and must be of type "${expected}"`);
96
96
  } else {
97
97
  code = "INVALID_TYPE";
98
98
  message = `Invalid type${pathDisplay}: ${firstError.message}`;
@@ -308,27 +308,27 @@ function extractSections(content) {
308
308
  }
309
309
  return sections.map((section) => buildAgentMapSection(section, lines));
310
310
  }
311
- function isExternalLink(path26) {
312
- return path26.startsWith("http://") || path26.startsWith("https://") || path26.startsWith("#") || path26.startsWith("mailto:");
311
+ function isExternalLink(path28) {
312
+ return path28.startsWith("http://") || path28.startsWith("https://") || path28.startsWith("#") || path28.startsWith("mailto:");
313
313
  }
314
314
  function resolveLinkPath(linkPath, baseDir) {
315
315
  return linkPath.startsWith(".") ? join(baseDir, linkPath) : linkPath;
316
316
  }
317
- async function validateAgentsMap(path26 = "./AGENTS.md") {
318
- const contentResult = await readFileContent(path26);
317
+ async function validateAgentsMap(path28 = "./AGENTS.md") {
318
+ const contentResult = await readFileContent(path28);
319
319
  if (!contentResult.ok) {
320
320
  return Err(
321
321
  createError(
322
322
  "PARSE_ERROR",
323
323
  `Failed to read AGENTS.md: ${contentResult.error.message}`,
324
- { path: path26 },
324
+ { path: path28 },
325
325
  ["Ensure the file exists", "Check file permissions"]
326
326
  )
327
327
  );
328
328
  }
329
329
  const content = contentResult.value;
330
330
  const sections = extractSections(content);
331
- const baseDir = dirname(path26);
331
+ const baseDir = dirname(path28);
332
332
  const sectionTitles = sections.map((s) => s.title);
333
333
  const missingSections = REQUIRED_SECTIONS.filter(
334
334
  (required) => !sectionTitles.some((title) => title.toLowerCase().includes(required.toLowerCase()))
@@ -469,8 +469,8 @@ async function checkDocCoverage(domain, options = {}) {
469
469
 
470
470
  // src/context/knowledge-map.ts
471
471
  import { join as join2, basename as basename2 } from "path";
472
- function suggestFix(path26, existingFiles) {
473
- const targetName = basename2(path26).toLowerCase();
472
+ function suggestFix(path28, existingFiles) {
473
+ const targetName = basename2(path28).toLowerCase();
474
474
  const similar = existingFiles.find((file) => {
475
475
  const fileName = basename2(file).toLowerCase();
476
476
  return fileName.includes(targetName) || targetName.includes(fileName);
@@ -478,7 +478,7 @@ function suggestFix(path26, existingFiles) {
478
478
  if (similar) {
479
479
  return `Did you mean "${similar}"?`;
480
480
  }
481
- return `Create the file "${path26}" or remove the link`;
481
+ return `Create the file "${path28}" or remove the link`;
482
482
  }
483
483
  async function validateKnowledgeMap(rootDir = process.cwd()) {
484
484
  const agentsPath = join2(rootDir, "AGENTS.md");
@@ -830,8 +830,8 @@ function createBoundaryValidator(schema, name) {
830
830
  return Ok(result.data);
831
831
  }
832
832
  const suggestions = result.error.issues.map((issue) => {
833
- const path26 = issue.path.join(".");
834
- return path26 ? `${path26}: ${issue.message}` : issue.message;
833
+ const path28 = issue.path.join(".");
834
+ return path28 ? `${path28}: ${issue.message}` : issue.message;
835
835
  });
836
836
  return Err(
837
837
  createError(
@@ -1463,11 +1463,11 @@ function processExportListSpecifiers(exportDecl, exports) {
1463
1463
  var TypeScriptParser = class {
1464
1464
  name = "typescript";
1465
1465
  extensions = [".ts", ".tsx", ".mts", ".cts"];
1466
- async parseFile(path26) {
1467
- const contentResult = await readFileContent(path26);
1466
+ async parseFile(path28) {
1467
+ const contentResult = await readFileContent(path28);
1468
1468
  if (!contentResult.ok) {
1469
1469
  return Err(
1470
- createParseError("NOT_FOUND", `File not found: ${path26}`, { path: path26 }, [
1470
+ createParseError("NOT_FOUND", `File not found: ${path28}`, { path: path28 }, [
1471
1471
  "Check that the file exists",
1472
1472
  "Verify the path is correct"
1473
1473
  ])
@@ -1477,7 +1477,7 @@ var TypeScriptParser = class {
1477
1477
  const ast = parse(contentResult.value, {
1478
1478
  loc: true,
1479
1479
  range: true,
1480
- jsx: path26.endsWith(".tsx"),
1480
+ jsx: path28.endsWith(".tsx"),
1481
1481
  errorOnUnknownASTType: false
1482
1482
  });
1483
1483
  return Ok({
@@ -1488,7 +1488,7 @@ var TypeScriptParser = class {
1488
1488
  } catch (e) {
1489
1489
  const error = e;
1490
1490
  return Err(
1491
- createParseError("SYNTAX_ERROR", `Failed to parse ${path26}: ${error.message}`, { path: path26 }, [
1491
+ createParseError("SYNTAX_ERROR", `Failed to parse ${path28}: ${error.message}`, { path: path28 }, [
1492
1492
  "Check for syntax errors in the file",
1493
1493
  "Ensure valid TypeScript syntax"
1494
1494
  ])
@@ -1673,22 +1673,22 @@ function extractInlineRefs(content) {
1673
1673
  }
1674
1674
  return refs;
1675
1675
  }
1676
- async function parseDocumentationFile(path26) {
1677
- const contentResult = await readFileContent(path26);
1676
+ async function parseDocumentationFile(path28) {
1677
+ const contentResult = await readFileContent(path28);
1678
1678
  if (!contentResult.ok) {
1679
1679
  return Err(
1680
1680
  createEntropyError(
1681
1681
  "PARSE_ERROR",
1682
- `Failed to read documentation file: ${path26}`,
1683
- { file: path26 },
1682
+ `Failed to read documentation file: ${path28}`,
1683
+ { file: path28 },
1684
1684
  ["Check that the file exists"]
1685
1685
  )
1686
1686
  );
1687
1687
  }
1688
1688
  const content = contentResult.value;
1689
- const type = path26.endsWith(".md") ? "markdown" : "text";
1689
+ const type = path28.endsWith(".md") ? "markdown" : "text";
1690
1690
  return Ok({
1691
- path: path26,
1691
+ path: path28,
1692
1692
  type,
1693
1693
  content,
1694
1694
  codeBlocks: extractCodeBlocks(content),
@@ -4681,10 +4681,10 @@ function resolveThresholds(scope, config) {
4681
4681
  }
4682
4682
  const merged = { ...projectThresholds };
4683
4683
  for (const [category, moduleValue] of Object.entries(moduleOverrides)) {
4684
- const projectValue = projectThresholds[category];
4685
- if (projectValue !== void 0 && typeof projectValue === "object" && !Array.isArray(projectValue) && typeof moduleValue === "object" && !Array.isArray(moduleValue)) {
4684
+ const projectValue2 = projectThresholds[category];
4685
+ if (projectValue2 !== void 0 && typeof projectValue2 === "object" && !Array.isArray(projectValue2) && typeof moduleValue === "object" && !Array.isArray(moduleValue)) {
4686
4686
  merged[category] = {
4687
- ...projectValue,
4687
+ ...projectValue2,
4688
4688
  ...moduleValue
4689
4689
  };
4690
4690
  } else {
@@ -4694,77 +4694,1173 @@ function resolveThresholds(scope, config) {
4694
4694
  return merged;
4695
4695
  }
4696
4696
 
4697
- // src/state/types.ts
4697
+ // src/architecture/timeline-types.ts
4698
4698
  import { z as z3 } from "zod";
4699
- var FailureEntrySchema = z3.object({
4700
- date: z3.string(),
4701
- skill: z3.string(),
4702
- type: z3.string(),
4703
- description: z3.string()
4699
+ var CategorySnapshotSchema = z3.object({
4700
+ /** Aggregate metric value (e.g., violation count, avg complexity) */
4701
+ value: z3.number(),
4702
+ /** Count of violations in this category */
4703
+ violationCount: z3.number()
4704
+ });
4705
+ var TimelineSnapshotSchema = z3.object({
4706
+ /** ISO 8601 timestamp of capture */
4707
+ capturedAt: z3.string().datetime(),
4708
+ /** Git commit hash at capture time */
4709
+ commitHash: z3.string(),
4710
+ /** Composite stability score (0-100, higher is healthier) */
4711
+ stabilityScore: z3.number().min(0).max(100),
4712
+ /** Per-category metric aggregates */
4713
+ metrics: z3.record(ArchMetricCategorySchema, CategorySnapshotSchema)
4714
+ });
4715
+ var TimelineFileSchema = z3.object({
4716
+ version: z3.literal(1),
4717
+ snapshots: z3.array(TimelineSnapshotSchema)
4718
+ });
4719
+ var TrendLineSchema = z3.object({
4720
+ /** Current value */
4721
+ current: z3.number(),
4722
+ /** Previous value (from comparison snapshot) */
4723
+ previous: z3.number(),
4724
+ /** Absolute delta (current - previous) */
4725
+ delta: z3.number(),
4726
+ /** Direction indicator */
4727
+ direction: z3.enum(["improving", "stable", "declining"])
4728
+ });
4729
+ var TrendResultSchema = z3.object({
4730
+ /** Overall stability trend */
4731
+ stability: TrendLineSchema,
4732
+ /** Per-category trends */
4733
+ categories: z3.record(ArchMetricCategorySchema, TrendLineSchema),
4734
+ /** Number of snapshots analyzed */
4735
+ snapshotCount: z3.number(),
4736
+ /** Time range covered */
4737
+ from: z3.string(),
4738
+ to: z3.string()
4739
+ });
4740
+ var DEFAULT_STABILITY_THRESHOLDS = {
4741
+ "circular-deps": 5,
4742
+ "layer-violations": 10,
4743
+ complexity: 100,
4744
+ coupling: 2,
4745
+ "forbidden-imports": 5,
4746
+ "module-size": 10,
4747
+ "dependency-depth": 10
4748
+ };
4749
+
4750
+ // src/architecture/timeline-manager.ts
4751
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, renameSync, mkdirSync as mkdirSync3, existsSync as existsSync3 } from "fs";
4752
+ import { randomBytes } from "crypto";
4753
+ import { join as join8, dirname as dirname8 } from "path";
4754
+ var ALL_CATEGORIES = ArchMetricCategorySchema.options;
4755
+ var TimelineManager = class {
4756
+ timelinePath;
4757
+ constructor(rootDir) {
4758
+ this.timelinePath = join8(rootDir, ".harness", "arch", "timeline.json");
4759
+ }
4760
+ /**
4761
+ * Load timeline from disk.
4762
+ * Returns empty TimelineFile if file does not exist or is invalid.
4763
+ */
4764
+ load() {
4765
+ if (!existsSync3(this.timelinePath)) {
4766
+ return { version: 1, snapshots: [] };
4767
+ }
4768
+ try {
4769
+ const raw = readFileSync3(this.timelinePath, "utf-8");
4770
+ const data = JSON.parse(raw);
4771
+ const parsed = TimelineFileSchema.safeParse(data);
4772
+ if (!parsed.success) {
4773
+ console.error(
4774
+ `Timeline validation failed for ${this.timelinePath}:`,
4775
+ parsed.error.format()
4776
+ );
4777
+ return { version: 1, snapshots: [] };
4778
+ }
4779
+ return parsed.data;
4780
+ } catch (error) {
4781
+ console.error(`Error loading timeline from ${this.timelinePath}:`, error);
4782
+ return { version: 1, snapshots: [] };
4783
+ }
4784
+ }
4785
+ /**
4786
+ * Save timeline to disk using atomic write (temp file + rename).
4787
+ * Creates parent directories if they do not exist.
4788
+ */
4789
+ save(timeline) {
4790
+ const dir = dirname8(this.timelinePath);
4791
+ if (!existsSync3(dir)) {
4792
+ mkdirSync3(dir, { recursive: true });
4793
+ }
4794
+ const tmp = this.timelinePath + "." + randomBytes(4).toString("hex") + ".tmp";
4795
+ writeFileSync3(tmp, JSON.stringify(timeline, null, 2));
4796
+ renameSync(tmp, this.timelinePath);
4797
+ }
4798
+ /**
4799
+ * Capture a new snapshot from current metric results.
4800
+ * Aggregates MetricResult[] by category, computes stability score,
4801
+ * appends to timeline (or replaces if same commitHash), and saves.
4802
+ */
4803
+ capture(results, commitHash) {
4804
+ const metrics = this.aggregateByCategory(results);
4805
+ const stabilityScore = this.computeStabilityScore(metrics);
4806
+ const snapshot = {
4807
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
4808
+ commitHash,
4809
+ stabilityScore,
4810
+ metrics
4811
+ };
4812
+ const timeline = this.load();
4813
+ const lastIndex = timeline.snapshots.length - 1;
4814
+ if (lastIndex >= 0 && timeline.snapshots[lastIndex].commitHash === commitHash) {
4815
+ timeline.snapshots[lastIndex] = snapshot;
4816
+ } else {
4817
+ timeline.snapshots.push(snapshot);
4818
+ }
4819
+ this.save(timeline);
4820
+ return snapshot;
4821
+ }
4822
+ /**
4823
+ * Compute trends between snapshots over a window.
4824
+ * @param options.last - Number of recent snapshots to analyze (default: 10)
4825
+ * @param options.since - ISO date string to filter snapshots from
4826
+ */
4827
+ trends(options) {
4828
+ const timeline = this.load();
4829
+ let snapshots = timeline.snapshots;
4830
+ if (options?.since) {
4831
+ const sinceDate = new Date(options.since);
4832
+ snapshots = snapshots.filter((s) => new Date(s.capturedAt) >= sinceDate);
4833
+ }
4834
+ if (options?.last && snapshots.length > options.last) {
4835
+ snapshots = snapshots.slice(-options.last);
4836
+ }
4837
+ if (snapshots.length === 0) {
4838
+ return this.emptyTrendResult();
4839
+ }
4840
+ if (snapshots.length === 1) {
4841
+ const only = snapshots[0];
4842
+ const m = only.metrics;
4843
+ return {
4844
+ stability: this.buildTrendLine(only.stabilityScore, only.stabilityScore, true),
4845
+ categories: this.buildCategoryTrends(m, m),
4846
+ snapshotCount: 1,
4847
+ from: only.capturedAt,
4848
+ to: only.capturedAt
4849
+ };
4850
+ }
4851
+ const first = snapshots[0];
4852
+ const last = snapshots[snapshots.length - 1];
4853
+ return {
4854
+ stability: this.buildTrendLine(last.stabilityScore, first.stabilityScore, true),
4855
+ categories: this.buildCategoryTrends(
4856
+ last.metrics,
4857
+ first.metrics
4858
+ ),
4859
+ snapshotCount: snapshots.length,
4860
+ from: first.capturedAt,
4861
+ to: last.capturedAt
4862
+ };
4863
+ }
4864
+ /**
4865
+ * Compute composite stability score from category metrics.
4866
+ * Equal weight across all categories. Score is 0-100 (higher = healthier).
4867
+ * health = max(0, 1 - (value / threshold)) per category.
4868
+ */
4869
+ computeStabilityScore(metrics, thresholds = DEFAULT_STABILITY_THRESHOLDS) {
4870
+ const healthScores = [];
4871
+ for (const category of ALL_CATEGORIES) {
4872
+ const snapshot = metrics[category];
4873
+ if (!snapshot) {
4874
+ healthScores.push(1);
4875
+ continue;
4876
+ }
4877
+ const threshold = thresholds[category] ?? 10;
4878
+ const health = Math.max(0, 1 - snapshot.value / threshold);
4879
+ healthScores.push(health);
4880
+ }
4881
+ const mean = healthScores.reduce((sum, h) => sum + h, 0) / healthScores.length;
4882
+ return Math.round(mean * 100);
4883
+ }
4884
+ // --- Private helpers ---
4885
+ aggregateByCategory(results) {
4886
+ const metrics = {};
4887
+ for (const result of results) {
4888
+ const existing = metrics[result.category];
4889
+ if (existing) {
4890
+ existing.value += result.value;
4891
+ existing.violationCount += result.violations.length;
4892
+ } else {
4893
+ metrics[result.category] = {
4894
+ value: result.value,
4895
+ violationCount: result.violations.length
4896
+ };
4897
+ }
4898
+ }
4899
+ for (const category of ALL_CATEGORIES) {
4900
+ if (!metrics[category]) {
4901
+ metrics[category] = { value: 0, violationCount: 0 };
4902
+ }
4903
+ }
4904
+ return metrics;
4905
+ }
4906
+ buildTrendLine(current, previous, isStabilityScore) {
4907
+ const delta = current - previous;
4908
+ let direction;
4909
+ if (Math.abs(delta) < 2) {
4910
+ direction = "stable";
4911
+ } else if (isStabilityScore) {
4912
+ direction = delta > 0 ? "improving" : "declining";
4913
+ } else {
4914
+ direction = delta < 0 ? "improving" : "declining";
4915
+ }
4916
+ return { current, previous, delta, direction };
4917
+ }
4918
+ buildCategoryTrends(currentMetrics, previousMetrics) {
4919
+ const trends = {};
4920
+ for (const category of ALL_CATEGORIES) {
4921
+ const current = currentMetrics[category]?.value ?? 0;
4922
+ const previous = previousMetrics[category]?.value ?? 0;
4923
+ trends[category] = this.buildTrendLine(current, previous, false);
4924
+ }
4925
+ return trends;
4926
+ }
4927
+ emptyTrendResult() {
4928
+ const zeroLine = { current: 0, previous: 0, delta: 0, direction: "stable" };
4929
+ const categories = {};
4930
+ for (const category of ALL_CATEGORIES) {
4931
+ categories[category] = { ...zeroLine };
4932
+ }
4933
+ return {
4934
+ stability: { ...zeroLine },
4935
+ categories,
4936
+ snapshotCount: 0,
4937
+ from: "",
4938
+ to: ""
4939
+ };
4940
+ }
4941
+ };
4942
+
4943
+ // src/architecture/prediction-types.ts
4944
+ import { z as z4 } from "zod";
4945
+ var ConfidenceTierSchema = z4.enum(["high", "medium", "low"]);
4946
+ var RegressionResultSchema = z4.object({
4947
+ slope: z4.number(),
4948
+ intercept: z4.number(),
4949
+ rSquared: z4.number().min(0).max(1),
4950
+ dataPoints: z4.number().int().min(0)
4951
+ });
4952
+ var DirectionSchema = z4.enum(["improving", "stable", "declining"]);
4953
+ var CategoryForecastSchema = z4.object({
4954
+ category: ArchMetricCategorySchema,
4955
+ current: z4.number(),
4956
+ threshold: z4.number(),
4957
+ projectedValue4w: z4.number(),
4958
+ projectedValue8w: z4.number(),
4959
+ projectedValue12w: z4.number(),
4960
+ thresholdCrossingWeeks: z4.number().nullable(),
4961
+ confidence: ConfidenceTierSchema,
4962
+ regression: RegressionResultSchema,
4963
+ direction: DirectionSchema
4964
+ });
4965
+ var SpecImpactSignalsSchema = z4.object({
4966
+ newFileCount: z4.number().int().min(0),
4967
+ affectedLayers: z4.array(z4.string()),
4968
+ newDependencies: z4.number().int().min(0),
4969
+ phaseCount: z4.number().int().min(0)
4970
+ });
4971
+ var SpecImpactEstimateSchema = z4.object({
4972
+ specPath: z4.string(),
4973
+ featureName: z4.string(),
4974
+ signals: SpecImpactSignalsSchema,
4975
+ deltas: z4.record(ArchMetricCategorySchema, z4.number()).optional()
4976
+ });
4977
+ var ContributingFeatureSchema = z4.object({
4978
+ name: z4.string(),
4979
+ specPath: z4.string(),
4980
+ delta: z4.number()
4981
+ });
4982
+ var AdjustedForecastSchema = z4.object({
4983
+ baseline: CategoryForecastSchema,
4984
+ adjusted: CategoryForecastSchema,
4985
+ contributingFeatures: z4.array(ContributingFeatureSchema)
4986
+ });
4987
+ var PredictionWarningSchema = z4.object({
4988
+ severity: z4.enum(["critical", "warning", "info"]),
4989
+ category: ArchMetricCategorySchema,
4990
+ message: z4.string(),
4991
+ weeksUntil: z4.number(),
4992
+ confidence: ConfidenceTierSchema,
4993
+ contributingFeatures: z4.array(z4.string())
4994
+ });
4995
+ var StabilityForecastSchema = z4.object({
4996
+ current: z4.number(),
4997
+ projected4w: z4.number(),
4998
+ projected8w: z4.number(),
4999
+ projected12w: z4.number(),
5000
+ confidence: ConfidenceTierSchema,
5001
+ direction: DirectionSchema
5002
+ });
5003
+ var PredictionResultSchema = z4.object({
5004
+ generatedAt: z4.string(),
5005
+ snapshotsUsed: z4.number().int().min(0),
5006
+ timelineRange: z4.object({
5007
+ from: z4.string(),
5008
+ to: z4.string()
5009
+ }),
5010
+ stabilityForecast: StabilityForecastSchema,
5011
+ categories: z4.record(ArchMetricCategorySchema, AdjustedForecastSchema),
5012
+ warnings: z4.array(PredictionWarningSchema)
5013
+ });
5014
+ var PredictionOptionsSchema = z4.object({
5015
+ horizon: z4.number().int().min(1).default(12),
5016
+ includeRoadmap: z4.boolean().default(true),
5017
+ categories: z4.array(ArchMetricCategorySchema).optional(),
5018
+ thresholds: z4.record(ArchMetricCategorySchema, z4.number()).optional()
4704
5019
  });
4705
- var HandoffSchema = z3.object({
4706
- timestamp: z3.string(),
4707
- fromSkill: z3.string(),
4708
- phase: z3.string(),
4709
- summary: z3.string(),
4710
- completed: z3.array(z3.string()).default([]),
4711
- pending: z3.array(z3.string()).default([]),
4712
- concerns: z3.array(z3.string()).default([]),
4713
- decisions: z3.array(
4714
- z3.object({
4715
- what: z3.string(),
4716
- why: z3.string()
5020
+
5021
+ // src/architecture/regression.ts
5022
+ function computeWeightedSums(points) {
5023
+ let sumW = 0, sumWt = 0, sumWv = 0, sumWtt = 0, sumWtv = 0;
5024
+ for (const p of points) {
5025
+ const w = p.weight;
5026
+ sumW += w;
5027
+ sumWt += w * p.t;
5028
+ sumWv += w * p.value;
5029
+ sumWtt += w * p.t * p.t;
5030
+ sumWtv += w * p.t * p.value;
5031
+ }
5032
+ return { sumW, sumWt, sumWv, sumWtt, sumWtv };
5033
+ }
5034
+ function computeRSquared(points, slope, intercept, meanV) {
5035
+ let ssRes = 0, ssTot = 0;
5036
+ for (const p of points) {
5037
+ const predicted = slope * p.t + intercept;
5038
+ ssRes += p.weight * (p.value - predicted) ** 2;
5039
+ ssTot += p.weight * (p.value - meanV) ** 2;
5040
+ }
5041
+ return ssTot < 1e-12 ? 1 : Math.max(0, 1 - ssRes / ssTot);
5042
+ }
5043
+ function weightedLinearRegression(points) {
5044
+ if (points.length < 2) {
5045
+ throw new Error(`Regression requires at least 2 data points, got ${points.length}`);
5046
+ }
5047
+ const n = points.length;
5048
+ const { sumW, sumWt, sumWv, sumWtt, sumWtv } = computeWeightedSums(points);
5049
+ const meanT = sumWt / sumW;
5050
+ const meanV = sumWv / sumW;
5051
+ const denominator = sumWtt - sumWt * sumWt / sumW;
5052
+ if (Math.abs(denominator) < 1e-12) {
5053
+ return { slope: 0, intercept: meanV, rSquared: 0, dataPoints: n };
5054
+ }
5055
+ const slope = (sumWtv - sumWt * sumWv / sumW) / denominator;
5056
+ const intercept = meanV - slope * meanT;
5057
+ const rSquared = computeRSquared(points, slope, intercept, meanV);
5058
+ return { slope, intercept, rSquared, dataPoints: n };
5059
+ }
5060
+ function applyRecencyWeights(values, decay = 0.85) {
5061
+ const n = values.length;
5062
+ return values.map((v, i) => ({
5063
+ t: v.t,
5064
+ value: v.value,
5065
+ weight: Math.pow(decay, n - 1 - i)
5066
+ }));
5067
+ }
5068
+ function projectValue(fit, t) {
5069
+ return fit.slope * t + fit.intercept;
5070
+ }
5071
+ function weeksUntilThreshold(fit, currentT, threshold) {
5072
+ if (fit.slope <= 0) {
5073
+ return null;
5074
+ }
5075
+ const currentProjected = projectValue(fit, currentT);
5076
+ if (currentProjected >= threshold) {
5077
+ return null;
5078
+ }
5079
+ const weeks = (threshold - currentProjected) / fit.slope;
5080
+ return Math.ceil(weeks);
5081
+ }
5082
+ function classifyConfidence(rSquared, dataPoints) {
5083
+ if (rSquared >= 0.7 && dataPoints >= 5) return "high";
5084
+ if (rSquared >= 0.4 && dataPoints >= 3) return "medium";
5085
+ return "low";
5086
+ }
5087
+
5088
+ // src/architecture/prediction-engine.ts
5089
+ import * as fs5 from "fs";
5090
+ import * as path2 from "path";
5091
+
5092
+ // src/roadmap/parse.ts
5093
+ import { Ok as Ok2, Err as Err2 } from "@harness-engineering/types";
5094
+ var VALID_STATUSES = /* @__PURE__ */ new Set([
5095
+ "backlog",
5096
+ "planned",
5097
+ "in-progress",
5098
+ "done",
5099
+ "blocked"
5100
+ ]);
5101
+ var EM_DASH = "\u2014";
5102
+ var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
5103
+ function parseRoadmap(markdown) {
5104
+ const fmMatch = markdown.match(/^---\n([\s\S]*?)\n---/);
5105
+ if (!fmMatch) {
5106
+ return Err2(new Error("Missing or malformed YAML frontmatter"));
5107
+ }
5108
+ const fmResult = parseFrontmatter(fmMatch[1]);
5109
+ if (!fmResult.ok) return fmResult;
5110
+ const body = markdown.slice(fmMatch[0].length);
5111
+ const milestonesResult = parseMilestones(body);
5112
+ if (!milestonesResult.ok) return milestonesResult;
5113
+ const historyResult = parseAssignmentHistory(body);
5114
+ if (!historyResult.ok) return historyResult;
5115
+ return Ok2({
5116
+ frontmatter: fmResult.value,
5117
+ milestones: milestonesResult.value,
5118
+ assignmentHistory: historyResult.value
5119
+ });
5120
+ }
5121
+ function parseFrontmatter(raw) {
5122
+ const lines = raw.split("\n");
5123
+ const map = /* @__PURE__ */ new Map();
5124
+ for (const line of lines) {
5125
+ const idx = line.indexOf(":");
5126
+ if (idx === -1) continue;
5127
+ const key = line.slice(0, idx).trim();
5128
+ const val = line.slice(idx + 1).trim();
5129
+ map.set(key, val);
5130
+ }
5131
+ const project = map.get("project");
5132
+ const versionStr = map.get("version");
5133
+ const lastSynced = map.get("last_synced");
5134
+ const lastManualEdit = map.get("last_manual_edit");
5135
+ const created = map.get("created");
5136
+ const updated = map.get("updated");
5137
+ if (!project || !versionStr || !lastSynced || !lastManualEdit) {
5138
+ return Err2(
5139
+ new Error(
5140
+ "Frontmatter missing required fields: project, version, last_synced, last_manual_edit"
5141
+ )
5142
+ );
5143
+ }
5144
+ const version = parseInt(versionStr, 10);
5145
+ if (isNaN(version)) {
5146
+ return Err2(new Error("Frontmatter version must be a number"));
5147
+ }
5148
+ const fm = { project, version, lastSynced, lastManualEdit };
5149
+ if (created) fm.created = created;
5150
+ if (updated) fm.updated = updated;
5151
+ return Ok2(fm);
5152
+ }
5153
+ function parseMilestones(body) {
5154
+ const milestones = [];
5155
+ const h2Pattern = /^## (.+)$/gm;
5156
+ const h2Matches = [];
5157
+ let match;
5158
+ let bodyEnd = body.length;
5159
+ while ((match = h2Pattern.exec(body)) !== null) {
5160
+ if (match[1] === "Assignment History") {
5161
+ bodyEnd = match.index;
5162
+ break;
5163
+ }
5164
+ h2Matches.push({ heading: match[1], startIndex: match.index, fullMatch: match[0] });
5165
+ }
5166
+ for (let i = 0; i < h2Matches.length; i++) {
5167
+ const h2 = h2Matches[i];
5168
+ const nextStart = i + 1 < h2Matches.length ? h2Matches[i + 1].startIndex : bodyEnd;
5169
+ const sectionBody = body.slice(h2.startIndex + h2.fullMatch.length, nextStart);
5170
+ const isBacklog = h2.heading === "Backlog";
5171
+ const milestoneName = isBacklog ? "Backlog" : h2.heading.replace(/^Milestone:\s*/, "");
5172
+ const featuresResult = parseFeatures(sectionBody);
5173
+ if (!featuresResult.ok) return featuresResult;
5174
+ milestones.push({
5175
+ name: milestoneName,
5176
+ isBacklog,
5177
+ features: featuresResult.value
5178
+ });
5179
+ }
5180
+ return Ok2(milestones);
5181
+ }
5182
+ function parseFeatures(sectionBody) {
5183
+ const features = [];
5184
+ const h3Pattern = /^### (?:Feature: )?(.+)$/gm;
5185
+ const h3Matches = [];
5186
+ let match;
5187
+ while ((match = h3Pattern.exec(sectionBody)) !== null) {
5188
+ h3Matches.push({ name: match[1], startIndex: match.index, fullMatch: match[0] });
5189
+ }
5190
+ for (let i = 0; i < h3Matches.length; i++) {
5191
+ const h3 = h3Matches[i];
5192
+ const nextStart = i + 1 < h3Matches.length ? h3Matches[i + 1].startIndex : sectionBody.length;
5193
+ const featureBody = sectionBody.slice(h3.startIndex + h3.fullMatch.length, nextStart);
5194
+ const featureResult = parseFeatureFields(h3.name, featureBody);
5195
+ if (!featureResult.ok) return featureResult;
5196
+ features.push(featureResult.value);
5197
+ }
5198
+ return Ok2(features);
5199
+ }
5200
+ function extractFieldMap(body) {
5201
+ const fieldMap = /* @__PURE__ */ new Map();
5202
+ const fieldPattern = /^- \*\*(.+?):\*\* (.+)$/gm;
5203
+ let match;
5204
+ while ((match = fieldPattern.exec(body)) !== null) {
5205
+ fieldMap.set(match[1], match[2]);
5206
+ }
5207
+ return fieldMap;
5208
+ }
5209
+ function parseListField(fieldMap, ...keys) {
5210
+ let raw = EM_DASH;
5211
+ for (const key of keys) {
5212
+ const val = fieldMap.get(key);
5213
+ if (val !== void 0) {
5214
+ raw = val;
5215
+ break;
5216
+ }
5217
+ }
5218
+ if (raw === EM_DASH || raw === "none") return [];
5219
+ return raw.split(",").map((s) => s.trim());
5220
+ }
5221
+ function parseFeatureFields(name, body) {
5222
+ const fieldMap = extractFieldMap(body);
5223
+ const statusRaw = fieldMap.get("Status");
5224
+ if (!statusRaw || !VALID_STATUSES.has(statusRaw)) {
5225
+ return Err2(
5226
+ new Error(
5227
+ `Feature "${name}" has invalid status: "${statusRaw ?? "(missing)"}". Valid statuses: ${[...VALID_STATUSES].join(", ")}`
5228
+ )
5229
+ );
5230
+ }
5231
+ const specRaw = fieldMap.get("Spec") ?? EM_DASH;
5232
+ const plans = parseListField(fieldMap, "Plans", "Plan");
5233
+ const blockedBy = parseListField(fieldMap, "Blocked by", "Blockers");
5234
+ const assigneeRaw = fieldMap.get("Assignee") ?? EM_DASH;
5235
+ const priorityRaw = fieldMap.get("Priority") ?? EM_DASH;
5236
+ const externalIdRaw = fieldMap.get("External-ID") ?? EM_DASH;
5237
+ if (priorityRaw !== EM_DASH && !VALID_PRIORITIES.has(priorityRaw)) {
5238
+ return Err2(
5239
+ new Error(
5240
+ `Feature "${name}" has invalid priority: "${priorityRaw}". Valid priorities: ${[...VALID_PRIORITIES].join(", ")}`
5241
+ )
5242
+ );
5243
+ }
5244
+ return Ok2({
5245
+ name,
5246
+ status: statusRaw,
5247
+ spec: specRaw === EM_DASH ? null : specRaw,
5248
+ plans,
5249
+ blockedBy,
5250
+ summary: fieldMap.get("Summary") ?? "",
5251
+ assignee: assigneeRaw === EM_DASH ? null : assigneeRaw,
5252
+ priority: priorityRaw === EM_DASH ? null : priorityRaw,
5253
+ externalId: externalIdRaw === EM_DASH ? null : externalIdRaw
5254
+ });
5255
+ }
5256
+ function parseAssignmentHistory(body) {
5257
+ const historyMatch = body.match(/^## Assignment History\s*\n/m);
5258
+ if (!historyMatch || historyMatch.index === void 0) return Ok2([]);
5259
+ const historyStart = historyMatch.index + historyMatch[0].length;
5260
+ const rawHistoryBody = body.slice(historyStart);
5261
+ const nextH2 = rawHistoryBody.search(/^## /m);
5262
+ const historyBody = nextH2 === -1 ? rawHistoryBody : rawHistoryBody.slice(0, nextH2);
5263
+ const records = [];
5264
+ const lines = historyBody.split("\n");
5265
+ let pastHeader = false;
5266
+ for (const line of lines) {
5267
+ const trimmed = line.trim();
5268
+ if (!trimmed.startsWith("|")) continue;
5269
+ if (!pastHeader) {
5270
+ if (trimmed.match(/^\|[-\s|]+\|$/)) {
5271
+ pastHeader = true;
5272
+ }
5273
+ continue;
5274
+ }
5275
+ const cells = trimmed.split("|").map((c) => c.trim()).filter((c) => c.length > 0);
5276
+ if (cells.length < 4) continue;
5277
+ const action = cells[2];
5278
+ if (!["assigned", "completed", "unassigned"].includes(action)) continue;
5279
+ records.push({
5280
+ feature: cells[0],
5281
+ assignee: cells[1],
5282
+ action,
5283
+ date: cells[3]
5284
+ });
5285
+ }
5286
+ return Ok2(records);
5287
+ }
5288
+
5289
+ // src/architecture/prediction-engine.ts
5290
+ var ALL_CATEGORIES2 = ArchMetricCategorySchema.options;
5291
+ var DIRECTION_THRESHOLD = 1e-3;
5292
+ var PredictionEngine = class {
5293
+ constructor(rootDir, timelineManager, estimator) {
5294
+ this.rootDir = rootDir;
5295
+ this.timelineManager = timelineManager;
5296
+ this.estimator = estimator;
5297
+ }
5298
+ rootDir;
5299
+ timelineManager;
5300
+ estimator;
5301
+ /**
5302
+ * Produce a PredictionResult with per-category forecasts and warnings.
5303
+ * Throws if fewer than 3 snapshots are available.
5304
+ */
5305
+ predict(options) {
5306
+ const opts = this.resolveOptions(options);
5307
+ const timeline = this.timelineManager.load();
5308
+ const snapshots = timeline.snapshots;
5309
+ if (snapshots.length < 3) {
5310
+ throw new Error(
5311
+ `PredictionEngine requires at least 3 snapshots, got ${snapshots.length}. Run "harness snapshot" to capture more data points.`
5312
+ );
5313
+ }
5314
+ const thresholds = this.resolveThresholds(opts);
5315
+ const categoriesToProcess = opts.categories ?? [...ALL_CATEGORIES2];
5316
+ const firstDate = new Date(snapshots[0].capturedAt).getTime();
5317
+ const lastSnapshot = snapshots[snapshots.length - 1];
5318
+ const currentT = (new Date(lastSnapshot.capturedAt).getTime() - firstDate) / (7 * 24 * 60 * 60 * 1e3);
5319
+ const baselines = {};
5320
+ for (const category of ALL_CATEGORIES2) {
5321
+ const threshold = thresholds[category];
5322
+ const shouldProcess = categoriesToProcess.includes(category);
5323
+ if (!shouldProcess) {
5324
+ baselines[category] = this.zeroForecast(category, threshold);
5325
+ continue;
5326
+ }
5327
+ const timeSeries = this.extractTimeSeries(snapshots, category, firstDate);
5328
+ baselines[category] = this.forecastCategory(
5329
+ category,
5330
+ timeSeries,
5331
+ currentT,
5332
+ threshold,
5333
+ opts.horizon
5334
+ );
5335
+ }
5336
+ const specImpacts = this.computeSpecImpacts(opts);
5337
+ const categories = {};
5338
+ for (const category of ALL_CATEGORIES2) {
5339
+ const baseline = baselines[category];
5340
+ const threshold = thresholds[category];
5341
+ if (!specImpacts || specImpacts.length === 0) {
5342
+ categories[category] = {
5343
+ baseline,
5344
+ adjusted: baseline,
5345
+ contributingFeatures: []
5346
+ };
5347
+ continue;
5348
+ }
5349
+ let totalDelta = 0;
5350
+ const contributing = [];
5351
+ for (const impact of specImpacts) {
5352
+ const delta = impact.deltas?.[category] ?? 0;
5353
+ if (delta !== 0) {
5354
+ totalDelta += delta;
5355
+ contributing.push({
5356
+ name: impact.featureName,
5357
+ specPath: impact.specPath,
5358
+ delta
5359
+ });
5360
+ }
5361
+ }
5362
+ if (totalDelta === 0) {
5363
+ categories[category] = {
5364
+ baseline,
5365
+ adjusted: baseline,
5366
+ contributingFeatures: []
5367
+ };
5368
+ continue;
5369
+ }
5370
+ const adjusted = {
5371
+ ...baseline,
5372
+ projectedValue4w: baseline.projectedValue4w + totalDelta,
5373
+ projectedValue8w: baseline.projectedValue8w + totalDelta,
5374
+ projectedValue12w: baseline.projectedValue12w + totalDelta
5375
+ };
5376
+ const adjustedFit = {
5377
+ slope: baseline.regression.slope,
5378
+ intercept: baseline.regression.intercept + totalDelta,
5379
+ rSquared: baseline.regression.rSquared,
5380
+ dataPoints: baseline.regression.dataPoints
5381
+ };
5382
+ adjusted.thresholdCrossingWeeks = weeksUntilThreshold(adjustedFit, currentT, threshold);
5383
+ adjusted.regression = {
5384
+ slope: adjustedFit.slope,
5385
+ intercept: adjustedFit.intercept,
5386
+ rSquared: adjustedFit.rSquared,
5387
+ dataPoints: adjustedFit.dataPoints
5388
+ };
5389
+ categories[category] = {
5390
+ baseline,
5391
+ adjusted,
5392
+ contributingFeatures: contributing
5393
+ };
5394
+ }
5395
+ const warnings = this.generateWarnings(
5396
+ categories,
5397
+ opts.horizon
5398
+ );
5399
+ const stabilityForecast = this.computeStabilityForecast(
5400
+ categories,
5401
+ thresholds,
5402
+ snapshots
5403
+ );
5404
+ return {
5405
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
5406
+ snapshotsUsed: snapshots.length,
5407
+ timelineRange: {
5408
+ from: snapshots[0].capturedAt,
5409
+ to: lastSnapshot.capturedAt
5410
+ },
5411
+ stabilityForecast,
5412
+ categories,
5413
+ warnings
5414
+ };
5415
+ }
5416
+ // --- Private helpers ---
5417
+ resolveOptions(options) {
5418
+ return {
5419
+ horizon: options?.horizon ?? 12,
5420
+ includeRoadmap: options?.includeRoadmap ?? true,
5421
+ categories: options?.categories,
5422
+ thresholds: options?.thresholds
5423
+ };
5424
+ }
5425
+ resolveThresholds(opts) {
5426
+ const base = { ...DEFAULT_STABILITY_THRESHOLDS };
5427
+ if (opts.thresholds) {
5428
+ for (const [key, value] of Object.entries(opts.thresholds)) {
5429
+ if (value !== void 0) {
5430
+ base[key] = value;
5431
+ }
5432
+ }
5433
+ }
5434
+ return base;
5435
+ }
5436
+ /**
5437
+ * Extract time series for a single category from snapshots.
5438
+ * Returns array of { t (weeks from first), value } sorted oldest first.
5439
+ */
5440
+ extractTimeSeries(snapshots, category, firstDateMs) {
5441
+ return snapshots.map((s) => {
5442
+ const t = (new Date(s.capturedAt).getTime() - firstDateMs) / (7 * 24 * 60 * 60 * 1e3);
5443
+ const metrics = s.metrics;
5444
+ const value = metrics[category]?.value ?? 0;
5445
+ return { t, value };
5446
+ });
5447
+ }
5448
+ /**
5449
+ * Produce a CategoryForecast for a single category using regression.
5450
+ */
5451
+ forecastCategory(category, timeSeries, currentT, threshold, horizon = 12) {
5452
+ const weighted = applyRecencyWeights(timeSeries, 0.85);
5453
+ const fit = weightedLinearRegression(weighted);
5454
+ const current = timeSeries[timeSeries.length - 1].value;
5455
+ const h3 = Math.round(horizon / 3);
5456
+ const h2 = Math.round(horizon * 2 / 3);
5457
+ const projected4w = projectValue(fit, currentT + h3);
5458
+ const projected8w = projectValue(fit, currentT + h2);
5459
+ const projected12w = projectValue(fit, currentT + horizon);
5460
+ const crossing = weeksUntilThreshold(fit, currentT, threshold);
5461
+ const confidence = classifyConfidence(fit.rSquared, fit.dataPoints);
5462
+ const direction = this.classifyDirection(fit.slope);
5463
+ return {
5464
+ category,
5465
+ current,
5466
+ threshold,
5467
+ projectedValue4w: projected4w,
5468
+ projectedValue8w: projected8w,
5469
+ projectedValue12w: projected12w,
5470
+ thresholdCrossingWeeks: crossing,
5471
+ confidence,
5472
+ regression: {
5473
+ slope: fit.slope,
5474
+ intercept: fit.intercept,
5475
+ rSquared: fit.rSquared,
5476
+ dataPoints: fit.dataPoints
5477
+ },
5478
+ direction
5479
+ };
5480
+ }
5481
+ classifyDirection(slope) {
5482
+ if (Math.abs(slope) < DIRECTION_THRESHOLD) return "stable";
5483
+ return slope > 0 ? "declining" : "improving";
5484
+ }
5485
+ zeroForecast(category, threshold) {
5486
+ return {
5487
+ category,
5488
+ current: 0,
5489
+ threshold,
5490
+ projectedValue4w: 0,
5491
+ projectedValue8w: 0,
5492
+ projectedValue12w: 0,
5493
+ thresholdCrossingWeeks: null,
5494
+ confidence: "low",
5495
+ regression: { slope: 0, intercept: 0, rSquared: 0, dataPoints: 0 },
5496
+ direction: "stable"
5497
+ };
5498
+ }
5499
+ /**
5500
+ * Generate warnings based on severity rules from spec:
5501
+ * - critical: threshold crossing <= 4w, confidence high or medium
5502
+ * - warning: threshold crossing <= 8w, confidence high or medium
5503
+ * - info: threshold crossing <= 12w, any confidence
5504
+ */
5505
+ generateWarnings(categories, horizon = 12) {
5506
+ const warnings = [];
5507
+ const criticalWindow = Math.round(horizon / 3);
5508
+ const warningWindow = Math.round(horizon * 2 / 3);
5509
+ for (const category of ALL_CATEGORIES2) {
5510
+ const af = categories[category];
5511
+ if (!af) continue;
5512
+ const forecast = af.adjusted;
5513
+ const crossing = forecast.thresholdCrossingWeeks;
5514
+ if (crossing === null || crossing <= 0) continue;
5515
+ let severity = null;
5516
+ if (crossing <= criticalWindow && (forecast.confidence === "high" || forecast.confidence === "medium")) {
5517
+ severity = "critical";
5518
+ } else if (crossing <= warningWindow && (forecast.confidence === "high" || forecast.confidence === "medium")) {
5519
+ severity = "warning";
5520
+ } else if (crossing <= horizon) {
5521
+ severity = "info";
5522
+ }
5523
+ if (severity) {
5524
+ const contributingNames = af.contributingFeatures.map((f) => f.name);
5525
+ warnings.push({
5526
+ severity,
5527
+ category,
5528
+ message: `${category} projected to exceed threshold (~${crossing}w, ${forecast.confidence} confidence)`,
5529
+ weeksUntil: crossing,
5530
+ confidence: forecast.confidence,
5531
+ contributingFeatures: contributingNames
5532
+ });
5533
+ }
5534
+ }
5535
+ return warnings;
5536
+ }
5537
+ /**
5538
+ * Compute composite stability forecast by projecting per-category values
5539
+ * forward and computing stability scores at each horizon.
5540
+ */
5541
+ computeStabilityForecast(categories, thresholds, _snapshots) {
5542
+ const currentMetrics = this.buildMetricsFromForecasts(categories, "current");
5543
+ const current = this.timelineManager.computeStabilityScore(currentMetrics, thresholds);
5544
+ const metrics4w = this.buildMetricsFromForecasts(categories, "4w");
5545
+ const projected4w = this.timelineManager.computeStabilityScore(metrics4w, thresholds);
5546
+ const metrics8w = this.buildMetricsFromForecasts(categories, "8w");
5547
+ const projected8w = this.timelineManager.computeStabilityScore(metrics8w, thresholds);
5548
+ const metrics12w = this.buildMetricsFromForecasts(categories, "12w");
5549
+ const projected12w = this.timelineManager.computeStabilityScore(metrics12w, thresholds);
5550
+ const delta = projected12w - current;
5551
+ let direction;
5552
+ if (Math.abs(delta) < 2) {
5553
+ direction = "stable";
5554
+ } else {
5555
+ direction = delta > 0 ? "improving" : "declining";
5556
+ }
5557
+ const confidences = ALL_CATEGORIES2.map((c) => categories[c]?.adjusted.confidence ?? "low");
5558
+ const confidence = this.medianConfidence(confidences);
5559
+ return { current, projected4w, projected8w, projected12w, confidence, direction };
5560
+ }
5561
+ buildMetricsFromForecasts(categories, horizon) {
5562
+ const metrics = {};
5563
+ for (const cat of ALL_CATEGORIES2) {
5564
+ const forecast = categories[cat]?.adjusted;
5565
+ let value = 0;
5566
+ if (forecast) {
5567
+ switch (horizon) {
5568
+ case "current":
5569
+ value = forecast.current;
5570
+ break;
5571
+ case "4w":
5572
+ value = forecast.projectedValue4w;
5573
+ break;
5574
+ case "8w":
5575
+ value = forecast.projectedValue8w;
5576
+ break;
5577
+ case "12w":
5578
+ value = forecast.projectedValue12w;
5579
+ break;
5580
+ }
5581
+ }
5582
+ metrics[cat] = { value: Math.max(0, value), violationCount: 0 };
5583
+ }
5584
+ return metrics;
5585
+ }
5586
+ medianConfidence(confidences) {
5587
+ const order = { low: 0, medium: 1, high: 2 };
5588
+ const sorted = [...confidences].sort((a, b) => order[a] - order[b]);
5589
+ const mid = Math.floor(sorted.length / 2);
5590
+ return sorted[mid] ?? "low";
5591
+ }
5592
+ /**
5593
+ * Load roadmap features, estimate spec impacts via the estimator.
5594
+ * Returns null if estimator is null or includeRoadmap is false.
5595
+ */
5596
+ computeSpecImpacts(opts) {
5597
+ if (!this.estimator || !opts.includeRoadmap) {
5598
+ return null;
5599
+ }
5600
+ try {
5601
+ const roadmapPath = path2.join(this.rootDir, "roadmap.md");
5602
+ const raw = fs5.readFileSync(roadmapPath, "utf-8");
5603
+ const parseResult = parseRoadmap(raw);
5604
+ if (!parseResult.ok) return null;
5605
+ const features = [];
5606
+ for (const milestone of parseResult.value.milestones) {
5607
+ for (const feature of milestone.features) {
5608
+ if (feature.status === "planned" || feature.status === "in-progress") {
5609
+ features.push({ name: feature.name, spec: feature.spec });
5610
+ }
5611
+ }
5612
+ }
5613
+ if (features.length === 0) return null;
5614
+ return this.estimator.estimateAll(features);
5615
+ } catch {
5616
+ return null;
5617
+ }
5618
+ }
5619
+ };
5620
+
5621
+ // src/architecture/spec-impact-estimator.ts
5622
+ import * as fs6 from "fs";
5623
+ import * as path3 from "path";
5624
+ var DEFAULT_COEFFICIENTS = {
5625
+ newFileModuleSize: 0.3,
5626
+ newFileComplexity: 1.5,
5627
+ layerViolation: 0.5,
5628
+ depCoupling: 0.2,
5629
+ depDepth: 0.3,
5630
+ phaseComplexity: 2
5631
+ };
5632
+ var SpecImpactEstimator = class {
5633
+ constructor(rootDir, coefficients) {
5634
+ this.rootDir = rootDir;
5635
+ this.coefficients = { ...DEFAULT_COEFFICIENTS, ...coefficients };
5636
+ this.layerNames = this.loadLayerNames();
5637
+ }
5638
+ rootDir;
5639
+ coefficients;
5640
+ layerNames;
5641
+ /**
5642
+ * Estimate impact of a single spec file.
5643
+ * @param specPath - Relative path from rootDir to the spec file.
5644
+ */
5645
+ estimate(specPath) {
5646
+ const absolutePath = path3.join(this.rootDir, specPath);
5647
+ const content = fs6.readFileSync(absolutePath, "utf-8");
5648
+ const newFileCount = this.extractNewFileCount(content);
5649
+ const affectedLayers = this.extractAffectedLayers(content);
5650
+ const newDependencies = this.extractNewDependencies(content);
5651
+ const phaseCount = this.extractPhaseCount(content);
5652
+ const deltas = this.computeDeltas(
5653
+ newFileCount,
5654
+ affectedLayers.length,
5655
+ newDependencies,
5656
+ phaseCount
5657
+ );
5658
+ const h1Match = content.match(/^#\s+(.+)$/m);
5659
+ const featureName = h1Match ? h1Match[1].trim() : path3.basename(specPath, ".md");
5660
+ return {
5661
+ specPath,
5662
+ featureName,
5663
+ signals: {
5664
+ newFileCount,
5665
+ affectedLayers,
5666
+ newDependencies,
5667
+ phaseCount
5668
+ },
5669
+ deltas
5670
+ };
5671
+ }
5672
+ /**
5673
+ * Estimate impact for all planned features that have specs.
5674
+ * Skips features with null specs or specs that don't exist on disk.
5675
+ */
5676
+ estimateAll(features) {
5677
+ const results = [];
5678
+ for (const feature of features) {
5679
+ if (!feature.spec) continue;
5680
+ const absolutePath = path3.join(this.rootDir, feature.spec);
5681
+ if (!fs6.existsSync(absolutePath)) continue;
5682
+ const estimate = this.estimate(feature.spec);
5683
+ results.push({ ...estimate, featureName: feature.name });
5684
+ }
5685
+ return results;
5686
+ }
5687
+ // --- Private: Signal Extraction ---
5688
+ /**
5689
+ * Count file paths in Technical Design sections that don't exist on disk.
5690
+ * Looks for paths in code blocks (```) under ## Technical Design.
5691
+ */
5692
+ extractNewFileCount(content) {
5693
+ const techDesignMatch = content.match(/## Technical Design\b[\s\S]*?(?=\n## |\n# |$)/i);
5694
+ if (!techDesignMatch) return 0;
5695
+ const section = techDesignMatch[0];
5696
+ const codeBlocks = section.match(/```[\s\S]*?```/g) ?? [];
5697
+ const filePaths = [];
5698
+ for (const block of codeBlocks) {
5699
+ const inner = block.replace(/^```\w*\n?/, "").replace(/\n?```$/, "");
5700
+ for (const line of inner.split("\n")) {
5701
+ const trimmed = line.trim();
5702
+ if (trimmed.match(/^[\w@.-]+\/[\w./-]+\.\w+$/)) {
5703
+ filePaths.push(trimmed);
5704
+ }
5705
+ }
5706
+ }
5707
+ let count = 0;
5708
+ for (const fp of filePaths) {
5709
+ const absolute = path3.join(this.rootDir, fp);
5710
+ if (!fs6.existsSync(absolute)) {
5711
+ count++;
5712
+ }
5713
+ }
5714
+ return count;
5715
+ }
5716
+ /**
5717
+ * Match layer names from harness.config.json mentioned in the spec.
5718
+ * Returns deduplicated array of matched layer names.
5719
+ */
5720
+ extractAffectedLayers(content) {
5721
+ if (this.layerNames.length === 0) return [];
5722
+ const matched = /* @__PURE__ */ new Set();
5723
+ for (const layer of this.layerNames) {
5724
+ const pattern = new RegExp(`\\b${this.escapeRegex(layer)}\\b`, "i");
5725
+ if (pattern.test(content)) {
5726
+ matched.add(layer);
5727
+ }
5728
+ }
5729
+ return [...matched].sort();
5730
+ }
5731
+ /**
5732
+ * Count dependency-related keywords: "import", "depend" (covers depends/dependency),
5733
+ * "package" in dependency context.
5734
+ */
5735
+ extractNewDependencies(content) {
5736
+ const patterns = [/\bimport\b/gi, /\bdepend\w*\b/gi, /\bpackage\b/gi];
5737
+ let count = 0;
5738
+ for (const pattern of patterns) {
5739
+ const matches = content.match(pattern);
5740
+ if (matches) count += matches.length;
5741
+ }
5742
+ return count;
5743
+ }
5744
+ /**
5745
+ * Count H3/H4 headings under "Implementation" or "Implementation Order" sections.
5746
+ */
5747
+ extractPhaseCount(content) {
5748
+ const implMatch = content.match(/## Implementation\b[\s\S]*?(?=\n## |\n# |$)/i);
5749
+ if (!implMatch) return 0;
5750
+ const section = implMatch[0];
5751
+ const headings = section.match(/^#{3,4}\s+.+$/gm);
5752
+ return headings ? headings.length : 0;
5753
+ }
5754
+ // --- Private: Delta Computation ---
5755
+ computeDeltas(newFileCount, crossLayerCount, newDependencies, phaseCount) {
5756
+ const deltas = {};
5757
+ const c = this.coefficients;
5758
+ const addDelta = (category, value) => {
5759
+ deltas[category] = (deltas[category] ?? 0) + value;
5760
+ };
5761
+ if (newFileCount > 0) {
5762
+ addDelta("module-size", newFileCount * c.newFileModuleSize);
5763
+ addDelta("complexity", newFileCount * c.newFileComplexity);
5764
+ }
5765
+ if (crossLayerCount > 0) {
5766
+ addDelta("layer-violations", crossLayerCount * c.layerViolation);
5767
+ }
5768
+ if (newDependencies > 0) {
5769
+ addDelta("coupling", newDependencies * c.depCoupling);
5770
+ addDelta("dependency-depth", newDependencies * c.depDepth);
5771
+ }
5772
+ if (phaseCount > 1) {
5773
+ addDelta("complexity", (phaseCount - 1) * c.phaseComplexity);
5774
+ }
5775
+ return deltas;
5776
+ }
5777
+ // --- Private: Config Loading ---
5778
+ loadLayerNames() {
5779
+ try {
5780
+ const configPath = path3.join(this.rootDir, "harness.config.json");
5781
+ const raw = fs6.readFileSync(configPath, "utf-8");
5782
+ const config = JSON.parse(raw);
5783
+ return (config.layers ?? []).map((l) => l.name);
5784
+ } catch {
5785
+ return [];
5786
+ }
5787
+ }
5788
+ escapeRegex(str) {
5789
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5790
+ }
5791
+ };
5792
+
5793
+ // src/state/types.ts
5794
+ import { z as z5 } from "zod";
5795
+ var FailureEntrySchema = z5.object({
5796
+ date: z5.string(),
5797
+ skill: z5.string(),
5798
+ type: z5.string(),
5799
+ description: z5.string()
5800
+ });
5801
+ var HandoffSchema = z5.object({
5802
+ timestamp: z5.string(),
5803
+ fromSkill: z5.string(),
5804
+ phase: z5.string(),
5805
+ summary: z5.string(),
5806
+ completed: z5.array(z5.string()).default([]),
5807
+ pending: z5.array(z5.string()).default([]),
5808
+ concerns: z5.array(z5.string()).default([]),
5809
+ decisions: z5.array(
5810
+ z5.object({
5811
+ what: z5.string(),
5812
+ why: z5.string()
4717
5813
  })
4718
5814
  ).default([]),
4719
- blockers: z3.array(z3.string()).default([]),
4720
- contextKeywords: z3.array(z3.string()).default([])
5815
+ blockers: z5.array(z5.string()).default([]),
5816
+ contextKeywords: z5.array(z5.string()).default([])
4721
5817
  });
4722
- var GateCheckSchema = z3.object({
4723
- name: z3.string(),
4724
- passed: z3.boolean(),
4725
- command: z3.string(),
4726
- output: z3.string().optional(),
4727
- duration: z3.number().optional()
5818
+ var GateCheckSchema = z5.object({
5819
+ name: z5.string(),
5820
+ passed: z5.boolean(),
5821
+ command: z5.string(),
5822
+ output: z5.string().optional(),
5823
+ duration: z5.number().optional()
4728
5824
  });
4729
- var GateResultSchema = z3.object({
4730
- passed: z3.boolean(),
4731
- checks: z3.array(GateCheckSchema)
5825
+ var GateResultSchema = z5.object({
5826
+ passed: z5.boolean(),
5827
+ checks: z5.array(GateCheckSchema)
4732
5828
  });
4733
- var GateConfigSchema = z3.object({
4734
- checks: z3.array(
4735
- z3.object({
4736
- name: z3.string(),
4737
- command: z3.string()
5829
+ var GateConfigSchema = z5.object({
5830
+ checks: z5.array(
5831
+ z5.object({
5832
+ name: z5.string(),
5833
+ command: z5.string()
4738
5834
  })
4739
5835
  ).optional(),
4740
- trace: z3.boolean().optional()
5836
+ trace: z5.boolean().optional()
4741
5837
  });
4742
- var HarnessStateSchema = z3.object({
4743
- schemaVersion: z3.literal(1),
4744
- position: z3.object({
4745
- phase: z3.string().optional(),
4746
- task: z3.string().optional()
5838
+ var HarnessStateSchema = z5.object({
5839
+ schemaVersion: z5.literal(1),
5840
+ position: z5.object({
5841
+ phase: z5.string().optional(),
5842
+ task: z5.string().optional()
4747
5843
  }).default({}),
4748
- decisions: z3.array(
4749
- z3.object({
4750
- date: z3.string(),
4751
- decision: z3.string(),
4752
- context: z3.string()
5844
+ decisions: z5.array(
5845
+ z5.object({
5846
+ date: z5.string(),
5847
+ decision: z5.string(),
5848
+ context: z5.string()
4753
5849
  })
4754
5850
  ).default([]),
4755
- blockers: z3.array(
4756
- z3.object({
4757
- id: z3.string(),
4758
- description: z3.string(),
4759
- status: z3.enum(["open", "resolved"])
5851
+ blockers: z5.array(
5852
+ z5.object({
5853
+ id: z5.string(),
5854
+ description: z5.string(),
5855
+ status: z5.enum(["open", "resolved"])
4760
5856
  })
4761
5857
  ).default([]),
4762
- progress: z3.record(z3.enum(["pending", "in_progress", "complete"])).default({}),
4763
- lastSession: z3.object({
4764
- date: z3.string(),
4765
- summary: z3.string(),
4766
- lastSkill: z3.string().optional(),
4767
- pendingTasks: z3.array(z3.string()).optional()
5858
+ progress: z5.record(z5.enum(["pending", "in_progress", "complete"])).default({}),
5859
+ lastSession: z5.object({
5860
+ date: z5.string(),
5861
+ summary: z5.string(),
5862
+ lastSkill: z5.string().optional(),
5863
+ pendingTasks: z5.array(z5.string()).optional()
4768
5864
  }).optional()
4769
5865
  });
4770
5866
  var DEFAULT_STATE = {
@@ -4776,30 +5872,30 @@ var DEFAULT_STATE = {
4776
5872
  };
4777
5873
 
4778
5874
  // src/state/state-persistence.ts
4779
- import * as fs8 from "fs";
4780
- import * as path5 from "path";
5875
+ import * as fs10 from "fs";
5876
+ import * as path7 from "path";
4781
5877
 
4782
5878
  // src/state/state-shared.ts
4783
- import * as fs7 from "fs";
4784
- import * as path4 from "path";
5879
+ import * as fs9 from "fs";
5880
+ import * as path6 from "path";
4785
5881
 
4786
5882
  // src/state/stream-resolver.ts
4787
- import * as fs5 from "fs";
4788
- import * as path2 from "path";
5883
+ import * as fs7 from "fs";
5884
+ import * as path4 from "path";
4789
5885
  import { execSync } from "child_process";
4790
5886
 
4791
5887
  // src/state/stream-types.ts
4792
- import { z as z4 } from "zod";
4793
- var StreamInfoSchema = z4.object({
4794
- name: z4.string(),
4795
- branch: z4.string().optional(),
4796
- createdAt: z4.string(),
4797
- lastActiveAt: z4.string()
5888
+ import { z as z6 } from "zod";
5889
+ var StreamInfoSchema = z6.object({
5890
+ name: z6.string(),
5891
+ branch: z6.string().optional(),
5892
+ createdAt: z6.string(),
5893
+ lastActiveAt: z6.string()
4798
5894
  });
4799
- var StreamIndexSchema = z4.object({
4800
- schemaVersion: z4.literal(1),
4801
- activeStream: z4.string().nullable(),
4802
- streams: z4.record(StreamInfoSchema)
5895
+ var StreamIndexSchema = z6.object({
5896
+ schemaVersion: z6.literal(1),
5897
+ activeStream: z6.string().nullable(),
5898
+ streams: z6.record(StreamInfoSchema)
4803
5899
  });
4804
5900
  var DEFAULT_STREAM_INDEX = {
4805
5901
  schemaVersion: 1,
@@ -4827,10 +5923,10 @@ var EVENTS_FILE = "events.jsonl";
4827
5923
  var STREAMS_DIR = "streams";
4828
5924
  var STREAM_NAME_REGEX = /^[a-z0-9][a-z0-9._-]*$/;
4829
5925
  function streamsDir(projectPath) {
4830
- return path2.join(projectPath, HARNESS_DIR, STREAMS_DIR);
5926
+ return path4.join(projectPath, HARNESS_DIR, STREAMS_DIR);
4831
5927
  }
4832
5928
  function indexPath(projectPath) {
4833
- return path2.join(streamsDir(projectPath), INDEX_FILE);
5929
+ return path4.join(streamsDir(projectPath), INDEX_FILE);
4834
5930
  }
4835
5931
  function validateStreamName(name) {
4836
5932
  if (!STREAM_NAME_REGEX.test(name)) {
@@ -4844,11 +5940,11 @@ function validateStreamName(name) {
4844
5940
  }
4845
5941
  async function loadStreamIndex(projectPath) {
4846
5942
  const idxPath = indexPath(projectPath);
4847
- if (!fs5.existsSync(idxPath)) {
5943
+ if (!fs7.existsSync(idxPath)) {
4848
5944
  return Ok({ ...DEFAULT_STREAM_INDEX, streams: {} });
4849
5945
  }
4850
5946
  try {
4851
- const raw = fs5.readFileSync(idxPath, "utf-8");
5947
+ const raw = fs7.readFileSync(idxPath, "utf-8");
4852
5948
  const parsed = JSON.parse(raw);
4853
5949
  const result = StreamIndexSchema.safeParse(parsed);
4854
5950
  if (!result.success) {
@@ -4866,8 +5962,8 @@ async function loadStreamIndex(projectPath) {
4866
5962
  async function saveStreamIndex(projectPath, index) {
4867
5963
  const dir = streamsDir(projectPath);
4868
5964
  try {
4869
- fs5.mkdirSync(dir, { recursive: true });
4870
- fs5.writeFileSync(indexPath(projectPath), JSON.stringify(index, null, 2));
5965
+ fs7.mkdirSync(dir, { recursive: true });
5966
+ fs7.writeFileSync(indexPath(projectPath), JSON.stringify(index, null, 2));
4871
5967
  return Ok(void 0);
4872
5968
  } catch (error) {
4873
5969
  return Err(
@@ -4908,18 +6004,18 @@ async function resolveStreamPath(projectPath, options) {
4908
6004
  )
4909
6005
  );
4910
6006
  }
4911
- return Ok(path2.join(streamsDir(projectPath), options.stream));
6007
+ return Ok(path4.join(streamsDir(projectPath), options.stream));
4912
6008
  }
4913
6009
  const branch = getCurrentBranch(projectPath);
4914
6010
  if (branch && branch !== "main" && branch !== "master") {
4915
6011
  for (const [name, info] of Object.entries(index.streams)) {
4916
6012
  if (info.branch === branch) {
4917
- return Ok(path2.join(streamsDir(projectPath), name));
6013
+ return Ok(path4.join(streamsDir(projectPath), name));
4918
6014
  }
4919
6015
  }
4920
6016
  }
4921
6017
  if (index.activeStream && index.streams[index.activeStream]) {
4922
- return Ok(path2.join(streamsDir(projectPath), index.activeStream));
6018
+ return Ok(path4.join(streamsDir(projectPath), index.activeStream));
4923
6019
  }
4924
6020
  return Err(
4925
6021
  new Error(
@@ -4947,9 +6043,9 @@ async function createStream(projectPath, name, branch) {
4947
6043
  if (index.streams[name]) {
4948
6044
  return Err(new Error(`Stream '${name}' already exists`));
4949
6045
  }
4950
- const streamPath = path2.join(streamsDir(projectPath), name);
6046
+ const streamPath = path4.join(streamsDir(projectPath), name);
4951
6047
  try {
4952
- fs5.mkdirSync(streamPath, { recursive: true });
6048
+ fs7.mkdirSync(streamPath, { recursive: true });
4953
6049
  } catch (error) {
4954
6050
  return Err(
4955
6051
  new Error(
@@ -4990,12 +6086,12 @@ async function archiveStream(projectPath, name) {
4990
6086
  if (!index.streams[name]) {
4991
6087
  return Err(new Error(`Stream '${name}' not found`));
4992
6088
  }
4993
- const streamPath = path2.join(streamsDir(projectPath), name);
4994
- const archiveDir = path2.join(projectPath, HARNESS_DIR, "archive", "streams");
6089
+ const streamPath = path4.join(streamsDir(projectPath), name);
6090
+ const archiveDir = path4.join(projectPath, HARNESS_DIR, "archive", "streams");
4995
6091
  try {
4996
- fs5.mkdirSync(archiveDir, { recursive: true });
6092
+ fs7.mkdirSync(archiveDir, { recursive: true });
4997
6093
  const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
4998
- fs5.renameSync(streamPath, path2.join(archiveDir, `${name}-${date}`));
6094
+ fs7.renameSync(streamPath, path4.join(archiveDir, `${name}-${date}`));
4999
6095
  } catch (error) {
5000
6096
  return Err(
5001
6097
  new Error(
@@ -5017,19 +6113,19 @@ function getStreamForBranch(index, branch) {
5017
6113
  }
5018
6114
  var STATE_FILES = ["state.json", "handoff.json", "learnings.md", "failures.md"];
5019
6115
  async function migrateToStreams(projectPath) {
5020
- const harnessDir = path2.join(projectPath, HARNESS_DIR);
5021
- if (fs5.existsSync(indexPath(projectPath))) {
6116
+ const harnessDir = path4.join(projectPath, HARNESS_DIR);
6117
+ if (fs7.existsSync(indexPath(projectPath))) {
5022
6118
  return Ok(void 0);
5023
6119
  }
5024
- const filesToMove = STATE_FILES.filter((f) => fs5.existsSync(path2.join(harnessDir, f)));
6120
+ const filesToMove = STATE_FILES.filter((f) => fs7.existsSync(path4.join(harnessDir, f)));
5025
6121
  if (filesToMove.length === 0) {
5026
6122
  return Ok(void 0);
5027
6123
  }
5028
- const defaultDir = path2.join(streamsDir(projectPath), "default");
6124
+ const defaultDir = path4.join(streamsDir(projectPath), "default");
5029
6125
  try {
5030
- fs5.mkdirSync(defaultDir, { recursive: true });
6126
+ fs7.mkdirSync(defaultDir, { recursive: true });
5031
6127
  for (const file of filesToMove) {
5032
- fs5.renameSync(path2.join(harnessDir, file), path2.join(defaultDir, file));
6128
+ fs7.renameSync(path4.join(harnessDir, file), path4.join(defaultDir, file));
5033
6129
  }
5034
6130
  } catch (error) {
5035
6131
  return Err(
@@ -5052,8 +6148,8 @@ async function migrateToStreams(projectPath) {
5052
6148
  }
5053
6149
 
5054
6150
  // src/state/session-resolver.ts
5055
- import * as fs6 from "fs";
5056
- import * as path3 from "path";
6151
+ import * as fs8 from "fs";
6152
+ import * as path5 from "path";
5057
6153
  function resolveSessionDir(projectPath, sessionSlug, options) {
5058
6154
  if (!sessionSlug || sessionSlug.trim() === "") {
5059
6155
  return Err(new Error("Session slug must not be empty"));
@@ -5063,26 +6159,26 @@ function resolveSessionDir(projectPath, sessionSlug, options) {
5063
6159
  new Error(`Invalid session slug '${sessionSlug}': must not contain path traversal characters`)
5064
6160
  );
5065
6161
  }
5066
- const sessionDir = path3.join(projectPath, HARNESS_DIR, SESSIONS_DIR, sessionSlug);
6162
+ const sessionDir = path5.join(projectPath, HARNESS_DIR, SESSIONS_DIR, sessionSlug);
5067
6163
  if (options?.create) {
5068
- fs6.mkdirSync(sessionDir, { recursive: true });
6164
+ fs8.mkdirSync(sessionDir, { recursive: true });
5069
6165
  }
5070
6166
  return Ok(sessionDir);
5071
6167
  }
5072
6168
  function updateSessionIndex(projectPath, sessionSlug, description) {
5073
- const sessionsDir = path3.join(projectPath, HARNESS_DIR, SESSIONS_DIR);
5074
- fs6.mkdirSync(sessionsDir, { recursive: true });
5075
- const indexPath2 = path3.join(sessionsDir, SESSION_INDEX_FILE);
6169
+ const sessionsDir = path5.join(projectPath, HARNESS_DIR, SESSIONS_DIR);
6170
+ fs8.mkdirSync(sessionsDir, { recursive: true });
6171
+ const indexPath2 = path5.join(sessionsDir, SESSION_INDEX_FILE);
5076
6172
  const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
5077
6173
  const newLine = `- [${sessionSlug}](${sessionSlug}/summary.md) \u2014 ${description} (${date})`;
5078
- if (!fs6.existsSync(indexPath2)) {
5079
- fs6.writeFileSync(indexPath2, `## Active Sessions
6174
+ if (!fs8.existsSync(indexPath2)) {
6175
+ fs8.writeFileSync(indexPath2, `## Active Sessions
5080
6176
 
5081
6177
  ${newLine}
5082
6178
  `);
5083
6179
  return;
5084
6180
  }
5085
- const content = fs6.readFileSync(indexPath2, "utf-8");
6181
+ const content = fs8.readFileSync(indexPath2, "utf-8");
5086
6182
  const lines = content.split("\n");
5087
6183
  const slugPattern = `- [${sessionSlug}]`;
5088
6184
  const existingIdx = lines.findIndex((l) => l.startsWith(slugPattern));
@@ -5092,7 +6188,7 @@ ${newLine}
5092
6188
  const lastNonEmpty = lines.reduce((last, line, i) => line.trim() !== "" ? i : last, 0);
5093
6189
  lines.splice(lastNonEmpty + 1, 0, newLine);
5094
6190
  }
5095
- fs6.writeFileSync(indexPath2, lines.join("\n"));
6191
+ fs8.writeFileSync(indexPath2, lines.join("\n"));
5096
6192
  }
5097
6193
 
5098
6194
  // src/state/state-shared.ts
@@ -5108,8 +6204,8 @@ async function getStateDir(projectPath, stream, session) {
5108
6204
  const sessionResult = resolveSessionDir(projectPath, session, { create: true });
5109
6205
  return sessionResult;
5110
6206
  }
5111
- const streamsIndexPath = path4.join(projectPath, HARNESS_DIR, "streams", INDEX_FILE);
5112
- const hasStreams = fs7.existsSync(streamsIndexPath);
6207
+ const streamsIndexPath = path6.join(projectPath, HARNESS_DIR, "streams", INDEX_FILE);
6208
+ const hasStreams = fs9.existsSync(streamsIndexPath);
5113
6209
  if (stream || hasStreams) {
5114
6210
  const result = await resolveStreamPath(projectPath, stream ? { stream } : void 0);
5115
6211
  if (result.ok) {
@@ -5119,7 +6215,7 @@ async function getStateDir(projectPath, stream, session) {
5119
6215
  return result;
5120
6216
  }
5121
6217
  }
5122
- return Ok(path4.join(projectPath, HARNESS_DIR));
6218
+ return Ok(path6.join(projectPath, HARNESS_DIR));
5123
6219
  }
5124
6220
 
5125
6221
  // src/state/state-persistence.ts
@@ -5128,11 +6224,11 @@ async function loadState(projectPath, stream, session) {
5128
6224
  const dirResult = await getStateDir(projectPath, stream, session);
5129
6225
  if (!dirResult.ok) return dirResult;
5130
6226
  const stateDir = dirResult.value;
5131
- const statePath = path5.join(stateDir, STATE_FILE);
5132
- if (!fs8.existsSync(statePath)) {
6227
+ const statePath = path7.join(stateDir, STATE_FILE);
6228
+ if (!fs10.existsSync(statePath)) {
5133
6229
  return Ok({ ...DEFAULT_STATE });
5134
6230
  }
5135
- const raw = fs8.readFileSync(statePath, "utf-8");
6231
+ const raw = fs10.readFileSync(statePath, "utf-8");
5136
6232
  const parsed = JSON.parse(raw);
5137
6233
  const result = HarnessStateSchema.safeParse(parsed);
5138
6234
  if (!result.success) {
@@ -5150,9 +6246,9 @@ async function saveState(projectPath, state, stream, session) {
5150
6246
  const dirResult = await getStateDir(projectPath, stream, session);
5151
6247
  if (!dirResult.ok) return dirResult;
5152
6248
  const stateDir = dirResult.value;
5153
- const statePath = path5.join(stateDir, STATE_FILE);
5154
- fs8.mkdirSync(stateDir, { recursive: true });
5155
- fs8.writeFileSync(statePath, JSON.stringify(state, null, 2));
6249
+ const statePath = path7.join(stateDir, STATE_FILE);
6250
+ fs10.mkdirSync(stateDir, { recursive: true });
6251
+ fs10.writeFileSync(statePath, JSON.stringify(state, null, 2));
5156
6252
  return Ok(void 0);
5157
6253
  } catch (error) {
5158
6254
  return Err(
@@ -5162,10 +6258,10 @@ async function saveState(projectPath, state, stream, session) {
5162
6258
  }
5163
6259
 
5164
6260
  // src/state/learnings.ts
5165
- import * as fs9 from "fs";
5166
- import * as path6 from "path";
6261
+ import * as fs11 from "fs";
6262
+ import * as path8 from "path";
5167
6263
  import * as crypto from "crypto";
5168
- function parseFrontmatter(line) {
6264
+ function parseFrontmatter2(line) {
5169
6265
  const match = line.match(/^<!--\s+hash:([a-f0-9]+)(?:\s+tags:([^\s]+))?\s+-->/);
5170
6266
  if (!match) return null;
5171
6267
  const hash = match[1];
@@ -5191,10 +6287,10 @@ function computeContentHash(text) {
5191
6287
  return crypto.createHash("sha256").update(text).digest("hex").slice(0, 16);
5192
6288
  }
5193
6289
  function loadContentHashes(stateDir) {
5194
- const hashesPath = path6.join(stateDir, CONTENT_HASHES_FILE);
5195
- if (!fs9.existsSync(hashesPath)) return {};
6290
+ const hashesPath = path8.join(stateDir, CONTENT_HASHES_FILE);
6291
+ if (!fs11.existsSync(hashesPath)) return {};
5196
6292
  try {
5197
- const raw = fs9.readFileSync(hashesPath, "utf-8");
6293
+ const raw = fs11.readFileSync(hashesPath, "utf-8");
5198
6294
  const parsed = JSON.parse(raw);
5199
6295
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
5200
6296
  return parsed;
@@ -5203,13 +6299,13 @@ function loadContentHashes(stateDir) {
5203
6299
  }
5204
6300
  }
5205
6301
  function saveContentHashes(stateDir, index) {
5206
- const hashesPath = path6.join(stateDir, CONTENT_HASHES_FILE);
5207
- fs9.writeFileSync(hashesPath, JSON.stringify(index, null, 2) + "\n");
6302
+ const hashesPath = path8.join(stateDir, CONTENT_HASHES_FILE);
6303
+ fs11.writeFileSync(hashesPath, JSON.stringify(index, null, 2) + "\n");
5208
6304
  }
5209
6305
  function rebuildContentHashes(stateDir) {
5210
- const learningsPath = path6.join(stateDir, LEARNINGS_FILE);
5211
- if (!fs9.existsSync(learningsPath)) return {};
5212
- const content = fs9.readFileSync(learningsPath, "utf-8");
6306
+ const learningsPath = path8.join(stateDir, LEARNINGS_FILE);
6307
+ if (!fs11.existsSync(learningsPath)) return {};
6308
+ const content = fs11.readFileSync(learningsPath, "utf-8");
5213
6309
  const lines = content.split("\n");
5214
6310
  const index = {};
5215
6311
  for (let i = 0; i < lines.length; i++) {
@@ -5252,18 +6348,18 @@ async function appendLearning(projectPath, learning, skillName, outcome, stream,
5252
6348
  const dirResult = await getStateDir(projectPath, stream, session);
5253
6349
  if (!dirResult.ok) return dirResult;
5254
6350
  const stateDir = dirResult.value;
5255
- const learningsPath = path6.join(stateDir, LEARNINGS_FILE);
5256
- fs9.mkdirSync(stateDir, { recursive: true });
6351
+ const learningsPath = path8.join(stateDir, LEARNINGS_FILE);
6352
+ fs11.mkdirSync(stateDir, { recursive: true });
5257
6353
  const normalizedContent = normalizeLearningContent(learning);
5258
6354
  const contentHash = computeContentHash(normalizedContent);
5259
- const hashesPath = path6.join(stateDir, CONTENT_HASHES_FILE);
6355
+ const hashesPath = path8.join(stateDir, CONTENT_HASHES_FILE);
5260
6356
  let contentHashes;
5261
- if (fs9.existsSync(hashesPath)) {
6357
+ if (fs11.existsSync(hashesPath)) {
5262
6358
  contentHashes = loadContentHashes(stateDir);
5263
- if (Object.keys(contentHashes).length === 0 && fs9.existsSync(learningsPath)) {
6359
+ if (Object.keys(contentHashes).length === 0 && fs11.existsSync(learningsPath)) {
5264
6360
  contentHashes = rebuildContentHashes(stateDir);
5265
6361
  }
5266
- } else if (fs9.existsSync(learningsPath)) {
6362
+ } else if (fs11.existsSync(learningsPath)) {
5267
6363
  contentHashes = rebuildContentHashes(stateDir);
5268
6364
  } else {
5269
6365
  contentHashes = {};
@@ -5291,14 +6387,14 @@ ${frontmatter}
5291
6387
  ${bulletLine}
5292
6388
  `;
5293
6389
  let existingLineCount;
5294
- if (!fs9.existsSync(learningsPath)) {
5295
- fs9.writeFileSync(learningsPath, `# Learnings
6390
+ if (!fs11.existsSync(learningsPath)) {
6391
+ fs11.writeFileSync(learningsPath, `# Learnings
5296
6392
  ${entry}`);
5297
6393
  existingLineCount = 1;
5298
6394
  } else {
5299
- const existingContent = fs9.readFileSync(learningsPath, "utf-8");
6395
+ const existingContent = fs11.readFileSync(learningsPath, "utf-8");
5300
6396
  existingLineCount = existingContent.split("\n").length;
5301
- fs9.appendFileSync(learningsPath, entry);
6397
+ fs11.appendFileSync(learningsPath, entry);
5302
6398
  }
5303
6399
  const bulletLine_lineNum = existingLineCount + 2;
5304
6400
  contentHashes[contentHash] = { date: timestamp ?? "", line: bulletLine_lineNum };
@@ -5412,18 +6508,18 @@ async function loadIndexEntries(projectPath, skillName, stream, session) {
5412
6508
  const dirResult = await getStateDir(projectPath, stream, session);
5413
6509
  if (!dirResult.ok) return dirResult;
5414
6510
  const stateDir = dirResult.value;
5415
- const learningsPath = path6.join(stateDir, LEARNINGS_FILE);
5416
- if (!fs9.existsSync(learningsPath)) {
6511
+ const learningsPath = path8.join(stateDir, LEARNINGS_FILE);
6512
+ if (!fs11.existsSync(learningsPath)) {
5417
6513
  return Ok([]);
5418
6514
  }
5419
- const content = fs9.readFileSync(learningsPath, "utf-8");
6515
+ const content = fs11.readFileSync(learningsPath, "utf-8");
5420
6516
  const lines = content.split("\n");
5421
6517
  const indexEntries = [];
5422
6518
  let pendingFrontmatter = null;
5423
6519
  let currentBlock = [];
5424
6520
  for (const line of lines) {
5425
6521
  if (line.startsWith("# ")) continue;
5426
- const fm = parseFrontmatter(line);
6522
+ const fm = parseFrontmatter2(line);
5427
6523
  if (fm) {
5428
6524
  pendingFrontmatter = fm;
5429
6525
  continue;
@@ -5474,18 +6570,18 @@ async function loadRelevantLearnings(projectPath, skillName, stream, session) {
5474
6570
  const dirResult = await getStateDir(projectPath, stream, session);
5475
6571
  if (!dirResult.ok) return dirResult;
5476
6572
  const stateDir = dirResult.value;
5477
- const learningsPath = path6.join(stateDir, LEARNINGS_FILE);
5478
- if (!fs9.existsSync(learningsPath)) {
6573
+ const learningsPath = path8.join(stateDir, LEARNINGS_FILE);
6574
+ if (!fs11.existsSync(learningsPath)) {
5479
6575
  return Ok([]);
5480
6576
  }
5481
- const stats = fs9.statSync(learningsPath);
6577
+ const stats = fs11.statSync(learningsPath);
5482
6578
  const cacheKey = learningsPath;
5483
6579
  const cached = learningsCacheMap.get(cacheKey);
5484
6580
  let entries;
5485
6581
  if (cached && cached.mtimeMs === stats.mtimeMs) {
5486
6582
  entries = cached.entries;
5487
6583
  } else {
5488
- const content = fs9.readFileSync(learningsPath, "utf-8");
6584
+ const content = fs11.readFileSync(learningsPath, "utf-8");
5489
6585
  const lines = content.split("\n");
5490
6586
  entries = [];
5491
6587
  let currentBlock = [];
@@ -5527,16 +6623,16 @@ async function archiveLearnings(projectPath, entries, stream) {
5527
6623
  const dirResult = await getStateDir(projectPath, stream);
5528
6624
  if (!dirResult.ok) return dirResult;
5529
6625
  const stateDir = dirResult.value;
5530
- const archiveDir = path6.join(stateDir, "learnings-archive");
5531
- fs9.mkdirSync(archiveDir, { recursive: true });
6626
+ const archiveDir = path8.join(stateDir, "learnings-archive");
6627
+ fs11.mkdirSync(archiveDir, { recursive: true });
5532
6628
  const now = /* @__PURE__ */ new Date();
5533
6629
  const yearMonth = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
5534
- const archivePath = path6.join(archiveDir, `${yearMonth}.md`);
6630
+ const archivePath = path8.join(archiveDir, `${yearMonth}.md`);
5535
6631
  const archiveContent = entries.join("\n\n") + "\n";
5536
- if (fs9.existsSync(archivePath)) {
5537
- fs9.appendFileSync(archivePath, "\n" + archiveContent);
6632
+ if (fs11.existsSync(archivePath)) {
6633
+ fs11.appendFileSync(archivePath, "\n" + archiveContent);
5538
6634
  } else {
5539
- fs9.writeFileSync(archivePath, `# Learnings Archive
6635
+ fs11.writeFileSync(archivePath, `# Learnings Archive
5540
6636
 
5541
6637
  ${archiveContent}`);
5542
6638
  }
@@ -5554,8 +6650,8 @@ async function pruneLearnings(projectPath, stream) {
5554
6650
  const dirResult = await getStateDir(projectPath, stream);
5555
6651
  if (!dirResult.ok) return dirResult;
5556
6652
  const stateDir = dirResult.value;
5557
- const learningsPath = path6.join(stateDir, LEARNINGS_FILE);
5558
- if (!fs9.existsSync(learningsPath)) {
6653
+ const learningsPath = path8.join(stateDir, LEARNINGS_FILE);
6654
+ if (!fs11.existsSync(learningsPath)) {
5559
6655
  return Ok({ kept: 0, archived: 0, patterns: [] });
5560
6656
  }
5561
6657
  const loadResult = await loadRelevantLearnings(projectPath, void 0, stream);
@@ -5586,7 +6682,7 @@ async function pruneLearnings(projectPath, stream) {
5586
6682
  if (!archiveResult.ok) return archiveResult;
5587
6683
  }
5588
6684
  const newContent = "# Learnings\n\n" + toKeep.join("\n\n") + "\n";
5589
- fs9.writeFileSync(learningsPath, newContent);
6685
+ fs11.writeFileSync(learningsPath, newContent);
5590
6686
  learningsCacheMap.delete(learningsPath);
5591
6687
  return Ok({
5592
6688
  kept: toKeep.length,
@@ -5631,19 +6727,19 @@ async function promoteSessionLearnings(projectPath, sessionSlug, stream) {
5631
6727
  const dirResult = await getStateDir(projectPath, stream);
5632
6728
  if (!dirResult.ok) return dirResult;
5633
6729
  const stateDir = dirResult.value;
5634
- const globalPath = path6.join(stateDir, LEARNINGS_FILE);
5635
- const existingGlobal = fs9.existsSync(globalPath) ? fs9.readFileSync(globalPath, "utf-8") : "";
6730
+ const globalPath = path8.join(stateDir, LEARNINGS_FILE);
6731
+ const existingGlobal = fs11.existsSync(globalPath) ? fs11.readFileSync(globalPath, "utf-8") : "";
5636
6732
  const newEntries = toPromote.filter((entry) => !existingGlobal.includes(entry.trim()));
5637
6733
  if (newEntries.length === 0) {
5638
6734
  return Ok({ promoted: 0, skipped: skipped + toPromote.length });
5639
6735
  }
5640
6736
  const promotedContent = newEntries.join("\n\n") + "\n";
5641
6737
  if (!existingGlobal) {
5642
- fs9.writeFileSync(globalPath, `# Learnings
6738
+ fs11.writeFileSync(globalPath, `# Learnings
5643
6739
 
5644
6740
  ${promotedContent}`);
5645
6741
  } else {
5646
- fs9.appendFileSync(globalPath, "\n\n" + promotedContent);
6742
+ fs11.appendFileSync(globalPath, "\n\n" + promotedContent);
5647
6743
  }
5648
6744
  learningsCacheMap.delete(globalPath);
5649
6745
  return Ok({
@@ -5665,8 +6761,8 @@ async function countLearningEntries(projectPath, stream) {
5665
6761
  }
5666
6762
 
5667
6763
  // src/state/failures.ts
5668
- import * as fs10 from "fs";
5669
- import * as path7 from "path";
6764
+ import * as fs12 from "fs";
6765
+ import * as path9 from "path";
5670
6766
  var failuresCacheMap = /* @__PURE__ */ new Map();
5671
6767
  function clearFailuresCache() {
5672
6768
  failuresCacheMap.clear();
@@ -5677,17 +6773,17 @@ async function appendFailure(projectPath, description, skillName, type, stream,
5677
6773
  const dirResult = await getStateDir(projectPath, stream, session);
5678
6774
  if (!dirResult.ok) return dirResult;
5679
6775
  const stateDir = dirResult.value;
5680
- const failuresPath = path7.join(stateDir, FAILURES_FILE);
5681
- fs10.mkdirSync(stateDir, { recursive: true });
6776
+ const failuresPath = path9.join(stateDir, FAILURES_FILE);
6777
+ fs12.mkdirSync(stateDir, { recursive: true });
5682
6778
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
5683
6779
  const entry = `
5684
6780
  - **${timestamp} [skill:${skillName}] [type:${type}]:** ${description}
5685
6781
  `;
5686
- if (!fs10.existsSync(failuresPath)) {
5687
- fs10.writeFileSync(failuresPath, `# Failures
6782
+ if (!fs12.existsSync(failuresPath)) {
6783
+ fs12.writeFileSync(failuresPath, `# Failures
5688
6784
  ${entry}`);
5689
6785
  } else {
5690
- fs10.appendFileSync(failuresPath, entry);
6786
+ fs12.appendFileSync(failuresPath, entry);
5691
6787
  }
5692
6788
  failuresCacheMap.delete(failuresPath);
5693
6789
  return Ok(void 0);
@@ -5704,17 +6800,17 @@ async function loadFailures(projectPath, stream, session) {
5704
6800
  const dirResult = await getStateDir(projectPath, stream, session);
5705
6801
  if (!dirResult.ok) return dirResult;
5706
6802
  const stateDir = dirResult.value;
5707
- const failuresPath = path7.join(stateDir, FAILURES_FILE);
5708
- if (!fs10.existsSync(failuresPath)) {
6803
+ const failuresPath = path9.join(stateDir, FAILURES_FILE);
6804
+ if (!fs12.existsSync(failuresPath)) {
5709
6805
  return Ok([]);
5710
6806
  }
5711
- const stats = fs10.statSync(failuresPath);
6807
+ const stats = fs12.statSync(failuresPath);
5712
6808
  const cacheKey = failuresPath;
5713
6809
  const cached = failuresCacheMap.get(cacheKey);
5714
6810
  if (cached && cached.mtimeMs === stats.mtimeMs) {
5715
6811
  return Ok(cached.entries);
5716
6812
  }
5717
- const content = fs10.readFileSync(failuresPath, "utf-8");
6813
+ const content = fs12.readFileSync(failuresPath, "utf-8");
5718
6814
  const entries = [];
5719
6815
  for (const line of content.split("\n")) {
5720
6816
  const match = line.match(FAILURE_LINE_REGEX);
@@ -5743,20 +6839,20 @@ async function archiveFailures(projectPath, stream, session) {
5743
6839
  const dirResult = await getStateDir(projectPath, stream, session);
5744
6840
  if (!dirResult.ok) return dirResult;
5745
6841
  const stateDir = dirResult.value;
5746
- const failuresPath = path7.join(stateDir, FAILURES_FILE);
5747
- if (!fs10.existsSync(failuresPath)) {
6842
+ const failuresPath = path9.join(stateDir, FAILURES_FILE);
6843
+ if (!fs12.existsSync(failuresPath)) {
5748
6844
  return Ok(void 0);
5749
6845
  }
5750
- const archiveDir = path7.join(stateDir, "archive");
5751
- fs10.mkdirSync(archiveDir, { recursive: true });
6846
+ const archiveDir = path9.join(stateDir, "archive");
6847
+ fs12.mkdirSync(archiveDir, { recursive: true });
5752
6848
  const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
5753
6849
  let archiveName = `failures-${date}.md`;
5754
6850
  let counter = 2;
5755
- while (fs10.existsSync(path7.join(archiveDir, archiveName))) {
6851
+ while (fs12.existsSync(path9.join(archiveDir, archiveName))) {
5756
6852
  archiveName = `failures-${date}-${counter}.md`;
5757
6853
  counter++;
5758
6854
  }
5759
- fs10.renameSync(failuresPath, path7.join(archiveDir, archiveName));
6855
+ fs12.renameSync(failuresPath, path9.join(archiveDir, archiveName));
5760
6856
  failuresCacheMap.delete(failuresPath);
5761
6857
  return Ok(void 0);
5762
6858
  } catch (error) {
@@ -5769,16 +6865,16 @@ async function archiveFailures(projectPath, stream, session) {
5769
6865
  }
5770
6866
 
5771
6867
  // src/state/handoff.ts
5772
- import * as fs11 from "fs";
5773
- import * as path8 from "path";
6868
+ import * as fs13 from "fs";
6869
+ import * as path10 from "path";
5774
6870
  async function saveHandoff(projectPath, handoff, stream, session) {
5775
6871
  try {
5776
6872
  const dirResult = await getStateDir(projectPath, stream, session);
5777
6873
  if (!dirResult.ok) return dirResult;
5778
6874
  const stateDir = dirResult.value;
5779
- const handoffPath = path8.join(stateDir, HANDOFF_FILE);
5780
- fs11.mkdirSync(stateDir, { recursive: true });
5781
- fs11.writeFileSync(handoffPath, JSON.stringify(handoff, null, 2));
6875
+ const handoffPath = path10.join(stateDir, HANDOFF_FILE);
6876
+ fs13.mkdirSync(stateDir, { recursive: true });
6877
+ fs13.writeFileSync(handoffPath, JSON.stringify(handoff, null, 2));
5782
6878
  return Ok(void 0);
5783
6879
  } catch (error) {
5784
6880
  return Err(
@@ -5791,11 +6887,11 @@ async function loadHandoff(projectPath, stream, session) {
5791
6887
  const dirResult = await getStateDir(projectPath, stream, session);
5792
6888
  if (!dirResult.ok) return dirResult;
5793
6889
  const stateDir = dirResult.value;
5794
- const handoffPath = path8.join(stateDir, HANDOFF_FILE);
5795
- if (!fs11.existsSync(handoffPath)) {
6890
+ const handoffPath = path10.join(stateDir, HANDOFF_FILE);
6891
+ if (!fs13.existsSync(handoffPath)) {
5796
6892
  return Ok(null);
5797
6893
  }
5798
- const raw = fs11.readFileSync(handoffPath, "utf-8");
6894
+ const raw = fs13.readFileSync(handoffPath, "utf-8");
5799
6895
  const parsed = JSON.parse(raw);
5800
6896
  const result = HandoffSchema.safeParse(parsed);
5801
6897
  if (!result.success) {
@@ -5810,33 +6906,33 @@ async function loadHandoff(projectPath, stream, session) {
5810
6906
  }
5811
6907
 
5812
6908
  // src/state/mechanical-gate.ts
5813
- import * as fs12 from "fs";
5814
- import * as path9 from "path";
6909
+ import * as fs14 from "fs";
6910
+ import * as path11 from "path";
5815
6911
  import { execSync as execSync2 } from "child_process";
5816
6912
  var SAFE_GATE_COMMAND = /^(?:npm|pnpm|yarn)\s+(?:test|run\s+[\w.-]+|run-script\s+[\w.-]+)$|^go\s+(?:test|build|vet|fmt)\s+[\w./ -]+$|^(?:python|python3)\s+-m\s+[\w.-]+$|^make\s+[\w.-]+$|^cargo\s+(?:test|build|check|clippy)(?:\s+[\w./ -]+)?$|^(?:gradle|mvn)\s+[\w:.-]+$/;
5817
6913
  function loadChecksFromConfig(gateConfigPath) {
5818
- if (!fs12.existsSync(gateConfigPath)) return [];
5819
- const raw = JSON.parse(fs12.readFileSync(gateConfigPath, "utf-8"));
6914
+ if (!fs14.existsSync(gateConfigPath)) return [];
6915
+ const raw = JSON.parse(fs14.readFileSync(gateConfigPath, "utf-8"));
5820
6916
  const config = GateConfigSchema.safeParse(raw);
5821
6917
  if (config.success && config.data.checks) return config.data.checks;
5822
6918
  return [];
5823
6919
  }
5824
6920
  function discoverChecksFromProject(projectPath) {
5825
6921
  const checks = [];
5826
- const packageJsonPath = path9.join(projectPath, "package.json");
5827
- if (fs12.existsSync(packageJsonPath)) {
5828
- const pkg = JSON.parse(fs12.readFileSync(packageJsonPath, "utf-8"));
6922
+ const packageJsonPath = path11.join(projectPath, "package.json");
6923
+ if (fs14.existsSync(packageJsonPath)) {
6924
+ const pkg = JSON.parse(fs14.readFileSync(packageJsonPath, "utf-8"));
5829
6925
  const scripts = pkg.scripts || {};
5830
6926
  if (scripts.test) checks.push({ name: "test", command: "npm test" });
5831
6927
  if (scripts.lint) checks.push({ name: "lint", command: "npm run lint" });
5832
6928
  if (scripts.typecheck) checks.push({ name: "typecheck", command: "npm run typecheck" });
5833
6929
  if (scripts.build) checks.push({ name: "build", command: "npm run build" });
5834
6930
  }
5835
- if (fs12.existsSync(path9.join(projectPath, "go.mod"))) {
6931
+ if (fs14.existsSync(path11.join(projectPath, "go.mod"))) {
5836
6932
  checks.push({ name: "test", command: "go test ./..." });
5837
6933
  checks.push({ name: "build", command: "go build ./..." });
5838
6934
  }
5839
- if (fs12.existsSync(path9.join(projectPath, "pyproject.toml")) || fs12.existsSync(path9.join(projectPath, "setup.py"))) {
6935
+ if (fs14.existsSync(path11.join(projectPath, "pyproject.toml")) || fs14.existsSync(path11.join(projectPath, "setup.py"))) {
5840
6936
  checks.push({ name: "test", command: "python -m pytest" });
5841
6937
  }
5842
6938
  return checks;
@@ -5876,8 +6972,8 @@ function executeCheck(check, projectPath) {
5876
6972
  }
5877
6973
  }
5878
6974
  async function runMechanicalGate(projectPath) {
5879
- const harnessDir = path9.join(projectPath, HARNESS_DIR);
5880
- const gateConfigPath = path9.join(harnessDir, GATE_CONFIG_FILE);
6975
+ const harnessDir = path11.join(projectPath, HARNESS_DIR);
6976
+ const gateConfigPath = path11.join(harnessDir, GATE_CONFIG_FILE);
5881
6977
  try {
5882
6978
  let checks = loadChecksFromConfig(gateConfigPath);
5883
6979
  if (checks.length === 0) {
@@ -5898,8 +6994,8 @@ async function runMechanicalGate(projectPath) {
5898
6994
  }
5899
6995
 
5900
6996
  // src/state/session-summary.ts
5901
- import * as fs13 from "fs";
5902
- import * as path10 from "path";
6997
+ import * as fs15 from "fs";
6998
+ import * as path12 from "path";
5903
6999
  function formatSummary(data) {
5904
7000
  const lines = [
5905
7001
  "## Session Summary",
@@ -5937,9 +7033,9 @@ function writeSessionSummary(projectPath, sessionSlug, data) {
5937
7033
  const dirResult = resolveSessionDir(projectPath, sessionSlug, { create: true });
5938
7034
  if (!dirResult.ok) return dirResult;
5939
7035
  const sessionDir = dirResult.value;
5940
- const summaryPath = path10.join(sessionDir, SUMMARY_FILE);
7036
+ const summaryPath = path12.join(sessionDir, SUMMARY_FILE);
5941
7037
  const content = formatSummary(data);
5942
- fs13.writeFileSync(summaryPath, content);
7038
+ fs15.writeFileSync(summaryPath, content);
5943
7039
  const description = deriveIndexDescription(data);
5944
7040
  updateSessionIndex(projectPath, sessionSlug, description);
5945
7041
  return Ok(void 0);
@@ -5956,11 +7052,11 @@ function loadSessionSummary(projectPath, sessionSlug) {
5956
7052
  const dirResult = resolveSessionDir(projectPath, sessionSlug);
5957
7053
  if (!dirResult.ok) return dirResult;
5958
7054
  const sessionDir = dirResult.value;
5959
- const summaryPath = path10.join(sessionDir, SUMMARY_FILE);
5960
- if (!fs13.existsSync(summaryPath)) {
7055
+ const summaryPath = path12.join(sessionDir, SUMMARY_FILE);
7056
+ if (!fs15.existsSync(summaryPath)) {
5961
7057
  return Ok(null);
5962
7058
  }
5963
- const content = fs13.readFileSync(summaryPath, "utf-8");
7059
+ const content = fs15.readFileSync(summaryPath, "utf-8");
5964
7060
  return Ok(content);
5965
7061
  } catch (error) {
5966
7062
  return Err(
@@ -5972,11 +7068,11 @@ function loadSessionSummary(projectPath, sessionSlug) {
5972
7068
  }
5973
7069
  function listActiveSessions(projectPath) {
5974
7070
  try {
5975
- const indexPath2 = path10.join(projectPath, HARNESS_DIR, SESSIONS_DIR, SESSION_INDEX_FILE);
5976
- if (!fs13.existsSync(indexPath2)) {
7071
+ const indexPath2 = path12.join(projectPath, HARNESS_DIR, SESSIONS_DIR, SESSION_INDEX_FILE);
7072
+ if (!fs15.existsSync(indexPath2)) {
5977
7073
  return Ok(null);
5978
7074
  }
5979
- const content = fs13.readFileSync(indexPath2, "utf-8");
7075
+ const content = fs15.readFileSync(indexPath2, "utf-8");
5980
7076
  return Ok(content);
5981
7077
  } catch (error) {
5982
7078
  return Err(
@@ -5988,8 +7084,8 @@ function listActiveSessions(projectPath) {
5988
7084
  }
5989
7085
 
5990
7086
  // src/state/session-sections.ts
5991
- import * as fs14 from "fs";
5992
- import * as path11 from "path";
7087
+ import * as fs16 from "fs";
7088
+ import * as path13 from "path";
5993
7089
  import { SESSION_SECTION_NAMES } from "@harness-engineering/types";
5994
7090
  function emptySections() {
5995
7091
  const sections = {};
@@ -6002,12 +7098,12 @@ async function loadSessionState(projectPath, sessionSlug) {
6002
7098
  const dirResult = resolveSessionDir(projectPath, sessionSlug);
6003
7099
  if (!dirResult.ok) return dirResult;
6004
7100
  const sessionDir = dirResult.value;
6005
- const filePath = path11.join(sessionDir, SESSION_STATE_FILE);
6006
- if (!fs14.existsSync(filePath)) {
7101
+ const filePath = path13.join(sessionDir, SESSION_STATE_FILE);
7102
+ if (!fs16.existsSync(filePath)) {
6007
7103
  return Ok(emptySections());
6008
7104
  }
6009
7105
  try {
6010
- const raw = fs14.readFileSync(filePath, "utf-8");
7106
+ const raw = fs16.readFileSync(filePath, "utf-8");
6011
7107
  const parsed = JSON.parse(raw);
6012
7108
  const sections = emptySections();
6013
7109
  for (const name of SESSION_SECTION_NAMES) {
@@ -6028,9 +7124,9 @@ async function saveSessionState(projectPath, sessionSlug, sections) {
6028
7124
  const dirResult = resolveSessionDir(projectPath, sessionSlug, { create: true });
6029
7125
  if (!dirResult.ok) return dirResult;
6030
7126
  const sessionDir = dirResult.value;
6031
- const filePath = path11.join(sessionDir, SESSION_STATE_FILE);
7127
+ const filePath = path13.join(sessionDir, SESSION_STATE_FILE);
6032
7128
  try {
6033
- fs14.writeFileSync(filePath, JSON.stringify(sections, null, 2));
7129
+ fs16.writeFileSync(filePath, JSON.stringify(sections, null, 2));
6034
7130
  return Ok(void 0);
6035
7131
  } catch (error) {
6036
7132
  return Err(
@@ -6083,33 +7179,33 @@ function generateEntryId() {
6083
7179
  return `${timestamp}-${random}`;
6084
7180
  }
6085
7181
 
6086
- // src/state/session-archive.ts
6087
- import * as fs15 from "fs";
6088
- import * as path12 from "path";
7182
+ // src/state/session-archive.ts
7183
+ import * as fs17 from "fs";
7184
+ import * as path14 from "path";
6089
7185
  async function archiveSession(projectPath, sessionSlug) {
6090
7186
  const dirResult = resolveSessionDir(projectPath, sessionSlug);
6091
7187
  if (!dirResult.ok) return dirResult;
6092
7188
  const sessionDir = dirResult.value;
6093
- if (!fs15.existsSync(sessionDir)) {
7189
+ if (!fs17.existsSync(sessionDir)) {
6094
7190
  return Err(new Error(`Session '${sessionSlug}' not found at ${sessionDir}`));
6095
7191
  }
6096
- const archiveBase = path12.join(projectPath, HARNESS_DIR, ARCHIVE_DIR, "sessions");
7192
+ const archiveBase = path14.join(projectPath, HARNESS_DIR, ARCHIVE_DIR, "sessions");
6097
7193
  try {
6098
- fs15.mkdirSync(archiveBase, { recursive: true });
7194
+ fs17.mkdirSync(archiveBase, { recursive: true });
6099
7195
  const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
6100
7196
  let archiveName = `${sessionSlug}-${date}`;
6101
7197
  let counter = 1;
6102
- while (fs15.existsSync(path12.join(archiveBase, archiveName))) {
7198
+ while (fs17.existsSync(path14.join(archiveBase, archiveName))) {
6103
7199
  archiveName = `${sessionSlug}-${date}-${counter}`;
6104
7200
  counter++;
6105
7201
  }
6106
- const dest = path12.join(archiveBase, archiveName);
7202
+ const dest = path14.join(archiveBase, archiveName);
6107
7203
  try {
6108
- fs15.renameSync(sessionDir, dest);
7204
+ fs17.renameSync(sessionDir, dest);
6109
7205
  } catch (renameErr) {
6110
7206
  if (renameErr instanceof Error && "code" in renameErr && renameErr.code === "EXDEV") {
6111
- fs15.cpSync(sessionDir, dest, { recursive: true });
6112
- fs15.rmSync(sessionDir, { recursive: true });
7207
+ fs17.cpSync(sessionDir, dest, { recursive: true });
7208
+ fs17.rmSync(sessionDir, { recursive: true });
6113
7209
  } else {
6114
7210
  throw renameErr;
6115
7211
  }
@@ -6125,18 +7221,18 @@ async function archiveSession(projectPath, sessionSlug) {
6125
7221
  }
6126
7222
 
6127
7223
  // src/state/events.ts
6128
- import * as fs16 from "fs";
6129
- import * as path13 from "path";
6130
- import { z as z5 } from "zod";
6131
- var SkillEventSchema = z5.object({
6132
- timestamp: z5.string(),
6133
- skill: z5.string(),
6134
- session: z5.string().optional(),
6135
- type: z5.enum(["phase_transition", "decision", "gate_result", "handoff", "error", "checkpoint"]),
6136
- summary: z5.string(),
6137
- data: z5.record(z5.unknown()).optional(),
6138
- refs: z5.array(z5.string()).optional(),
6139
- contentHash: z5.string().optional()
7224
+ import * as fs18 from "fs";
7225
+ import * as path15 from "path";
7226
+ import { z as z7 } from "zod";
7227
+ var SkillEventSchema = z7.object({
7228
+ timestamp: z7.string(),
7229
+ skill: z7.string(),
7230
+ session: z7.string().optional(),
7231
+ type: z7.enum(["phase_transition", "decision", "gate_result", "handoff", "error", "checkpoint"]),
7232
+ summary: z7.string(),
7233
+ data: z7.record(z7.unknown()).optional(),
7234
+ refs: z7.array(z7.string()).optional(),
7235
+ contentHash: z7.string().optional()
6140
7236
  });
6141
7237
  function computeEventHash(event, session) {
6142
7238
  const identity = `${event.skill}|${event.type}|${event.summary}|${session ?? ""}`;
@@ -6147,8 +7243,8 @@ function loadKnownHashes(eventsPath) {
6147
7243
  const cached = knownHashesCache.get(eventsPath);
6148
7244
  if (cached) return cached;
6149
7245
  const hashes = /* @__PURE__ */ new Set();
6150
- if (fs16.existsSync(eventsPath)) {
6151
- const content = fs16.readFileSync(eventsPath, "utf-8");
7246
+ if (fs18.existsSync(eventsPath)) {
7247
+ const content = fs18.readFileSync(eventsPath, "utf-8");
6152
7248
  const lines = content.split("\n").filter((line) => line.trim() !== "");
6153
7249
  for (const line of lines) {
6154
7250
  try {
@@ -6171,8 +7267,8 @@ async function emitEvent(projectPath, event, options) {
6171
7267
  const dirResult = await getStateDir(projectPath, options?.stream, options?.session);
6172
7268
  if (!dirResult.ok) return dirResult;
6173
7269
  const stateDir = dirResult.value;
6174
- const eventsPath = path13.join(stateDir, EVENTS_FILE);
6175
- fs16.mkdirSync(stateDir, { recursive: true });
7270
+ const eventsPath = path15.join(stateDir, EVENTS_FILE);
7271
+ fs18.mkdirSync(stateDir, { recursive: true });
6176
7272
  const contentHash = computeEventHash(event, options?.session);
6177
7273
  const knownHashes = loadKnownHashes(eventsPath);
6178
7274
  if (knownHashes.has(contentHash)) {
@@ -6186,7 +7282,7 @@ async function emitEvent(projectPath, event, options) {
6186
7282
  if (options?.session) {
6187
7283
  fullEvent.session = options.session;
6188
7284
  }
6189
- fs16.appendFileSync(eventsPath, JSON.stringify(fullEvent) + "\n");
7285
+ fs18.appendFileSync(eventsPath, JSON.stringify(fullEvent) + "\n");
6190
7286
  knownHashes.add(contentHash);
6191
7287
  return Ok({ written: true });
6192
7288
  } catch (error) {
@@ -6200,11 +7296,11 @@ async function loadEvents(projectPath, options) {
6200
7296
  const dirResult = await getStateDir(projectPath, options?.stream, options?.session);
6201
7297
  if (!dirResult.ok) return dirResult;
6202
7298
  const stateDir = dirResult.value;
6203
- const eventsPath = path13.join(stateDir, EVENTS_FILE);
6204
- if (!fs16.existsSync(eventsPath)) {
7299
+ const eventsPath = path15.join(stateDir, EVENTS_FILE);
7300
+ if (!fs18.existsSync(eventsPath)) {
6205
7301
  return Ok([]);
6206
7302
  }
6207
- const content = fs16.readFileSync(eventsPath, "utf-8");
7303
+ const content = fs18.readFileSync(eventsPath, "utf-8");
6208
7304
  const lines = content.split("\n").filter((line) => line.trim() !== "");
6209
7305
  const events = [];
6210
7306
  for (const line of lines) {
@@ -6418,7 +7514,7 @@ async function runMultiTurnPipeline(initialContext, turnExecutor, options) {
6418
7514
  }
6419
7515
 
6420
7516
  // src/security/scanner.ts
6421
- import * as fs18 from "fs/promises";
7517
+ import * as fs20 from "fs/promises";
6422
7518
  import { minimatch as minimatch4 } from "minimatch";
6423
7519
 
6424
7520
  // src/security/rules/registry.ts
@@ -6450,7 +7546,7 @@ var RuleRegistry = class {
6450
7546
  };
6451
7547
 
6452
7548
  // src/security/config.ts
6453
- import { z as z6 } from "zod";
7549
+ import { z as z8 } from "zod";
6454
7550
 
6455
7551
  // src/security/types.ts
6456
7552
  var DEFAULT_SECURITY_CONFIG = {
@@ -6461,19 +7557,19 @@ var DEFAULT_SECURITY_CONFIG = {
6461
7557
  };
6462
7558
 
6463
7559
  // src/security/config.ts
6464
- var RuleOverrideSchema = z6.enum(["off", "error", "warning", "info"]);
6465
- var SecurityConfigSchema = z6.object({
6466
- enabled: z6.boolean().default(true),
6467
- strict: z6.boolean().default(false),
6468
- rules: z6.record(z6.string(), RuleOverrideSchema).optional().default({}),
6469
- exclude: z6.array(z6.string()).optional().default(["**/node_modules/**", "**/dist/**", "**/*.test.ts", "**/fixtures/**"]),
6470
- external: z6.object({
6471
- semgrep: z6.object({
6472
- enabled: z6.union([z6.literal("auto"), z6.boolean()]).default("auto"),
6473
- rulesets: z6.array(z6.string()).optional()
7560
+ var RuleOverrideSchema = z8.enum(["off", "error", "warning", "info"]);
7561
+ var SecurityConfigSchema = z8.object({
7562
+ enabled: z8.boolean().default(true),
7563
+ strict: z8.boolean().default(false),
7564
+ rules: z8.record(z8.string(), RuleOverrideSchema).optional().default({}),
7565
+ exclude: z8.array(z8.string()).optional().default(["**/node_modules/**", "**/dist/**", "**/*.test.ts", "**/fixtures/**"]),
7566
+ external: z8.object({
7567
+ semgrep: z8.object({
7568
+ enabled: z8.union([z8.literal("auto"), z8.boolean()]).default("auto"),
7569
+ rulesets: z8.array(z8.string()).optional()
6474
7570
  }).optional(),
6475
- gitleaks: z6.object({
6476
- enabled: z6.union([z6.literal("auto"), z6.boolean()]).default("auto")
7571
+ gitleaks: z8.object({
7572
+ enabled: z8.union([z8.literal("auto"), z8.boolean()]).default("auto")
6477
7573
  }).optional()
6478
7574
  }).optional()
6479
7575
  });
@@ -6506,15 +7602,15 @@ function resolveRuleSeverity(ruleId, defaultSeverity, overrides, strict) {
6506
7602
  }
6507
7603
 
6508
7604
  // src/security/stack-detector.ts
6509
- import * as fs17 from "fs";
6510
- import * as path14 from "path";
7605
+ import * as fs19 from "fs";
7606
+ import * as path16 from "path";
6511
7607
  function detectStack(projectRoot) {
6512
7608
  const stacks = [];
6513
- const pkgJsonPath = path14.join(projectRoot, "package.json");
6514
- if (fs17.existsSync(pkgJsonPath)) {
7609
+ const pkgJsonPath = path16.join(projectRoot, "package.json");
7610
+ if (fs19.existsSync(pkgJsonPath)) {
6515
7611
  stacks.push("node");
6516
7612
  try {
6517
- const pkgJson = JSON.parse(fs17.readFileSync(pkgJsonPath, "utf-8"));
7613
+ const pkgJson = JSON.parse(fs19.readFileSync(pkgJsonPath, "utf-8"));
6518
7614
  const allDeps = {
6519
7615
  ...pkgJson.dependencies,
6520
7616
  ...pkgJson.devDependencies
@@ -6529,13 +7625,13 @@ function detectStack(projectRoot) {
6529
7625
  } catch {
6530
7626
  }
6531
7627
  }
6532
- const goModPath = path14.join(projectRoot, "go.mod");
6533
- if (fs17.existsSync(goModPath)) {
7628
+ const goModPath = path16.join(projectRoot, "go.mod");
7629
+ if (fs19.existsSync(goModPath)) {
6534
7630
  stacks.push("go");
6535
7631
  }
6536
- const requirementsPath = path14.join(projectRoot, "requirements.txt");
6537
- const pyprojectPath = path14.join(projectRoot, "pyproject.toml");
6538
- if (fs17.existsSync(requirementsPath) || fs17.existsSync(pyprojectPath)) {
7632
+ const requirementsPath = path16.join(projectRoot, "requirements.txt");
7633
+ const pyprojectPath = path16.join(projectRoot, "pyproject.toml");
7634
+ if (fs19.existsSync(requirementsPath) || fs19.existsSync(pyprojectPath)) {
6539
7635
  stacks.push("python");
6540
7636
  }
6541
7637
  return stacks;
@@ -7366,7 +8462,7 @@ var SecurityScanner = class {
7366
8462
  }
7367
8463
  async scanFile(filePath) {
7368
8464
  if (!this.config.enabled) return [];
7369
- const content = await fs18.readFile(filePath, "utf-8");
8465
+ const content = await fs20.readFile(filePath, "utf-8");
7370
8466
  return this.scanContentForFile(content, filePath, 1);
7371
8467
  }
7372
8468
  scanContentForFile(content, filePath, startLine = 1) {
@@ -7693,19 +8789,19 @@ var DESTRUCTIVE_BASH = [
7693
8789
  ];
7694
8790
 
7695
8791
  // src/security/taint.ts
7696
- import { readFileSync as readFileSync14, writeFileSync as writeFileSync11, unlinkSync, mkdirSync as mkdirSync11, readdirSync as readdirSync3 } from "fs";
7697
- import { join as join21, dirname as dirname8 } from "path";
8792
+ import { readFileSync as readFileSync17, writeFileSync as writeFileSync12, unlinkSync, mkdirSync as mkdirSync12, readdirSync as readdirSync3 } from "fs";
8793
+ import { join as join24, dirname as dirname9 } from "path";
7698
8794
  var TAINT_DURATION_MS = 30 * 60 * 1e3;
7699
8795
  var DEFAULT_SESSION_ID = "default";
7700
8796
  function getTaintFilePath(projectRoot, sessionId) {
7701
8797
  const id = sessionId || DEFAULT_SESSION_ID;
7702
- return join21(projectRoot, ".harness", `session-taint-${id}.json`);
8798
+ return join24(projectRoot, ".harness", `session-taint-${id}.json`);
7703
8799
  }
7704
8800
  function readTaint(projectRoot, sessionId) {
7705
8801
  const filePath = getTaintFilePath(projectRoot, sessionId);
7706
8802
  let content;
7707
8803
  try {
7708
- content = readFileSync14(filePath, "utf8");
8804
+ content = readFileSync17(filePath, "utf8");
7709
8805
  } catch {
7710
8806
  return null;
7711
8807
  }
@@ -7749,8 +8845,8 @@ function writeTaint(projectRoot, sessionId, reason, findings, source) {
7749
8845
  const id = sessionId || DEFAULT_SESSION_ID;
7750
8846
  const filePath = getTaintFilePath(projectRoot, id);
7751
8847
  const now = (/* @__PURE__ */ new Date()).toISOString();
7752
- const dir = dirname8(filePath);
7753
- mkdirSync11(dir, { recursive: true });
8848
+ const dir = dirname9(filePath);
8849
+ mkdirSync12(dir, { recursive: true });
7754
8850
  const existing = readTaint(projectRoot, id);
7755
8851
  const maxSeverity = findings.some((f) => f.severity === "high") ? "high" : "medium";
7756
8852
  const taintFindings = findings.map((f) => ({
@@ -7768,7 +8864,7 @@ function writeTaint(projectRoot, sessionId, reason, findings, source) {
7768
8864
  severity: existing?.severity === "high" || maxSeverity === "high" ? "high" : "medium",
7769
8865
  findings: [...existing?.findings || [], ...taintFindings]
7770
8866
  };
7771
- writeFileSync11(filePath, JSON.stringify(state, null, 2) + "\n");
8867
+ writeFileSync12(filePath, JSON.stringify(state, null, 2) + "\n");
7772
8868
  return state;
7773
8869
  }
7774
8870
  function clearTaint(projectRoot, sessionId) {
@@ -7781,14 +8877,14 @@ function clearTaint(projectRoot, sessionId) {
7781
8877
  return 0;
7782
8878
  }
7783
8879
  }
7784
- const harnessDir = join21(projectRoot, ".harness");
8880
+ const harnessDir = join24(projectRoot, ".harness");
7785
8881
  let count = 0;
7786
8882
  try {
7787
8883
  const files = readdirSync3(harnessDir);
7788
8884
  for (const file of files) {
7789
8885
  if (file.startsWith("session-taint-") && file.endsWith(".json")) {
7790
8886
  try {
7791
- unlinkSync(join21(harnessDir, file));
8887
+ unlinkSync(join24(harnessDir, file));
7792
8888
  count++;
7793
8889
  } catch {
7794
8890
  }
@@ -7799,7 +8895,7 @@ function clearTaint(projectRoot, sessionId) {
7799
8895
  return count;
7800
8896
  }
7801
8897
  function listTaintedSessions(projectRoot) {
7802
- const harnessDir = join21(projectRoot, ".harness");
8898
+ const harnessDir = join24(projectRoot, ".harness");
7803
8899
  const sessions = [];
7804
8900
  try {
7805
8901
  const files = readdirSync3(harnessDir);
@@ -7869,7 +8965,8 @@ function mapSecurityFindings(secFindings, existing) {
7869
8965
  }
7870
8966
 
7871
8967
  // src/ci/check-orchestrator.ts
7872
- import * as path15 from "path";
8968
+ import * as path17 from "path";
8969
+ import { GraphStore, queryTraceability } from "@harness-engineering/graph";
7873
8970
  var ALL_CHECKS = [
7874
8971
  "validate",
7875
8972
  "deps",
@@ -7878,11 +8975,12 @@ var ALL_CHECKS = [
7878
8975
  "security",
7879
8976
  "perf",
7880
8977
  "phase-gate",
7881
- "arch"
8978
+ "arch",
8979
+ "traceability"
7882
8980
  ];
7883
8981
  async function runValidateCheck(projectRoot, config) {
7884
8982
  const issues = [];
7885
- const agentsPath = path15.join(projectRoot, config.agentsMapPath ?? "AGENTS.md");
8983
+ const agentsPath = path17.join(projectRoot, config.agentsMapPath ?? "AGENTS.md");
7886
8984
  const result = await validateAgentsMap(agentsPath);
7887
8985
  if (!result.ok) {
7888
8986
  issues.push({ severity: "error", message: result.error.message });
@@ -7939,7 +9037,7 @@ async function runDepsCheck(projectRoot, config) {
7939
9037
  }
7940
9038
  async function runDocsCheck(projectRoot, config) {
7941
9039
  const issues = [];
7942
- const docsDir = path15.join(projectRoot, config.docsDir ?? "docs");
9040
+ const docsDir = path17.join(projectRoot, config.docsDir ?? "docs");
7943
9041
  const entropyConfig = config.entropy || {};
7944
9042
  const result = await checkDocCoverage("project", {
7945
9043
  docsDir,
@@ -8120,6 +9218,39 @@ async function runArchCheck(projectRoot, config) {
8120
9218
  }
8121
9219
  return issues;
8122
9220
  }
9221
+ async function runTraceabilityCheck(projectRoot, config) {
9222
+ const issues = [];
9223
+ const traceConfig = config.traceability || {};
9224
+ if (traceConfig.enabled === false) return issues;
9225
+ const graphDir = path17.join(projectRoot, ".harness", "graph");
9226
+ const store = new GraphStore();
9227
+ const loaded = await store.load(graphDir);
9228
+ if (!loaded) {
9229
+ return issues;
9230
+ }
9231
+ const results = queryTraceability(store);
9232
+ if (results.length === 0) return issues;
9233
+ const minCoverage = traceConfig.minCoverage ?? 0;
9234
+ const severity = traceConfig.severity ?? "warning";
9235
+ for (const result of results) {
9236
+ const pct = result.summary.coveragePercent;
9237
+ if (pct < minCoverage) {
9238
+ issues.push({
9239
+ severity,
9240
+ message: `Traceability coverage for "${result.featureName}" is ${pct}% (minimum: ${minCoverage}%)`
9241
+ });
9242
+ }
9243
+ for (const req of result.requirements) {
9244
+ if (req.status === "none") {
9245
+ issues.push({
9246
+ severity: "warning",
9247
+ message: `Requirement "${req.requirementName}" has no traced code or tests`
9248
+ });
9249
+ }
9250
+ }
9251
+ }
9252
+ return issues;
9253
+ }
8123
9254
  async function runSingleCheck(name, projectRoot, config) {
8124
9255
  const start = Date.now();
8125
9256
  const issues = [];
@@ -8149,6 +9280,9 @@ async function runSingleCheck(name, projectRoot, config) {
8149
9280
  case "arch":
8150
9281
  issues.push(...await runArchCheck(projectRoot, config));
8151
9282
  break;
9283
+ case "traceability":
9284
+ issues.push(...await runTraceabilityCheck(projectRoot, config));
9285
+ break;
8152
9286
  }
8153
9287
  } catch (error) {
8154
9288
  issues.push({
@@ -8217,7 +9351,7 @@ async function runCIChecks(input) {
8217
9351
  }
8218
9352
 
8219
9353
  // src/review/mechanical-checks.ts
8220
- import * as path16 from "path";
9354
+ import * as path18 from "path";
8221
9355
  async function runMechanicalChecks(options) {
8222
9356
  const { projectRoot, config, skip = [], changedFiles } = options;
8223
9357
  const findings = [];
@@ -8229,7 +9363,7 @@ async function runMechanicalChecks(options) {
8229
9363
  };
8230
9364
  if (!skip.includes("validate")) {
8231
9365
  try {
8232
- const agentsPath = path16.join(projectRoot, config.agentsMapPath ?? "AGENTS.md");
9366
+ const agentsPath = path18.join(projectRoot, config.agentsMapPath ?? "AGENTS.md");
8233
9367
  const result = await validateAgentsMap(agentsPath);
8234
9368
  if (!result.ok) {
8235
9369
  statuses.validate = "fail";
@@ -8266,7 +9400,7 @@ async function runMechanicalChecks(options) {
8266
9400
  statuses.validate = "fail";
8267
9401
  findings.push({
8268
9402
  tool: "validate",
8269
- file: path16.join(projectRoot, "AGENTS.md"),
9403
+ file: path18.join(projectRoot, "AGENTS.md"),
8270
9404
  message: err instanceof Error ? err.message : String(err),
8271
9405
  severity: "error"
8272
9406
  });
@@ -8330,7 +9464,7 @@ async function runMechanicalChecks(options) {
8330
9464
  (async () => {
8331
9465
  const localFindings = [];
8332
9466
  try {
8333
- const docsDir = path16.join(projectRoot, config.docsDir ?? "docs");
9467
+ const docsDir = path18.join(projectRoot, config.docsDir ?? "docs");
8334
9468
  const result = await checkDocCoverage("project", { docsDir });
8335
9469
  if (!result.ok) {
8336
9470
  statuses["check-docs"] = "warn";
@@ -8357,7 +9491,7 @@ async function runMechanicalChecks(options) {
8357
9491
  statuses["check-docs"] = "warn";
8358
9492
  localFindings.push({
8359
9493
  tool: "check-docs",
8360
- file: path16.join(projectRoot, "docs"),
9494
+ file: path18.join(projectRoot, "docs"),
8361
9495
  message: err instanceof Error ? err.message : String(err),
8362
9496
  severity: "warning"
8363
9497
  });
@@ -8505,7 +9639,7 @@ function detectChangeType(commitMessage, diff2) {
8505
9639
  }
8506
9640
 
8507
9641
  // src/review/context-scoper.ts
8508
- import * as path17 from "path";
9642
+ import * as path19 from "path";
8509
9643
  var ALL_DOMAINS = ["compliance", "bug", "security", "architecture"];
8510
9644
  var SECURITY_PATTERNS = /auth|crypto|password|secret|token|session|cookie|hash|encrypt|decrypt|sql|shell|exec|eval/i;
8511
9645
  function computeContextBudget(diffLines) {
@@ -8513,18 +9647,18 @@ function computeContextBudget(diffLines) {
8513
9647
  return diffLines;
8514
9648
  }
8515
9649
  function isWithinProject(absPath, projectRoot) {
8516
- const resolvedRoot = path17.resolve(projectRoot) + path17.sep;
8517
- const resolvedPath = path17.resolve(absPath);
8518
- return resolvedPath.startsWith(resolvedRoot) || resolvedPath === path17.resolve(projectRoot);
9650
+ const resolvedRoot = path19.resolve(projectRoot) + path19.sep;
9651
+ const resolvedPath = path19.resolve(absPath);
9652
+ return resolvedPath.startsWith(resolvedRoot) || resolvedPath === path19.resolve(projectRoot);
8519
9653
  }
8520
9654
  async function readContextFile(projectRoot, filePath, reason) {
8521
- const absPath = path17.isAbsolute(filePath) ? filePath : path17.join(projectRoot, filePath);
9655
+ const absPath = path19.isAbsolute(filePath) ? filePath : path19.join(projectRoot, filePath);
8522
9656
  if (!isWithinProject(absPath, projectRoot)) return null;
8523
9657
  const result = await readFileContent(absPath);
8524
9658
  if (!result.ok) return null;
8525
9659
  const content = result.value;
8526
9660
  const lines = content.split("\n").length;
8527
- const relPath = path17.isAbsolute(filePath) ? relativePosix(projectRoot, filePath) : filePath;
9661
+ const relPath = path19.isAbsolute(filePath) ? relativePosix(projectRoot, filePath) : filePath;
8528
9662
  return { path: relPath, content, reason, lines };
8529
9663
  }
8530
9664
  function extractImportSources(content) {
@@ -8539,18 +9673,18 @@ function extractImportSources(content) {
8539
9673
  }
8540
9674
  async function resolveImportPath(projectRoot, fromFile, importSource) {
8541
9675
  if (!importSource.startsWith(".")) return null;
8542
- const fromDir = path17.dirname(path17.join(projectRoot, fromFile));
8543
- const basePath = path17.resolve(fromDir, importSource);
9676
+ const fromDir = path19.dirname(path19.join(projectRoot, fromFile));
9677
+ const basePath = path19.resolve(fromDir, importSource);
8544
9678
  if (!isWithinProject(basePath, projectRoot)) return null;
8545
9679
  const relBase = relativePosix(projectRoot, basePath);
8546
9680
  const candidates = [
8547
9681
  relBase + ".ts",
8548
9682
  relBase + ".tsx",
8549
9683
  relBase + ".mts",
8550
- path17.join(relBase, "index.ts")
9684
+ path19.join(relBase, "index.ts")
8551
9685
  ];
8552
9686
  for (const candidate of candidates) {
8553
- const absCandidate = path17.join(projectRoot, candidate);
9687
+ const absCandidate = path19.join(projectRoot, candidate);
8554
9688
  if (await fileExists(absCandidate)) {
8555
9689
  return candidate;
8556
9690
  }
@@ -8558,7 +9692,7 @@ async function resolveImportPath(projectRoot, fromFile, importSource) {
8558
9692
  return null;
8559
9693
  }
8560
9694
  async function findTestFiles(projectRoot, sourceFile) {
8561
- const baseName = path17.basename(sourceFile, path17.extname(sourceFile));
9695
+ const baseName = path19.basename(sourceFile, path19.extname(sourceFile));
8562
9696
  const pattern = `**/${baseName}.{test,spec}.{ts,tsx,mts}`;
8563
9697
  const results = await findFiles(pattern, projectRoot);
8564
9698
  return results.map((f) => relativePosix(projectRoot, f));
@@ -9367,7 +10501,7 @@ async function fanOutReview(options) {
9367
10501
  }
9368
10502
 
9369
10503
  // src/review/validate-findings.ts
9370
- import * as path18 from "path";
10504
+ import * as path20 from "path";
9371
10505
  var DOWNGRADE_MAP = {
9372
10506
  critical: "important",
9373
10507
  important: "suggestion",
@@ -9388,7 +10522,7 @@ function normalizePath(filePath, projectRoot) {
9388
10522
  let normalized = filePath;
9389
10523
  normalized = normalized.replace(/\\/g, "/");
9390
10524
  const normalizedRoot = projectRoot.replace(/\\/g, "/");
9391
- if (path18.isAbsolute(normalized)) {
10525
+ if (path20.isAbsolute(normalized)) {
9392
10526
  const root = normalizedRoot.endsWith("/") ? normalizedRoot : normalizedRoot + "/";
9393
10527
  if (normalized.startsWith(root)) {
9394
10528
  normalized = normalized.slice(root.length);
@@ -9413,12 +10547,12 @@ function followImportChain(fromFile, fileContents, maxDepth = 2) {
9413
10547
  while ((match = importRegex.exec(content)) !== null) {
9414
10548
  const importPath = match[1];
9415
10549
  if (!importPath.startsWith(".")) continue;
9416
- const dir = path18.dirname(current.file);
9417
- let resolved = path18.join(dir, importPath).replace(/\\/g, "/");
10550
+ const dir = path20.dirname(current.file);
10551
+ let resolved = path20.join(dir, importPath).replace(/\\/g, "/");
9418
10552
  if (!resolved.match(/\.(ts|tsx|js|jsx)$/)) {
9419
10553
  resolved += ".ts";
9420
10554
  }
9421
- resolved = path18.normalize(resolved).replace(/\\/g, "/");
10555
+ resolved = path20.normalize(resolved).replace(/\\/g, "/");
9422
10556
  if (!visited.has(resolved) && current.depth + 1 <= maxDepth) {
9423
10557
  queue.push({ file: resolved, depth: current.depth + 1 });
9424
10558
  }
@@ -9435,7 +10569,7 @@ async function validateFindings(options) {
9435
10569
  if (exclusionSet.isExcluded(normalizedFile, finding.lineRange) || exclusionSet.isExcluded(finding.file, finding.lineRange)) {
9436
10570
  continue;
9437
10571
  }
9438
- const absoluteFile = path18.isAbsolute(finding.file) ? finding.file : path18.join(projectRoot, finding.file).replace(/\\/g, "/");
10572
+ const absoluteFile = path20.isAbsolute(finding.file) ? finding.file : path20.join(projectRoot, finding.file).replace(/\\/g, "/");
9439
10573
  if (exclusionSet.isExcluded(absoluteFile, finding.lineRange)) {
9440
10574
  continue;
9441
10575
  }
@@ -10048,203 +11182,6 @@ async function runReviewPipeline(options) {
10048
11182
  };
10049
11183
  }
10050
11184
 
10051
- // src/roadmap/parse.ts
10052
- import { Ok as Ok2, Err as Err2 } from "@harness-engineering/types";
10053
- var VALID_STATUSES = /* @__PURE__ */ new Set([
10054
- "backlog",
10055
- "planned",
10056
- "in-progress",
10057
- "done",
10058
- "blocked"
10059
- ]);
10060
- var EM_DASH = "\u2014";
10061
- var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
10062
- function parseRoadmap(markdown) {
10063
- const fmMatch = markdown.match(/^---\n([\s\S]*?)\n---/);
10064
- if (!fmMatch) {
10065
- return Err2(new Error("Missing or malformed YAML frontmatter"));
10066
- }
10067
- const fmResult = parseFrontmatter2(fmMatch[1]);
10068
- if (!fmResult.ok) return fmResult;
10069
- const body = markdown.slice(fmMatch[0].length);
10070
- const milestonesResult = parseMilestones(body);
10071
- if (!milestonesResult.ok) return milestonesResult;
10072
- const historyResult = parseAssignmentHistory(body);
10073
- if (!historyResult.ok) return historyResult;
10074
- return Ok2({
10075
- frontmatter: fmResult.value,
10076
- milestones: milestonesResult.value,
10077
- assignmentHistory: historyResult.value
10078
- });
10079
- }
10080
- function parseFrontmatter2(raw) {
10081
- const lines = raw.split("\n");
10082
- const map = /* @__PURE__ */ new Map();
10083
- for (const line of lines) {
10084
- const idx = line.indexOf(":");
10085
- if (idx === -1) continue;
10086
- const key = line.slice(0, idx).trim();
10087
- const val = line.slice(idx + 1).trim();
10088
- map.set(key, val);
10089
- }
10090
- const project = map.get("project");
10091
- const versionStr = map.get("version");
10092
- const lastSynced = map.get("last_synced");
10093
- const lastManualEdit = map.get("last_manual_edit");
10094
- const created = map.get("created");
10095
- const updated = map.get("updated");
10096
- if (!project || !versionStr || !lastSynced || !lastManualEdit) {
10097
- return Err2(
10098
- new Error(
10099
- "Frontmatter missing required fields: project, version, last_synced, last_manual_edit"
10100
- )
10101
- );
10102
- }
10103
- const version = parseInt(versionStr, 10);
10104
- if (isNaN(version)) {
10105
- return Err2(new Error("Frontmatter version must be a number"));
10106
- }
10107
- const fm = { project, version, lastSynced, lastManualEdit };
10108
- if (created) fm.created = created;
10109
- if (updated) fm.updated = updated;
10110
- return Ok2(fm);
10111
- }
10112
- function parseMilestones(body) {
10113
- const milestones = [];
10114
- const h2Pattern = /^## (.+)$/gm;
10115
- const h2Matches = [];
10116
- let match;
10117
- let bodyEnd = body.length;
10118
- while ((match = h2Pattern.exec(body)) !== null) {
10119
- if (match[1] === "Assignment History") {
10120
- bodyEnd = match.index;
10121
- break;
10122
- }
10123
- h2Matches.push({ heading: match[1], startIndex: match.index, fullMatch: match[0] });
10124
- }
10125
- for (let i = 0; i < h2Matches.length; i++) {
10126
- const h2 = h2Matches[i];
10127
- const nextStart = i + 1 < h2Matches.length ? h2Matches[i + 1].startIndex : bodyEnd;
10128
- const sectionBody = body.slice(h2.startIndex + h2.fullMatch.length, nextStart);
10129
- const isBacklog = h2.heading === "Backlog";
10130
- const milestoneName = isBacklog ? "Backlog" : h2.heading.replace(/^Milestone:\s*/, "");
10131
- const featuresResult = parseFeatures(sectionBody);
10132
- if (!featuresResult.ok) return featuresResult;
10133
- milestones.push({
10134
- name: milestoneName,
10135
- isBacklog,
10136
- features: featuresResult.value
10137
- });
10138
- }
10139
- return Ok2(milestones);
10140
- }
10141
- function parseFeatures(sectionBody) {
10142
- const features = [];
10143
- const h3Pattern = /^### (?:Feature: )?(.+)$/gm;
10144
- const h3Matches = [];
10145
- let match;
10146
- while ((match = h3Pattern.exec(sectionBody)) !== null) {
10147
- h3Matches.push({ name: match[1], startIndex: match.index, fullMatch: match[0] });
10148
- }
10149
- for (let i = 0; i < h3Matches.length; i++) {
10150
- const h3 = h3Matches[i];
10151
- const nextStart = i + 1 < h3Matches.length ? h3Matches[i + 1].startIndex : sectionBody.length;
10152
- const featureBody = sectionBody.slice(h3.startIndex + h3.fullMatch.length, nextStart);
10153
- const featureResult = parseFeatureFields(h3.name, featureBody);
10154
- if (!featureResult.ok) return featureResult;
10155
- features.push(featureResult.value);
10156
- }
10157
- return Ok2(features);
10158
- }
10159
- function extractFieldMap(body) {
10160
- const fieldMap = /* @__PURE__ */ new Map();
10161
- const fieldPattern = /^- \*\*(.+?):\*\* (.+)$/gm;
10162
- let match;
10163
- while ((match = fieldPattern.exec(body)) !== null) {
10164
- fieldMap.set(match[1], match[2]);
10165
- }
10166
- return fieldMap;
10167
- }
10168
- function parseListField(fieldMap, ...keys) {
10169
- let raw = EM_DASH;
10170
- for (const key of keys) {
10171
- const val = fieldMap.get(key);
10172
- if (val !== void 0) {
10173
- raw = val;
10174
- break;
10175
- }
10176
- }
10177
- if (raw === EM_DASH || raw === "none") return [];
10178
- return raw.split(",").map((s) => s.trim());
10179
- }
10180
- function parseFeatureFields(name, body) {
10181
- const fieldMap = extractFieldMap(body);
10182
- const statusRaw = fieldMap.get("Status");
10183
- if (!statusRaw || !VALID_STATUSES.has(statusRaw)) {
10184
- return Err2(
10185
- new Error(
10186
- `Feature "${name}" has invalid status: "${statusRaw ?? "(missing)"}". Valid statuses: ${[...VALID_STATUSES].join(", ")}`
10187
- )
10188
- );
10189
- }
10190
- const specRaw = fieldMap.get("Spec") ?? EM_DASH;
10191
- const plans = parseListField(fieldMap, "Plans", "Plan");
10192
- const blockedBy = parseListField(fieldMap, "Blocked by", "Blockers");
10193
- const assigneeRaw = fieldMap.get("Assignee") ?? EM_DASH;
10194
- const priorityRaw = fieldMap.get("Priority") ?? EM_DASH;
10195
- const externalIdRaw = fieldMap.get("External-ID") ?? EM_DASH;
10196
- if (priorityRaw !== EM_DASH && !VALID_PRIORITIES.has(priorityRaw)) {
10197
- return Err2(
10198
- new Error(
10199
- `Feature "${name}" has invalid priority: "${priorityRaw}". Valid priorities: ${[...VALID_PRIORITIES].join(", ")}`
10200
- )
10201
- );
10202
- }
10203
- return Ok2({
10204
- name,
10205
- status: statusRaw,
10206
- spec: specRaw === EM_DASH ? null : specRaw,
10207
- plans,
10208
- blockedBy,
10209
- summary: fieldMap.get("Summary") ?? "",
10210
- assignee: assigneeRaw === EM_DASH ? null : assigneeRaw,
10211
- priority: priorityRaw === EM_DASH ? null : priorityRaw,
10212
- externalId: externalIdRaw === EM_DASH ? null : externalIdRaw
10213
- });
10214
- }
10215
- function parseAssignmentHistory(body) {
10216
- const historyMatch = body.match(/^## Assignment History\s*\n/m);
10217
- if (!historyMatch || historyMatch.index === void 0) return Ok2([]);
10218
- const historyStart = historyMatch.index + historyMatch[0].length;
10219
- const rawHistoryBody = body.slice(historyStart);
10220
- const nextH2 = rawHistoryBody.search(/^## /m);
10221
- const historyBody = nextH2 === -1 ? rawHistoryBody : rawHistoryBody.slice(0, nextH2);
10222
- const records = [];
10223
- const lines = historyBody.split("\n");
10224
- let pastHeader = false;
10225
- for (const line of lines) {
10226
- const trimmed = line.trim();
10227
- if (!trimmed.startsWith("|")) continue;
10228
- if (!pastHeader) {
10229
- if (trimmed.match(/^\|[-\s|]+\|$/)) {
10230
- pastHeader = true;
10231
- }
10232
- continue;
10233
- }
10234
- const cells = trimmed.split("|").map((c) => c.trim()).filter((c) => c.length > 0);
10235
- if (cells.length < 4) continue;
10236
- const action = cells[2];
10237
- if (!["assigned", "completed", "unassigned"].includes(action)) continue;
10238
- records.push({
10239
- feature: cells[0],
10240
- assignee: cells[1],
10241
- action,
10242
- date: cells[3]
10243
- });
10244
- }
10245
- return Ok2(records);
10246
- }
10247
-
10248
11185
  // src/roadmap/serialize.ts
10249
11186
  var EM_DASH2 = "\u2014";
10250
11187
  function serializeRoadmap(roadmap) {
@@ -10315,8 +11252,8 @@ function serializeAssignmentHistory(records) {
10315
11252
  }
10316
11253
 
10317
11254
  // src/roadmap/sync.ts
10318
- import * as fs19 from "fs";
10319
- import * as path19 from "path";
11255
+ import * as fs21 from "fs";
11256
+ import * as path21 from "path";
10320
11257
  import { Ok as Ok3 } from "@harness-engineering/types";
10321
11258
 
10322
11259
  // src/roadmap/status-rank.ts
@@ -10346,10 +11283,10 @@ function inferStatus(feature, projectPath, allFeatures) {
10346
11283
  const featuresWithPlans = allFeatures.filter((f) => f.plans.length > 0);
10347
11284
  const useRootState = featuresWithPlans.length <= 1;
10348
11285
  if (useRootState) {
10349
- const rootStatePath = path19.join(projectPath, ".harness", "state.json");
10350
- if (fs19.existsSync(rootStatePath)) {
11286
+ const rootStatePath = path21.join(projectPath, ".harness", "state.json");
11287
+ if (fs21.existsSync(rootStatePath)) {
10351
11288
  try {
10352
- const raw = fs19.readFileSync(rootStatePath, "utf-8");
11289
+ const raw = fs21.readFileSync(rootStatePath, "utf-8");
10353
11290
  const state = JSON.parse(raw);
10354
11291
  if (state.progress) {
10355
11292
  for (const status of Object.values(state.progress)) {
@@ -10360,16 +11297,16 @@ function inferStatus(feature, projectPath, allFeatures) {
10360
11297
  }
10361
11298
  }
10362
11299
  }
10363
- const sessionsDir = path19.join(projectPath, ".harness", "sessions");
10364
- if (fs19.existsSync(sessionsDir)) {
11300
+ const sessionsDir = path21.join(projectPath, ".harness", "sessions");
11301
+ if (fs21.existsSync(sessionsDir)) {
10365
11302
  try {
10366
- const sessionDirs = fs19.readdirSync(sessionsDir, { withFileTypes: true });
11303
+ const sessionDirs = fs21.readdirSync(sessionsDir, { withFileTypes: true });
10367
11304
  for (const entry of sessionDirs) {
10368
11305
  if (!entry.isDirectory()) continue;
10369
- const autopilotPath = path19.join(sessionsDir, entry.name, "autopilot-state.json");
10370
- if (!fs19.existsSync(autopilotPath)) continue;
11306
+ const autopilotPath = path21.join(sessionsDir, entry.name, "autopilot-state.json");
11307
+ if (!fs21.existsSync(autopilotPath)) continue;
10371
11308
  try {
10372
- const raw = fs19.readFileSync(autopilotPath, "utf-8");
11309
+ const raw = fs21.readFileSync(autopilotPath, "utf-8");
10373
11310
  const autopilot = JSON.parse(raw);
10374
11311
  if (!autopilot.phases) continue;
10375
11312
  const linkedPhases = autopilot.phases.filter(
@@ -10466,6 +11403,13 @@ function labelsForStatus(status, config) {
10466
11403
  return [...base];
10467
11404
  }
10468
11405
  var RETRY_DEFAULTS = { maxRetries: 5, baseDelayMs: 1e3 };
11406
+ function parseRepoParts(repo) {
11407
+ const parts = (repo ?? "").split("/");
11408
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
11409
+ throw new Error(`Invalid repo format: "${repo}". Expected "owner/repo".`);
11410
+ }
11411
+ return { owner: parts[0], repo: parts[1] };
11412
+ }
10469
11413
  function sleep(ms) {
10470
11414
  return new Promise((resolve5) => setTimeout(resolve5, ms));
10471
11415
  }
@@ -10509,12 +11453,9 @@ var GitHubIssuesSyncAdapter = class {
10509
11453
  maxRetries: options.maxRetries ?? RETRY_DEFAULTS.maxRetries,
10510
11454
  baseDelayMs: options.baseDelayMs ?? RETRY_DEFAULTS.baseDelayMs
10511
11455
  };
10512
- const repoParts = (options.config.repo ?? "").split("/");
10513
- if (repoParts.length !== 2 || !repoParts[0] || !repoParts[1]) {
10514
- throw new Error(`Invalid repo format: "${options.config.repo}". Expected "owner/repo".`);
10515
- }
10516
- this.owner = repoParts[0];
10517
- this.repo = repoParts[1];
11456
+ const { owner, repo } = parseRepoParts(options.config.repo);
11457
+ this.owner = owner;
11458
+ this.repo = repo;
10518
11459
  }
10519
11460
  /**
10520
11461
  * Fetch all GitHub milestones and build the name -> ID cache.
@@ -10765,7 +11706,7 @@ var GitHubIssuesSyncAdapter = class {
10765
11706
  };
10766
11707
 
10767
11708
  // src/roadmap/sync-engine.ts
10768
- import * as fs20 from "fs";
11709
+ import * as fs22 from "fs";
10769
11710
  function emptySyncResult() {
10770
11711
  return { created: [], updated: [], assignmentChanges: [], errors: [] };
10771
11712
  }
@@ -10853,7 +11794,7 @@ async function fullSync(roadmapPath, adapter, config, options) {
10853
11794
  });
10854
11795
  await previousSync;
10855
11796
  try {
10856
- const raw = fs20.readFileSync(roadmapPath, "utf-8");
11797
+ const raw = fs22.readFileSync(roadmapPath, "utf-8");
10857
11798
  const parseResult = parseRoadmap(raw);
10858
11799
  if (!parseResult.ok) {
10859
11800
  return {
@@ -10864,7 +11805,7 @@ async function fullSync(roadmapPath, adapter, config, options) {
10864
11805
  const roadmap = parseResult.value;
10865
11806
  const pushResult = await syncToExternal(roadmap, adapter, config);
10866
11807
  const pullResult = await syncFromExternal(roadmap, adapter, config, options);
10867
- fs20.writeFileSync(roadmapPath, serializeRoadmap(roadmap), "utf-8");
11808
+ fs22.writeFileSync(roadmapPath, serializeRoadmap(roadmap), "utf-8");
10868
11809
  return {
10869
11810
  created: pushResult.created,
10870
11811
  updated: pushResult.updated,
@@ -10994,47 +11935,47 @@ function assignFeature(roadmap, feature, assignee, date) {
10994
11935
  }
10995
11936
 
10996
11937
  // src/interaction/types.ts
10997
- import { z as z7 } from "zod";
10998
- var InteractionTypeSchema = z7.enum(["question", "confirmation", "transition"]);
10999
- var QuestionSchema = z7.object({
11000
- text: z7.string(),
11001
- options: z7.array(z7.string()).optional(),
11002
- default: z7.string().optional()
11938
+ import { z as z9 } from "zod";
11939
+ var InteractionTypeSchema = z9.enum(["question", "confirmation", "transition"]);
11940
+ var QuestionSchema = z9.object({
11941
+ text: z9.string(),
11942
+ options: z9.array(z9.string()).optional(),
11943
+ default: z9.string().optional()
11003
11944
  });
11004
- var ConfirmationSchema = z7.object({
11005
- text: z7.string(),
11006
- context: z7.string()
11945
+ var ConfirmationSchema = z9.object({
11946
+ text: z9.string(),
11947
+ context: z9.string()
11007
11948
  });
11008
- var TransitionSchema = z7.object({
11009
- completedPhase: z7.string(),
11010
- suggestedNext: z7.string(),
11011
- reason: z7.string(),
11012
- artifacts: z7.array(z7.string()),
11013
- requiresConfirmation: z7.boolean(),
11014
- summary: z7.string()
11949
+ var TransitionSchema = z9.object({
11950
+ completedPhase: z9.string(),
11951
+ suggestedNext: z9.string(),
11952
+ reason: z9.string(),
11953
+ artifacts: z9.array(z9.string()),
11954
+ requiresConfirmation: z9.boolean(),
11955
+ summary: z9.string()
11015
11956
  });
11016
- var EmitInteractionInputSchema = z7.object({
11017
- path: z7.string(),
11957
+ var EmitInteractionInputSchema = z9.object({
11958
+ path: z9.string(),
11018
11959
  type: InteractionTypeSchema,
11019
- stream: z7.string().optional(),
11960
+ stream: z9.string().optional(),
11020
11961
  question: QuestionSchema.optional(),
11021
11962
  confirmation: ConfirmationSchema.optional(),
11022
11963
  transition: TransitionSchema.optional()
11023
11964
  });
11024
11965
 
11025
11966
  // src/blueprint/scanner.ts
11026
- import * as fs21 from "fs/promises";
11027
- import * as path20 from "path";
11967
+ import * as fs23 from "fs/promises";
11968
+ import * as path22 from "path";
11028
11969
  var ProjectScanner = class {
11029
11970
  constructor(rootDir) {
11030
11971
  this.rootDir = rootDir;
11031
11972
  }
11032
11973
  rootDir;
11033
11974
  async scan() {
11034
- let projectName = path20.basename(this.rootDir);
11975
+ let projectName = path22.basename(this.rootDir);
11035
11976
  try {
11036
- const pkgPath = path20.join(this.rootDir, "package.json");
11037
- const pkgRaw = await fs21.readFile(pkgPath, "utf-8");
11977
+ const pkgPath = path22.join(this.rootDir, "package.json");
11978
+ const pkgRaw = await fs23.readFile(pkgPath, "utf-8");
11038
11979
  const pkg = JSON.parse(pkgRaw);
11039
11980
  if (pkg.name) projectName = pkg.name;
11040
11981
  } catch {
@@ -11075,8 +12016,8 @@ var ProjectScanner = class {
11075
12016
  };
11076
12017
 
11077
12018
  // src/blueprint/generator.ts
11078
- import * as fs22 from "fs/promises";
11079
- import * as path21 from "path";
12019
+ import * as fs24 from "fs/promises";
12020
+ import * as path23 from "path";
11080
12021
  import * as ejs from "ejs";
11081
12022
 
11082
12023
  // src/blueprint/templates.ts
@@ -11160,19 +12101,19 @@ var BlueprintGenerator = class {
11160
12101
  styles: STYLES,
11161
12102
  scripts: SCRIPTS
11162
12103
  });
11163
- await fs22.mkdir(options.outputDir, { recursive: true });
11164
- await fs22.writeFile(path21.join(options.outputDir, "index.html"), html);
12104
+ await fs24.mkdir(options.outputDir, { recursive: true });
12105
+ await fs24.writeFile(path23.join(options.outputDir, "index.html"), html);
11165
12106
  }
11166
12107
  };
11167
12108
 
11168
12109
  // src/update-checker.ts
11169
- import * as fs23 from "fs";
11170
- import * as path22 from "path";
12110
+ import * as fs25 from "fs";
12111
+ import * as path24 from "path";
11171
12112
  import * as os from "os";
11172
12113
  import { spawn } from "child_process";
11173
12114
  function getStatePath() {
11174
12115
  const home = process.env["HOME"] || os.homedir();
11175
- return path22.join(home, ".harness", "update-check.json");
12116
+ return path24.join(home, ".harness", "update-check.json");
11176
12117
  }
11177
12118
  function isUpdateCheckEnabled(configInterval) {
11178
12119
  if (process.env["HARNESS_NO_UPDATE_CHECK"] === "1") return false;
@@ -11185,7 +12126,7 @@ function shouldRunCheck(state, intervalMs) {
11185
12126
  }
11186
12127
  function readCheckState() {
11187
12128
  try {
11188
- const raw = fs23.readFileSync(getStatePath(), "utf-8");
12129
+ const raw = fs25.readFileSync(getStatePath(), "utf-8");
11189
12130
  const parsed = JSON.parse(raw);
11190
12131
  if (typeof parsed === "object" && parsed !== null && "lastCheckTime" in parsed && typeof parsed.lastCheckTime === "number" && "currentVersion" in parsed && typeof parsed.currentVersion === "string") {
11191
12132
  const state = parsed;
@@ -11202,7 +12143,7 @@ function readCheckState() {
11202
12143
  }
11203
12144
  function spawnBackgroundCheck(currentVersion) {
11204
12145
  const statePath = getStatePath();
11205
- const stateDir = path22.dirname(statePath);
12146
+ const stateDir = path24.dirname(statePath);
11206
12147
  const script = `
11207
12148
  const { execSync } = require('child_process');
11208
12149
  const fs = require('fs');
@@ -11291,9 +12232,9 @@ async function resolveWasmPath(grammarName) {
11291
12232
  const { createRequire } = await import("module");
11292
12233
  const require2 = createRequire(import.meta.url ?? __filename);
11293
12234
  const pkgPath = require2.resolve("tree-sitter-wasms/package.json");
11294
- const path26 = await import("path");
11295
- const pkgDir = path26.dirname(pkgPath);
11296
- return path26.join(pkgDir, "out", `${grammarName}.wasm`);
12235
+ const path28 = await import("path");
12236
+ const pkgDir = path28.dirname(pkgPath);
12237
+ return path28.join(pkgDir, "out", `${grammarName}.wasm`);
11297
12238
  }
11298
12239
  async function loadLanguage(lang) {
11299
12240
  const grammarName = GRAMMAR_MAP[lang];
@@ -11697,8 +12638,8 @@ function getModelPrice(model, dataset) {
11697
12638
  }
11698
12639
 
11699
12640
  // src/pricing/cache.ts
11700
- import * as fs24 from "fs/promises";
11701
- import * as path23 from "path";
12641
+ import * as fs26 from "fs/promises";
12642
+ import * as path25 from "path";
11702
12643
 
11703
12644
  // src/pricing/fallback.json
11704
12645
  var fallback_default = {
@@ -11751,14 +12692,14 @@ var LITELLM_PRICING_URL = "https://raw.githubusercontent.com/BerriAI/litellm/mai
11751
12692
  var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
11752
12693
  var STALENESS_WARNING_DAYS = 7;
11753
12694
  function getCachePath(projectRoot) {
11754
- return path23.join(projectRoot, ".harness", "cache", "pricing.json");
12695
+ return path25.join(projectRoot, ".harness", "cache", "pricing.json");
11755
12696
  }
11756
12697
  function getStalenessMarkerPath(projectRoot) {
11757
- return path23.join(projectRoot, ".harness", "cache", "staleness-marker.json");
12698
+ return path25.join(projectRoot, ".harness", "cache", "staleness-marker.json");
11758
12699
  }
11759
12700
  async function readDiskCache(projectRoot) {
11760
12701
  try {
11761
- const raw = await fs24.readFile(getCachePath(projectRoot), "utf-8");
12702
+ const raw = await fs26.readFile(getCachePath(projectRoot), "utf-8");
11762
12703
  return JSON.parse(raw);
11763
12704
  } catch {
11764
12705
  return null;
@@ -11766,8 +12707,8 @@ async function readDiskCache(projectRoot) {
11766
12707
  }
11767
12708
  async function writeDiskCache(projectRoot, data) {
11768
12709
  const cachePath = getCachePath(projectRoot);
11769
- await fs24.mkdir(path23.dirname(cachePath), { recursive: true });
11770
- await fs24.writeFile(cachePath, JSON.stringify(data, null, 2));
12710
+ await fs26.mkdir(path25.dirname(cachePath), { recursive: true });
12711
+ await fs26.writeFile(cachePath, JSON.stringify(data, null, 2));
11771
12712
  }
11772
12713
  async function fetchFromNetwork() {
11773
12714
  try {
@@ -11794,7 +12735,7 @@ function loadFallbackDataset() {
11794
12735
  async function checkAndWarnStaleness(projectRoot) {
11795
12736
  const markerPath = getStalenessMarkerPath(projectRoot);
11796
12737
  try {
11797
- const raw = await fs24.readFile(markerPath, "utf-8");
12738
+ const raw = await fs26.readFile(markerPath, "utf-8");
11798
12739
  const marker = JSON.parse(raw);
11799
12740
  const firstUse = new Date(marker.firstFallbackUse).getTime();
11800
12741
  const now = Date.now();
@@ -11806,8 +12747,8 @@ async function checkAndWarnStaleness(projectRoot) {
11806
12747
  }
11807
12748
  } catch {
11808
12749
  try {
11809
- await fs24.mkdir(path23.dirname(markerPath), { recursive: true });
11810
- await fs24.writeFile(
12750
+ await fs26.mkdir(path25.dirname(markerPath), { recursive: true });
12751
+ await fs26.writeFile(
11811
12752
  markerPath,
11812
12753
  JSON.stringify({ firstFallbackUse: (/* @__PURE__ */ new Date()).toISOString() })
11813
12754
  );
@@ -11817,7 +12758,7 @@ async function checkAndWarnStaleness(projectRoot) {
11817
12758
  }
11818
12759
  async function clearStalenessMarker(projectRoot) {
11819
12760
  try {
11820
- await fs24.unlink(getStalenessMarkerPath(projectRoot));
12761
+ await fs26.unlink(getStalenessMarkerPath(projectRoot));
11821
12762
  } catch {
11822
12763
  }
11823
12764
  }
@@ -11987,8 +12928,8 @@ function aggregateByDay(records) {
11987
12928
  }
11988
12929
 
11989
12930
  // src/usage/jsonl-reader.ts
11990
- import * as fs25 from "fs";
11991
- import * as path24 from "path";
12931
+ import * as fs27 from "fs";
12932
+ import * as path26 from "path";
11992
12933
  function parseLine(line, lineNumber) {
11993
12934
  let entry;
11994
12935
  try {
@@ -12027,10 +12968,10 @@ function parseLine(line, lineNumber) {
12027
12968
  return record;
12028
12969
  }
12029
12970
  function readCostRecords(projectRoot) {
12030
- const costsFile = path24.join(projectRoot, ".harness", "metrics", "costs.jsonl");
12971
+ const costsFile = path26.join(projectRoot, ".harness", "metrics", "costs.jsonl");
12031
12972
  let raw;
12032
12973
  try {
12033
- raw = fs25.readFileSync(costsFile, "utf-8");
12974
+ raw = fs27.readFileSync(costsFile, "utf-8");
12034
12975
  } catch {
12035
12976
  return [];
12036
12977
  }
@@ -12048,8 +12989,8 @@ function readCostRecords(projectRoot) {
12048
12989
  }
12049
12990
 
12050
12991
  // src/usage/cc-parser.ts
12051
- import * as fs26 from "fs";
12052
- import * as path25 from "path";
12992
+ import * as fs28 from "fs";
12993
+ import * as path27 from "path";
12053
12994
  import * as os2 from "os";
12054
12995
  function extractUsage(entry) {
12055
12996
  if (entry.type !== "assistant") return null;
@@ -12082,7 +13023,7 @@ function parseCCLine(line, filePath, lineNumber) {
12082
13023
  entry = JSON.parse(line);
12083
13024
  } catch {
12084
13025
  console.warn(
12085
- `[harness usage] Skipping malformed CC JSONL line ${lineNumber} in ${path25.basename(filePath)}`
13026
+ `[harness usage] Skipping malformed CC JSONL line ${lineNumber} in ${path27.basename(filePath)}`
12086
13027
  );
12087
13028
  return null;
12088
13029
  }
@@ -12096,7 +13037,7 @@ function parseCCLine(line, filePath, lineNumber) {
12096
13037
  function readCCFile(filePath) {
12097
13038
  let raw;
12098
13039
  try {
12099
- raw = fs26.readFileSync(filePath, "utf-8");
13040
+ raw = fs28.readFileSync(filePath, "utf-8");
12100
13041
  } catch {
12101
13042
  return [];
12102
13043
  }
@@ -12118,10 +13059,10 @@ function readCCFile(filePath) {
12118
13059
  }
12119
13060
  function parseCCRecords() {
12120
13061
  const homeDir = process.env.HOME ?? os2.homedir();
12121
- const projectsDir = path25.join(homeDir, ".claude", "projects");
13062
+ const projectsDir = path27.join(homeDir, ".claude", "projects");
12122
13063
  let projectDirs;
12123
13064
  try {
12124
- projectDirs = fs26.readdirSync(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => path25.join(projectsDir, d.name));
13065
+ projectDirs = fs28.readdirSync(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => path27.join(projectsDir, d.name));
12125
13066
  } catch {
12126
13067
  return [];
12127
13068
  }
@@ -12129,7 +13070,7 @@ function parseCCRecords() {
12129
13070
  for (const dir of projectDirs) {
12130
13071
  let files;
12131
13072
  try {
12132
- files = fs26.readdirSync(dir).filter((f) => f.endsWith(".jsonl")).map((f) => path25.join(dir, f));
13073
+ files = fs28.readdirSync(dir).filter((f) => f.endsWith(".jsonl")).map((f) => path27.join(dir, f));
12133
13074
  } catch {
12134
13075
  continue;
12135
13076
  }
@@ -12145,6 +13086,7 @@ var VERSION = "0.15.0";
12145
13086
  export {
12146
13087
  AGENT_DESCRIPTORS,
12147
13088
  ARCHITECTURE_DESCRIPTOR,
13089
+ AdjustedForecastSchema,
12148
13090
  AgentActionEmitter,
12149
13091
  ArchBaselineManager,
12150
13092
  ArchBaselineSchema,
@@ -12160,23 +13102,29 @@ export {
12160
13102
  CACHE_TTL_MS,
12161
13103
  COMPLIANCE_DESCRIPTOR,
12162
13104
  CategoryBaselineSchema,
13105
+ CategoryForecastSchema,
12163
13106
  CategoryRegressionSchema,
13107
+ CategorySnapshotSchema,
12164
13108
  ChecklistBuilder,
12165
13109
  CircularDepsCollector,
12166
13110
  ComplexityCollector,
13111
+ ConfidenceTierSchema,
12167
13112
  ConfirmationSchema,
12168
13113
  ConsoleSink,
12169
13114
  ConstraintRuleSchema,
12170
13115
  ContentPipeline,
13116
+ ContributingFeatureSchema,
12171
13117
  ContributionsSchema,
12172
13118
  CouplingCollector,
12173
13119
  CriticalPathResolver,
12174
13120
  DEFAULT_PROVIDER_TIERS,
12175
13121
  DEFAULT_SECURITY_CONFIG,
13122
+ DEFAULT_STABILITY_THRESHOLDS,
12176
13123
  DEFAULT_STATE,
12177
13124
  DEFAULT_STREAM_INDEX,
12178
13125
  DESTRUCTIVE_BASH,
12179
13126
  DepDepthCollector,
13127
+ DirectionSchema,
12180
13128
  EXTENSION_MAP,
12181
13129
  EmitInteractionInputSchema,
12182
13130
  EntropyAnalyzer,
@@ -12202,6 +13150,11 @@ export {
12202
13150
  NoOpSink,
12203
13151
  NoOpTelemetryAdapter,
12204
13152
  PatternConfigSchema,
13153
+ PredictionEngine,
13154
+ PredictionOptionsSchema,
13155
+ RegressionResultSchema as PredictionRegressionResultSchema,
13156
+ PredictionResultSchema,
13157
+ PredictionWarningSchema,
12205
13158
  ProjectScanner,
12206
13159
  QuestionSchema,
12207
13160
  REQUIRED_SECTIONS,
@@ -12217,10 +13170,19 @@ export {
12217
13170
  SharableLayerSchema,
12218
13171
  SharableSecurityRulesSchema,
12219
13172
  SkillEventSchema,
13173
+ SpecImpactEstimateSchema,
13174
+ SpecImpactEstimator,
13175
+ SpecImpactSignalsSchema,
13176
+ StabilityForecastSchema,
12220
13177
  StreamIndexSchema,
12221
13178
  StreamInfoSchema,
12222
13179
  ThresholdConfigSchema,
13180
+ TimelineFileSchema,
13181
+ TimelineManager,
13182
+ TimelineSnapshotSchema,
12223
13183
  TransitionSchema,
13184
+ TrendLineSchema,
13185
+ TrendResultSchema,
12224
13186
  TypeScriptParser,
12225
13187
  VERSION,
12226
13188
  ViolationSchema,
@@ -12235,6 +13197,7 @@ export {
12235
13197
  appendSessionEntry,
12236
13198
  applyFixes,
12237
13199
  applyHotspotDowngrade,
13200
+ applyRecencyWeights,
12238
13201
  applySyncChanges,
12239
13202
  archMatchers,
12240
13203
  archModule,
@@ -12252,6 +13215,7 @@ export {
12252
13215
  checkEligibility,
12253
13216
  checkEvidenceCoverage,
12254
13217
  checkTaint,
13218
+ classifyConfidence,
12255
13219
  classifyFinding,
12256
13220
  clearEventHashCache,
12257
13221
  clearFailuresCache,
@@ -12354,7 +13318,7 @@ export {
12354
13318
  parseDateFromEntry,
12355
13319
  parseDiff,
12356
13320
  parseFile,
12357
- parseFrontmatter,
13321
+ parseFrontmatter2 as parseFrontmatter,
12358
13322
  parseHarnessIgnore,
12359
13323
  parseLiteLLMData,
12360
13324
  parseManifest,
@@ -12363,6 +13327,7 @@ export {
12363
13327
  parseSize,
12364
13328
  pathTraversalRules,
12365
13329
  previewFix,
13330
+ projectValue,
12366
13331
  promoteSessionLearnings,
12367
13332
  pruneLearnings,
12368
13333
  reactRules,
@@ -12430,6 +13395,8 @@ export {
12430
13395
  validateKnowledgeMap,
12431
13396
  validatePatternConfig,
12432
13397
  violationId,
13398
+ weeksUntilThreshold,
13399
+ weightedLinearRegression,
12433
13400
  writeConfig,
12434
13401
  writeLockfile,
12435
13402
  writeSessionSummary,