helixevo 0.3.1 → 0.4.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/cli.js CHANGED
@@ -9405,6 +9405,62 @@ var init_ontology = __esm(() => {
9405
9405
  init_spec();
9406
9406
  });
9407
9407
 
9408
+ // src/core/network-topology.ts
9409
+ function detectMergeCandidates(enhances, coEvolves, skills) {
9410
+ const pairScores = {};
9411
+ for (const edge of [...enhances, ...coEvolves]) {
9412
+ const key = [edge.from, edge.to].sort().join("::");
9413
+ pairScores[key] = (pairScores[key] ?? 0) + edge.strength;
9414
+ }
9415
+ const candidates = [];
9416
+ for (const [key, score] of Object.entries(pairScores)) {
9417
+ if (score < 1.5)
9418
+ continue;
9419
+ const [a, b] = key.split("::");
9420
+ const skillA = skills.find((skill) => skill.slug === a);
9421
+ const skillB = skills.find((skill) => skill.slug === b);
9422
+ if (!skillA || !skillB)
9423
+ continue;
9424
+ if (skillA.meta.layer !== skillB.meta.layer)
9425
+ continue;
9426
+ candidates.push({
9427
+ slugs: [a, b],
9428
+ combinedStrength: score,
9429
+ confidence: Math.min(0.96, 0.62 + Math.max(0, score - 1.5) * 0.22)
9430
+ });
9431
+ }
9432
+ return candidates.sort((a, b) => b.combinedStrength - a.combinedStrength || a.slugs.join("::").localeCompare(b.slugs.join("::")));
9433
+ }
9434
+ function detectSplitCandidates(skills) {
9435
+ return skills.map((skill) => {
9436
+ const contentLength = skill.content.length;
9437
+ const failureCount = skill.meta.failureCount ?? 0;
9438
+ if (contentLength <= 5000 && failureCount <= 20)
9439
+ return null;
9440
+ const contentPressure = Math.max(0, (contentLength - 5000) / 4000);
9441
+ const failurePressure = Math.max(0, (failureCount - 20) / 20);
9442
+ return {
9443
+ slug: skill.slug,
9444
+ contentLength,
9445
+ failureCount,
9446
+ confidence: Math.min(0.97, 0.58 + Math.min(0.18, contentPressure * 0.12) + Math.min(0.2, failurePressure * 0.14))
9447
+ };
9448
+ }).filter((candidate) => candidate !== null).sort((a, b) => b.failureCount - a.failureCount || b.contentLength - a.contentLength || a.slug.localeCompare(b.slug));
9449
+ }
9450
+ function previewNetworkOptimization(graph, skills) {
9451
+ const enhances = graph.edges.filter((edge) => edge.type === "enhances" && edge.strength >= 0.8);
9452
+ const coEvolves = graph.edges.filter((edge) => edge.type === "co-evolves" && edge.strength >= 0.8);
9453
+ const conflicts = graph.edges.filter((edge) => edge.type === "conflicts").map((edge) => ({
9454
+ slugs: [edge.from, edge.to],
9455
+ strength: edge.strength
9456
+ })).sort((a, b) => b.strength - a.strength || a.slugs.join("::").localeCompare(b.slugs.join("::")));
9457
+ return {
9458
+ merged: detectMergeCandidates(enhances, coEvolves, skills),
9459
+ split: detectSplitCandidates(skills),
9460
+ conflicts
9461
+ };
9462
+ }
9463
+
9408
9464
  // src/utils/data.ts
9409
9465
  import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, appendFileSync, existsSync as existsSync3, readdirSync as readdirSync2 } from "node:fs";
9410
9466
  import { dirname, join as join3 } from "node:path";
@@ -9466,7 +9522,7 @@ function loadSkillTests() {
9466
9522
  function appendSkillTest(gc) {
9467
9523
  appendJsonl("skill-tests.jsonl", gc);
9468
9524
  }
9469
- function loadSkillGraph() {
9525
+ function loadStoredSkillGraph() {
9470
9526
  return readJson("skill-graph.json", {
9471
9527
  updated: new Date().toISOString(),
9472
9528
  nodes: [],
@@ -9474,21 +9530,1092 @@ function loadSkillGraph() {
9474
9530
  clusters: []
9475
9531
  });
9476
9532
  }
9533
+ function loadSkillGraph() {
9534
+ return applyTopologyOverridesToGraph(loadStoredSkillGraph());
9535
+ }
9477
9536
  function saveSkillGraph(graph) {
9478
9537
  writeJson("skill-graph.json", graph);
9479
9538
  }
9539
+ function loadNativeEvolutionArtifacts() {
9540
+ return readJsonl("evolution-artifacts.jsonl");
9541
+ }
9480
9542
  function appendEvolutionArtifact(artifact) {
9481
9543
  appendJsonl("evolution-artifacts.jsonl", artifact);
9482
9544
  }
9483
9545
  function appendActivationTrace(trace) {
9484
9546
  appendJsonl("activation-traces.jsonl", trace);
9485
9547
  }
9548
+ function loadNativePressureSignals() {
9549
+ return readJsonl("pressure-signals.jsonl");
9550
+ }
9486
9551
  function appendPressureSignal(signal) {
9487
9552
  appendJsonl("pressure-signals.jsonl", signal);
9488
9553
  }
9554
+ function loadNativePressureInterventions() {
9555
+ return readJsonl("pressure-interventions.jsonl");
9556
+ }
9557
+ function appendPressureIntervention(intervention) {
9558
+ appendJsonl("pressure-interventions.jsonl", intervention);
9559
+ }
9560
+ function loadNativeTransferEvents() {
9561
+ return readJsonl("transfer-events.jsonl");
9562
+ }
9563
+ function appendTransferEvent(event) {
9564
+ appendJsonl("transfer-events.jsonl", event);
9565
+ }
9566
+ function topologyChangeCounts() {
9567
+ return {
9568
+ merge: 0,
9569
+ split: 0,
9570
+ promote: 0,
9571
+ demote: 0,
9572
+ rewire: 0,
9573
+ prune: 0,
9574
+ consolidate: 0
9575
+ };
9576
+ }
9577
+ function uniqueStrings(values) {
9578
+ return [...new Set(values.filter((value) => Boolean(value && value.trim())))];
9579
+ }
9580
+ function mergeEvidence(existing, next) {
9581
+ return uniqueStrings([...existing ?? [], ...next ?? []]);
9582
+ }
9583
+ function maxTimestamp(...values) {
9584
+ return values.filter(Boolean).sort((a, b) => b.localeCompare(a))[0] ?? new Date(0).toISOString();
9585
+ }
9586
+ function priorityRank(priority) {
9587
+ return priority === "high" ? 3 : priority === "medium" ? 2 : 1;
9588
+ }
9589
+ function topologyStatusRank(status) {
9590
+ return status === "open" ? 4 : status === "deferred" ? 3 : status === "accepted" ? 2 : 1;
9591
+ }
9592
+ function priorityFromScore(score) {
9593
+ if (score >= 0.82)
9594
+ return "high";
9595
+ if (score >= 0.64)
9596
+ return "medium";
9597
+ return "low";
9598
+ }
9599
+ function governanceProfileForMode(mode) {
9600
+ return GOVERNANCE_PROFILES[mode];
9601
+ }
9602
+ function loadGovernanceState() {
9603
+ const state = readJson("governance-state.json", DEFAULT_GOVERNANCE_STATE);
9604
+ if (state.selectionMode === "manual" && state.manualMode)
9605
+ return state;
9606
+ return { ...DEFAULT_GOVERNANCE_STATE, ...state, selectionMode: state.selectionMode === "manual" && state.manualMode ? "manual" : "auto" };
9607
+ }
9608
+ function loadStoredTopologyReviewCandidates() {
9609
+ return readJson("topology-review-candidates.json", []).slice().sort((a, b) => b.lastObservedAt.localeCompare(a.lastObservedAt) || a.title.localeCompare(b.title));
9610
+ }
9611
+ function saveTopologyReviewCandidates(candidates) {
9612
+ writeJson("topology-review-candidates.json", candidates);
9613
+ }
9614
+ function loadTopologyReviewDecisions() {
9615
+ return readJsonl("topology-review-decisions.jsonl").slice().sort((a, b) => b.decidedAt.localeCompare(a.decidedAt));
9616
+ }
9617
+ function topologyEdgeKey(edge) {
9618
+ return `${edge.from}::${edge.to}::${edge.type}`;
9619
+ }
9620
+ function loadTopologyOverrides() {
9621
+ const overrides = readJson("topology-overrides.json", DEFAULT_TOPOLOGY_OVERRIDES);
9622
+ return {
9623
+ ...DEFAULT_TOPOLOGY_OVERRIDES,
9624
+ ...overrides,
9625
+ nodeOverrides: overrides.nodeOverrides ?? {},
9626
+ suppressedEdges: overrides.suppressedEdges ?? [],
9627
+ addedEdges: overrides.addedEdges ?? [],
9628
+ appliedPlanIds: overrides.appliedPlanIds ?? []
9629
+ };
9630
+ }
9631
+ function saveTopologyOverrides(overrides) {
9632
+ writeJson("topology-overrides.json", overrides);
9633
+ }
9634
+ function applyTopologyOverridesToGraph(graph, overrides = loadTopologyOverrides()) {
9635
+ const suppressed = new Set(overrides.suppressedEdges.map((edge) => topologyEdgeKey(edge)));
9636
+ const edgeMap = new Map;
9637
+ for (const edge of graph.edges) {
9638
+ const key = topologyEdgeKey(edge);
9639
+ if (!suppressed.has(key))
9640
+ edgeMap.set(key, edge);
9641
+ }
9642
+ for (const edge of overrides.addedEdges) {
9643
+ edgeMap.set(topologyEdgeKey(edge), edge);
9644
+ }
9645
+ return {
9646
+ ...graph,
9647
+ nodes: graph.nodes.map((node) => ({
9648
+ ...node,
9649
+ ...overrides.nodeOverrides[node.id] ?? {}
9650
+ })),
9651
+ edges: [...edgeMap.values()]
9652
+ };
9653
+ }
9654
+ function loadTopologySnapshots() {
9655
+ return readJson("topology-snapshots.json", []).slice().sort((a, b) => b.createdAt.localeCompare(a.createdAt));
9656
+ }
9657
+ function saveTopologySnapshots(snapshots) {
9658
+ writeJson("topology-snapshots.json", snapshots);
9659
+ }
9660
+ function loadTopologyApplyPlans() {
9661
+ return readJson("topology-apply-plans.json", []).slice().sort((a, b) => b.createdAt.localeCompare(a.createdAt));
9662
+ }
9663
+ function saveTopologyApplyPlans(plans) {
9664
+ writeJson("topology-apply-plans.json", plans);
9665
+ }
9666
+ function loadTopologyExecutionRecords() {
9667
+ return readJsonl("topology-executions.jsonl").slice().sort((a, b) => (b.executedAt ?? b.rolledBackAt ?? b.createdAt).localeCompare(a.executedAt ?? a.rolledBackAt ?? a.createdAt));
9668
+ }
9669
+ function appendTopologyExecutionRecord(record) {
9670
+ appendJsonl("topology-executions.jsonl", record);
9671
+ }
9672
+ function loadTopologyArtifacts() {
9673
+ return readJsonl("topology-artifacts.jsonl").slice().sort((a, b) => b.createdAt.localeCompare(a.createdAt));
9674
+ }
9675
+ function appendTopologyArtifact(artifact) {
9676
+ appendJsonl("topology-artifacts.jsonl", artifact);
9677
+ }
9678
+ function loadResolvedTopologyApplyPlans() {
9679
+ const plans = loadTopologyApplyPlans();
9680
+ const latestByPlan = new Map;
9681
+ for (const record of loadTopologyExecutionRecords()) {
9682
+ const existing = latestByPlan.get(record.planId);
9683
+ const recordAt = record.executedAt ?? record.rolledBackAt ?? record.createdAt;
9684
+ const existingAt = existing ? existing.executedAt ?? existing.rolledBackAt ?? existing.createdAt : "";
9685
+ if (!existing || recordAt > existingAt)
9686
+ latestByPlan.set(record.planId, record);
9687
+ }
9688
+ return plans.map((plan) => {
9689
+ const latestExecution = latestByPlan.get(plan.id);
9690
+ return {
9691
+ ...plan,
9692
+ latestStatus: latestExecution?.status ?? "pending",
9693
+ latestExecution,
9694
+ lastActivityAt: maxTimestamp(plan.createdAt, latestExecution?.executedAt, latestExecution?.rolledBackAt, latestExecution?.createdAt)
9695
+ };
9696
+ }).sort((a, b) => b.lastActivityAt.localeCompare(a.lastActivityAt) || (b.safeToApply === a.safeToApply ? 0 : b.safeToApply ? 1 : -1));
9697
+ }
9698
+ function getTopologyExecutionSummary() {
9699
+ const plans = loadResolvedTopologyApplyPlans();
9700
+ const snapshots = loadTopologySnapshots();
9701
+ const artifacts = loadTopologyArtifacts();
9702
+ const plannedCandidateIds = new Set(plans.map((plan) => plan.candidateId));
9703
+ const acceptedReady = loadResolvedTopologyReviewCandidates().filter((candidate) => candidate.status === "accepted" && !plannedCandidateIds.has(candidate.id)).slice(0, 8);
9704
+ return {
9705
+ plans: plans.length,
9706
+ pending: plans.filter((plan) => plan.latestStatus === "pending").length,
9707
+ prepared: plans.filter((plan) => plan.latestStatus === "prepared").length,
9708
+ applied: plans.filter((plan) => plan.latestStatus === "applied").length,
9709
+ rolledBack: plans.filter((plan) => plan.latestStatus === "rolled-back").length,
9710
+ failed: plans.filter((plan) => plan.latestStatus === "failed").length,
9711
+ safeToApply: plans.filter((plan) => plan.safeToApply).length,
9712
+ prepareOnly: plans.filter((plan) => plan.executionMode === "prepare-only").length,
9713
+ snapshots: snapshots.length,
9714
+ artifacts: artifacts.length,
9715
+ acceptedReady,
9716
+ preparedQueue: plans.filter((plan) => plan.latestStatus === "prepared").slice(0, 8),
9717
+ appliedHistory: plans.filter((plan) => plan.latestStatus === "applied" || plan.latestStatus === "rolled-back").slice(0, 8)
9718
+ };
9719
+ }
9720
+ function buildMergeReviewCandidate(params) {
9721
+ const { slugA, slugB, combinedStrength, confidence, governanceMode } = params;
9722
+ return {
9723
+ id: `topology_merge_${slugA}_${slugB}`.replace(/[^a-zA-Z0-9_-]/g, "-"),
9724
+ reviewKey: `merge:${[slugA, slugB].sort().join("::")}`,
9725
+ createdAt: new Date().toISOString(),
9726
+ lastObservedAt: new Date().toISOString(),
9727
+ source: "graph-optimize",
9728
+ changeType: "merge",
9729
+ title: `Merge overlap: ${slugA} + ${slugB}`,
9730
+ reasonSummary: "These skills show repeated high-strength overlap and likely duplicate the same abstraction boundary.",
9731
+ description: "Review whether the pair should collapse into one cleaner abstraction or remain separated with stronger scope boundaries.",
9732
+ priority: priorityFromScore(Math.max(confidence, Math.min(0.94, 0.58 + Math.max(0, combinedStrength - 1.5) * 0.22))),
9733
+ confidence,
9734
+ riskScore: Math.min(0.82, 0.52 + Math.max(0, combinedStrength - 1.5) * 0.14),
9735
+ governanceMode,
9736
+ affectedNodeIds: [slugA, slugB],
9737
+ evidence: [
9738
+ `${slugA} and ${slugB} share strong enhance/co-evolve signals.`,
9739
+ `Combined structural overlap score ${combinedStrength.toFixed(2)} suggests duplicate abstraction territory.`
9740
+ ]
9741
+ };
9742
+ }
9743
+ function buildSplitReviewCandidate(params) {
9744
+ const { slug, contentLength, failureCount, confidence, governanceMode } = params;
9745
+ return {
9746
+ id: `topology_split_${slug}`.replace(/[^a-zA-Z0-9_-]/g, "-"),
9747
+ reviewKey: `split:${slug}`,
9748
+ createdAt: new Date().toISOString(),
9749
+ lastObservedAt: new Date().toISOString(),
9750
+ source: "network-health",
9751
+ changeType: "split",
9752
+ title: `Split overloaded skill: ${slug}`,
9753
+ reasonSummary: "This skill is carrying too much content or too many failures to remain a clean single unit.",
9754
+ description: "Review whether the skill should be decomposed into narrower children or layered guidance blocks.",
9755
+ priority: priorityFromScore(Math.max(confidence, failureCount >= 28 ? 0.84 : failureCount >= 22 ? 0.72 : 0.6)),
9756
+ confidence,
9757
+ riskScore: Math.min(0.79, 0.48 + Math.min(0.16, Math.max(0, contentLength - 5000) / 6000) + Math.min(0.15, Math.max(0, failureCount - 20) / 30)),
9758
+ governanceMode,
9759
+ affectedNodeIds: [slug],
9760
+ evidence: [
9761
+ `${slug} has ${contentLength} characters of guidance.`,
9762
+ `${slug} has accumulated ${failureCount} failures, suggesting multiple pressure fronts are collapsing into one node.`
9763
+ ]
9764
+ };
9765
+ }
9766
+ function buildConflictReviewCandidate(params) {
9767
+ const { slugA, slugB, strength, governanceMode } = params;
9768
+ return {
9769
+ id: `topology_rewire_${slugA}_${slugB}`.replace(/[^a-zA-Z0-9_-]/g, "-"),
9770
+ reviewKey: `rewire:conflict:${[slugA, slugB].sort().join("::")}`,
9771
+ createdAt: new Date().toISOString(),
9772
+ lastObservedAt: new Date().toISOString(),
9773
+ source: "graph-optimize",
9774
+ changeType: "rewire",
9775
+ title: `Resolve conflicting pair: ${slugA} ↔ ${slugB}`,
9776
+ reasonSummary: "The graph already marks this pair as conflicting, which suggests the current relation topology may be mis-scoped.",
9777
+ description: "Review whether one rule should be narrowed, whether these nodes need cleaner lineage, or whether a missing bridge abstraction should absorb the conflict.",
9778
+ priority: priorityFromScore(Math.min(0.92, 0.7 + strength * 0.18)),
9779
+ confidence: Math.min(0.92, 0.66 + strength * 0.16),
9780
+ riskScore: Math.min(0.9, 0.68 + strength * 0.18),
9781
+ governanceMode,
9782
+ affectedNodeIds: [slugA, slugB],
9783
+ evidence: [
9784
+ `${slugA} and ${slugB} are linked by an explicit conflict edge.`,
9785
+ `Conflict strength ${strength.toFixed(2)} makes blind automated routing less trustworthy here.`
9786
+ ]
9787
+ };
9788
+ }
9789
+ function buildPromoteReviewCandidate(motif, governanceMode) {
9790
+ return {
9791
+ id: `topology_promote_${motif.id}`.replace(/[^a-zA-Z0-9_-]/g, "-"),
9792
+ reviewKey: `promote:${motif.id}`,
9793
+ createdAt: new Date().toISOString(),
9794
+ lastObservedAt: new Date().toISOString(),
9795
+ source: "pressure-motif",
9796
+ changeType: "promote",
9797
+ title: `Promote recurring motif: ${motif.capability ?? motif.kind}`,
9798
+ reasonSummary: "Recurring multi-project pressure suggests a reusable abstraction should move upward into the shared network backbone.",
9799
+ description: motif.responseSummary,
9800
+ priority: priorityFromScore(Math.max(motif.suggestedRoute.confidence, motif.highPriorityCount > 0 ? 0.84 : motif.recurrenceCount >= 3 ? 0.74 : 0.66)),
9801
+ confidence: motif.suggestedRoute.confidence,
9802
+ riskScore: Math.max(0.28, 0.62 - Math.min(0.18, motif.recurrenceCount * 0.05)),
9803
+ governanceMode,
9804
+ affectedNodeIds: motif.sourceSkillIds,
9805
+ projectIds: motif.projectIds,
9806
+ relatedSignalIds: motif.pressureSignalIds,
9807
+ relatedMotifIds: [motif.id],
9808
+ relatedInterventionIds: motif.linkedInterventionIds,
9809
+ evidence: mergeEvidence([
9810
+ `${motif.recurrenceCount} recurring signals now map to this motif.`,
9811
+ `${motif.projectIds.length} projects are contributing demand into the same region.`
9812
+ ], motif.suggestedRoute.reasons)
9813
+ };
9814
+ }
9815
+ function buildConsolidateReviewCandidate(motif, governanceMode) {
9816
+ return {
9817
+ id: `topology_consolidate_${motif.id}`.replace(/[^a-zA-Z0-9_-]/g, "-"),
9818
+ reviewKey: `consolidate:${motif.id}`,
9819
+ createdAt: new Date().toISOString(),
9820
+ lastObservedAt: new Date().toISOString(),
9821
+ source: "transfer-pattern",
9822
+ changeType: "consolidate",
9823
+ title: `Consolidate transfer-backed motif: ${motif.capability ?? motif.kind}`,
9824
+ reasonSummary: "Realized transfer evidence suggests the network can now stabilize this motif as a cleaner long-lived abstraction cluster.",
9825
+ description: motif.responseSummary,
9826
+ priority: priorityFromScore(motif.linkedTransferEventIds.length >= 2 ? 0.8 : 0.7),
9827
+ confidence: Math.min(0.9, 0.7 + Math.min(0.14, motif.linkedTransferEventIds.length * 0.06)),
9828
+ riskScore: Math.max(0.24, 0.54 - Math.min(0.16, motif.linkedTransferEventIds.length * 0.05)),
9829
+ governanceMode,
9830
+ affectedNodeIds: motif.sourceSkillIds,
9831
+ projectIds: motif.projectIds,
9832
+ relatedSignalIds: motif.pressureSignalIds,
9833
+ relatedMotifIds: [motif.id],
9834
+ relatedTransferEventIds: motif.linkedTransferEventIds,
9835
+ relatedInterventionIds: motif.linkedInterventionIds,
9836
+ evidence: [
9837
+ `${motif.linkedTransferEventIds.length} transfer event(s) are already linked to this motif.`,
9838
+ `${motif.projectIds.length} project contexts now share reusable evidence in the same region.`,
9839
+ motif.responseSummary
9840
+ ]
9841
+ };
9842
+ }
9843
+ function buildManualRouteReviewCandidate(signals, governanceMode) {
9844
+ const first = signals[0];
9845
+ const regionKey = pressureRegionKey(first);
9846
+ const projectIds = uniqueStrings(signals.flatMap((signal) => [signal.projectId, signal.projectPath]));
9847
+ const affectedNodeIds = uniqueStrings(signals.flatMap((signal) => signal.skillIds ?? []));
9848
+ const relatedInterventionIds = uniqueStrings(signals.flatMap((signal) => signal.linkedInterventions.map((intervention) => intervention.id)));
9849
+ const routeReasons = signals.flatMap((signal) => signal.routeRecommendation?.reasons ?? []);
9850
+ const maxConfidence = signals.reduce((max, signal) => Math.max(max, signal.routeRecommendation?.confidence ?? 0), 0);
9851
+ const hasHighPriority = signals.some((signal) => signal.priority === "high");
9852
+ return {
9853
+ id: `topology_manual_${regionKey}`.replace(/[^a-zA-Z0-9_-]/g, "-"),
9854
+ reviewKey: `manual-route:${regionKey}`,
9855
+ createdAt: new Date().toISOString(),
9856
+ lastObservedAt: maxTimestamp(...signals.map((signal) => signal.lastActivityAt)),
9857
+ source: "manual-route",
9858
+ changeType: "rewire",
9859
+ title: `Manual topology review: ${first.capability ?? first.kind}`,
9860
+ reasonSummary: "Multiple response lanes are touching the same pressure region without trustworthy closure, so the structure itself likely needs review.",
9861
+ description: first.responseSummary,
9862
+ priority: hasHighPriority || projectIds.length >= 2 ? "high" : priorityFromScore(Math.max(maxConfidence, 0.66)),
9863
+ confidence: Math.max(maxConfidence, 0.58),
9864
+ riskScore: Math.min(0.91, 0.72 + (hasHighPriority ? 0.05 : 0) + Math.min(0.08, Math.max(0, projectIds.length - 1) * 0.04)),
9865
+ governanceMode,
9866
+ affectedNodeIds,
9867
+ projectIds,
9868
+ relatedSignalIds: signals.map((signal) => signal.id),
9869
+ relatedMotifIds: uniqueStrings(signals.flatMap((signal) => signal.motifIds ?? [])),
9870
+ relatedInterventionIds,
9871
+ evidence: mergeEvidence([
9872
+ `${signals.length} active signals are converging on manual review in this region.`,
9873
+ `${relatedInterventionIds.length} linked intervention attempts already touched it.`
9874
+ ], routeReasons)
9875
+ };
9876
+ }
9877
+ function mergeTopologyCandidate(existing, next) {
9878
+ return {
9879
+ ...existing,
9880
+ lastObservedAt: maxTimestamp(existing.lastObservedAt, next.lastObservedAt),
9881
+ title: next.title,
9882
+ reasonSummary: next.reasonSummary,
9883
+ description: next.description ?? existing.description,
9884
+ priority: priorityRank(next.priority) > priorityRank(existing.priority) ? next.priority : existing.priority,
9885
+ confidence: Math.max(existing.confidence, next.confidence),
9886
+ riskScore: Math.max(existing.riskScore, next.riskScore),
9887
+ governanceMode: next.governanceMode,
9888
+ affectedNodeIds: uniqueStrings([...existing.affectedNodeIds, ...next.affectedNodeIds]),
9889
+ affectedRelationIds: uniqueStrings([...existing.affectedRelationIds ?? [], ...next.affectedRelationIds ?? []]),
9890
+ projectIds: uniqueStrings([...existing.projectIds ?? [], ...next.projectIds ?? []]),
9891
+ relatedSignalIds: uniqueStrings([...existing.relatedSignalIds ?? [], ...next.relatedSignalIds ?? []]),
9892
+ relatedMotifIds: uniqueStrings([...existing.relatedMotifIds ?? [], ...next.relatedMotifIds ?? []]),
9893
+ relatedTransferEventIds: uniqueStrings([...existing.relatedTransferEventIds ?? [], ...next.relatedTransferEventIds ?? []]),
9894
+ relatedInterventionIds: uniqueStrings([...existing.relatedInterventionIds ?? [], ...next.relatedInterventionIds ?? []]),
9895
+ evidence: mergeEvidence(existing.evidence, next.evidence)
9896
+ };
9897
+ }
9898
+ function deriveTopologyReviewCandidates() {
9899
+ const graph = loadSkillGraph();
9900
+ const skills = loadAllGeneralSkills();
9901
+ const preview = previewNetworkOptimization(graph, skills);
9902
+ const governance = getActiveGovernanceSummary();
9903
+ const pressureMotifs = loadPressureMotifs();
9904
+ const resolvedSignals = loadResolvedPressureSignals();
9905
+ const candidates = [];
9906
+ for (const merge of preview.merged) {
9907
+ candidates.push(buildMergeReviewCandidate({
9908
+ slugA: merge.slugs[0],
9909
+ slugB: merge.slugs[1],
9910
+ combinedStrength: merge.combinedStrength,
9911
+ confidence: merge.confidence,
9912
+ governanceMode: governance.activeMode
9913
+ }));
9914
+ }
9915
+ for (const split of preview.split) {
9916
+ candidates.push(buildSplitReviewCandidate({
9917
+ slug: split.slug,
9918
+ contentLength: split.contentLength,
9919
+ failureCount: split.failureCount,
9920
+ confidence: split.confidence,
9921
+ governanceMode: governance.activeMode
9922
+ }));
9923
+ }
9924
+ for (const conflict of preview.conflicts) {
9925
+ candidates.push(buildConflictReviewCandidate({
9926
+ slugA: conflict.slugs[0],
9927
+ slugB: conflict.slugs[1],
9928
+ strength: conflict.strength,
9929
+ governanceMode: governance.activeMode
9930
+ }));
9931
+ }
9932
+ for (const motif of pressureMotifs) {
9933
+ if (motif.lifecycle !== "addressed" && motif.suggestedRoute.route === "generalize") {
9934
+ candidates.push(buildPromoteReviewCandidate(motif, governance.activeMode));
9935
+ }
9936
+ if (motif.lifecycle === "addressed" && (motif.linkedTransferEventIds?.length ?? 0) > 0 && motif.sourceSkillIds.length > 0) {
9937
+ candidates.push(buildConsolidateReviewCandidate(motif, governance.activeMode));
9938
+ }
9939
+ }
9940
+ const manualGroups = new Map;
9941
+ for (const signal of resolvedSignals) {
9942
+ if (signal.lifecycle === "addressed")
9943
+ continue;
9944
+ if (signal.routeRecommendation?.route !== "manual-review")
9945
+ continue;
9946
+ const regionKey = pressureRegionKey(signal);
9947
+ const entries = manualGroups.get(regionKey) ?? [];
9948
+ entries.push(signal);
9949
+ manualGroups.set(regionKey, entries);
9950
+ }
9951
+ for (const signals of manualGroups.values()) {
9952
+ candidates.push(buildManualRouteReviewCandidate(signals, governance.activeMode));
9953
+ }
9954
+ return candidates.sort((a, b) => priorityRank(b.priority) - priorityRank(a.priority) || b.confidence - a.confidence || b.lastObservedAt.localeCompare(a.lastObservedAt) || a.title.localeCompare(b.title));
9955
+ }
9956
+ function refreshTopologyReviewCandidates() {
9957
+ const existing = loadStoredTopologyReviewCandidates();
9958
+ const derived = deriveTopologyReviewCandidates();
9959
+ const byKey = new Map(existing.map((candidate) => [candidate.reviewKey, candidate]));
9960
+ let created = 0;
9961
+ let updated = 0;
9962
+ for (const candidate of derived) {
9963
+ const current = byKey.get(candidate.reviewKey);
9964
+ if (!current) {
9965
+ byKey.set(candidate.reviewKey, candidate);
9966
+ created += 1;
9967
+ continue;
9968
+ }
9969
+ byKey.set(candidate.reviewKey, mergeTopologyCandidate(current, candidate));
9970
+ updated += 1;
9971
+ }
9972
+ const merged = [...byKey.values()].sort((a, b) => b.lastObservedAt.localeCompare(a.lastObservedAt) || a.title.localeCompare(b.title));
9973
+ saveTopologyReviewCandidates(merged);
9974
+ const resolved = loadResolvedTopologyReviewCandidates();
9975
+ return {
9976
+ created,
9977
+ updated,
9978
+ total: resolved.length,
9979
+ open: resolved.filter((candidate) => candidate.status === "open").length,
9980
+ candidates: resolved
9981
+ };
9982
+ }
9983
+ function loadResolvedTopologyReviewCandidates() {
9984
+ const candidates = loadStoredTopologyReviewCandidates();
9985
+ const latestDecisionByCandidate = new Map;
9986
+ for (const decision of loadTopologyReviewDecisions()) {
9987
+ const existing = latestDecisionByCandidate.get(decision.candidateId);
9988
+ if (!existing || decision.decidedAt > existing.decidedAt) {
9989
+ latestDecisionByCandidate.set(decision.candidateId, decision);
9990
+ }
9991
+ }
9992
+ return candidates.map((candidate) => {
9993
+ const latestDecision = latestDecisionByCandidate.get(candidate.id);
9994
+ const status = latestDecision?.decision ?? "open";
9995
+ return {
9996
+ ...candidate,
9997
+ status,
9998
+ latestDecision,
9999
+ lastActivityAt: maxTimestamp(candidate.lastObservedAt, latestDecision?.decidedAt)
10000
+ };
10001
+ }).sort((a, b) => topologyStatusRank(b.status) - topologyStatusRank(a.status) || priorityRank(b.priority) - priorityRank(a.priority) || b.confidence - a.confidence || b.lastActivityAt.localeCompare(a.lastActivityAt) || a.title.localeCompare(b.title));
10002
+ }
10003
+ function getTopologyReviewSummary() {
10004
+ const candidates = loadResolvedTopologyReviewCandidates();
10005
+ const byChangeType = topologyChangeCounts();
10006
+ for (const candidate of candidates) {
10007
+ byChangeType[candidate.changeType] += 1;
10008
+ }
10009
+ return {
10010
+ total: candidates.length,
10011
+ open: candidates.filter((candidate) => candidate.status === "open").length,
10012
+ accepted: candidates.filter((candidate) => candidate.status === "accepted").length,
10013
+ rejected: candidates.filter((candidate) => candidate.status === "rejected").length,
10014
+ deferred: candidates.filter((candidate) => candidate.status === "deferred").length,
10015
+ highPriorityOpen: candidates.filter((candidate) => candidate.status === "open" && candidate.priority === "high").length,
10016
+ generatedFromManualReview: candidates.filter((candidate) => candidate.source === "manual-route").length,
10017
+ byChangeType,
10018
+ backlog: candidates.filter((candidate) => candidate.status === "open" || candidate.status === "deferred").slice(0, 10),
10019
+ recentDecisions: candidates.filter((candidate) => candidate.status !== "open").slice(0, 8)
10020
+ };
10021
+ }
10022
+ function buildDerivedEvolutionArtifact(iteration, proposal) {
10023
+ return {
10024
+ id: `artifact_derived_${iteration.id}_${proposal.id}`,
10025
+ createdAt: iteration.timestamp,
10026
+ artifactType: "evolution",
10027
+ provenance: "derived-history",
10028
+ sourceId: iteration.id,
10029
+ iterationId: iteration.id,
10030
+ proposalId: proposal.id,
10031
+ title: `${proposal.targetSkill} · ${proposal.action}`,
10032
+ summary: proposal.description,
10033
+ targetSkill: proposal.targetSkill,
10034
+ operation: proposal.action,
10035
+ outcome: proposal.outcome,
10036
+ trigger: iteration.trigger,
10037
+ judgeScores: {
10038
+ taskCompletion: proposal.judges.taskCompletion.score,
10039
+ correctionAlignment: proposal.judges.correctionAlignment.score,
10040
+ sideEffectCheck: proposal.judges.sideEffectCheck.score
10041
+ },
10042
+ regression: proposal.regressionResult,
10043
+ metrics: {
10044
+ regressionPassRate: proposal.regressionResult.passRate,
10045
+ proposalConsensus: proposal.consensus ? 1 : 0
10046
+ }
10047
+ };
10048
+ }
10049
+ function deriveEvolutionArtifactsFromHistory(history = loadHistory()) {
10050
+ const artifacts = [];
10051
+ for (const iteration of history.iterations) {
10052
+ for (const proposal of iteration.proposals) {
10053
+ artifacts.push(buildDerivedEvolutionArtifact(iteration, proposal));
10054
+ }
10055
+ }
10056
+ return artifacts.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
10057
+ }
10058
+ function inferFailurePressurePriority(failure) {
10059
+ if (failure.correctionType === "manual_edit" || failure.correctionType === "mode_switch")
10060
+ return "high";
10061
+ if ((failure.skillsActive.length ?? 0) === 0)
10062
+ return "high";
10063
+ if (failure.correctionType === "retry")
10064
+ return "medium";
10065
+ return "low";
10066
+ }
10067
+ function inferFailurePressureSeverity(failure) {
10068
+ const base = failure.correctionType === "manual_edit" ? 0.95 : failure.correctionType === "mode_switch" ? 0.9 : failure.correctionType === "retry" ? 0.72 : 0.65;
10069
+ return failure.skillsActive.length === 0 ? Math.min(1, base + 0.08) : base;
10070
+ }
10071
+ function buildDerivedPressureSignalFromFailure(failure) {
10072
+ return {
10073
+ id: `pressure_derived_${failure.id}`,
10074
+ kind: `correction:${failure.correctionType}`,
10075
+ provenance: "failure-derived",
10076
+ sourceType: "failure",
10077
+ sourceId: failure.id,
10078
+ projectId: failure.project ?? undefined,
10079
+ detectedAt: failure.timestamp,
10080
+ severity: inferFailurePressureSeverity(failure),
10081
+ priority: inferFailurePressurePriority(failure),
10082
+ description: failure.correction,
10083
+ relatedFailureId: failure.id,
10084
+ relatedActivationTraceId: failure.activationTraceId,
10085
+ skillIds: failure.skillsActive,
10086
+ status: failure.resolved ? "addressed" : "open"
10087
+ };
10088
+ }
10089
+ function loadProjectProfilesForPressure() {
10090
+ const projectsDir = join3(getHelixDir(), "projects");
10091
+ if (!existsSync3(projectsDir))
10092
+ return [];
10093
+ try {
10094
+ return readdirSync2(projectsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => {
10095
+ const profilePath = join3(projectsDir, entry.name, "profile.json");
10096
+ if (!existsSync3(profilePath))
10097
+ return null;
10098
+ try {
10099
+ return JSON.parse(readFileSync3(profilePath, "utf-8"));
10100
+ } catch {
10101
+ return null;
10102
+ }
10103
+ }).filter((profile) => profile !== null);
10104
+ } catch {
10105
+ return [];
10106
+ }
10107
+ }
10108
+ function buildDerivedProfileGapPressureSignals(profile) {
10109
+ return profile.gaps.map((gap, index) => ({
10110
+ id: `pressure_profile_${profile.name}_${index}`,
10111
+ kind: `gap:${gap.area}`,
10112
+ provenance: "profile-gap-derived",
10113
+ sourceType: "project-analysis",
10114
+ sourceId: profile.path,
10115
+ projectId: profile.name,
10116
+ projectPath: profile.path,
10117
+ detectedAt: profile.analyzedAt,
10118
+ severity: gap.priority === "high" ? 0.95 : gap.priority === "medium" ? 0.75 : 0.55,
10119
+ priority: gap.priority,
10120
+ capability: gap.area,
10121
+ description: gap.description,
10122
+ suggestedAction: gap.suggestedAction,
10123
+ skillIds: profile.matchedSkills.map((skill) => skill.slug),
10124
+ status: "open"
10125
+ }));
10126
+ }
10127
+ function derivePressureSignalsFromFailures(failures = loadFailures()) {
10128
+ return failures.map(buildDerivedPressureSignalFromFailure).sort((a, b) => b.detectedAt.localeCompare(a.detectedAt));
10129
+ }
10130
+ function loadPressureSignals() {
10131
+ const nativeSignals = loadNativePressureSignals();
10132
+ const derivedFailureSignals = derivePressureSignalsFromFailures();
10133
+ const derivedProfileSignals = loadProjectProfilesForPressure().flatMap(buildDerivedProfileGapPressureSignals);
10134
+ const nativeKeys = new Set(nativeSignals.map((signal) => `${signal.sourceId}:${signal.kind}`));
10135
+ return [
10136
+ ...nativeSignals,
10137
+ ...derivedFailureSignals.filter((signal) => !nativeKeys.has(`${signal.sourceId}:${signal.kind}`)),
10138
+ ...derivedProfileSignals.filter((signal) => !nativeKeys.has(`${signal.sourceId}:${signal.kind}`))
10139
+ ].sort((a, b) => b.detectedAt.localeCompare(a.detectedAt));
10140
+ }
10141
+ function loadPressureInterventions() {
10142
+ return loadNativePressureInterventions().slice().sort((a, b) => (b.completedAt ?? b.createdAt).localeCompare(a.completedAt ?? a.createdAt));
10143
+ }
10144
+ function loadTransferEvents() {
10145
+ return loadNativeTransferEvents().slice().sort((a, b) => b.timestamp.localeCompare(a.timestamp));
10146
+ }
10147
+ function normalizePressureValue(value) {
10148
+ return (value ?? "").trim().toLowerCase().replace(/[\s_/]+/g, "-");
10149
+ }
10150
+ function pressureRegionKey(signal) {
10151
+ return normalizePressureValue(signal.capability ?? signal.region ?? signal.kind) || "general-pressure";
10152
+ }
10153
+ function motifIdForSignal(signal) {
10154
+ return `motif_${pressureRegionKey(signal)}`;
10155
+ }
10156
+ function pressureComparisonKey(signal) {
10157
+ const projectKey = normalizePressureValue(signal.projectId ?? signal.projectPath ?? "global");
10158
+ return `${projectKey}::${pressureRegionKey(signal)}`;
10159
+ }
10160
+ function interventionComparisonKeys(intervention) {
10161
+ const projectKey = normalizePressureValue(intervention.projectId ?? intervention.projectPath ?? "global");
10162
+ const capabilityKeys = (intervention.capabilities?.length ?? 0) > 0 ? intervention.capabilities ?? [] : [""];
10163
+ return capabilityKeys.map((capability) => `${projectKey}::${normalizePressureValue(capability)}`);
10164
+ }
10165
+ function signalMatchesInterventionFallback(signal, intervention) {
10166
+ if (intervention.pressureSignalIds?.includes(signal.id))
10167
+ return true;
10168
+ if (intervention.scope === "network")
10169
+ return false;
10170
+ if (signal.relatedFailureId && intervention.relatedFailureIds?.includes(signal.relatedFailureId))
10171
+ return true;
10172
+ if (signal.relatedActivationTraceId && intervention.relatedActivationTraceIds?.includes(signal.relatedActivationTraceId))
10173
+ return true;
10174
+ const signalKey = pressureComparisonKey(signal);
10175
+ if (interventionComparisonKeys(intervention).includes(signalKey))
10176
+ return true;
10177
+ if (signal.projectId && intervention.projectId && signal.projectId !== intervention.projectId)
10178
+ return false;
10179
+ if (signal.projectPath && intervention.projectPath && signal.projectPath !== intervention.projectPath)
10180
+ return false;
10181
+ if (signal.capability && (intervention.capabilities?.length ?? 0) > 0) {
10182
+ return intervention.capabilities?.some((capability) => normalizePressureValue(capability) === normalizePressureValue(signal.capability)) ?? false;
10183
+ }
10184
+ return false;
10185
+ }
10186
+ function motifMatchesIntervention(motifId, signals, intervention) {
10187
+ if (intervention.motifIds?.includes(motifId))
10188
+ return true;
10189
+ if (signals.some((signal) => intervention.pressureSignalIds?.includes(signal.id)))
10190
+ return true;
10191
+ if (intervention.scope === "network") {
10192
+ const signalKeys = new Set(signals.map((signal) => pressureRegionKey(signal)));
10193
+ return (intervention.capabilities ?? []).some((capability) => signalKeys.has(normalizePressureValue(capability)));
10194
+ }
10195
+ return false;
10196
+ }
10197
+ function transferMatchesMotif(motifId, signals, event) {
10198
+ if (event.motifIds?.includes(motifId))
10199
+ return true;
10200
+ const capabilities = new Set(signals.map((signal) => normalizePressureValue(signal.capability ?? signal.kind)).filter(Boolean));
10201
+ return (event.capabilityIds ?? []).some((capability) => capabilities.has(normalizePressureValue(capability)));
10202
+ }
10203
+ function deriveGovernanceSummary(pressureSignals = loadPressureSignals(), interventions = loadPressureInterventions()) {
10204
+ const highPrioritySignals = pressureSignals.filter((signal) => signal.priority === "high" && signal.status !== "addressed").length;
10205
+ const recurringCrossProjectRegions = new Map;
10206
+ for (const signal of pressureSignals) {
10207
+ const projectId = signal.projectId ?? signal.projectPath;
10208
+ if (!projectId)
10209
+ continue;
10210
+ const projects = recurringCrossProjectRegions.get(pressureRegionKey(signal)) ?? new Set;
10211
+ projects.add(projectId);
10212
+ recurringCrossProjectRegions.set(pressureRegionKey(signal), projects);
10213
+ }
10214
+ const crossProjectMotifs = [...recurringCrossProjectRegions.values()].filter((projects) => projects.size >= 2).length;
10215
+ const exploratoryRuns = interventions.filter((intervention) => intervention.status === "dry-run" || intervention.impact === "exploratory").length;
10216
+ const failedRuns = interventions.filter((intervention) => intervention.status === "failed").length;
10217
+ const resolvingRuns = interventions.filter((intervention) => intervention.status === "completed" && intervention.impact === "resolving").length;
10218
+ let derivedMode = "balanced";
10219
+ let derivedRationale = [
10220
+ "Current pressure is mixed across local response and broader transfer opportunities.",
10221
+ "Governance should keep local adaptation and cross-project learning in balance."
10222
+ ];
10223
+ if (crossProjectMotifs >= 2) {
10224
+ derivedMode = "transfer-focused";
10225
+ derivedRationale = [
10226
+ `${crossProjectMotifs} recurring pressure motifs now span multiple projects.`,
10227
+ "Promoting reusable abstractions has higher leverage than only local adaptation right now."
10228
+ ];
10229
+ } else if (highPrioritySignals >= 4) {
10230
+ derivedMode = "project-critical";
10231
+ derivedRationale = [
10232
+ `${highPrioritySignals} high-priority pressure signals still need response.`,
10233
+ "Governance should bias toward project-bounded stabilization before broader promotion work."
10234
+ ];
10235
+ } else if (exploratoryRuns > resolvingRuns + 1) {
10236
+ derivedMode = "exploration";
10237
+ derivedRationale = [
10238
+ `${exploratoryRuns} exploratory responses outweigh resolving outcomes.`,
10239
+ "The network is still learning where the strongest durable response should be routed."
10240
+ ];
10241
+ } else if (failedRuns >= 2 && resolvingRuns === 0) {
10242
+ derivedMode = "conservative";
10243
+ derivedRationale = [
10244
+ `${failedRuns} recent response attempts failed without resolving evidence.`,
10245
+ "Governance should slow promotion and prefer clearer evidence before acting broadly."
10246
+ ];
10247
+ } else if (resolvingRuns >= 3 && highPrioritySignals <= 2) {
10248
+ derivedMode = "consolidation-focused";
10249
+ derivedRationale = [
10250
+ `${resolvingRuns} resolving responses already landed recently.`,
10251
+ "The network is in a good position to consolidate what it has learned."
10252
+ ];
10253
+ }
10254
+ const state = loadGovernanceState();
10255
+ if (state.selectionMode === "manual" && state.manualMode) {
10256
+ return {
10257
+ activeMode: state.manualMode,
10258
+ derivedMode,
10259
+ source: "operator-selected",
10260
+ manualMode: state.manualMode,
10261
+ rationale: [
10262
+ `Operator selected ${state.manualMode} governance on ${state.updatedAt}.`,
10263
+ `Derived mode would currently be ${derivedMode}.`,
10264
+ ...derivedRationale
10265
+ ],
10266
+ profile: governanceProfileForMode(state.manualMode)
10267
+ };
10268
+ }
10269
+ return {
10270
+ activeMode: derivedMode,
10271
+ derivedMode,
10272
+ source: "derived",
10273
+ rationale: derivedRationale,
10274
+ profile: governanceProfileForMode(derivedMode)
10275
+ };
10276
+ }
10277
+ function getActiveGovernanceSummary() {
10278
+ return deriveGovernanceSummary();
10279
+ }
10280
+ function buildRouteRecommendation(params) {
10281
+ const { signal, relatedSignals, linkedInterventions, governanceMode } = params;
10282
+ const projectSpread = new Set(relatedSignals.map((entry) => entry.projectId ?? entry.projectPath).filter(Boolean)).size;
10283
+ const recurrenceCount = relatedSignals.length;
10284
+ const activeTypes = [...new Set(linkedInterventions.map((intervention) => intervention.interventionType))];
10285
+ const reasons = [];
10286
+ if (activeTypes.length >= 3 && !linkedInterventions.some((intervention) => intervention.status === "completed" && intervention.impact === "resolving")) {
10287
+ reasons.push("Multiple intervention lanes already touch this pressure region without clear closure.");
10288
+ reasons.push("Operator review is safer than blindly piling on another automated response.");
10289
+ return {
10290
+ route: "manual-review",
10291
+ scope: projectSpread >= 2 ? "network" : signal.projectId ? "project" : "local",
10292
+ confidence: 0.58,
10293
+ governanceMode,
10294
+ triggeredBy: "mixed-signal",
10295
+ reasons
10296
+ };
10297
+ }
10298
+ if (projectSpread >= 2 && recurrenceCount >= 2) {
10299
+ reasons.push(`This pressure region repeats across ${projectSpread} projects.`);
10300
+ reasons.push("A network-level abstraction can absorb repeated local demand more efficiently than one-off fixes.");
10301
+ if (governanceMode !== "project-critical") {
10302
+ return {
10303
+ route: "generalize",
10304
+ scope: "network",
10305
+ confidence: Math.min(0.95, 0.7 + Math.min(0.15, (projectSpread - 1) * 0.08)),
10306
+ governanceMode,
10307
+ triggeredBy: "recurring-cross-project-gap",
10308
+ reasons
10309
+ };
10310
+ }
10311
+ }
10312
+ if (signal.projectId && signal.priority === "high" && signal.suggestedAction === "specialize") {
10313
+ reasons.push("The pressure is high priority and bounded to a known project context.");
10314
+ reasons.push("Project-layer adaptation is the shortest truthful path to response.");
10315
+ return {
10316
+ route: "specialize",
10317
+ scope: "project",
10318
+ confidence: 0.82,
10319
+ governanceMode,
10320
+ triggeredBy: "high-priority-local-gap",
10321
+ reasons
10322
+ };
10323
+ }
10324
+ if (signal.relatedFailureId && signal.severity >= 0.8) {
10325
+ reasons.push("This pressure is tied to a concrete failure with strong corrective evidence.");
10326
+ reasons.push("An evolution proposal can test a direct skill-level response against replay and regression evidence.");
10327
+ return {
10328
+ route: "evolve",
10329
+ scope: signal.projectId ? "project" : "local",
10330
+ confidence: 0.78,
10331
+ governanceMode,
10332
+ triggeredBy: "accepted-iteration-pattern",
10333
+ reasons
10334
+ };
10335
+ }
10336
+ if (governanceMode === "exploration" || signal.suggestedAction === "research" || !signal.projectId) {
10337
+ reasons.push("The pressure still looks under-specified or capability-oriented.");
10338
+ reasons.push("Research is the best way to widen evidence before committing to structural change.");
10339
+ return {
10340
+ route: "research",
10341
+ scope: signal.projectId ? "project" : "local",
10342
+ confidence: 0.68,
10343
+ governanceMode,
10344
+ triggeredBy: "insufficient-evidence",
10345
+ reasons
10346
+ };
10347
+ }
10348
+ if (signal.projectId) {
10349
+ reasons.push("The pressure is project-scoped and can likely be handled without network-wide promotion yet.");
10350
+ reasons.push("Specialization is the most direct bounded intervention available.");
10351
+ return {
10352
+ route: "specialize",
10353
+ scope: "project",
10354
+ confidence: 0.7,
10355
+ governanceMode,
10356
+ triggeredBy: "project-bounded-recurrence",
10357
+ reasons
10358
+ };
10359
+ }
10360
+ reasons.push("No single route is dominant from the current evidence.");
10361
+ reasons.push("Research keeps the loop moving while preserving optionality.");
10362
+ return {
10363
+ route: "research",
10364
+ scope: "local",
10365
+ confidence: 0.6,
10366
+ governanceMode,
10367
+ triggeredBy: "insufficient-evidence",
10368
+ reasons
10369
+ };
10370
+ }
10371
+ function interventionEvidenceAccepted(intervention, acceptedProposalIds, acceptedArtifactIds, realizedTransferEventIds) {
10372
+ if (intervention.status !== "completed" || intervention.impact !== "resolving")
10373
+ return false;
10374
+ if (intervention.relatedTransferEventIds?.some((id) => realizedTransferEventIds.has(id)))
10375
+ return true;
10376
+ if (intervention.relatedProposalIds?.some((id) => acceptedProposalIds.has(id))) {
10377
+ return !(intervention.interventionType === "generalize" && intervention.scope === "network");
10378
+ }
10379
+ if (intervention.relatedArtifactIds?.some((id) => acceptedArtifactIds.has(id))) {
10380
+ return !(intervention.interventionType === "generalize" && intervention.scope === "network");
10381
+ }
10382
+ if (intervention.interventionType !== "generalize")
10383
+ return Boolean(intervention.relatedSkillSlugs?.length);
10384
+ return false;
10385
+ }
10386
+ function interventionKeepsPressureActive(impact, status) {
10387
+ if (status === "failed" || status === "dry-run")
10388
+ return false;
10389
+ return impact === "exploratory" || impact === "structural" || impact === "resolving";
10390
+ }
10391
+ function loadResolvedPressureSignals() {
10392
+ const pressureSignals = loadPressureSignals();
10393
+ const interventions = loadPressureInterventions();
10394
+ const transferEvents = loadTransferEvents();
10395
+ const history = loadHistory();
10396
+ const artifacts = loadEvolutionArtifacts();
10397
+ const governance = deriveGovernanceSummary(pressureSignals, interventions);
10398
+ const acceptedProposalIds = new Set(history.iterations.flatMap((iteration) => iteration.proposals).filter((proposal) => proposal.outcome === "accepted").map((proposal) => proposal.id));
10399
+ const acceptedArtifactIds = new Set(artifacts.filter((artifact) => artifact.outcome === "accepted").map((artifact) => artifact.id));
10400
+ const realizedTransferEventIds = new Set(transferEvents.filter((event) => event.status === "realized").map((event) => event.id));
10401
+ const latestSignalByKey = new Map;
10402
+ const signalsByRegion = new Map;
10403
+ for (const signal of pressureSignals) {
10404
+ const comparisonKey = pressureComparisonKey(signal);
10405
+ const existing = latestSignalByKey.get(comparisonKey);
10406
+ if (!existing || signal.detectedAt > existing) {
10407
+ latestSignalByKey.set(comparisonKey, signal.detectedAt);
10408
+ }
10409
+ const regionKey = pressureRegionKey(signal);
10410
+ const regionSignals = signalsByRegion.get(regionKey) ?? [];
10411
+ regionSignals.push(signal);
10412
+ signalsByRegion.set(regionKey, regionSignals);
10413
+ }
10414
+ return pressureSignals.map((signal) => {
10415
+ const linkedInterventions = interventions.filter((intervention) => signalMatchesInterventionFallback(signal, intervention));
10416
+ const addressingInterventions = linkedInterventions.filter((intervention) => interventionEvidenceAccepted(intervention, acceptedProposalIds, acceptedArtifactIds, realizedTransferEventIds));
10417
+ const activeInterventions = linkedInterventions.filter((intervention) => interventionKeepsPressureActive(intervention.impact, intervention.status));
10418
+ const latestInterventionAt = linkedInterventions.map((intervention) => intervention.completedAt ?? intervention.createdAt).sort((a, b) => b.localeCompare(a))[0];
10419
+ const latestSignalAt = latestSignalByKey.get(pressureComparisonKey(signal)) ?? signal.detectedAt;
10420
+ const hasNewerEquivalentSignal = latestSignalAt > signal.detectedAt;
10421
+ const relatedSignals = signalsByRegion.get(pressureRegionKey(signal)) ?? [signal];
10422
+ const projectSpread = new Set(relatedSignals.map((entry) => entry.projectId ?? entry.projectPath).filter(Boolean)).size;
10423
+ const motifIds = relatedSignals.length >= 2 || projectSpread >= 2 ? [motifIdForSignal(signal)] : [];
10424
+ const routeRecommendation = buildRouteRecommendation({
10425
+ signal,
10426
+ relatedSignals,
10427
+ linkedInterventions,
10428
+ governanceMode: governance.activeMode
10429
+ });
10430
+ const lifecycle = addressingInterventions.length > 0 || signal.status === "addressed" ? "addressed" : activeInterventions.length > 0 ? "in-progress" : hasNewerEquivalentSignal ? "stale" : "open";
10431
+ const lastActivityAt = [signal.detectedAt, latestInterventionAt].filter(Boolean).sort((a, b) => b.localeCompare(a))[0] ?? signal.detectedAt;
10432
+ const interventionTypes = [...new Set(linkedInterventions.map((intervention) => intervention.interventionType))];
10433
+ const addressedAt = addressingInterventions.map((intervention) => intervention.completedAt ?? intervention.createdAt).sort((a, b) => b.localeCompare(a))[0];
10434
+ const responseSummary = lifecycle === "addressed" ? `Addressed by ${interventionTypes.join(", ") || "linked intervention"} evidence` : lifecycle === "in-progress" ? `Response underway via ${interventionTypes.join(", ")}` : lifecycle === "stale" ? "Superseded by newer pressure in the same project/capability region" : `No linked response yet • recommend ${routeRecommendation.route}`;
10435
+ return {
10436
+ ...signal,
10437
+ lifecycle,
10438
+ linkedInterventions,
10439
+ interventionTypes,
10440
+ motifIds,
10441
+ routeRecommendation,
10442
+ lastActivityAt,
10443
+ addressedAt,
10444
+ responseSummary
10445
+ };
10446
+ });
10447
+ }
10448
+ function loadPressureMotifs() {
10449
+ const resolvedSignals = loadResolvedPressureSignals();
10450
+ const interventions = loadPressureInterventions();
10451
+ const transferEvents = loadTransferEvents();
10452
+ const groups = new Map;
10453
+ for (const signal of resolvedSignals) {
10454
+ const regionKey = pressureRegionKey(signal);
10455
+ const entries = groups.get(regionKey) ?? [];
10456
+ entries.push(signal);
10457
+ groups.set(regionKey, entries);
10458
+ }
10459
+ const motifs = [];
10460
+ for (const [regionKey, signals] of groups.entries()) {
10461
+ const projectIds = [...new Set(signals.map((signal) => signal.projectId ?? signal.projectPath).filter(Boolean))];
10462
+ if (signals.length < 2 && projectIds.length < 2)
10463
+ continue;
10464
+ const motifId = `motif_${regionKey}`;
10465
+ const linkedInterventions = interventions.filter((intervention) => motifMatchesIntervention(motifId, signals, intervention));
10466
+ const linkedTransferEvents = transferEvents.filter((event) => transferMatchesMotif(motifId, signals, event));
10467
+ const activeInterventions = linkedInterventions.filter((intervention) => interventionKeepsPressureActive(intervention.impact, intervention.status));
10468
+ const realizedTransfers = linkedTransferEvents.filter((event) => event.status === "realized");
10469
+ const allAddressed = signals.every((signal) => signal.lifecycle === "addressed");
10470
+ const allStale = signals.every((signal) => signal.lifecycle === "stale");
10471
+ const lifecycle = realizedTransfers.length > 0 || allAddressed ? "addressed" : activeInterventions.length > 0 ? "in-progress" : allStale ? "stale" : "open";
10472
+ const interventionTypes = [...new Set(linkedInterventions.map((intervention) => intervention.interventionType))];
10473
+ const recommendation = signals.find((signal) => signal.routeRecommendation?.route === "generalize")?.routeRecommendation ?? signals[0]?.routeRecommendation ?? {
10474
+ route: "research",
10475
+ scope: projectIds.length >= 2 ? "network" : "project",
10476
+ confidence: 0.5,
10477
+ governanceMode: deriveGovernanceSummary().activeMode,
10478
+ triggeredBy: "insufficient-evidence",
10479
+ reasons: ["Recurring pressure exists, but routing evidence remains coarse."]
10480
+ };
10481
+ const lastActivityAt = [
10482
+ ...signals.map((signal) => signal.lastActivityAt),
10483
+ ...linkedInterventions.map((intervention) => intervention.completedAt ?? intervention.createdAt),
10484
+ ...linkedTransferEvents.map((event) => event.timestamp)
10485
+ ].sort((a, b) => b.localeCompare(a))[0] ?? signals[0]?.lastActivityAt ?? new Date(0).toISOString();
10486
+ const addressedAt = [
10487
+ ...realizedTransfers.map((event) => event.timestamp),
10488
+ ...signals.map((signal) => signal.addressedAt).filter(Boolean)
10489
+ ].sort((a, b) => b.localeCompare(a))[0];
10490
+ const capability = signals.find((signal) => Boolean(signal.capability))?.capability;
10491
+ const description = signals.find((signal) => Boolean(signal.description))?.description ?? `Recurring pressure in ${capability ?? regionKey}`;
10492
+ const sourceSkillIds = [...new Set(signals.flatMap((signal) => signal.skillIds ?? []))];
10493
+ const linkedInterventionIds = linkedInterventions.map((intervention) => intervention.id);
10494
+ const linkedTransferEventIds = linkedTransferEvents.map((event) => event.id);
10495
+ const highPriorityCount = signals.filter((signal) => signal.priority === "high").length;
10496
+ const responseSummary = lifecycle === "addressed" ? realizedTransfers.length > 0 ? "Promoted into reusable transfer evidence" : "All linked pressure in this recurring region is addressed" : lifecycle === "in-progress" ? `Promotion or response underway via ${interventionTypes.join(", ") || "linked intervention"}` : lifecycle === "stale" ? "Recurring pressure has been superseded by newer equivalent signals" : `Recurring demand is waiting for ${recommendation.route}`;
10497
+ motifs.push({
10498
+ id: motifId,
10499
+ key: regionKey,
10500
+ kind: signals[0]?.kind ?? regionKey,
10501
+ capability,
10502
+ description,
10503
+ createdAt: signals.map((signal) => signal.detectedAt).sort()[0] ?? lastActivityAt,
10504
+ lastSeenAt: signals.map((signal) => signal.detectedAt).sort((a, b) => b.localeCompare(a))[0] ?? lastActivityAt,
10505
+ projectIds,
10506
+ pressureSignalIds: signals.map((signal) => signal.id),
10507
+ sourceSkillIds,
10508
+ recurrenceCount: signals.length,
10509
+ highPriorityCount,
10510
+ linkedInterventionIds,
10511
+ linkedTransferEventIds,
10512
+ suggestedRoute: recommendation,
10513
+ lifecycle,
10514
+ interventionTypes,
10515
+ lastActivityAt,
10516
+ addressedAt,
10517
+ responseSummary
10518
+ });
10519
+ }
10520
+ return motifs.sort((a, b) => {
10521
+ const lifecycleWeight = (value) => value === "open" ? 4 : value === "in-progress" ? 3 : value === "addressed" ? 2 : 1;
10522
+ return lifecycleWeight(b.lifecycle) - lifecycleWeight(a.lifecycle) || b.highPriorityCount - a.highPriorityCount || b.recurrenceCount - a.recurrenceCount || b.lastActivityAt.localeCompare(a.lastActivityAt);
10523
+ });
10524
+ }
10525
+ function matchPressureSignals(filters) {
10526
+ const capabilities = new Set((filters.capabilities ?? []).map((value) => normalizePressureValue(value)));
10527
+ const relatedFailureIds = new Set(filters.relatedFailureIds ?? []);
10528
+ const relatedActivationTraceIds = new Set(filters.relatedActivationTraceIds ?? []);
10529
+ return loadResolvedPressureSignals().filter((signal) => filters.includeAddressed || signal.lifecycle !== "addressed").filter((signal) => !filters.projectId || signal.projectId === filters.projectId).filter((signal) => !filters.projectPath || signal.projectPath === filters.projectPath).filter((signal) => !filters.suggestedAction || signal.suggestedAction === filters.suggestedAction).filter((signal) => {
10530
+ if (capabilities.size === 0)
10531
+ return true;
10532
+ const normalizedCapability = normalizePressureValue(signal.capability);
10533
+ return normalizedCapability.length > 0 && capabilities.has(normalizedCapability);
10534
+ }).filter((signal) => relatedFailureIds.size === 0 || Boolean(signal.relatedFailureId && relatedFailureIds.has(signal.relatedFailureId))).filter((signal) => relatedActivationTraceIds.size === 0 || Boolean(signal.relatedActivationTraceId && relatedActivationTraceIds.has(signal.relatedActivationTraceId))).sort((a, b) => {
10535
+ const priorityScore = (value) => value === "high" ? 3 : value === "medium" ? 2 : 1;
10536
+ return priorityScore(b.priority) - priorityScore(a.priority) || b.severity - a.severity || b.detectedAt.localeCompare(a.detectedAt);
10537
+ });
10538
+ }
10539
+ function matchPressureMotifs(filters) {
10540
+ const capabilities = new Set((filters.capabilities ?? []).map((value) => normalizePressureValue(value)));
10541
+ const skillIds = new Set(filters.skillIds ?? []);
10542
+ return loadPressureMotifs().filter((motif) => filters.includeAddressed || motif.lifecycle !== "addressed").filter((motif) => {
10543
+ if (capabilities.size === 0)
10544
+ return true;
10545
+ const values = [motif.capability, motif.key, motif.kind].map((value) => normalizePressureValue(value)).filter(Boolean);
10546
+ return values.some((value) => capabilities.has(value));
10547
+ }).filter((motif) => skillIds.size === 0 || motif.sourceSkillIds.some((skillId) => skillIds.has(skillId))).sort((a, b) => b.highPriorityCount - a.highPriorityCount || b.recurrenceCount - a.recurrenceCount || b.lastActivityAt.localeCompare(a.lastActivityAt));
10548
+ }
10549
+ function loadEvolutionArtifacts() {
10550
+ const nativeArtifacts = loadNativeEvolutionArtifacts();
10551
+ const derivedArtifacts = deriveEvolutionArtifactsFromHistory();
10552
+ const nativeProposalIds = new Set(nativeArtifacts.map((artifact) => artifact.proposalId).filter(Boolean));
10553
+ const merged = [
10554
+ ...nativeArtifacts,
10555
+ ...derivedArtifacts.filter((artifact) => !artifact.proposalId || !nativeProposalIds.has(artifact.proposalId))
10556
+ ];
10557
+ return merged.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
10558
+ }
10559
+ var DEFAULT_GOVERNANCE_STATE, GOVERNANCE_PROFILES, DEFAULT_TOPOLOGY_OVERRIDES;
9489
10560
  var init_data = __esm(() => {
9490
10561
  init_ontology();
9491
10562
  init_config();
10563
+ init_skills();
10564
+ DEFAULT_GOVERNANCE_STATE = {
10565
+ selectionMode: "auto",
10566
+ updatedAt: new Date(0).toISOString()
10567
+ };
10568
+ GOVERNANCE_PROFILES = {
10569
+ conservative: {
10570
+ mode: "conservative",
10571
+ reviewThreshold: 0.86,
10572
+ riskTolerance: 0.28,
10573
+ plasticityBias: 0.26,
10574
+ explanation: "Prefer stronger evidence and slower structural change until risk falls."
10575
+ },
10576
+ balanced: {
10577
+ mode: "balanced",
10578
+ reviewThreshold: 0.74,
10579
+ riskTolerance: 0.5,
10580
+ plasticityBias: 0.5,
10581
+ explanation: "Keep local stabilization and structural adaptation in balance."
10582
+ },
10583
+ exploration: {
10584
+ mode: "exploration",
10585
+ reviewThreshold: 0.62,
10586
+ riskTolerance: 0.7,
10587
+ plasticityBias: 0.72,
10588
+ explanation: "Favor evidence gathering and wider structural exploration before locking in."
10589
+ },
10590
+ "project-critical": {
10591
+ mode: "project-critical",
10592
+ reviewThreshold: 0.82,
10593
+ riskTolerance: 0.35,
10594
+ plasticityBias: 0.34,
10595
+ explanation: "Bias toward project-bounded stability and cautious structural change."
10596
+ },
10597
+ "consolidation-focused": {
10598
+ mode: "consolidation-focused",
10599
+ reviewThreshold: 0.78,
10600
+ riskTolerance: 0.42,
10601
+ plasticityBias: 0.44,
10602
+ explanation: "Favor consolidating proven patterns into stable reusable structure."
10603
+ },
10604
+ "transfer-focused": {
10605
+ mode: "transfer-focused",
10606
+ reviewThreshold: 0.68,
10607
+ riskTolerance: 0.6,
10608
+ plasticityBias: 0.68,
10609
+ explanation: "Bias toward cross-project promotion and reusable topology upgrades."
10610
+ }
10611
+ };
10612
+ DEFAULT_TOPOLOGY_OVERRIDES = {
10613
+ updatedAt: new Date(0).toISOString(),
10614
+ nodeOverrides: {},
10615
+ suppressedEdges: [],
10616
+ addedEdges: [],
10617
+ appliedPlanIds: []
10618
+ };
9492
10619
  });
9493
10620
 
9494
10621
  // src/utils/llm.ts
@@ -10810,9 +11937,11 @@ async function evolveCommand(options) {
10810
11937
  }
10811
11938
  const generation = getCurrentGeneration() + 1;
10812
11939
  const iterationId = generateIterationId();
11940
+ const governance = getActiveGovernanceSummary();
10813
11941
  const proposals = [];
10814
11942
  const failureClusters = [];
10815
11943
  const artifacts = [];
11944
+ const interventions = [];
10816
11945
  let proposalCount = 0;
10817
11946
  for (const cluster of clusters.clusters) {
10818
11947
  if (proposalCount >= maxProposals)
@@ -10941,6 +12070,32 @@ async function evolveCommand(options) {
10941
12070
  regressionPassRate: regressionResult.passRate
10942
12071
  }
10943
12072
  });
12073
+ const matchedPressureSignals = matchPressureSignals({
12074
+ relatedFailureIds: clusterFailures.map((failure) => failure.id),
12075
+ relatedActivationTraceIds: clusterFailures.map((failure) => failure.activationTraceId).filter(Boolean)
12076
+ });
12077
+ interventions.push({
12078
+ id: `intervention_evolve_${proposalId}`,
12079
+ interventionType: "evolve",
12080
+ status: dryRun ? "dry-run" : "completed",
12081
+ impact: finalAccepted ? "resolving" : consensus || passedRegression ? "exploratory" : "none",
12082
+ createdAt: artifactCreatedAt,
12083
+ completedAt: artifactCreatedAt,
12084
+ sourceId: `evolve:${iterationId}:${proposalId}`,
12085
+ projectId: matchedPressureSignals[0]?.projectId ?? clusterFailures.find((failure) => Boolean(failure.project))?.project ?? undefined,
12086
+ projectPath: matchedPressureSignals[0]?.projectPath,
12087
+ scope: matchedPressureSignals.some((signal) => Boolean(signal.projectId ?? signal.projectPath)) ? "project" : "local",
12088
+ governanceMode: governance.activeMode,
12089
+ capabilities: matchedPressureSignals.map((signal) => signal.capability).filter(Boolean),
12090
+ pressureSignalIds: matchedPressureSignals.map((signal) => signal.id),
12091
+ relatedFailureIds: clusterFailures.map((failure) => failure.id),
12092
+ relatedActivationTraceIds: clusterFailures.map((failure) => failure.activationTraceId).filter(Boolean),
12093
+ relatedProposalIds: [proposalId],
12094
+ relatedArtifactIds: dryRun ? [] : [`artifact_${proposalId}`],
12095
+ relatedSkillSlugs: finalAccepted ? [skillSlug] : [],
12096
+ reasonSummary: `Evolution processed cluster "${cluster.type}" across ${clusterFailures.length} failures.`,
12097
+ notes: dryRun ? "Evolution ran in dry-run mode, so proposal evidence is exploratory only." : undefined
12098
+ });
10944
12099
  if (finalAccepted && !dryRun) {
10945
12100
  const { meta, content } = parseSkillMd(proposalOutput.proposedSkillMd);
10946
12101
  const skillSlug2 = proposalOutput.targetSkill ?? proposal.targetSkill;
@@ -11006,6 +12161,9 @@ async function evolveCommand(options) {
11006
12161
  appendEvolutionArtifact(artifact);
11007
12162
  }
11008
12163
  }
12164
+ for (const intervention of interventions) {
12165
+ appendPressureIntervention(intervention);
12166
+ }
11009
12167
  if (hasGraph) {
11010
12168
  const accepted2 = proposals.filter((p) => p.outcome === "accepted");
11011
12169
  if (accepted2.length > 0) {
@@ -11335,12 +12493,16 @@ async function reportCommand(options) {
11335
12493
  // src/commands/generalize.ts
11336
12494
  init_config();
11337
12495
  init_skills();
12496
+ init_data();
11338
12497
  init_llm();
11339
12498
  import { join as join11 } from "node:path";
11340
12499
  async function generalizeCommand(options) {
11341
12500
  const verbose = options.verbose ?? false;
11342
12501
  const dryRun = options.dryRun ?? false;
11343
12502
  const skills = loadAllGeneralSkills();
12503
+ const iterationId = generateIterationId();
12504
+ const startedAt = new Date().toISOString();
12505
+ const governance = getActiveGovernanceSummary();
11344
12506
  console.log(`\uD83D\uDD2C HelixEvo Generalization Analysis
11345
12507
  `);
11346
12508
  if (skills.length < 2) {
@@ -11356,6 +12518,9 @@ async function generalizeCommand(options) {
11356
12518
  console.log(` → ${candidates.length} candidate(s) found
11357
12519
  `);
11358
12520
  const proposals = [];
12521
+ const artifacts = [];
12522
+ const transferEvents = [];
12523
+ const interventions = [];
11359
12524
  for (const candidate of candidates) {
11360
12525
  console.log(` Candidate: "${candidate.suggestedName}" (${candidate.suggestedLayer} layer)`);
11361
12526
  console.log(` Skills: ${candidate.skills.join(", ")}`);
@@ -11376,6 +12541,12 @@ async function generalizeCommand(options) {
11376
12541
  `);
11377
12542
  }
11378
12543
  const proposalId = generateProposalId();
12544
+ const matchedMotifs = matchPressureMotifs({ skillIds: candidate.skills });
12545
+ const matchedCapabilities = [...new Set(matchedMotifs.map((motif) => motif.capability ?? motif.key).filter(Boolean))];
12546
+ const sourceProjects = [...new Set(matchedMotifs.flatMap((motif) => motif.projectIds))];
12547
+ const artifactId = `artifact_generalize_${proposalId}`;
12548
+ const transferEventId = `transfer_generalize_${proposalId}`;
12549
+ const completedAt = new Date().toISOString();
11379
12550
  const proposal = {
11380
12551
  id: proposalId,
11381
12552
  targetSkill: candidate.suggestedName,
@@ -11398,7 +12569,7 @@ async function generalizeCommand(options) {
11398
12569
  meta.layer = candidate.suggestedLayer;
11399
12570
  meta.generation = 1;
11400
12571
  meta.score = candidate.confidence;
11401
- meta.lastEvolved = new Date().toISOString();
12572
+ meta.lastEvolved = completedAt;
11402
12573
  const skillDir = join11(getGeneralSkillsPath(), candidate.suggestedName);
11403
12574
  writeSkill(skillDir, meta, content);
11404
12575
  console.log(` ✓ Created: ${candidate.suggestedName} (${candidate.suggestedLayer} layer)
@@ -11411,20 +12582,83 @@ async function generalizeCommand(options) {
11411
12582
  console.log(` ↳ ${sourceSkill.slug}.parent → ${candidate.suggestedName}`);
11412
12583
  }
11413
12584
  }
12585
+ artifacts.push({
12586
+ id: artifactId,
12587
+ createdAt: completedAt,
12588
+ artifactType: "evolution",
12589
+ provenance: "native-evolution",
12590
+ title: `${candidate.suggestedName} generalized from recurring overlap`,
12591
+ summary: `Promoted ${candidate.skills.join(", ")} into reusable ${candidate.suggestedLayer}-layer knowledge.`,
12592
+ sourceId: `generalize:${iterationId}`,
12593
+ iterationId,
12594
+ proposalId,
12595
+ targetSkill: candidate.suggestedName,
12596
+ operation: "generalize",
12597
+ outcome: "accepted",
12598
+ trigger: "weekly_generalize",
12599
+ metrics: {
12600
+ motifCount: matchedMotifs.length,
12601
+ projectSpread: new Set(sourceProjects).size,
12602
+ sourceSkillCount: candidate.skills.length
12603
+ }
12604
+ });
12605
+ transferEvents.push({
12606
+ id: transferEventId,
12607
+ timestamp: completedAt,
12608
+ sourceSkillIds: candidate.skills,
12609
+ targetSkillIds: [candidate.suggestedName],
12610
+ capabilityIds: matchedCapabilities,
12611
+ motifIds: matchedMotifs.map((motif) => motif.id),
12612
+ transferType: sourceProjects.length >= 2 ? "cross-project" : "domain-transfer",
12613
+ evidenceIds: [proposalId, artifactId],
12614
+ benefitEstimate: Math.min(1, 0.55 + matchedMotifs.length * 0.1 + Math.max(0, sourceProjects.length - 1) * 0.08),
12615
+ status: "realized",
12616
+ summary: `Generalized ${candidate.skills.join(", ")} into ${candidate.suggestedName} from ${matchedMotifs.length} recurring motif(s).`
12617
+ });
11414
12618
  } else {
11415
12619
  console.log(` [DRY RUN] Would create: ${candidate.suggestedName}
11416
12620
  `);
11417
12621
  }
12622
+ interventions.push({
12623
+ id: `intervention_generalize_${proposalId}`,
12624
+ interventionType: "generalize",
12625
+ status: dryRun ? "dry-run" : "completed",
12626
+ impact: matchedMotifs.length > 0 ? "resolving" : "structural",
12627
+ createdAt: startedAt,
12628
+ completedAt,
12629
+ sourceId: `generalize:${iterationId}:${proposalId}`,
12630
+ scope: "network",
12631
+ governanceMode: governance.activeMode,
12632
+ capabilities: matchedCapabilities.length > 0 ? matchedCapabilities : [candidate.suggestedName],
12633
+ motifIds: matchedMotifs.map((motif) => motif.id),
12634
+ relatedProposalIds: [proposalId],
12635
+ relatedArtifactIds: dryRun ? [] : [artifactId],
12636
+ relatedTransferEventIds: dryRun ? [] : [transferEventId],
12637
+ relatedSkillSlugs: dryRun ? [] : [candidate.suggestedName],
12638
+ reasonSummary: `Generalization promoted overlap shared by ${candidate.skills.join(", ")} into ${candidate.suggestedName}.`,
12639
+ notes: dryRun ? "Generalization ran in dry-run mode, so promotion remained exploratory only." : matchedMotifs.length > 0 ? `Linked to ${matchedMotifs.length} recurring motif(s) under ${governance.activeMode} governance.` : `No explicit recurring motif matched, so this generalization should be treated as structural rather than motif-closing evidence.`
12640
+ });
11418
12641
  proposals.push(proposal);
11419
12642
  }
11420
12643
  addIteration({
11421
- id: generateIterationId(),
12644
+ id: iterationId,
11422
12645
  timestamp: new Date().toISOString(),
11423
12646
  trigger: "weekly_generalize",
11424
12647
  failureCount: 0,
11425
12648
  failureClusters: [],
11426
12649
  proposals
11427
12650
  });
12651
+ if (!dryRun) {
12652
+ for (const artifact of artifacts) {
12653
+ appendEvolutionArtifact(artifact);
12654
+ }
12655
+ for (const event of transferEvents) {
12656
+ appendTransferEvent(event);
12657
+ }
12658
+ }
12659
+ for (const intervention of interventions) {
12660
+ appendPressureIntervention(intervention);
12661
+ }
11428
12662
  const created = proposals.filter((p) => p.outcome === "accepted").length;
11429
12663
  console.log(`
11430
12664
  ─── Generalization Summary ───`);
@@ -11512,6 +12746,8 @@ async function specializeCommand(options) {
11512
12746
  const verbose = options.verbose ?? false;
11513
12747
  const dryRun = options.dryRun ?? false;
11514
12748
  const projectName = options.project;
12749
+ const startedAt = new Date().toISOString();
12750
+ const governance = getActiveGovernanceSummary();
11515
12751
  if (!projectName) {
11516
12752
  console.error(" Error: --project <name> is required");
11517
12753
  process.exit(1);
@@ -11525,9 +12761,41 @@ async function specializeCommand(options) {
11525
12761
  console.log(" Keep working on this project — failures are captured automatically.");
11526
12762
  return;
11527
12763
  }
12764
+ const persistSpecializationIntervention = (params) => {
12765
+ const matchedSignals = matchPressureSignals({
12766
+ projectId: projectName,
12767
+ relatedFailureIds: failures.map((failure) => failure.id),
12768
+ suggestedAction: "specialize"
12769
+ });
12770
+ const fallbackSignals = matchedSignals.length > 0 ? matchedSignals : matchPressureSignals({
12771
+ projectId: projectName,
12772
+ relatedFailureIds: failures.map((failure) => failure.id)
12773
+ });
12774
+ appendPressureIntervention({
12775
+ id: `intervention_specialize_${Date.now()}`,
12776
+ interventionType: "specialize",
12777
+ status: dryRun ? "dry-run" : "completed",
12778
+ impact: params.createdSkills && params.createdSkills.length > 0 ? "resolving" : (params.proposals?.length ?? 0) > 0 || (params.candidatePatterns?.length ?? 0) > 0 ? "structural" : "none",
12779
+ createdAt: startedAt,
12780
+ completedAt: new Date().toISOString(),
12781
+ sourceId: params.iterationId ? `specialize:${params.iterationId}` : `specialize:${projectName}`,
12782
+ projectId: projectName,
12783
+ scope: "project",
12784
+ governanceMode: governance.activeMode,
12785
+ capabilities: params.candidatePatterns ?? fallbackSignals.map((signal) => signal.capability).filter(Boolean),
12786
+ pressureSignalIds: fallbackSignals.map((signal) => signal.id),
12787
+ relatedFailureIds: failures.map((failure) => failure.id),
12788
+ relatedActivationTraceIds: failures.map((failure) => failure.activationTraceId).filter(Boolean),
12789
+ relatedProposalIds: params.proposals?.map((proposal) => proposal.id) ?? [],
12790
+ relatedSkillSlugs: params.createdSkills ?? [],
12791
+ reasonSummary: `Specialization reviewed ${failures.length} unresolved project failures for ${projectName}.`,
12792
+ notes: dryRun ? "Specialization ran in dry-run mode and did not create project-layer skills." : undefined
12793
+ });
12794
+ };
11528
12795
  console.log(" Step 1: Detecting specialization opportunities...");
11529
12796
  const candidates = await detectSpecializationCandidates(skills, failures, projectName);
11530
12797
  if (candidates.length === 0) {
12798
+ persistSpecializationIntervention({ candidatePatterns: [] });
11531
12799
  console.log(" No specialization candidates found.");
11532
12800
  return;
11533
12801
  }
@@ -11589,8 +12857,9 @@ async function specializeCommand(options) {
11589
12857
  }
11590
12858
  proposals.push(proposal);
11591
12859
  }
12860
+ const iterationId = generateIterationId();
11592
12861
  addIteration({
11593
- id: generateIterationId(),
12862
+ id: iterationId,
11594
12863
  timestamp: new Date().toISOString(),
11595
12864
  trigger: "manual",
11596
12865
  failureCount: failures.length,
@@ -11598,6 +12867,12 @@ async function specializeCommand(options) {
11598
12867
  proposals
11599
12868
  });
11600
12869
  const created = proposals.filter((p) => p.outcome === "accepted").length;
12870
+ persistSpecializationIntervention({
12871
+ proposals,
12872
+ iterationId,
12873
+ candidatePatterns: candidates.flatMap((candidate) => candidate.projectPatterns),
12874
+ createdSkills: proposals.filter((proposal) => proposal.outcome === "accepted").map((proposal) => proposal.targetSkill)
12875
+ });
11601
12876
  console.log(`
11602
12877
  ─── Specialization Summary ───`);
11603
12878
  console.log(` Project: ${projectName}`);
@@ -11769,9 +13044,10 @@ async function fullRebuild(verbose) {
11769
13044
  clusters
11770
13045
  };
11771
13046
  saveSkillGraph(graph);
13047
+ const effectiveGraph = loadSkillGraph();
11772
13048
  if (verbose)
11773
13049
  console.log(` ✓ Graph saved: ${nodes.length} nodes, ${graph.edges.length} edges, ${clusters.length} clusters`);
11774
- return graph;
13050
+ return effectiveGraph;
11775
13051
  }
11776
13052
  async function inferRelationships(skills) {
11777
13053
  const summaries = skills.map((s) => `- ${s.slug} (${s.meta.layer ?? "domain"}): ${s.meta.description?.slice(0, 100)}`).join(`
@@ -11938,6 +13214,9 @@ function nid(slug) {
11938
13214
  return slug.replace(/[^a-zA-Z0-9]/g, "_");
11939
13215
  }
11940
13216
 
13217
+ // src/commands/graph.ts
13218
+ init_data();
13219
+
11941
13220
  // src/core/network-opt.ts
11942
13221
  init_data();
11943
13222
  init_skills();
@@ -11945,8 +13224,13 @@ init_llm();
11945
13224
  async function optimizeNetwork(verbose = false) {
11946
13225
  const graph = loadSkillGraph();
11947
13226
  const skills = loadAllGeneralSkills();
11948
- const result = { merged: [], split: [], conflictsResolved: [] };
11949
- const conflicts = graph.edges.filter((e) => e.type === "conflicts");
13227
+ const preview = previewNetworkOptimization(graph, skills);
13228
+ const result = {
13229
+ merged: preview.merged.map((candidate) => candidate.slugs),
13230
+ split: preview.split.map((candidate) => candidate.slug),
13231
+ conflictsResolved: []
13232
+ };
13233
+ const conflicts = graph.edges.filter((edge) => edge.type === "conflicts");
11950
13234
  if (conflicts.length > 0) {
11951
13235
  if (verbose)
11952
13236
  console.log(` Resolving ${conflicts.length} conflict(s)...`);
@@ -11956,21 +13240,12 @@ async function optimizeNetwork(verbose = false) {
11956
13240
  result.conflictsResolved.push([conflict.from, conflict.to]);
11957
13241
  }
11958
13242
  }
11959
- const enhances = graph.edges.filter((e) => e.type === "enhances" && e.strength >= 0.8);
11960
- const coEvolves = graph.edges.filter((e) => e.type === "co-evolves" && e.strength >= 0.8);
11961
- const mergeCandidates = detectMergeCandidates(enhances, coEvolves, skills);
11962
- for (const [slugA, slugB] of mergeCandidates) {
11963
- if (verbose)
11964
- console.log(` Merge candidate: ${slugA} + ${slugB}`);
11965
- result.merged.push([slugA, slugB]);
11966
- }
11967
- for (const skill of skills) {
11968
- const contentLength = skill.content.length;
11969
- const failureCount = skill.meta.failureCount ?? 0;
11970
- if (contentLength > 5000 || failureCount > 20) {
11971
- if (verbose)
11972
- console.log(` Split candidate: ${skill.slug} (${contentLength} chars, ${failureCount} failures)`);
11973
- result.split.push(skill.slug);
13243
+ if (verbose) {
13244
+ for (const candidate of preview.merged) {
13245
+ console.log(` Merge candidate: ${candidate.slugs[0]} + ${candidate.slugs[1]} (${candidate.confidence.toFixed(2)})`);
13246
+ }
13247
+ for (const candidate of preview.split) {
13248
+ console.log(` Split candidate: ${candidate.slug} (${candidate.contentLength} chars, ${candidate.failureCount} failures, ${candidate.confidence.toFixed(2)})`);
11974
13249
  }
11975
13250
  }
11976
13251
  return result;
@@ -12012,25 +13287,6 @@ Return JSON:
12012
13287
  }
12013
13288
  return result.resolved;
12014
13289
  }
12015
- function detectMergeCandidates(enhances, coEvolves, skills) {
12016
- const pairScores = {};
12017
- for (const edge of [...enhances, ...coEvolves]) {
12018
- const key = [edge.from, edge.to].sort().join("::");
12019
- pairScores[key] = (pairScores[key] ?? 0) + edge.strength;
12020
- }
12021
- const candidates = [];
12022
- for (const [key, score] of Object.entries(pairScores)) {
12023
- if (score >= 1.5) {
12024
- const [a, b] = key.split("::");
12025
- const skillA = skills.find((s) => s.slug === a);
12026
- const skillB = skills.find((s) => s.slug === b);
12027
- if (skillA?.meta.layer === skillB?.meta.layer) {
12028
- candidates.push([a, b]);
12029
- }
12030
- }
12031
- }
12032
- return candidates;
12033
- }
12034
13290
 
12035
13291
  // src/core/obsidian.ts
12036
13292
  init_data();
@@ -12225,6 +13481,9 @@ ${C.bold} Network Optimization${C.reset}`);
12225
13481
  if (result.merged.length === 0 && result.split.length === 0 && result.conflictsResolved.length === 0) {
12226
13482
  console.log(` ${C.green}✓${C.reset} Network is healthy`);
12227
13483
  }
13484
+ const topology = refreshTopologyReviewCandidates();
13485
+ console.log(` ${C.blue}↺${C.reset} Topology review queue refreshed: ${topology.total} total • ${topology.open} open`);
13486
+ console.log(` ${C.dim} ${topology.created} created • ${topology.updated} updated${C.reset}`);
12228
13487
  }
12229
13488
  if (options.obsidian) {
12230
13489
  console.log(`
@@ -12482,12 +13741,16 @@ function renderScoreBar(score) {
12482
13741
  init_config();
12483
13742
  init_skills();
12484
13743
  init_llm();
12485
- import { join as join15 } from "node:path";
13744
+ import { isAbsolute, join as join15, resolve } from "node:path";
12486
13745
  import { readFileSync as readFileSync7, existsSync as existsSync9 } from "node:fs";
13746
+ init_data();
12487
13747
  async function researchCommand(options) {
12488
13748
  const verbose = options.verbose ?? false;
12489
13749
  const dryRun = options.dryRun ?? false;
12490
13750
  const maxHypotheses = parseInt(options.maxHypotheses ?? "3");
13751
+ const startedAt = new Date().toISOString();
13752
+ const governance = getActiveGovernanceSummary();
13753
+ const resolvedProjectPath = options.project ? isAbsolute(options.project) ? options.project : resolve(process.cwd(), options.project) : undefined;
12491
13754
  console.log(`\uD83D\uDD2C HelixEvo Proactive Research
12492
13755
  `);
12493
13756
  const skills = loadAllGeneralSkills();
@@ -12499,6 +13762,32 @@ async function researchCommand(options) {
12499
13762
  }
12500
13763
  console.log(` → ${goals.gaps.length} capability gap(s) identified
12501
13764
  `);
13765
+ const persistResearchIntervention = (params) => {
13766
+ const matchedSignals = matchPressureSignals({
13767
+ projectPath: resolvedProjectPath,
13768
+ capabilities: goals.gaps,
13769
+ suggestedAction: "research"
13770
+ });
13771
+ appendPressureIntervention({
13772
+ id: `intervention_research_${Date.now()}`,
13773
+ interventionType: "research",
13774
+ status: dryRun ? "dry-run" : "completed",
13775
+ impact: params.createdSkills && params.createdSkills.length > 0 ? "resolving" : params.discoveriesCount > 0 || params.hypothesesCount > 0 ? "exploratory" : "none",
13776
+ createdAt: startedAt,
13777
+ completedAt: new Date().toISOString(),
13778
+ sourceId: params.iterationId ? `research:${params.iterationId}` : params.proposals && params.proposals.length > 0 ? `research:${params.proposals.map((proposal) => proposal.id).join(",")}` : "research:discovery-scan",
13779
+ projectId: matchedSignals[0]?.projectId,
13780
+ projectPath: resolvedProjectPath,
13781
+ scope: resolvedProjectPath ? "project" : "local",
13782
+ governanceMode: governance.activeMode,
13783
+ capabilities: goals.gaps,
13784
+ pressureSignalIds: matchedSignals.map((signal) => signal.id),
13785
+ relatedProposalIds: params.proposals?.map((proposal) => proposal.id) ?? [],
13786
+ relatedSkillSlugs: params.createdSkills ?? [],
13787
+ reasonSummary: `Research analyzed ${goals.gaps.length} capability gaps and produced ${params.discoveriesCount} discoveries / ${params.hypothesesCount} hypotheses.`,
13788
+ notes: dryRun ? "Research ran in dry-run mode, so results should be treated as exploratory only." : undefined
13789
+ });
13790
+ };
12502
13791
  console.log(" Step 2: Scanning frontier (web search)...");
12503
13792
  const gapsToSearch = goals.gaps.slice(0, 3);
12504
13793
  if (verbose) {
@@ -12549,6 +13838,7 @@ Only include discoveries that are NOT already covered by current skills.`
12549
13838
  `);
12550
13839
  }
12551
13840
  if (discoveries.length === 0) {
13841
+ persistResearchIntervention({ discoveriesCount: 0, hypothesesCount: 0 });
12552
13842
  console.log(" No new discoveries. Current skills are up to date.");
12553
13843
  return;
12554
13844
  }
@@ -12564,6 +13854,7 @@ Only include discoveries that are NOT already covered by current skills.`
12564
13854
  console.log(` → ${filtered.length} high-confidence hypothesis(es)
12565
13855
  `);
12566
13856
  if (filtered.length === 0) {
13857
+ persistResearchIntervention({ discoveriesCount: discoveries.length, hypothesesCount: 0 });
12567
13858
  console.log(" No high-confidence hypotheses. Discoveries may not be actionable yet.");
12568
13859
  return;
12569
13860
  }
@@ -12623,8 +13914,9 @@ Only include discoveries that are NOT already covered by current skills.`
12623
13914
  }
12624
13915
  proposals.push(proposal);
12625
13916
  }
13917
+ const iterationId = generateIterationId();
12626
13918
  addIteration({
12627
- id: generateIterationId(),
13919
+ id: iterationId,
12628
13920
  timestamp: new Date().toISOString(),
12629
13921
  trigger: "manual",
12630
13922
  failureCount: 0,
@@ -12632,6 +13924,13 @@ Only include discoveries that are NOT already covered by current skills.`
12632
13924
  proposals
12633
13925
  });
12634
13926
  const created = proposals.filter((p) => p.outcome === "accepted").length;
13927
+ persistResearchIntervention({
13928
+ discoveriesCount: discoveries.length,
13929
+ hypothesesCount: filtered.length,
13930
+ proposals,
13931
+ iterationId,
13932
+ createdSkills: proposals.filter((proposal) => proposal.outcome === "accepted").map((proposal) => proposal.targetSkill)
13933
+ });
12635
13934
  console.log("─── Research Summary ───");
12636
13935
  console.log(` Gaps analyzed: ${goals.gaps.length}`);
12637
13936
  console.log(` Discoveries: ${discoveries.length}`);
@@ -13203,14 +14502,14 @@ function isProcessAlive(pid) {
13203
14502
  }
13204
14503
  }
13205
14504
  async function isPortAvailable(port) {
13206
- return await new Promise((resolve) => {
14505
+ return await new Promise((resolve2) => {
13207
14506
  const server = createServer();
13208
14507
  server.unref();
13209
14508
  server.once("error", () => {
13210
- resolve(false);
14509
+ resolve2(false);
13211
14510
  });
13212
14511
  server.listen(port, () => {
13213
- server.close(() => resolve(true));
14512
+ server.close(() => resolve2(true));
13214
14513
  });
13215
14514
  });
13216
14515
  }
@@ -13860,7 +15159,7 @@ async function metricsCommand(options) {
13860
15159
  init_data();
13861
15160
  init_skills();
13862
15161
  init_llm();
13863
- import { isAbsolute, join as join20, normalize, resolve } from "node:path";
15162
+ import { isAbsolute as isAbsolute2, join as join20, normalize, resolve as resolve2 } from "node:path";
13864
15163
  import { existsSync as existsSync15, readFileSync as readFileSync12, writeFileSync as writeFileSync12, mkdirSync as mkdirSync6, readdirSync as readdirSync4 } from "node:fs";
13865
15164
  import { homedir as homedir5 } from "node:os";
13866
15165
 
@@ -13940,7 +15239,7 @@ function expandHomePath(path) {
13940
15239
  }
13941
15240
  function normalizeProjectPath(projectPath) {
13942
15241
  const expanded = expandHomePath(projectPath.trim());
13943
- const resolvedPath = isAbsolute(expanded) ? normalize(expanded) : resolve(process.cwd(), expanded);
15242
+ const resolvedPath = isAbsolute2(expanded) ? normalize(expanded) : resolve2(process.cwd(), expanded);
13944
15243
  return stripTrailingSeparator(resolvedPath);
13945
15244
  }
13946
15245
  function readProjectContext(projectPath) {
@@ -14155,6 +15454,427 @@ async function projectSetupCommand(projectPath, options) {
14155
15454
  console.log();
14156
15455
  }
14157
15456
 
15457
+ // src/commands/topology.ts
15458
+ init_data();
15459
+
15460
+ // src/core/topology-apply.ts
15461
+ init_config();
15462
+ init_data();
15463
+ import { copyFileSync, existsSync as existsSync16, mkdirSync as mkdirSync7, readFileSync as readFileSync13, writeFileSync as writeFileSync13 } from "node:fs";
15464
+ import { join as join21 } from "node:path";
15465
+ function uniqueStrings2(values) {
15466
+ return [...new Set(values.filter((value) => Boolean(value && value.trim())))];
15467
+ }
15468
+ function edgeKey(edge) {
15469
+ return `${edge.from}::${edge.to}::${edge.type}`;
15470
+ }
15471
+ function findAcceptedCandidate(candidateId) {
15472
+ const candidate = loadResolvedTopologyReviewCandidates().find((entry) => entry.id === candidateId);
15473
+ if (!candidate)
15474
+ throw new Error(`Unknown topology review candidate: ${candidateId}`);
15475
+ if (candidate.status !== "accepted" || !candidate.latestDecision) {
15476
+ throw new Error(`Topology candidate ${candidateId} must be accepted before preparing an apply plan`);
15477
+ }
15478
+ return candidate;
15479
+ }
15480
+ function findPlan(planId) {
15481
+ const plan = loadResolvedTopologyApplyPlans().find((entry) => entry.id === planId);
15482
+ if (!plan)
15483
+ throw new Error(`Unknown topology apply plan: ${planId}`);
15484
+ return plan;
15485
+ }
15486
+ function snapshotId(prefix) {
15487
+ return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`.replace(/[^a-zA-Z0-9_-]/g, "-");
15488
+ }
15489
+ function readSkillRaw(slug) {
15490
+ const path = join21(getGeneralSkillsPath(), slug, "SKILL.md");
15491
+ if (!existsSync16(path))
15492
+ return null;
15493
+ return { path, raw: readFileSync13(path, "utf-8") };
15494
+ }
15495
+ function createSnapshot(params) {
15496
+ const id = snapshotId("topology_snapshot");
15497
+ const snapshotDir = join21(getHelixDir(), "topology-snapshots", id);
15498
+ mkdirSync7(snapshotDir, { recursive: true });
15499
+ const graphPath = join21(snapshotDir, "skill-graph.json");
15500
+ const overridesPath = join21(snapshotDir, "topology-overrides.json");
15501
+ writeFileSync13(graphPath, JSON.stringify(loadSkillGraph(), null, 2));
15502
+ writeFileSync13(overridesPath, JSON.stringify(loadTopologyOverrides(), null, 2));
15503
+ const skillFileSnapshots = uniqueStrings2(params.skillSlugs ?? []).flatMap((slug) => {
15504
+ const skill = readSkillRaw(slug);
15505
+ if (!skill)
15506
+ return [];
15507
+ const snapshotPath = join21(snapshotDir, `${slug}.SKILL.md`);
15508
+ copyFileSync(skill.path, snapshotPath);
15509
+ return [{ slug, path: skill.path, snapshotPath }];
15510
+ });
15511
+ const ref = {
15512
+ id,
15513
+ createdAt: new Date().toISOString(),
15514
+ label: params.label,
15515
+ candidateId: params.candidateId,
15516
+ planId: params.planId,
15517
+ executionRecordId: params.executionRecordId,
15518
+ snapshotDir,
15519
+ graphPath,
15520
+ overridesPath,
15521
+ skillFileSnapshots
15522
+ };
15523
+ const snapshots = loadTopologySnapshots();
15524
+ saveTopologySnapshots([ref, ...snapshots]);
15525
+ return ref;
15526
+ }
15527
+ function buildPlan(candidate, graph, beforeSnapshotId) {
15528
+ const targetSkillSlugs = candidate.affectedNodeIds;
15529
+ const plannedFileChanges = [];
15530
+ const plannedGraphChanges = [];
15531
+ const warnings = [];
15532
+ const plannedNodeOverrides = {};
15533
+ const plannedEdgeSuppressions = [];
15534
+ const plannedAddedEdges = [];
15535
+ let safeToApply = false;
15536
+ let executionMode = "prepare-only";
15537
+ let rollbackStrategy = "Restore the before-apply topology snapshot and revert topology-overrides.json.";
15538
+ if (candidate.changeType === "rewire") {
15539
+ const relevantEdges = graph.edges.filter((edge) => edge.type === "conflicts" && candidate.affectedNodeIds.includes(edge.from) && candidate.affectedNodeIds.includes(edge.to));
15540
+ if (relevantEdges.length > 0) {
15541
+ safeToApply = true;
15542
+ executionMode = "apply";
15543
+ for (const edge of relevantEdges) {
15544
+ plannedEdgeSuppressions.push({ from: edge.from, to: edge.to, type: edge.type });
15545
+ }
15546
+ plannedGraphChanges.push(`Suppress ${relevantEdges.length} conflict edge(s) covered by the reviewed rewire decision.`);
15547
+ rollbackStrategy = "Restore the previous conflict edges by reloading the before-apply override snapshot.";
15548
+ } else {
15549
+ warnings.push("No active conflict edge currently matches this reviewed rewire candidate, so the plan remains prepare-only.");
15550
+ plannedGraphChanges.push("No direct graph mutation is safe to apply until a matching conflict edge is present.");
15551
+ }
15552
+ } else if (candidate.changeType === "promote") {
15553
+ const promotable = graph.nodes.find((node) => candidate.affectedNodeIds.includes(node.id) && node.layer === "project");
15554
+ if (promotable) {
15555
+ safeToApply = true;
15556
+ executionMode = "apply";
15557
+ plannedNodeOverrides[promotable.id] = {
15558
+ layer: "domain",
15559
+ plasticityState: "consolidated"
15560
+ };
15561
+ plannedGraphChanges.push(`Promote ${promotable.id} from project to domain layer via a reviewed topology override.`);
15562
+ rollbackStrategy = "Restore the previous node layer through the before-apply topology snapshot.";
15563
+ } else {
15564
+ warnings.push("Safe promotion currently requires a single project-layer target; this reviewed promotion remains prepare-only.");
15565
+ plannedGraphChanges.push("Promotion recorded as structural intent only until a project-layer target can be promoted safely.");
15566
+ }
15567
+ } else if (candidate.changeType === "consolidate") {
15568
+ if ((candidate.relatedTransferEventIds?.length ?? 0) > 0 && candidate.affectedNodeIds.length > 0) {
15569
+ safeToApply = true;
15570
+ executionMode = "apply";
15571
+ const lineageId = candidate.relatedMotifIds?.[0] ?? candidate.id;
15572
+ for (const slug of candidate.affectedNodeIds) {
15573
+ plannedNodeOverrides[slug] = {
15574
+ plasticityState: "consolidated",
15575
+ lineageId
15576
+ };
15577
+ }
15578
+ plannedGraphChanges.push(`Mark ${candidate.affectedNodeIds.length} node(s) as consolidated under shared reviewed lineage ${lineageId}.`);
15579
+ rollbackStrategy = "Restore the previous node consolidation metadata from the before-apply topology snapshot.";
15580
+ } else {
15581
+ warnings.push("Consolidation currently requires realized transfer evidence and at least one target node; this plan remains prepare-only.");
15582
+ plannedGraphChanges.push("Consolidation intent captured without applying structural metadata yet.");
15583
+ }
15584
+ } else {
15585
+ warnings.push(`Safe reviewed execution for ${candidate.changeType} is not enabled in Milestone 7.`);
15586
+ plannedGraphChanges.push(`${candidate.changeType} remains prepare-only so the reviewed structural intent is preserved without destructive execution.`);
15587
+ }
15588
+ if (!safeToApply) {
15589
+ rollbackStrategy = "No structural mutation applied; keep the plan as prepare-only until a later reviewed milestone or safer candidate form is available.";
15590
+ }
15591
+ return {
15592
+ candidateId: candidate.id,
15593
+ acceptedDecisionId: candidate.latestDecision.id,
15594
+ changeType: candidate.changeType,
15595
+ safeToApply,
15596
+ executionMode,
15597
+ targetSkillSlugs,
15598
+ plannedFileChanges,
15599
+ plannedGraphChanges,
15600
+ plannedNodeOverrides: Object.keys(plannedNodeOverrides).length > 0 ? plannedNodeOverrides : undefined,
15601
+ plannedEdgeSuppressions: plannedEdgeSuppressions.length > 0 ? plannedEdgeSuppressions : undefined,
15602
+ plannedAddedEdges: plannedAddedEdges.length > 0 ? plannedAddedEdges : undefined,
15603
+ warnings,
15604
+ rollbackStrategy,
15605
+ beforeSnapshotId,
15606
+ reasonSummary: candidate.reasonSummary
15607
+ };
15608
+ }
15609
+ function mergePlan(plans, nextPlan) {
15610
+ return [
15611
+ nextPlan,
15612
+ ...plans.filter((plan) => !(plan.candidateId === nextPlan.candidateId && plan.acceptedDecisionId === nextPlan.acceptedDecisionId))
15613
+ ];
15614
+ }
15615
+ function appendArtifact(params) {
15616
+ const artifact = {
15617
+ id: snapshotId("topology_artifact"),
15618
+ createdAt: new Date().toISOString(),
15619
+ artifactType: "topology-apply",
15620
+ provenance: "native-topology",
15621
+ title: params.title,
15622
+ summary: params.summary,
15623
+ sourceId: params.sourceId,
15624
+ targetSkill: params.plan.targetSkillSlugs[0],
15625
+ operation: params.operation,
15626
+ metrics: params.metrics
15627
+ };
15628
+ appendTopologyArtifact(artifact);
15629
+ return artifact.id;
15630
+ }
15631
+ function prepareTopologyApplyPlan(candidateId) {
15632
+ const candidate = findAcceptedCandidate(candidateId);
15633
+ const beforeSnapshot = createSnapshot({
15634
+ label: `Before prepare · ${candidate.title}`,
15635
+ candidateId: candidate.id,
15636
+ skillSlugs: candidate.affectedNodeIds
15637
+ });
15638
+ const governance = getActiveGovernanceSummary();
15639
+ const planSeed = buildPlan(candidate, loadSkillGraph(), beforeSnapshot.id);
15640
+ const plan = {
15641
+ id: snapshotId("topology_plan"),
15642
+ createdAt: new Date().toISOString(),
15643
+ governanceMode: governance.activeMode,
15644
+ ...planSeed
15645
+ };
15646
+ saveTopologyApplyPlans(mergePlan(loadTopologyApplyPlans(), plan));
15647
+ const artifactId = appendArtifact({
15648
+ title: `Prepared topology apply · ${candidate.title}`,
15649
+ summary: plan.safeToApply ? `Prepared reviewed ${plan.changeType} plan with ${plan.executionMode} execution mode.` : `Prepared reviewed ${plan.changeType} plan in prepare-only mode because no safe execution path is currently available.`,
15650
+ sourceId: plan.id,
15651
+ operation: `topology:${plan.changeType}:prepare`,
15652
+ plan,
15653
+ metrics: {
15654
+ safeToApply: plan.safeToApply ? 1 : 0,
15655
+ plannedGraphChanges: plan.plannedGraphChanges.length,
15656
+ warnings: plan.warnings.length
15657
+ }
15658
+ });
15659
+ appendTopologyExecutionRecord({
15660
+ id: snapshotId("topology_exec_prepared"),
15661
+ planId: plan.id,
15662
+ candidateId: plan.candidateId,
15663
+ changeType: plan.changeType,
15664
+ status: "prepared",
15665
+ createdAt: new Date().toISOString(),
15666
+ executedAt: new Date().toISOString(),
15667
+ governanceMode: governance.activeMode,
15668
+ beforeSnapshotId: beforeSnapshot.id,
15669
+ artifactIds: [artifactId],
15670
+ notes: plan.warnings
15671
+ });
15672
+ return plan;
15673
+ }
15674
+ function applyPlanToOverrides(plan, overrides) {
15675
+ const nextNodeOverrides = { ...overrides.nodeOverrides };
15676
+ for (const [slug, override] of Object.entries(plan.plannedNodeOverrides ?? {})) {
15677
+ nextNodeOverrides[slug] = { ...nextNodeOverrides[slug] ?? {}, ...override };
15678
+ }
15679
+ const suppressedMap = new Map(overrides.suppressedEdges.map((edge) => [edgeKey(edge), edge]));
15680
+ for (const edge of plan.plannedEdgeSuppressions ?? []) {
15681
+ suppressedMap.set(edgeKey(edge), edge);
15682
+ }
15683
+ const addedMap = new Map(overrides.addedEdges.map((edge) => [edgeKey(edge), edge]));
15684
+ for (const edge of plan.plannedAddedEdges ?? []) {
15685
+ addedMap.set(edgeKey(edge), edge);
15686
+ }
15687
+ return {
15688
+ updatedAt: new Date().toISOString(),
15689
+ nodeOverrides: nextNodeOverrides,
15690
+ suppressedEdges: [...suppressedMap.values()],
15691
+ addedEdges: [...addedMap.values()],
15692
+ appliedPlanIds: uniqueStrings2([...overrides.appliedPlanIds ?? [], plan.id])
15693
+ };
15694
+ }
15695
+ function applyPreparedTopologyPlan(planId) {
15696
+ const plan = findPlan(planId);
15697
+ if (plan.latestStatus !== "prepared") {
15698
+ throw new Error(`Topology plan ${planId} must be prepared before apply`);
15699
+ }
15700
+ if (!plan.safeToApply || plan.executionMode !== "apply") {
15701
+ throw new Error(`Topology plan ${planId} is prepare-only and cannot be applied safely in Milestone 7`);
15702
+ }
15703
+ const nextOverrides = applyPlanToOverrides(plan, loadTopologyOverrides());
15704
+ saveTopologyOverrides(nextOverrides);
15705
+ const executionRecordId = snapshotId("topology_exec_applied");
15706
+ const afterSnapshot = createSnapshot({
15707
+ label: `After apply · ${plan.id}`,
15708
+ candidateId: plan.candidateId,
15709
+ planId: plan.id,
15710
+ executionRecordId,
15711
+ skillSlugs: plan.targetSkillSlugs
15712
+ });
15713
+ const artifactId = appendArtifact({
15714
+ title: `Applied topology plan · ${plan.changeType}`,
15715
+ summary: `Applied reviewed ${plan.changeType} plan with ${plan.plannedGraphChanges.length} graph mutation step(s).`,
15716
+ sourceId: plan.id,
15717
+ operation: `topology:${plan.changeType}:apply`,
15718
+ plan,
15719
+ metrics: {
15720
+ suppressedEdges: plan.plannedEdgeSuppressions?.length ?? 0,
15721
+ addedEdges: plan.plannedAddedEdges?.length ?? 0,
15722
+ nodeOverrides: Object.keys(plan.plannedNodeOverrides ?? {}).length
15723
+ }
15724
+ });
15725
+ const record = {
15726
+ id: executionRecordId,
15727
+ planId: plan.id,
15728
+ candidateId: plan.candidateId,
15729
+ changeType: plan.changeType,
15730
+ status: "applied",
15731
+ createdAt: new Date().toISOString(),
15732
+ executedAt: new Date().toISOString(),
15733
+ governanceMode: getActiveGovernanceSummary().activeMode,
15734
+ beforeSnapshotId: plan.beforeSnapshotId,
15735
+ afterSnapshotId: afterSnapshot.id,
15736
+ artifactIds: [artifactId],
15737
+ notes: plan.plannedGraphChanges
15738
+ };
15739
+ appendTopologyExecutionRecord(record);
15740
+ return record;
15741
+ }
15742
+ function rollbackAppliedTopologyPlan(planId) {
15743
+ const plan = findPlan(planId);
15744
+ if (plan.latestStatus !== "applied") {
15745
+ throw new Error(`Topology plan ${planId} must be applied before rollback`);
15746
+ }
15747
+ const beforeSnapshot = loadTopologySnapshots().find((snapshot) => snapshot.id === plan.beforeSnapshotId);
15748
+ if (!beforeSnapshot || !existsSync16(beforeSnapshot.overridesPath)) {
15749
+ throw new Error(`Before-apply snapshot for ${planId} is missing or incomplete`);
15750
+ }
15751
+ const restoredOverrides = JSON.parse(readFileSync13(beforeSnapshot.overridesPath, "utf-8"));
15752
+ saveTopologyOverrides({
15753
+ ...restoredOverrides,
15754
+ updatedAt: new Date().toISOString()
15755
+ });
15756
+ const executionRecordId = snapshotId("topology_exec_rollback");
15757
+ const afterSnapshot = createSnapshot({
15758
+ label: `After rollback · ${plan.id}`,
15759
+ candidateId: plan.candidateId,
15760
+ planId: plan.id,
15761
+ executionRecordId,
15762
+ skillSlugs: plan.targetSkillSlugs
15763
+ });
15764
+ const artifactId = appendArtifact({
15765
+ title: `Rolled back topology plan · ${plan.changeType}`,
15766
+ summary: `Rolled back reviewed ${plan.changeType} plan by restoring the before-apply topology snapshot.`,
15767
+ sourceId: plan.id,
15768
+ operation: `topology:${plan.changeType}:rollback`,
15769
+ plan,
15770
+ metrics: {
15771
+ restoredSnapshot: 1,
15772
+ targetSkills: plan.targetSkillSlugs.length
15773
+ }
15774
+ });
15775
+ const record = {
15776
+ id: executionRecordId,
15777
+ planId: plan.id,
15778
+ candidateId: plan.candidateId,
15779
+ changeType: plan.changeType,
15780
+ status: "rolled-back",
15781
+ createdAt: new Date().toISOString(),
15782
+ rolledBackAt: new Date().toISOString(),
15783
+ governanceMode: getActiveGovernanceSummary().activeMode,
15784
+ beforeSnapshotId: plan.beforeSnapshotId,
15785
+ afterSnapshotId: afterSnapshot.id,
15786
+ artifactIds: [artifactId],
15787
+ notes: ["Restored topology-overrides.json from the before-apply snapshot."]
15788
+ };
15789
+ appendTopologyExecutionRecord(record);
15790
+ return record;
15791
+ }
15792
+
15793
+ // src/commands/topology.ts
15794
+ function printStatus() {
15795
+ const review = getTopologyReviewSummary();
15796
+ const execution = getTopologyExecutionSummary();
15797
+ console.log(`
15798
+ Topology Control`);
15799
+ console.log(" ────────────────────────────────────────");
15800
+ console.log(` Review queue: ${review.total} total • ${review.open} open • ${review.accepted} accepted • ${review.deferred} deferred`);
15801
+ console.log(` Execution: ${execution.plans} plans • ${execution.prepared} prepared • ${execution.applied} applied • ${execution.rolledBack} rolled back`);
15802
+ console.log(` Safety: ${execution.safeToApply} safe-to-apply • ${execution.prepareOnly} prepare-only • ${execution.snapshots} snapshots • ${execution.artifacts} artifacts`);
15803
+ const acceptedReady = execution.acceptedReady.slice(0, 5);
15804
+ if (acceptedReady.length > 0) {
15805
+ console.log(`
15806
+ Accepted candidates ready for prepare/apply`);
15807
+ for (const candidate of acceptedReady) {
15808
+ console.log(` • ${candidate.id} (${candidate.changeType})`);
15809
+ }
15810
+ }
15811
+ const prepared = execution.preparedQueue.slice(0, 5);
15812
+ if (prepared.length > 0) {
15813
+ console.log(`
15814
+ Prepared plans`);
15815
+ for (const plan of prepared) {
15816
+ console.log(` • ${plan.id} (${plan.changeType}) • ${plan.executionMode}`);
15817
+ }
15818
+ }
15819
+ console.log();
15820
+ }
15821
+ async function topologyCommand(options) {
15822
+ const actions = [options.prepare, options.apply, options.rollback].filter(Boolean);
15823
+ if (actions.length > 1) {
15824
+ throw new Error("Use only one of --prepare, --apply, or --rollback at a time");
15825
+ }
15826
+ if (options.prepare) {
15827
+ const plan = prepareTopologyApplyPlan(options.prepare);
15828
+ console.log(`
15829
+ ✓ Prepared topology plan: ${plan.id}`);
15830
+ console.log(` candidate: ${plan.candidateId}`);
15831
+ console.log(` change: ${plan.changeType}`);
15832
+ console.log(` mode: ${plan.executionMode}`);
15833
+ console.log(` safe-to-apply: ${plan.safeToApply ? "yes" : "no"}`);
15834
+ if (plan.plannedGraphChanges.length > 0) {
15835
+ for (const line of plan.plannedGraphChanges)
15836
+ console.log(` graph: ${line}`);
15837
+ }
15838
+ if (plan.warnings.length > 0) {
15839
+ for (const line of plan.warnings)
15840
+ console.log(` warning: ${line}`);
15841
+ }
15842
+ console.log();
15843
+ return;
15844
+ }
15845
+ if (options.apply) {
15846
+ const record = applyPreparedTopologyPlan(options.apply);
15847
+ console.log(`
15848
+ ✓ Applied topology plan: ${record.planId}`);
15849
+ console.log(` candidate: ${record.candidateId}`);
15850
+ console.log(` change: ${record.changeType}`);
15851
+ console.log(` after snapshot: ${record.afterSnapshotId ?? "—"}`);
15852
+ console.log();
15853
+ return;
15854
+ }
15855
+ if (options.rollback) {
15856
+ const record = rollbackAppliedTopologyPlan(options.rollback);
15857
+ console.log(`
15858
+ ✓ Rolled back topology plan: ${record.planId}`);
15859
+ console.log(` candidate: ${record.candidateId}`);
15860
+ console.log(` change: ${record.changeType}`);
15861
+ console.log(` after snapshot: ${record.afterSnapshotId ?? "—"}`);
15862
+ console.log();
15863
+ return;
15864
+ }
15865
+ printStatus();
15866
+ if (options.verbose) {
15867
+ const plans = loadResolvedTopologyApplyPlans().slice(0, 8);
15868
+ if (plans.length > 0) {
15869
+ console.log(" Recent topology plans");
15870
+ for (const plan of plans) {
15871
+ console.log(` • ${plan.id} • ${plan.changeType} • ${plan.latestStatus} • ${plan.executionMode}`);
15872
+ }
15873
+ console.log();
15874
+ }
15875
+ }
15876
+ }
15877
+
14158
15878
  // src/cli.ts
14159
15879
  var program2 = new Command;
14160
15880
  program2.name("helixevo").description("Self-evolving skill ecosystem for AI agents").version(VERSION).addHelpText("after", `
@@ -14173,7 +15893,11 @@ Examples:
14173
15893
  $ helixevo graph --mermaid Open graph in browser
14174
15894
  $ helixevo graph --obsidian ~/vault Sync to Obsidian vault
14175
15895
  $ helixevo graph --rebuild Re-infer relationships (LLM call)
14176
- $ helixevo graph --optimize Detect merge/split/conflicts
15896
+ $ helixevo graph --optimize Detect structural candidates + refresh topology review queue
15897
+ $ helixevo topology --status Show reviewed topology execution state
15898
+ $ helixevo topology --prepare <id> Prepare an accepted topology candidate
15899
+ $ helixevo topology --apply <planId> Apply a safe prepared topology plan
15900
+ $ helixevo topology --rollback <planId> Roll back an applied topology plan
14177
15901
  $ helixevo report --days 7 Weekly evolution report
14178
15902
  $ helixevo capture <session.json> Extract failures from session`);
14179
15903
  program2.command("init").description("Import existing skills + generate skill tests").option("--skills-paths <paths...>", "Paths to scan for existing skills").option("--skip-tests", "Skip skill test generation").action(initCommand);
@@ -14181,7 +15905,7 @@ program2.command("capture").description("Capture failures from a Craft Agent ses
14181
15905
  program2.command("evolve").description("Evolve skills from failures [--dry-run] [--verbose] [--max-proposals <n>]").option("--dry-run", "Show proposals without applying").option("--verbose", "Show detailed LLM interactions").option("--max-proposals <n>", "Max proposals per run", "5").action(evolveCommand);
14182
15906
  program2.command("generalize").description("Promote cross-skill patterns to higher layer ↑ [--dry-run] [--verbose]").option("--dry-run", "Show candidates without applying").option("--verbose", "Show detailed analysis").action(generalizeCommand);
14183
15907
  program2.command("specialize").description("Create project-specific skills ↓ --project <name> [--dry-run] [--verbose]").requiredOption("--project <name>", "Project name").option("--dry-run", "Show candidates without applying").option("--verbose", "Show detailed analysis").action(specializeCommand);
14184
- program2.command("graph").description("Skill network [--mermaid] [--obsidian <path>] [--rebuild] [--optimize]").option("--mermaid", "Render as Mermaid diagram in browser").option("--rebuild", "Force rebuild (re-infer relationships via LLM)").option("--obsidian <path>", "Sync to Obsidian vault at path").option("--optimize", "Run network optimization (merge/split/conflict detection)").option("--verbose", "Show detailed analysis").action(graphCommand);
15908
+ program2.command("graph").description("Skill network [--mermaid] [--obsidian <path>] [--rebuild] [--optimize refreshes topology review]").option("--mermaid", "Render as Mermaid diagram in browser").option("--rebuild", "Force rebuild (re-infer relationships via LLM)").option("--obsidian <path>", "Sync to Obsidian vault at path").option("--optimize", "Run network optimization (merge/split/conflict detection)").option("--verbose", "Show detailed analysis").action(graphCommand);
14185
15909
  program2.command("research").description("Proactive research via web [--project <path>] [--dry-run] [--verbose]").option("--project <path>", "Project path for goal extraction").option("--dry-run", "Show discoveries without creating skills").option("--verbose", "Show detailed research steps").option("--max-hypotheses <n>", "Max hypotheses to test", "3").action(researchCommand);
14186
15910
  program2.command("dashboard").description("Open web dashboard (default: http://localhost:3847, falls forward if occupied)").option("--background", "Run in background (detach from terminal)").option("--stop", "Stop a background dashboard").option("--port <port>", "Preferred port to try first (default: 3847)").option("--no-auto-update", "Skip automatic update check before launching the dashboard").action(async (options) => {
14187
15911
  await dashboardCommand(options);
@@ -14196,6 +15920,7 @@ program2.command("health").description("Assess network health: cohesion, coverag
14196
15920
  });
14197
15921
  program2.command("metrics").description("Show correction rates, skill trends, and evolution impact").option("--verbose", "Show detailed per-skill breakdown").action(metricsCommand);
14198
15922
  program2.command("project-setup").description("Analyze a project folder and match it against your skill set").argument("<path>", "Path to the project folder").option("--verbose", "Show detailed analysis").option("--dry-run", "Analyze without saving project profile").action(projectSetupCommand);
15923
+ program2.command("topology").description("Reviewed topology execution control [--status] [--prepare <candidateId>] [--apply <planId>] [--rollback <planId>]").option("--status", "Show topology review and execution state").option("--prepare <candidateId>", "Prepare an accepted topology review candidate").option("--apply <planId>", "Apply a safe prepared topology plan").option("--rollback <planId>", "Roll back an applied topology plan").option("--verbose", "Show detailed topology plan state").action(topologyCommand);
14199
15924
  program2.hook("postAction", () => {
14200
15925
  checkForUpdates().catch(() => {});
14201
15926
  });