helixevo 0.3.0 → 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/CHANGELOG.md +35 -0
- package/README.md +33 -8
- package/dashboard/app/api/governance/route.ts +49 -0
- package/dashboard/app/api/run/route.ts +31 -4
- package/dashboard/app/api/topology-apply/route.ts +80 -0
- package/dashboard/app/api/topology-review/route.ts +51 -0
- package/dashboard/app/coevolution/client.tsx +560 -0
- package/dashboard/app/coevolution/page.tsx +9 -0
- package/dashboard/app/commands/page.tsx +30 -7
- package/dashboard/app/guide/page.tsx +20 -4
- package/dashboard/app/network/client.tsx +76 -2
- package/dashboard/app/page.tsx +46 -0
- package/dashboard/app/projects/client.tsx +83 -10
- package/dashboard/app/projects/page.tsx +8 -27
- package/dashboard/app/research/client.tsx +48 -8
- package/dashboard/app/topology/client.tsx +575 -0
- package/dashboard/app/topology/page.tsx +10 -0
- package/dashboard/components/sidebar-nav.tsx +2 -0
- package/dashboard/lib/data.ts +1301 -3
- package/dist/cli.js +1981 -124
- package/package.json +2 -2
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
|
|
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 =
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
11949
|
-
const
|
|
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
|
-
|
|
11960
|
-
|
|
11961
|
-
|
|
11962
|
-
|
|
11963
|
-
|
|
11964
|
-
console.log(`
|
|
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:
|
|
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}`);
|
|
@@ -12809,9 +14108,10 @@ ${replay.slice(0, 800)}`
|
|
|
12809
14108
|
// src/commands/dashboard.ts
|
|
12810
14109
|
import { execSync as execSync2, spawn as spawn2 } from "node:child_process";
|
|
12811
14110
|
import { join as join17, dirname as dirname3 } from "node:path";
|
|
12812
|
-
import { existsSync as existsSync11, cpSync as cpSync3, mkdirSync as mkdirSync5, readdirSync as readdirSync3, readFileSync as readFileSync9, rmSync as rmSync2, writeFileSync as writeFileSync10 } from "node:fs";
|
|
14111
|
+
import { existsSync as existsSync11, cpSync as cpSync3, mkdirSync as mkdirSync5, openSync, readdirSync as readdirSync3, readFileSync as readFileSync9, rmSync as rmSync2, writeFileSync as writeFileSync10 } from "node:fs";
|
|
12813
14112
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
12814
14113
|
import { homedir as homedir4 } from "node:os";
|
|
14114
|
+
import { createServer } from "node:net";
|
|
12815
14115
|
init_skills();
|
|
12816
14116
|
init_data();
|
|
12817
14117
|
|
|
@@ -12920,11 +14220,23 @@ function printUpdateBanner(currentVersion, latestVersion) {
|
|
|
12920
14220
|
|
|
12921
14221
|
// src/commands/dashboard.ts
|
|
12922
14222
|
var __filename = "/Users/tianchichen/Documents/GitHub/helixevo/src/commands/dashboard.ts";
|
|
12923
|
-
var
|
|
14223
|
+
var HELIX_HOME_DIR = join17(homedir4(), ".helix");
|
|
14224
|
+
var HELIX_DASHBOARD_DIR = join17(HELIX_HOME_DIR, "dashboard");
|
|
14225
|
+
var DASHBOARD_LOG_FILE = join17(HELIX_HOME_DIR, "dashboard.log");
|
|
14226
|
+
var DASHBOARD_PID_FILE = join17(HELIX_HOME_DIR, "dashboard.pid");
|
|
14227
|
+
var DASHBOARD_STATE_FILE = join17(HELIX_HOME_DIR, "dashboard-state.json");
|
|
12924
14228
|
var DASHBOARD_SKIP_AUTO_UPDATE_ENV = "HELIXEVO_DASHBOARD_SKIP_AUTO_UPDATE";
|
|
14229
|
+
var DEFAULT_DASHBOARD_PORT = 3847;
|
|
14230
|
+
var MAX_PORT_SCAN_ATTEMPTS = 25;
|
|
14231
|
+
var RESTART_EXIT_CODE = 75;
|
|
12925
14232
|
async function dashboardCommand(options) {
|
|
14233
|
+
if (options.stop) {
|
|
14234
|
+
stopDashboard();
|
|
14235
|
+
return;
|
|
14236
|
+
}
|
|
14237
|
+
const preferredPort = parseDashboardPort(options.port);
|
|
12926
14238
|
if (options.autoUpdate !== false && process.env[DASHBOARD_SKIP_AUTO_UPDATE_ENV] !== "1" && !findDevDashboard()) {
|
|
12927
|
-
const didRelaunch = await maybeAutoUpdateAndRelaunch(options);
|
|
14239
|
+
const didRelaunch = await maybeAutoUpdateAndRelaunch({ ...options, port: preferredPort });
|
|
12928
14240
|
if (didRelaunch)
|
|
12929
14241
|
return;
|
|
12930
14242
|
}
|
|
@@ -12934,14 +14246,14 @@ async function dashboardCommand(options) {
|
|
|
12934
14246
|
`);
|
|
12935
14247
|
console.error(" You can install and run it manually:");
|
|
12936
14248
|
console.error(" git clone https://github.com/danielchen26/helixevo.git");
|
|
12937
|
-
console.error(
|
|
14249
|
+
console.error(` cd helixevo/dashboard && npm install && npx next dev --port ${preferredPort}`);
|
|
12938
14250
|
process.exit(1);
|
|
12939
14251
|
}
|
|
12940
14252
|
if (!existsSync11(join17(dir, "node_modules"))) {
|
|
12941
14253
|
console.log(" Installing dashboard dependencies...");
|
|
12942
14254
|
try {
|
|
12943
14255
|
execSync2("npm install --no-audit --no-fund", { cwd: dir, stdio: "inherit" });
|
|
12944
|
-
} catch
|
|
14256
|
+
} catch {
|
|
12945
14257
|
console.error(" Failed to install dashboard dependencies.");
|
|
12946
14258
|
console.error(" Try running manually:");
|
|
12947
14259
|
console.error(` cd ${dir} && npm install`);
|
|
@@ -12949,43 +14261,39 @@ async function dashboardCommand(options) {
|
|
|
12949
14261
|
}
|
|
12950
14262
|
}
|
|
12951
14263
|
ensureSkillGraph();
|
|
12952
|
-
|
|
12953
|
-
|
|
12954
|
-
|
|
12955
|
-
|
|
12956
|
-
|
|
12957
|
-
|
|
12958
|
-
|
|
12959
|
-
|
|
12960
|
-
|
|
12961
|
-
|
|
12962
|
-
|
|
12963
|
-
|
|
12964
|
-
|
|
12965
|
-
|
|
12966
|
-
child.unref();
|
|
12967
|
-
writeFileSync10(join17(homedir4(), ".helix", "dashboard.pid"), String(child.pid));
|
|
12968
|
-
console.log(` \uD83C\uDF10 HelixEvo Dashboard v${currentVersion2} running in background`);
|
|
12969
|
-
console.log(` http://localhost:3847`);
|
|
12970
|
-
console.log(` Logs: ${logFile}`);
|
|
12971
|
-
console.log(` Stop: helixevo dashboard --stop
|
|
14264
|
+
let target;
|
|
14265
|
+
try {
|
|
14266
|
+
target = await resolveLaunchTarget(preferredPort);
|
|
14267
|
+
} catch (error) {
|
|
14268
|
+
console.error(` ${error.message}`);
|
|
14269
|
+
process.exit(1);
|
|
14270
|
+
}
|
|
14271
|
+
const currentVersion = readCurrentVersion();
|
|
14272
|
+
if (target.source === "existing" && target.state) {
|
|
14273
|
+
const url = getDashboardUrl(target.state.port);
|
|
14274
|
+
console.log(` ✓ HelixEvo Dashboard is already running at ${url}`);
|
|
14275
|
+
if (options.background) {
|
|
14276
|
+
console.log(` Logs: ${DASHBOARD_LOG_FILE}`);
|
|
14277
|
+
console.log(` Stop: helixevo dashboard --stop
|
|
12972
14278
|
`);
|
|
12973
|
-
|
|
12974
|
-
|
|
12975
|
-
|
|
12976
|
-
|
|
12977
|
-
|
|
12978
|
-
} catch {}
|
|
12979
|
-
}, 2000);
|
|
12980
|
-
process.exit(0);
|
|
14279
|
+
} else {
|
|
14280
|
+
console.log("");
|
|
14281
|
+
}
|
|
14282
|
+
openDashboardUrl(target.state.port);
|
|
14283
|
+
return;
|
|
12981
14284
|
}
|
|
12982
|
-
|
|
12983
|
-
|
|
12984
|
-
|
|
12985
|
-
} catch {}
|
|
12986
|
-
console.log(` \uD83C\uDF10 Starting HelixEvo Dashboard v${currentVersion} at http://localhost:3847
|
|
14285
|
+
if (target.source === "fallback") {
|
|
14286
|
+
console.log(` Port ${preferredPort} is already in use.`);
|
|
14287
|
+
console.log(` Falling forward to ${getDashboardUrl(target.port)} instead.
|
|
12987
14288
|
`);
|
|
12988
|
-
|
|
14289
|
+
}
|
|
14290
|
+
if (options.background) {
|
|
14291
|
+
launchBackgroundDashboard(dir, target.port, currentVersion);
|
|
14292
|
+
return;
|
|
14293
|
+
}
|
|
14294
|
+
console.log(` \uD83C\uDF10 Starting HelixEvo Dashboard v${currentVersion} at ${getDashboardUrl(target.port)}
|
|
14295
|
+
`);
|
|
14296
|
+
launchDashboard(dir, true, target.port);
|
|
12989
14297
|
}
|
|
12990
14298
|
async function maybeAutoUpdateAndRelaunch(options) {
|
|
12991
14299
|
const update = await getUpdateInfo(VERSION);
|
|
@@ -13015,7 +14323,9 @@ async function maybeAutoUpdateAndRelaunch(options) {
|
|
|
13015
14323
|
args.push("--background");
|
|
13016
14324
|
if (options.autoUpdate === false)
|
|
13017
14325
|
args.push("--no-auto-update");
|
|
13018
|
-
|
|
14326
|
+
if (options.port !== undefined)
|
|
14327
|
+
args.push("--port", String(options.port));
|
|
14328
|
+
await new Promise((_resolve, reject) => {
|
|
13019
14329
|
const child = spawn2(executable, args, {
|
|
13020
14330
|
stdio: "inherit",
|
|
13021
14331
|
env: { ...process.env, [DASHBOARD_SKIP_AUTO_UPDATE_ENV]: "1" }
|
|
@@ -13027,30 +14337,49 @@ async function maybeAutoUpdateAndRelaunch(options) {
|
|
|
13027
14337
|
});
|
|
13028
14338
|
return true;
|
|
13029
14339
|
}
|
|
13030
|
-
|
|
13031
|
-
|
|
13032
|
-
|
|
13033
|
-
|
|
13034
|
-
|
|
13035
|
-
|
|
13036
|
-
|
|
14340
|
+
function launchBackgroundDashboard(dir, port, version) {
|
|
14341
|
+
mkdirSync5(HELIX_HOME_DIR, { recursive: true });
|
|
14342
|
+
const out = openSync(DASHBOARD_LOG_FILE, "a");
|
|
14343
|
+
const err = openSync(DASHBOARD_LOG_FILE, "a");
|
|
14344
|
+
const child = spawn2("npx", ["next", "dev", "--port", String(port)], {
|
|
14345
|
+
cwd: dir,
|
|
14346
|
+
stdio: ["ignore", out, err],
|
|
14347
|
+
env: { ...process.env, HELIXEVO_VERSION: version },
|
|
14348
|
+
detached: true
|
|
14349
|
+
});
|
|
14350
|
+
child.unref();
|
|
14351
|
+
writeDashboardState({
|
|
14352
|
+
pid: child.pid ?? -1,
|
|
14353
|
+
port,
|
|
14354
|
+
version,
|
|
14355
|
+
startedAt: new Date().toISOString()
|
|
14356
|
+
});
|
|
14357
|
+
console.log(` \uD83C\uDF10 HelixEvo Dashboard v${version} running in background`);
|
|
14358
|
+
console.log(` ${getDashboardUrl(port)}`);
|
|
14359
|
+
console.log(` Logs: ${DASHBOARD_LOG_FILE}`);
|
|
14360
|
+
console.log(` Stop: helixevo dashboard --stop
|
|
14361
|
+
`);
|
|
14362
|
+
setTimeout(() => {
|
|
14363
|
+
openDashboardUrl(port);
|
|
14364
|
+
}, 2000);
|
|
14365
|
+
process.exit(0);
|
|
14366
|
+
}
|
|
14367
|
+
function launchDashboard(dir, openBrowser, port) {
|
|
14368
|
+
const currentVersion = readCurrentVersion();
|
|
14369
|
+
const child = spawn2("npx", ["next", "dev", "--port", String(port)], {
|
|
13037
14370
|
cwd: dir,
|
|
13038
14371
|
stdio: "inherit",
|
|
13039
14372
|
env: { ...process.env, HELIXEVO_VERSION: currentVersion }
|
|
13040
14373
|
});
|
|
13041
14374
|
if (openBrowser) {
|
|
13042
14375
|
setTimeout(() => {
|
|
13043
|
-
|
|
13044
|
-
const platform = process.platform;
|
|
13045
|
-
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
|
|
13046
|
-
execSync2(`${cmd} http://localhost:3847`, { stdio: "ignore" });
|
|
13047
|
-
} catch {}
|
|
14376
|
+
openDashboardUrl(port);
|
|
13048
14377
|
}, 3000);
|
|
13049
14378
|
}
|
|
13050
14379
|
child.on("close", (code) => {
|
|
13051
14380
|
if (code === RESTART_EXIT_CODE) {
|
|
13052
14381
|
console.log(`
|
|
13053
|
-
\uD83D\uDD04 Restarting dashboard after update...
|
|
14382
|
+
\uD83D\uDD04 Restarting dashboard after update on ${getDashboardUrl(port)}...
|
|
13054
14383
|
`);
|
|
13055
14384
|
setTimeout(() => {
|
|
13056
14385
|
const nextCache = join17(dir, ".next");
|
|
@@ -13060,19 +14389,138 @@ function launchDashboard(dir, openBrowser) {
|
|
|
13060
14389
|
} catch {}
|
|
13061
14390
|
}
|
|
13062
14391
|
ensureSkillGraph();
|
|
13063
|
-
|
|
13064
|
-
|
|
13065
|
-
updatedVersion = execSync2("helixevo --version 2>/dev/null", { encoding: "utf-8" }).trim() || currentVersion;
|
|
13066
|
-
} catch {}
|
|
13067
|
-
console.log(` \uD83C\uDF10 HelixEvo Dashboard v${updatedVersion} at http://localhost:3847
|
|
14392
|
+
const updatedVersion = readCurrentVersion(currentVersion);
|
|
14393
|
+
console.log(` \uD83C\uDF10 HelixEvo Dashboard v${updatedVersion} at ${getDashboardUrl(port)}
|
|
13068
14394
|
`);
|
|
13069
|
-
launchDashboard(dir, false);
|
|
14395
|
+
launchDashboard(dir, false, port);
|
|
13070
14396
|
}, 2000);
|
|
13071
14397
|
} else {
|
|
13072
14398
|
process.exit(code ?? 0);
|
|
13073
14399
|
}
|
|
13074
14400
|
});
|
|
13075
14401
|
}
|
|
14402
|
+
async function resolveLaunchTarget(preferredPort) {
|
|
14403
|
+
const existingState = readDashboardState();
|
|
14404
|
+
if (existingState && existingState.port === preferredPort) {
|
|
14405
|
+
return { port: preferredPort, source: "existing", state: existingState };
|
|
14406
|
+
}
|
|
14407
|
+
if (await isPortAvailable(preferredPort)) {
|
|
14408
|
+
return { port: preferredPort, source: "preferred" };
|
|
14409
|
+
}
|
|
14410
|
+
const fallbackPort = await findAvailablePort(preferredPort + 1);
|
|
14411
|
+
return { port: fallbackPort, source: "fallback" };
|
|
14412
|
+
}
|
|
14413
|
+
function stopDashboard() {
|
|
14414
|
+
const state = readDashboardState();
|
|
14415
|
+
if (!state) {
|
|
14416
|
+
console.log(" No background dashboard running.");
|
|
14417
|
+
return;
|
|
14418
|
+
}
|
|
14419
|
+
try {
|
|
14420
|
+
process.kill(state.pid, "SIGTERM");
|
|
14421
|
+
} catch {}
|
|
14422
|
+
clearDashboardState();
|
|
14423
|
+
console.log(` Dashboard stopped (${getDashboardUrl(state.port)}).`);
|
|
14424
|
+
}
|
|
14425
|
+
function parseDashboardPort(port) {
|
|
14426
|
+
if (port === undefined)
|
|
14427
|
+
return DEFAULT_DASHBOARD_PORT;
|
|
14428
|
+
const parsed = typeof port === "number" ? port : Number.parseInt(String(port), 10);
|
|
14429
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
|
|
14430
|
+
console.error(` Invalid dashboard port: ${port}`);
|
|
14431
|
+
process.exit(1);
|
|
14432
|
+
}
|
|
14433
|
+
return parsed;
|
|
14434
|
+
}
|
|
14435
|
+
function readCurrentVersion(fallback = VERSION) {
|
|
14436
|
+
try {
|
|
14437
|
+
return execSync2("helixevo --version 2>/dev/null", { encoding: "utf-8" }).trim() || fallback;
|
|
14438
|
+
} catch {
|
|
14439
|
+
return fallback;
|
|
14440
|
+
}
|
|
14441
|
+
}
|
|
14442
|
+
function getDashboardUrl(port) {
|
|
14443
|
+
return `http://localhost:${port}`;
|
|
14444
|
+
}
|
|
14445
|
+
function openDashboardUrl(port) {
|
|
14446
|
+
try {
|
|
14447
|
+
const platform = process.platform;
|
|
14448
|
+
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
|
|
14449
|
+
execSync2(`${cmd} ${getDashboardUrl(port)}`, { stdio: "ignore" });
|
|
14450
|
+
} catch {}
|
|
14451
|
+
}
|
|
14452
|
+
function writeDashboardState(state) {
|
|
14453
|
+
mkdirSync5(HELIX_HOME_DIR, { recursive: true });
|
|
14454
|
+
writeFileSync10(DASHBOARD_PID_FILE, String(state.pid));
|
|
14455
|
+
writeFileSync10(DASHBOARD_STATE_FILE, JSON.stringify(state, null, 2));
|
|
14456
|
+
}
|
|
14457
|
+
function readDashboardState() {
|
|
14458
|
+
let state = null;
|
|
14459
|
+
if (existsSync11(DASHBOARD_STATE_FILE)) {
|
|
14460
|
+
try {
|
|
14461
|
+
const parsed = JSON.parse(readFileSync9(DASHBOARD_STATE_FILE, "utf-8"));
|
|
14462
|
+
if (Number.isInteger(parsed.pid) && Number.isInteger(parsed.port)) {
|
|
14463
|
+
state = parsed;
|
|
14464
|
+
}
|
|
14465
|
+
} catch {}
|
|
14466
|
+
}
|
|
14467
|
+
if (!state && existsSync11(DASHBOARD_PID_FILE)) {
|
|
14468
|
+
try {
|
|
14469
|
+
const pid = Number.parseInt(readFileSync9(DASHBOARD_PID_FILE, "utf-8").trim(), 10);
|
|
14470
|
+
if (Number.isInteger(pid)) {
|
|
14471
|
+
state = {
|
|
14472
|
+
pid,
|
|
14473
|
+
port: DEFAULT_DASHBOARD_PORT,
|
|
14474
|
+
version: VERSION,
|
|
14475
|
+
startedAt: ""
|
|
14476
|
+
};
|
|
14477
|
+
}
|
|
14478
|
+
} catch {}
|
|
14479
|
+
}
|
|
14480
|
+
if (!state)
|
|
14481
|
+
return null;
|
|
14482
|
+
if (state.pid <= 0 || !isProcessAlive(state.pid)) {
|
|
14483
|
+
clearDashboardState();
|
|
14484
|
+
return null;
|
|
14485
|
+
}
|
|
14486
|
+
return state;
|
|
14487
|
+
}
|
|
14488
|
+
function clearDashboardState() {
|
|
14489
|
+
try {
|
|
14490
|
+
rmSync2(DASHBOARD_PID_FILE, { force: true });
|
|
14491
|
+
} catch {}
|
|
14492
|
+
try {
|
|
14493
|
+
rmSync2(DASHBOARD_STATE_FILE, { force: true });
|
|
14494
|
+
} catch {}
|
|
14495
|
+
}
|
|
14496
|
+
function isProcessAlive(pid) {
|
|
14497
|
+
try {
|
|
14498
|
+
process.kill(pid, 0);
|
|
14499
|
+
return true;
|
|
14500
|
+
} catch {
|
|
14501
|
+
return false;
|
|
14502
|
+
}
|
|
14503
|
+
}
|
|
14504
|
+
async function isPortAvailable(port) {
|
|
14505
|
+
return await new Promise((resolve2) => {
|
|
14506
|
+
const server = createServer();
|
|
14507
|
+
server.unref();
|
|
14508
|
+
server.once("error", () => {
|
|
14509
|
+
resolve2(false);
|
|
14510
|
+
});
|
|
14511
|
+
server.listen(port, () => {
|
|
14512
|
+
server.close(() => resolve2(true));
|
|
14513
|
+
});
|
|
14514
|
+
});
|
|
14515
|
+
}
|
|
14516
|
+
async function findAvailablePort(startPort) {
|
|
14517
|
+
for (let attempt = 0;attempt < MAX_PORT_SCAN_ATTEMPTS; attempt += 1) {
|
|
14518
|
+
const candidate = startPort + attempt;
|
|
14519
|
+
if (await isPortAvailable(candidate))
|
|
14520
|
+
return candidate;
|
|
14521
|
+
}
|
|
14522
|
+
throw new Error(`Unable to find an open dashboard port after checking ${MAX_PORT_SCAN_ATTEMPTS} ports starting at ${startPort}.`);
|
|
14523
|
+
}
|
|
13076
14524
|
function prepareDashboard() {
|
|
13077
14525
|
const devDir = findDevDashboard();
|
|
13078
14526
|
if (devDir)
|
|
@@ -13711,7 +15159,7 @@ async function metricsCommand(options) {
|
|
|
13711
15159
|
init_data();
|
|
13712
15160
|
init_skills();
|
|
13713
15161
|
init_llm();
|
|
13714
|
-
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";
|
|
13715
15163
|
import { existsSync as existsSync15, readFileSync as readFileSync12, writeFileSync as writeFileSync12, mkdirSync as mkdirSync6, readdirSync as readdirSync4 } from "node:fs";
|
|
13716
15164
|
import { homedir as homedir5 } from "node:os";
|
|
13717
15165
|
|
|
@@ -13791,7 +15239,7 @@ function expandHomePath(path) {
|
|
|
13791
15239
|
}
|
|
13792
15240
|
function normalizeProjectPath(projectPath) {
|
|
13793
15241
|
const expanded = expandHomePath(projectPath.trim());
|
|
13794
|
-
const resolvedPath =
|
|
15242
|
+
const resolvedPath = isAbsolute2(expanded) ? normalize(expanded) : resolve2(process.cwd(), expanded);
|
|
13795
15243
|
return stripTrailingSeparator(resolvedPath);
|
|
13796
15244
|
}
|
|
13797
15245
|
function readProjectContext(projectPath) {
|
|
@@ -14006,10 +15454,428 @@ async function projectSetupCommand(projectPath, options) {
|
|
|
14006
15454
|
console.log();
|
|
14007
15455
|
}
|
|
14008
15456
|
|
|
14009
|
-
// src/
|
|
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";
|
|
14010
15464
|
import { join as join21 } from "node:path";
|
|
14011
|
-
|
|
14012
|
-
|
|
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
|
+
|
|
15878
|
+
// src/cli.ts
|
|
14013
15879
|
var program2 = new Command;
|
|
14014
15880
|
program2.name("helixevo").description("Self-evolving skill ecosystem for AI agents").version(VERSION).addHelpText("after", `
|
|
14015
15881
|
Examples:
|
|
@@ -14027,7 +15893,11 @@ Examples:
|
|
|
14027
15893
|
$ helixevo graph --mermaid Open graph in browser
|
|
14028
15894
|
$ helixevo graph --obsidian ~/vault Sync to Obsidian vault
|
|
14029
15895
|
$ helixevo graph --rebuild Re-infer relationships (LLM call)
|
|
14030
|
-
$ helixevo graph --optimize Detect
|
|
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
|
|
14031
15901
|
$ helixevo report --days 7 Weekly evolution report
|
|
14032
15902
|
$ helixevo capture <session.json> Extract failures from session`);
|
|
14033
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);
|
|
@@ -14035,23 +15905,9 @@ program2.command("capture").description("Capture failures from a Craft Agent ses
|
|
|
14035
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);
|
|
14036
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);
|
|
14037
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);
|
|
14038
|
-
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);
|
|
14039
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);
|
|
14040
|
-
program2.command("dashboard").description("Open web dashboard
|
|
14041
|
-
if (options.stop) {
|
|
14042
|
-
const pidFile = join21(homedir6(), ".helix", "dashboard.pid");
|
|
14043
|
-
if (existsSync16(pidFile)) {
|
|
14044
|
-
const pid = parseInt(readFileSync13(pidFile, "utf-8").trim());
|
|
14045
|
-
try {
|
|
14046
|
-
process.kill(pid, "SIGTERM");
|
|
14047
|
-
} catch {}
|
|
14048
|
-
rmSync3(pidFile);
|
|
14049
|
-
console.log(" Dashboard stopped.");
|
|
14050
|
-
} else {
|
|
14051
|
-
console.log(" No background dashboard running.");
|
|
14052
|
-
}
|
|
14053
|
-
return;
|
|
14054
|
-
}
|
|
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) => {
|
|
14055
15911
|
await dashboardCommand(options);
|
|
14056
15912
|
});
|
|
14057
15913
|
program2.command("status").description("Show frontier, skills, failures, and network health").action(statusCommand);
|
|
@@ -14064,6 +15920,7 @@ program2.command("health").description("Assess network health: cohesion, coverag
|
|
|
14064
15920
|
});
|
|
14065
15921
|
program2.command("metrics").description("Show correction rates, skill trends, and evolution impact").option("--verbose", "Show detailed per-skill breakdown").action(metricsCommand);
|
|
14066
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);
|
|
14067
15924
|
program2.hook("postAction", () => {
|
|
14068
15925
|
checkForUpdates().catch(() => {});
|
|
14069
15926
|
});
|