selftune 0.2.23 → 0.2.24
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 +6 -0
- package/README.md +93 -15
- package/apps/local-dashboard/dist/assets/index-DgY2KGP-.css +1 -0
- package/apps/local-dashboard/dist/assets/index-Dmx7LPVX.js +15 -0
- package/apps/local-dashboard/dist/assets/vendor-react-C5oyHiV1.js +11 -0
- package/apps/local-dashboard/dist/assets/{vendor-table-BIiI3YhS.js → vendor-table-Bc_bbKd8.js} +1 -1
- package/apps/local-dashboard/dist/assets/vendor-ui-B3BPIYy7.js +1 -0
- package/apps/local-dashboard/dist/index.html +5 -5
- package/cli/selftune/adapters/codex/install.ts +310 -78
- package/cli/selftune/adapters/opencode/install.ts +3 -4
- package/cli/selftune/alpha-upload/build-payloads.ts +3 -3
- package/cli/selftune/alpha-upload/stage-canonical.ts +17 -11
- package/cli/selftune/auto-update.ts +200 -8
- package/cli/selftune/canonical-export.ts +55 -25
- package/cli/selftune/command-surface.ts +397 -0
- package/cli/selftune/contribute/contribute.ts +64 -13
- package/cli/selftune/contribution-config.ts +57 -3
- package/cli/selftune/contribution-preferences.ts +117 -0
- package/cli/selftune/contribution-signals.ts +8 -4
- package/cli/selftune/contribution-staging.ts +13 -2
- package/cli/selftune/contributions.ts +55 -121
- package/cli/selftune/creator-contributions.ts +29 -10
- package/cli/selftune/cron/setup.ts +7 -3
- package/cli/selftune/dashboard-contract.ts +73 -0
- package/cli/selftune/dashboard-server.ts +168 -17
- package/cli/selftune/dashboard.ts +350 -17
- package/cli/selftune/eval/baseline.ts +21 -5
- package/cli/selftune/eval/execution-eval.ts +170 -0
- package/cli/selftune/eval/family-overlap.ts +2 -2
- package/cli/selftune/eval/hooks-to-evals.ts +228 -82
- package/cli/selftune/eval/import-skillsbench.ts +2 -2
- package/cli/selftune/eval/invocation-classifier.ts +56 -0
- package/cli/selftune/eval/synthetic-evals.ts +5 -3
- package/cli/selftune/eval/unit-test-cli.ts +7 -4
- package/cli/selftune/evolution/apply-proposal.ts +295 -0
- package/cli/selftune/evolution/engines/replay-engine.ts +79 -57
- package/cli/selftune/evolution/evolve-body.ts +100 -39
- package/cli/selftune/evolution/evolve.ts +244 -52
- package/cli/selftune/evolution/rollback.ts +0 -1
- package/cli/selftune/evolution/validate-body.ts +68 -42
- package/cli/selftune/evolution/validate-host-replay.ts +510 -60
- package/cli/selftune/evolution/validate-proposal.ts +11 -150
- package/cli/selftune/evolution/validate-routing.ts +43 -41
- package/cli/selftune/evolution/validation-contract.ts +91 -0
- package/cli/selftune/grading/auto-grade.ts +11 -7
- package/cli/selftune/grading/grade-session.ts +10 -16
- package/cli/selftune/index.ts +35 -10
- package/cli/selftune/ingestors/claude-replay.ts +15 -10
- package/cli/selftune/ingestors/codex-wrapper.ts +3 -3
- package/cli/selftune/ingestors/opencode-ingest.ts +2 -2
- package/cli/selftune/ingestors/pi-ingest.ts +3 -2
- package/cli/selftune/init.ts +27 -3
- package/cli/selftune/localdb/direct-write.ts +35 -1
- package/cli/selftune/localdb/queries/cron.ts +34 -0
- package/cli/selftune/localdb/queries/dashboard.ts +834 -0
- package/cli/selftune/localdb/queries/evolution.ts +158 -0
- package/cli/selftune/localdb/queries/execution.ts +133 -0
- package/cli/selftune/localdb/queries/json.ts +18 -0
- package/cli/selftune/localdb/queries/monitoring.ts +263 -0
- package/cli/selftune/localdb/queries/raw.ts +95 -0
- package/cli/selftune/localdb/queries/staging.ts +270 -0
- package/cli/selftune/localdb/queries/trust.ts +392 -0
- package/cli/selftune/localdb/queries.ts +60 -2288
- package/cli/selftune/localdb/schema.ts +21 -0
- package/cli/selftune/monitoring/watch.ts +96 -29
- package/cli/selftune/normalization.ts +3 -0
- package/cli/selftune/observability.ts +4 -2
- package/cli/selftune/orchestrate/cli.ts +161 -0
- package/cli/selftune/orchestrate/execute.ts +295 -0
- package/cli/selftune/orchestrate/finalize.ts +157 -0
- package/cli/selftune/orchestrate/locks.ts +40 -0
- package/cli/selftune/orchestrate/plan.ts +131 -0
- package/cli/selftune/orchestrate/post-run.ts +59 -0
- package/cli/selftune/orchestrate/prepare.ts +334 -0
- package/cli/selftune/orchestrate/report.ts +182 -0
- package/cli/selftune/orchestrate/runtime.ts +120 -0
- package/cli/selftune/orchestrate/signals.ts +48 -0
- package/cli/selftune/orchestrate.ts +150 -1173
- package/cli/selftune/repair/skill-usage.ts +5 -2
- package/cli/selftune/routes/overview.ts +5 -2
- package/cli/selftune/routes/skill-report.ts +15 -2
- package/cli/selftune/schedule.ts +5 -5
- package/cli/selftune/status.ts +39 -2
- package/cli/selftune/testing-readiness.ts +597 -0
- package/cli/selftune/types.ts +44 -4
- package/cli/selftune/uninstall.ts +2 -1
- package/cli/selftune/utils/canonical-log.ts +1 -9
- package/cli/selftune/utils/cli-error.ts +9 -0
- package/cli/selftune/utils/llm-call.ts +126 -6
- package/cli/selftune/utils/skill-discovery.ts +2 -0
- package/cli/selftune/workflows/proposals.ts +184 -0
- package/cli/selftune/workflows/skill-scaffold.ts +241 -0
- package/cli/selftune/workflows/workflows.ts +100 -26
- package/node_modules/@selftune/telemetry-contract/fixtures/complete-push.ts +1 -1
- package/node_modules/@selftune/telemetry-contract/fixtures/evidence-only-push.ts +1 -1
- package/node_modules/@selftune/telemetry-contract/fixtures/partial-push-no-sessions.ts +1 -1
- package/node_modules/@selftune/telemetry-contract/fixtures/partial-push-unresolved-parents.ts +1 -1
- package/node_modules/@selftune/telemetry-contract/src/schemas.ts +41 -1
- package/node_modules/@selftune/telemetry-contract/src/types.ts +103 -2
- package/package.json +25 -9
- package/packages/dashboard-core/AGENTS.md +18 -0
- package/packages/dashboard-core/README.md +30 -0
- package/packages/dashboard-core/index.ts +3 -0
- package/packages/dashboard-core/package.json +39 -0
- package/packages/dashboard-core/src/chrome/DashboardChrome.tsx +74 -0
- package/packages/dashboard-core/src/chrome/DashboardHeader.tsx +200 -0
- package/packages/dashboard-core/src/chrome/DashboardSidebar.tsx +219 -0
- package/packages/dashboard-core/src/chrome/RuntimeBadge.tsx +46 -0
- package/packages/dashboard-core/src/chrome/index.ts +14 -0
- package/packages/dashboard-core/src/chrome/types.ts +81 -0
- package/packages/dashboard-core/src/chrome/utils.ts +23 -0
- package/packages/dashboard-core/src/gates/FeatureGate.tsx +11 -0
- package/packages/dashboard-core/src/gates/LockedRoute.tsx +29 -0
- package/packages/dashboard-core/src/gates/UpgradeCard.tsx +89 -0
- package/packages/dashboard-core/src/gates/index.ts +3 -0
- package/packages/dashboard-core/src/host/DashboardHostProvider.tsx +62 -0
- package/packages/dashboard-core/src/host/adapter.ts +47 -0
- package/packages/dashboard-core/src/host/capabilities.ts +55 -0
- package/packages/dashboard-core/src/host/index.ts +3 -0
- package/packages/dashboard-core/src/models/analytics.ts +39 -0
- package/packages/dashboard-core/src/models/index.ts +4 -0
- package/packages/dashboard-core/src/models/overview.ts +98 -0
- package/packages/dashboard-core/src/models/runtime.ts +7 -0
- package/packages/dashboard-core/src/models/skills.ts +34 -0
- package/packages/dashboard-core/src/routes/index.ts +2 -0
- package/packages/dashboard-core/src/routes/manifest.test.ts +70 -0
- package/packages/dashboard-core/src/routes/manifest.ts +451 -0
- package/packages/dashboard-core/src/routes/types.ts +39 -0
- package/packages/dashboard-core/src/screens/analytics/AnalyticsScreen.tsx +278 -0
- package/packages/dashboard-core/src/screens/analytics/index.ts +1 -0
- package/packages/dashboard-core/src/screens/index.ts +37 -0
- package/packages/dashboard-core/src/screens/overview/OverviewComparisonSurface.test.ts +101 -0
- package/packages/dashboard-core/src/screens/overview/OverviewComparisonSurface.tsx +393 -0
- package/packages/dashboard-core/src/screens/overview/OverviewCompositionSurface.test.tsx +113 -0
- package/packages/dashboard-core/src/screens/overview/OverviewCompositionSurface.tsx +72 -0
- package/packages/dashboard-core/src/screens/overview/OverviewCoreSurface.tsx +71 -0
- package/packages/dashboard-core/src/screens/overview/OverviewOnboardingBanner.tsx +90 -0
- package/packages/dashboard-core/src/screens/overview/OverviewRunSummary.tsx +40 -0
- package/packages/dashboard-core/src/screens/overview/index.ts +16 -0
- package/packages/dashboard-core/src/screens/overview/types.ts +13 -0
- package/packages/dashboard-core/src/screens/skill-report/SkillReportDailyBreakdownSection.tsx +99 -0
- package/packages/dashboard-core/src/screens/skill-report/SkillReportDataQualityTabContent.tsx +35 -0
- package/packages/dashboard-core/src/screens/skill-report/SkillReportEvidenceRail.tsx +71 -0
- package/packages/dashboard-core/src/screens/skill-report/SkillReportEvidenceSection.tsx +63 -0
- package/packages/dashboard-core/src/screens/skill-report/SkillReportEvidenceTabContent.tsx +25 -0
- package/packages/dashboard-core/src/screens/skill-report/SkillReportInvocationsSection.tsx +24 -0
- package/packages/dashboard-core/src/screens/skill-report/SkillReportMissedQueriesSection.tsx +79 -0
- package/packages/dashboard-core/src/screens/skill-report/SkillReportScaffold.tsx +150 -0
- package/packages/dashboard-core/src/screens/skill-report/SkillReportSections.test.tsx +224 -0
- package/packages/dashboard-core/src/screens/skill-report/SkillReportTabs.test.tsx +76 -0
- package/packages/dashboard-core/src/screens/skill-report/SkillReportTabs.tsx +88 -0
- package/packages/dashboard-core/src/screens/skill-report/SkillReportTrendSection.tsx +33 -0
- package/packages/dashboard-core/src/screens/skill-report/SkillReportTrustBadge.tsx +67 -0
- package/packages/dashboard-core/src/screens/skill-report/index.ts +45 -0
- package/packages/dashboard-core/src/screens/skills/SkillsLibraryScreen.tsx +162 -0
- package/packages/dashboard-core/src/screens/skills/index.ts +6 -0
- package/packages/telemetry-contract/fixtures/complete-push.ts +1 -1
- package/packages/telemetry-contract/fixtures/evidence-only-push.ts +1 -1
- package/packages/telemetry-contract/fixtures/partial-push-no-sessions.ts +1 -1
- package/packages/telemetry-contract/fixtures/partial-push-unresolved-parents.ts +1 -1
- package/packages/telemetry-contract/src/schemas.ts +41 -1
- package/packages/telemetry-contract/src/types.ts +103 -2
- package/packages/ui/src/components/EvidenceViewer.tsx +80 -25
- package/packages/ui/src/components/OverviewPanels.tsx +67 -26
- package/packages/ui/src/primitives/tabs.tsx +7 -6
- package/packages/ui/src/types.ts +10 -0
- package/skill/SKILL.md +130 -332
- package/skill/agents/diagnosis-analyst.md +3 -3
- package/skill/agents/evolution-reviewer.md +3 -3
- package/skill/agents/integration-guide.md +3 -3
- package/skill/agents/pattern-analyst.md +2 -2
- package/skill/references/cli-quick-reference.md +89 -0
- package/skill/references/creator-playbook.md +131 -0
- package/skill/references/examples.md +48 -0
- package/skill/references/troubleshooting.md +47 -0
- package/skill/references/version-history.md +1 -1
- package/skill/selftune.contribute.json +11 -0
- package/skill/{Workflows → workflows}/Baseline.md +20 -1
- package/skill/{Workflows → workflows}/Contribute.md +23 -10
- package/skill/{Workflows → workflows}/Contributions.md +13 -5
- package/skill/workflows/CreateTestDeploy.md +170 -0
- package/skill/{Workflows → workflows}/CreatorContributions.md +18 -6
- package/skill/{Workflows → workflows}/Cron.md +1 -1
- package/skill/{Workflows → workflows}/Dashboard.md +20 -0
- package/skill/{Workflows → workflows}/Doctor.md +1 -1
- package/skill/{Workflows → workflows}/Evals.md +67 -2
- package/skill/{Workflows → workflows}/Evolve.md +119 -30
- package/skill/{Workflows → workflows}/EvolveBody.md +41 -1
- package/skill/{Workflows → workflows}/Grade.md +1 -1
- package/skill/{Workflows → workflows}/Initialize.md +8 -4
- package/skill/{Workflows → workflows}/Orchestrate.md +13 -3
- package/skill/{Workflows → workflows}/Schedule.md +3 -3
- package/skill/workflows/SignalsDashboard.md +87 -0
- package/skill/{Workflows → workflows}/UnitTest.md +19 -0
- package/skill/{Workflows → workflows}/Watch.md +42 -2
- package/skill/{Workflows → workflows}/Workflows.md +39 -2
- package/apps/local-dashboard/dist/assets/index-CwOtTrUS.css +0 -1
- package/apps/local-dashboard/dist/assets/index-f1HQpbeH.js +0 -59
- package/apps/local-dashboard/dist/assets/vendor-react-CKkiCskZ.js +0 -11
- package/apps/local-dashboard/dist/assets/vendor-ui-jVSaIZey.js +0 -12
- /package/skill/{Workflows → workflows}/AlphaUpload.md +0 -0
- /package/skill/{Workflows → workflows}/AutoActivation.md +0 -0
- /package/skill/{Workflows → workflows}/Badge.md +0 -0
- /package/skill/{Workflows → workflows}/Composability.md +0 -0
- /package/skill/{Workflows → workflows}/EvolutionMemory.md +0 -0
- /package/skill/{Workflows → workflows}/ExportCanonical.md +0 -0
- /package/skill/{Workflows → workflows}/Hook.md +0 -0
- /package/skill/{Workflows → workflows}/ImportSkillsBench.md +0 -0
- /package/skill/{Workflows → workflows}/Ingest.md +0 -0
- /package/skill/{Workflows → workflows}/PlatformHooks.md +0 -0
- /package/skill/{Workflows → workflows}/Quickstart.md +0 -0
- /package/skill/{Workflows → workflows}/Recover.md +0 -0
- /package/skill/{Workflows → workflows}/Registry.md +0 -0
- /package/skill/{Workflows → workflows}/RepairSkillUsage.md +0 -0
- /package/skill/{Workflows → workflows}/Replay.md +0 -0
- /package/skill/{Workflows → workflows}/Rollback.md +0 -0
- /package/skill/{Workflows → workflows}/Sync.md +0 -0
- /package/skill/{Workflows → workflows}/Telemetry.md +0 -0
- /package/skill/{Workflows → workflows}/Uninstall.md +0 -0
|
@@ -9,43 +9,43 @@
|
|
|
9
9
|
* explicit dry-run and review-required modes for human-in-the-loop operation.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
13
|
-
import { homedir } from "node:os";
|
|
14
|
-
import { dirname, join } from "node:path";
|
|
15
|
-
import { parseArgs } from "node:util";
|
|
16
|
-
|
|
17
|
-
import { readAlphaIdentity } from "./alpha-identity.js";
|
|
18
12
|
import type { UploadCycleSummary } from "./alpha-upload/index.js";
|
|
19
|
-
import { getOrchestrateLockPath, SELFTUNE_CONFIG_PATH } from "./constants.js";
|
|
20
|
-
import type { OrchestrateRunReport, OrchestrateRunSkillAction } from "./dashboard-contract.js";
|
|
21
13
|
import type { EvolveOptions, EvolveResult } from "./evolution/evolve.js";
|
|
22
|
-
import {
|
|
23
|
-
buildDefaultGradingOutputPath,
|
|
24
|
-
deriveExpectationsFromSkill,
|
|
25
|
-
gradeSession,
|
|
26
|
-
resolveLatestSessionForSkill,
|
|
27
|
-
} from "./grading/grade-session.js";
|
|
28
14
|
import { readGradingResultsForSkill } from "./grading/results.js";
|
|
29
15
|
import { getDb } from "./localdb/db.js";
|
|
16
|
+
import { writeCronRunToDb } from "./localdb/direct-write.js";
|
|
17
|
+
import type { WatchResult } from "./monitoring/watch.js";
|
|
30
18
|
import {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
} from "./localdb/direct-write.js";
|
|
19
|
+
buildOrchestrateJsonOutput,
|
|
20
|
+
parseOrchestrateCliArgs,
|
|
21
|
+
renderOrchestrateHelp,
|
|
22
|
+
} from "./orchestrate/cli.js";
|
|
36
23
|
import {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
} from "./
|
|
43
|
-
import
|
|
24
|
+
autoGradeFreshDeploys,
|
|
25
|
+
buildReplayValidationOptions,
|
|
26
|
+
runEvolutionPhase,
|
|
27
|
+
watchRecentDeploys,
|
|
28
|
+
} from "./orchestrate/execute.js";
|
|
29
|
+
import { finalizeOrchestrateRun } from "./orchestrate/finalize.js";
|
|
30
|
+
import { acquireLock, releaseLock } from "./orchestrate/locks.js";
|
|
31
|
+
import { runPostOrchestrateSideEffects } from "./orchestrate/post-run.js";
|
|
32
|
+
import {
|
|
33
|
+
autoGradeTopUngraded,
|
|
34
|
+
detectCrossSkillOverlap,
|
|
35
|
+
prepareOrchestrateRun,
|
|
36
|
+
} from "./orchestrate/prepare.js";
|
|
37
|
+
import {
|
|
38
|
+
DEFAULT_COOLDOWN_HOURS,
|
|
39
|
+
MIN_CANDIDATE_EVIDENCE,
|
|
40
|
+
selectCandidates,
|
|
41
|
+
} from "./orchestrate/plan.js";
|
|
42
|
+
import { formatOrchestrateReport } from "./orchestrate/report.js";
|
|
43
|
+
import { resolveOrchestrateRuntime } from "./orchestrate/runtime.js";
|
|
44
44
|
import { doctor } from "./observability.js";
|
|
45
|
-
import type {
|
|
45
|
+
import type { StatusResult } from "./status.js";
|
|
46
46
|
import { computeStatus } from "./status.js";
|
|
47
47
|
import type { SyncResult } from "./sync.js";
|
|
48
|
-
import {
|
|
48
|
+
import { syncSources } from "./sync.js";
|
|
49
49
|
import type {
|
|
50
50
|
AlphaIdentity,
|
|
51
51
|
EvolutionAuditEntry,
|
|
@@ -54,103 +54,23 @@ import type {
|
|
|
54
54
|
SessionTelemetryRecord,
|
|
55
55
|
SkillUsageRecord,
|
|
56
56
|
} from "./types.js";
|
|
57
|
-
import {
|
|
58
|
-
import {
|
|
59
|
-
import { getSelftuneVersion, readConfiguredAgentType } from "./utils/selftune-meta.js";
|
|
57
|
+
import { handleCLIError } from "./utils/cli-error.js";
|
|
58
|
+
import { detectLlmAgent } from "./utils/llm-call.js";
|
|
60
59
|
import {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
} from "./
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const LOCK_STALE_MS = 30 * 60 * 1000; // 30 minutes
|
|
77
|
-
|
|
78
|
-
export function acquireLock(lockPath: string = getOrchestrateLockPath()): boolean {
|
|
79
|
-
try {
|
|
80
|
-
if (existsSync(lockPath)) {
|
|
81
|
-
try {
|
|
82
|
-
const raw = readFileSync(lockPath, "utf-8");
|
|
83
|
-
const info: LockInfo = JSON.parse(raw);
|
|
84
|
-
const lockAge = Date.now() - Date.parse(info.timestamp);
|
|
85
|
-
if (lockAge < LOCK_STALE_MS) {
|
|
86
|
-
return false; // lock is fresh, cannot acquire
|
|
87
|
-
}
|
|
88
|
-
// Lock is stale, fall through to overwrite
|
|
89
|
-
} catch {
|
|
90
|
-
// Corrupted lock file, treat as stale and overwrite
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
const lock: LockInfo = { pid: process.pid, timestamp: new Date().toISOString() };
|
|
94
|
-
writeFileSync(lockPath, JSON.stringify(lock));
|
|
95
|
-
return true;
|
|
96
|
-
} catch {
|
|
97
|
-
// Fail-open: if we can't check/write, allow the run
|
|
98
|
-
return true;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export function releaseLock(lockPath: string = getOrchestrateLockPath()): void {
|
|
103
|
-
try {
|
|
104
|
-
unlinkSync(lockPath);
|
|
105
|
-
} catch {
|
|
106
|
-
// Silent on errors (file may not exist)
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// ---------------------------------------------------------------------------
|
|
111
|
-
// Signal reading helpers
|
|
112
|
-
// ---------------------------------------------------------------------------
|
|
113
|
-
|
|
114
|
-
function readPendingSignals(reader?: () => ImprovementSignalRecord[]): ImprovementSignalRecord[] {
|
|
115
|
-
const _read =
|
|
116
|
-
reader ??
|
|
117
|
-
(() => {
|
|
118
|
-
const db = getDb();
|
|
119
|
-
return queryImprovementSignals(db, false) as ImprovementSignalRecord[];
|
|
120
|
-
});
|
|
121
|
-
try {
|
|
122
|
-
return _read().filter((s) => !s.consumed);
|
|
123
|
-
} catch {
|
|
124
|
-
return [];
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
export function groupSignalsBySkill(signals: ImprovementSignalRecord[]): Map<string, number> {
|
|
129
|
-
const map = new Map<string, number>();
|
|
130
|
-
for (const s of signals) {
|
|
131
|
-
if (s.mentioned_skill) {
|
|
132
|
-
const key = s.mentioned_skill.toLowerCase();
|
|
133
|
-
map.set(key, (map.get(key) ?? 0) + 1);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
return map;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export function markSignalsConsumed(signals: ImprovementSignalRecord[], runId: string): void {
|
|
140
|
-
try {
|
|
141
|
-
if (signals.length === 0) return;
|
|
142
|
-
for (const signal of signals) {
|
|
143
|
-
const ok = updateSignalConsumed(signal.session_id, signal.query, signal.signal_type, runId);
|
|
144
|
-
if (!ok) {
|
|
145
|
-
console.error(
|
|
146
|
-
`[orchestrate] failed to mark signal consumed: session_id=${signal.session_id}, signal_type=${signal.signal_type}`,
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
} catch {
|
|
151
|
-
// Silent on errors
|
|
152
|
-
}
|
|
153
|
-
}
|
|
60
|
+
discoverWorkflowSkillProposals,
|
|
61
|
+
persistWorkflowSkillProposal,
|
|
62
|
+
type WorkflowSkillProposal,
|
|
63
|
+
} from "./workflows/proposals.js";
|
|
64
|
+
|
|
65
|
+
export { acquireLock, releaseLock } from "./orchestrate/locks.js";
|
|
66
|
+
export {
|
|
67
|
+
DEFAULT_COOLDOWN_HOURS,
|
|
68
|
+
MIN_CANDIDATE_EVIDENCE,
|
|
69
|
+
selectCandidates,
|
|
70
|
+
} from "./orchestrate/plan.js";
|
|
71
|
+
export { autoGradeTopUngraded, detectCrossSkillOverlap } from "./orchestrate/prepare.js";
|
|
72
|
+
export { formatOrchestrateReport } from "./orchestrate/report.js";
|
|
73
|
+
export { groupSignalsBySkill, markSignalsConsumed } from "./orchestrate/signals.js";
|
|
154
74
|
|
|
155
75
|
// ---------------------------------------------------------------------------
|
|
156
76
|
// Types
|
|
@@ -181,11 +101,24 @@ export interface SkillAction {
|
|
|
181
101
|
watchResult?: WatchResult;
|
|
182
102
|
}
|
|
183
103
|
|
|
104
|
+
/** Context for candidate selection beyond simple status checks. */
|
|
105
|
+
export interface CandidateContext {
|
|
106
|
+
skillFilter?: string;
|
|
107
|
+
maxSkills: number;
|
|
108
|
+
auditEntries?: EvolutionAuditEntry[];
|
|
109
|
+
/** Hours since last deploy before a skill can be re-evolved. */
|
|
110
|
+
cooldownHours?: number;
|
|
111
|
+
/** Skill name (lowercase) to improvement signal count. */
|
|
112
|
+
signaledSkills?: Map<string, number>;
|
|
113
|
+
}
|
|
114
|
+
|
|
184
115
|
export interface OrchestrateResult {
|
|
185
116
|
syncResult: SyncResult;
|
|
186
117
|
statusResult: StatusResult;
|
|
187
118
|
candidates: SkillAction[];
|
|
119
|
+
workflowProposals: WorkflowSkillProposal[];
|
|
188
120
|
uploadSummary?: UploadCycleSummary;
|
|
121
|
+
contributionRelaySummary?: { attempted: number; sent: number; failed: number };
|
|
189
122
|
summary: {
|
|
190
123
|
totalSkills: number;
|
|
191
124
|
evaluated: number;
|
|
@@ -194,190 +127,13 @@ export interface OrchestrateResult {
|
|
|
194
127
|
watched: number;
|
|
195
128
|
skipped: number;
|
|
196
129
|
autoGraded: number;
|
|
130
|
+
freshlyWatchedSkills: string[];
|
|
197
131
|
dryRun: boolean;
|
|
198
132
|
approvalMode: "auto" | "review";
|
|
199
133
|
elapsedMs: number;
|
|
200
134
|
};
|
|
201
135
|
}
|
|
202
136
|
|
|
203
|
-
// ---------------------------------------------------------------------------
|
|
204
|
-
// Human-readable decision report
|
|
205
|
-
// ---------------------------------------------------------------------------
|
|
206
|
-
|
|
207
|
-
function formatSyncPhase(syncResult: SyncResult): string[] {
|
|
208
|
-
const lines: string[] = ["Phase 1: Sync"];
|
|
209
|
-
const sources: [string, keyof SyncResult["sources"]][] = [
|
|
210
|
-
["Claude", "claude"],
|
|
211
|
-
["Codex", "codex"],
|
|
212
|
-
["OpenCode", "opencode"],
|
|
213
|
-
["OpenClaw", "openclaw"],
|
|
214
|
-
];
|
|
215
|
-
|
|
216
|
-
for (const [label, key] of sources) {
|
|
217
|
-
const s = syncResult.sources[key];
|
|
218
|
-
if (!s.available) {
|
|
219
|
-
lines.push(` ${label.padEnd(12)}not available`);
|
|
220
|
-
} else if (s.synced > 0) {
|
|
221
|
-
lines.push(` ${label.padEnd(12)}scanned ${s.scanned}, synced ${s.synced}`);
|
|
222
|
-
} else {
|
|
223
|
-
lines.push(` ${label.padEnd(12)}scanned ${s.scanned}, up to date`);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
if (syncResult.repair.ran && syncResult.repair.repaired_records > 0) {
|
|
228
|
-
lines.push(
|
|
229
|
-
` Repair ${syncResult.repair.repaired_records} records across ${syncResult.repair.repaired_sessions} sessions`,
|
|
230
|
-
);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
return lines;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function formatStatusPhase(statusResult: StatusResult): string[] {
|
|
237
|
-
const lines: string[] = ["Phase 2: Status"];
|
|
238
|
-
const byStatus: Record<string, number> = {};
|
|
239
|
-
for (const skill of statusResult.skills) {
|
|
240
|
-
byStatus[skill.status] = (byStatus[skill.status] ?? 0) + 1;
|
|
241
|
-
}
|
|
242
|
-
const healthLabel = statusResult.system.healthy ? "healthy" : "UNHEALTHY";
|
|
243
|
-
lines.push(` ${statusResult.skills.length} skills found, system ${healthLabel}`);
|
|
244
|
-
|
|
245
|
-
const parts: string[] = [];
|
|
246
|
-
for (const s of ["CRITICAL", "WARNING", "HEALTHY", "UNGRADED", "UNKNOWN"]) {
|
|
247
|
-
if (byStatus[s]) parts.push(`${byStatus[s]} ${s}`);
|
|
248
|
-
}
|
|
249
|
-
if (parts.length > 0) lines.push(` ${parts.join(", ")}`);
|
|
250
|
-
|
|
251
|
-
return lines;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function formatDecisionPhase(candidates: SkillAction[]): string[] {
|
|
255
|
-
const lines: string[] = ["Phase 3: Skill Decisions"];
|
|
256
|
-
if (candidates.length === 0) {
|
|
257
|
-
lines.push(" (no skills to evaluate)");
|
|
258
|
-
return lines;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
for (const c of candidates) {
|
|
262
|
-
const icon = c.action === "skip" ? "⊘" : c.action === "watch" ? "○" : "→";
|
|
263
|
-
const actionLabel = c.action.toUpperCase().padEnd(7);
|
|
264
|
-
lines.push(` ${icon} ${c.skill.padEnd(20)} ${actionLabel} ${c.reason}`);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
return lines;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function formatEvolutionPhase(candidates: SkillAction[]): string[] {
|
|
271
|
-
const evolved = candidates.filter((c) => c.action === "evolve" && c.evolveResult !== undefined);
|
|
272
|
-
if (evolved.length === 0) return [];
|
|
273
|
-
|
|
274
|
-
const lines: string[] = ["Phase 4: Evolution Results"];
|
|
275
|
-
for (const c of evolved) {
|
|
276
|
-
const r = c.evolveResult as NonNullable<typeof c.evolveResult>;
|
|
277
|
-
const status = r.deployed ? "deployed" : "not deployed";
|
|
278
|
-
const detail = r.reason;
|
|
279
|
-
const validation = r.validation
|
|
280
|
-
? ` (${(r.validation.before_pass_rate * 100).toFixed(0)}% → ${(r.validation.after_pass_rate * 100).toFixed(0)}%)`
|
|
281
|
-
: "";
|
|
282
|
-
lines.push(` ${c.skill.padEnd(20)} ${status}${validation}`);
|
|
283
|
-
lines.push(` ${"".padEnd(20)} ${detail}`);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
return lines;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
function formatWatchPhase(candidates: SkillAction[]): string[] {
|
|
290
|
-
const watched = candidates.filter((c) => c.action === "watch");
|
|
291
|
-
if (watched.length === 0) return [];
|
|
292
|
-
|
|
293
|
-
const lines: string[] = ["Phase 5: Watch"];
|
|
294
|
-
for (const c of watched) {
|
|
295
|
-
const snap = c.watchResult?.snapshot;
|
|
296
|
-
const metrics = snap
|
|
297
|
-
? ` (pass_rate=${snap.pass_rate.toFixed(2)}, baseline=${snap.baseline_pass_rate.toFixed(2)})`
|
|
298
|
-
: "";
|
|
299
|
-
const alertTag = c.watchResult?.alert ? " [ALERT]" : "";
|
|
300
|
-
const rollbackTag = c.watchResult?.rolledBack ? " [ROLLED BACK]" : "";
|
|
301
|
-
lines.push(` ${c.skill.padEnd(20)} ${c.reason}${alertTag}${rollbackTag}${metrics}`);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
return lines;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
export function formatOrchestrateReport(result: OrchestrateResult): string {
|
|
308
|
-
const sep = "═".repeat(48);
|
|
309
|
-
const lines: string[] = [];
|
|
310
|
-
|
|
311
|
-
lines.push(sep);
|
|
312
|
-
lines.push("selftune orchestrate — decision report");
|
|
313
|
-
lines.push(sep);
|
|
314
|
-
lines.push("");
|
|
315
|
-
|
|
316
|
-
// Mode banner
|
|
317
|
-
if (result.summary.dryRun) {
|
|
318
|
-
lines.push("Mode: DRY RUN (no mutations applied)");
|
|
319
|
-
} else if (result.summary.approvalMode === "review") {
|
|
320
|
-
lines.push("Mode: REVIEW (proposals validated but not deployed)");
|
|
321
|
-
} else {
|
|
322
|
-
lines.push("Mode: AUTONOMOUS (validated changes deployed automatically)");
|
|
323
|
-
}
|
|
324
|
-
lines.push("");
|
|
325
|
-
|
|
326
|
-
// Phase 1: Sync
|
|
327
|
-
lines.push(...formatSyncPhase(result.syncResult));
|
|
328
|
-
lines.push("");
|
|
329
|
-
|
|
330
|
-
// Phase 2: Status
|
|
331
|
-
lines.push(...formatStatusPhase(result.statusResult));
|
|
332
|
-
lines.push("");
|
|
333
|
-
|
|
334
|
-
// Phase 3: Skill decisions
|
|
335
|
-
lines.push(...formatDecisionPhase(result.candidates));
|
|
336
|
-
lines.push("");
|
|
337
|
-
|
|
338
|
-
// Phase 4: Evolution results (only if any evolve ran)
|
|
339
|
-
const evoLines = formatEvolutionPhase(result.candidates);
|
|
340
|
-
if (evoLines.length > 0) {
|
|
341
|
-
lines.push(...evoLines);
|
|
342
|
-
lines.push("");
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// Phase 5: Watch (only if any watched)
|
|
346
|
-
const watchLines = formatWatchPhase(result.candidates);
|
|
347
|
-
if (watchLines.length > 0) {
|
|
348
|
-
lines.push(...watchLines);
|
|
349
|
-
lines.push("");
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// Final summary
|
|
353
|
-
lines.push("Summary");
|
|
354
|
-
lines.push(` Auto-graded: ${result.summary.autoGraded}`);
|
|
355
|
-
lines.push(` Evaluated: ${result.summary.evaluated} skills`);
|
|
356
|
-
lines.push(` Deployed: ${result.summary.deployed}`);
|
|
357
|
-
lines.push(` Watched: ${result.summary.watched}`);
|
|
358
|
-
lines.push(` Skipped: ${result.summary.skipped}`);
|
|
359
|
-
lines.push(` Elapsed: ${(result.summary.elapsedMs / 1000).toFixed(1)}s`);
|
|
360
|
-
|
|
361
|
-
if (result.summary.dryRun && result.summary.evaluated > 0) {
|
|
362
|
-
lines.push("");
|
|
363
|
-
lines.push(" Rerun without --dry-run to allow validated deployments.");
|
|
364
|
-
} else if (result.summary.approvalMode === "review" && result.summary.evaluated > 0) {
|
|
365
|
-
lines.push("");
|
|
366
|
-
lines.push(" Rerun without --review-required to allow validated deployments.");
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
return lines.join("\n");
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
/** Candidate selection criteria. */
|
|
373
|
-
const CANDIDATE_STATUSES = new Set(["CRITICAL", "WARNING", "UNGRADED"]);
|
|
374
|
-
|
|
375
|
-
/** Minimum skill_checks before autonomous evolution is allowed. */
|
|
376
|
-
export const MIN_CANDIDATE_EVIDENCE = 3;
|
|
377
|
-
|
|
378
|
-
/** Default cooldown hours after a deploy before re-evolving the same skill. */
|
|
379
|
-
export const DEFAULT_COOLDOWN_HOURS = 24;
|
|
380
|
-
|
|
381
137
|
type AutonomousEvolveDefaults = Pick<
|
|
382
138
|
EvolveOptions,
|
|
383
139
|
| "paretoEnabled"
|
|
@@ -405,15 +161,6 @@ const AUTONOMOUS_EVOLVE_DEFAULTS: AutonomousEvolveDefaults = {
|
|
|
405
161
|
proposalModel: "haiku",
|
|
406
162
|
};
|
|
407
163
|
|
|
408
|
-
function candidatePriority(skill: SkillStatus, signalCount = 0): number {
|
|
409
|
-
const statusWeight = skill.status === "CRITICAL" ? 300 : skill.status === "WARNING" ? 200 : 100;
|
|
410
|
-
const missedWeight = Math.min(skill.missedQueries, 50);
|
|
411
|
-
const passPenalty = skill.passRate === null ? 0 : Math.round((1 - skill.passRate) * 100);
|
|
412
|
-
const trendBoost = skill.trend === "down" ? 30 : 0;
|
|
413
|
-
const signalBoost = Math.min(signalCount * 150, 450);
|
|
414
|
-
return statusWeight + missedWeight + passPenalty + trendBoost + signalBoost;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
164
|
/**
|
|
418
165
|
* Injectable dependencies for orchestrate(). Pass overrides in tests.
|
|
419
166
|
*/
|
|
@@ -422,7 +169,7 @@ export interface OrchestrateDeps {
|
|
|
422
169
|
computeStatus?: typeof computeStatus;
|
|
423
170
|
evolve?: typeof import("./evolution/evolve.js").evolve;
|
|
424
171
|
watch?: typeof import("./monitoring/watch.js").watch;
|
|
425
|
-
detectAgent?: typeof
|
|
172
|
+
detectAgent?: typeof detectLlmAgent;
|
|
426
173
|
doctor?: typeof doctor;
|
|
427
174
|
readTelemetry?: () => SessionTelemetryRecord[];
|
|
428
175
|
readSkillRecords?: () => SkillUsageRecord[];
|
|
@@ -432,341 +179,9 @@ export interface OrchestrateDeps {
|
|
|
432
179
|
readGradingResults?: (skillName: string) => ReturnType<typeof readGradingResultsForSkill>;
|
|
433
180
|
readSignals?: () => ImprovementSignalRecord[];
|
|
434
181
|
readAlphaIdentity?: () => AlphaIdentity | null;
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
// Skill path resolution
|
|
439
|
-
// ---------------------------------------------------------------------------
|
|
440
|
-
|
|
441
|
-
function getSkillSearchDirs(): string[] {
|
|
442
|
-
const home = homedir();
|
|
443
|
-
const cwd = process.cwd();
|
|
444
|
-
return [
|
|
445
|
-
join(home, ".claude", "skills"),
|
|
446
|
-
join(home, ".agents", "skills"),
|
|
447
|
-
join(home, ".codex", "skills"),
|
|
448
|
-
...findRepositorySkillDirs(cwd),
|
|
449
|
-
...findRepositoryClaudeSkillDirs(cwd),
|
|
450
|
-
];
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
function defaultResolveSkillPath(skillName: string): string | undefined {
|
|
454
|
-
return findInstalledSkillPath(skillName, getSkillSearchDirs());
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
// ---------------------------------------------------------------------------
|
|
458
|
-
// Cross-skill eval set overlap detection (internal — exported for testing only)
|
|
459
|
-
// ---------------------------------------------------------------------------
|
|
460
|
-
|
|
461
|
-
/**
|
|
462
|
-
* Detects significant overlap between the positive eval sets of evolution
|
|
463
|
-
* candidates. When two skills share >30% of their positive queries, it
|
|
464
|
-
* suggests a routing boundary problem. Console-only — no persistence.
|
|
465
|
-
*
|
|
466
|
-
* @internal Exported solely for unit testing.
|
|
467
|
-
*/
|
|
468
|
-
export async function detectCrossSkillOverlap(
|
|
469
|
-
candidates: Array<{ skill: string }>,
|
|
470
|
-
skillRecords: SkillUsageRecord[],
|
|
471
|
-
queryRecords: QueryLogRecord[],
|
|
472
|
-
): Promise<
|
|
473
|
-
Array<{ skill_a: string; skill_b: string; overlap_pct: number; shared_queries: string[] }>
|
|
474
|
-
> {
|
|
475
|
-
if (candidates.length < 2) return [];
|
|
476
|
-
|
|
477
|
-
const { buildEvalSet } = await import("./eval/hooks-to-evals.js");
|
|
478
|
-
|
|
479
|
-
const evalSets = new Map<string, Set<string>>();
|
|
480
|
-
|
|
481
|
-
for (const c of candidates) {
|
|
482
|
-
const evalSet = buildEvalSet(skillRecords, queryRecords, c.skill);
|
|
483
|
-
const positives = new Set(
|
|
484
|
-
evalSet
|
|
485
|
-
.filter((e: { should_trigger: boolean }) => e.should_trigger)
|
|
486
|
-
.map((e: { query: string }) => e.query.toLowerCase()),
|
|
487
|
-
);
|
|
488
|
-
evalSets.set(c.skill, positives);
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
const overlaps: Array<{
|
|
492
|
-
skill_a: string;
|
|
493
|
-
skill_b: string;
|
|
494
|
-
overlap_pct: number;
|
|
495
|
-
shared_queries: string[];
|
|
496
|
-
}> = [];
|
|
497
|
-
const skillNames = [...evalSets.keys()];
|
|
498
|
-
|
|
499
|
-
for (let i = 0; i < skillNames.length; i++) {
|
|
500
|
-
for (let j = i + 1; j < skillNames.length; j++) {
|
|
501
|
-
const setA = evalSets.get(skillNames[i]);
|
|
502
|
-
const setB = evalSets.get(skillNames[j]);
|
|
503
|
-
if (!setA || !setB) continue;
|
|
504
|
-
|
|
505
|
-
if (setA.size === 0 || setB.size === 0) continue;
|
|
506
|
-
|
|
507
|
-
const shared: string[] = [];
|
|
508
|
-
for (const q of setA) {
|
|
509
|
-
if (setB.has(q)) shared.push(q);
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
const overlapPct = shared.length / Math.min(setA.size, setB.size);
|
|
513
|
-
|
|
514
|
-
if (overlapPct > 0.3) {
|
|
515
|
-
overlaps.push({
|
|
516
|
-
skill_a: skillNames[i],
|
|
517
|
-
skill_b: skillNames[j],
|
|
518
|
-
overlap_pct: overlapPct,
|
|
519
|
-
shared_queries: shared.slice(0, 10),
|
|
520
|
-
});
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
return overlaps;
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
// ---------------------------------------------------------------------------
|
|
529
|
-
// Candidate selection
|
|
530
|
-
// ---------------------------------------------------------------------------
|
|
531
|
-
|
|
532
|
-
/** Context for candidate selection beyond simple status checks. */
|
|
533
|
-
export interface CandidateContext {
|
|
534
|
-
skillFilter?: string;
|
|
535
|
-
maxSkills: number;
|
|
536
|
-
auditEntries?: EvolutionAuditEntry[];
|
|
537
|
-
/** Hours since last deploy before a skill can be re-evolved. */
|
|
538
|
-
cooldownHours?: number;
|
|
539
|
-
/** Skill name (lowercase) to improvement signal count. */
|
|
540
|
-
signaledSkills?: Map<string, number>;
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
export function selectCandidates(skills: SkillStatus[], options: CandidateContext): SkillAction[] {
|
|
544
|
-
const actions: SkillAction[] = [];
|
|
545
|
-
const orderedSkills = [...skills].sort((a, b) => {
|
|
546
|
-
const aSignals = options.signaledSkills?.get(a.name.toLowerCase()) ?? 0;
|
|
547
|
-
const bSignals = options.signaledSkills?.get(b.name.toLowerCase()) ?? 0;
|
|
548
|
-
return candidatePriority(b, bSignals) - candidatePriority(a, aSignals);
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
const cooldownHours = options.cooldownHours ?? DEFAULT_COOLDOWN_HOURS;
|
|
552
|
-
const recentlyDeployed = findRecentlyDeployedSkills(options.auditEntries ?? [], cooldownHours);
|
|
553
|
-
|
|
554
|
-
for (const skill of orderedSkills) {
|
|
555
|
-
const signalCount = options.signaledSkills?.get(skill.name.toLowerCase()) ?? 0;
|
|
556
|
-
|
|
557
|
-
// Apply skill filter
|
|
558
|
-
if (options.skillFilter && skill.name !== options.skillFilter) {
|
|
559
|
-
actions.push({
|
|
560
|
-
skill: skill.name,
|
|
561
|
-
action: "skip",
|
|
562
|
-
reason: `filtered out (--skill ${options.skillFilter})`,
|
|
563
|
-
});
|
|
564
|
-
continue;
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
// Check if skill is a candidate
|
|
568
|
-
if (!CANDIDATE_STATUSES.has(skill.status)) {
|
|
569
|
-
actions.push({
|
|
570
|
-
skill: skill.name,
|
|
571
|
-
action: "skip",
|
|
572
|
-
reason: `status=${skill.status} — no action needed`,
|
|
573
|
-
});
|
|
574
|
-
continue;
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
// Gate: cooldown — skip if this skill was deployed recently
|
|
578
|
-
if (recentlyDeployed.has(skill.name)) {
|
|
579
|
-
actions.push({
|
|
580
|
-
skill: skill.name,
|
|
581
|
-
action: "skip",
|
|
582
|
-
reason: `recently evolved (cooldown ${cooldownHours}h) — let it bake`,
|
|
583
|
-
});
|
|
584
|
-
continue;
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
// Gate: insufficient evidence — need enough data points for autonomous action
|
|
588
|
-
// Bypass if there are improvement signals for this skill
|
|
589
|
-
const skillChecks = skill.snapshot?.skill_checks ?? 0;
|
|
590
|
-
if (skillChecks < MIN_CANDIDATE_EVIDENCE && skill.status !== "UNGRADED" && signalCount === 0) {
|
|
591
|
-
actions.push({
|
|
592
|
-
skill: skill.name,
|
|
593
|
-
action: "skip",
|
|
594
|
-
reason: `insufficient evidence (${skillChecks}/${MIN_CANDIDATE_EVIDENCE} checks) — need more data`,
|
|
595
|
-
});
|
|
596
|
-
continue;
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
// UNGRADED: only evolve if there are missed queries (some signal)
|
|
600
|
-
// Bypass if there are improvement signals for this skill
|
|
601
|
-
if (skill.status === "UNGRADED" && skill.missedQueries === 0 && signalCount === 0) {
|
|
602
|
-
actions.push({
|
|
603
|
-
skill: skill.name,
|
|
604
|
-
action: "skip",
|
|
605
|
-
reason: "UNGRADED with 0 missed queries — insufficient signal",
|
|
606
|
-
});
|
|
607
|
-
continue;
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
// Gate: weak WARNING signal — skip if no missed queries and trend isn't declining
|
|
611
|
-
if (skill.status === "WARNING" && skill.missedQueries === 0 && skill.trend !== "down") {
|
|
612
|
-
actions.push({
|
|
613
|
-
skill: skill.name,
|
|
614
|
-
action: "skip",
|
|
615
|
-
reason: `WARNING but no missed queries and trend=${skill.trend} — weak signal`,
|
|
616
|
-
});
|
|
617
|
-
continue;
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
actions.push({
|
|
621
|
-
skill: skill.name,
|
|
622
|
-
action: "evolve",
|
|
623
|
-
reason: `status=${skill.status}, passRate=${skill.passRate !== null ? `${(skill.passRate * 100).toFixed(0)}%` : "—"}, missed=${skill.missedQueries}, trend=${skill.trend}`,
|
|
624
|
-
});
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
// Apply max-skills cap to evolve candidates only
|
|
628
|
-
let evolveCount = 0;
|
|
629
|
-
for (const action of actions) {
|
|
630
|
-
if (action.action === "evolve") {
|
|
631
|
-
evolveCount++;
|
|
632
|
-
if (evolveCount > options.maxSkills) {
|
|
633
|
-
action.action = "skip";
|
|
634
|
-
action.reason = `capped by --max-skills ${options.maxSkills}`;
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
return actions;
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
/**
|
|
643
|
-
* Find skills deployed within the given window.
|
|
644
|
-
* Used for both cooldown gating (don't re-evolve) and watch targeting
|
|
645
|
-
* (monitor recently deployed skills for regressions).
|
|
646
|
-
*/
|
|
647
|
-
function findRecentlyDeployedSkills(
|
|
648
|
-
auditEntries: EvolutionAuditEntry[],
|
|
649
|
-
windowHours: number,
|
|
650
|
-
): Set<string> {
|
|
651
|
-
const cutoffMs = Date.now() - windowHours * 60 * 60 * 1000;
|
|
652
|
-
const names = new Set<string>();
|
|
653
|
-
for (const entry of auditEntries) {
|
|
654
|
-
const deployedAtMs = Date.parse(entry.timestamp);
|
|
655
|
-
if (
|
|
656
|
-
entry.action === "deployed" &&
|
|
657
|
-
entry.skill_name &&
|
|
658
|
-
Number.isFinite(deployedAtMs) &&
|
|
659
|
-
deployedAtMs >= cutoffMs
|
|
660
|
-
) {
|
|
661
|
-
names.add(entry.skill_name);
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
return names;
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
// ---------------------------------------------------------------------------
|
|
668
|
-
// Auto-grade ungraded skills
|
|
669
|
-
// ---------------------------------------------------------------------------
|
|
670
|
-
|
|
671
|
-
/**
|
|
672
|
-
* Auto-grade the top ungraded skills that have some session data.
|
|
673
|
-
* Fail-open: individual grading errors are logged but never propagated.
|
|
674
|
-
*
|
|
675
|
-
* @returns Number of skills successfully graded.
|
|
676
|
-
*/
|
|
677
|
-
export async function autoGradeTopUngraded(
|
|
678
|
-
skills: SkillStatus[],
|
|
679
|
-
maxAutoGrade: number,
|
|
680
|
-
agent: string,
|
|
681
|
-
deps: {
|
|
682
|
-
readTelemetry: () => SessionTelemetryRecord[];
|
|
683
|
-
readSkillRecords: () => SkillUsageRecord[];
|
|
684
|
-
},
|
|
685
|
-
): Promise<number> {
|
|
686
|
-
// Filter: UNGRADED skills with some data (skill_checks > 0)
|
|
687
|
-
const ungradedWithData = skills
|
|
688
|
-
.filter((s) => s.status === "UNGRADED" && (s.snapshot?.skill_checks ?? 0) > 0)
|
|
689
|
-
.sort((a, b) => (b.snapshot?.skill_checks ?? 0) - (a.snapshot?.skill_checks ?? 0))
|
|
690
|
-
.slice(0, maxAutoGrade);
|
|
691
|
-
|
|
692
|
-
if (ungradedWithData.length === 0) return 0;
|
|
693
|
-
|
|
694
|
-
let graded = 0;
|
|
695
|
-
|
|
696
|
-
for (const skill of ungradedWithData) {
|
|
697
|
-
try {
|
|
698
|
-
const telemetry = deps.readTelemetry();
|
|
699
|
-
const skillUsage = deps.readSkillRecords();
|
|
700
|
-
|
|
701
|
-
// Resolve the latest session for this skill
|
|
702
|
-
const resolved = resolveLatestSessionForSkill(telemetry, skillUsage, skill.name);
|
|
703
|
-
if (!resolved) {
|
|
704
|
-
console.error(` [auto-grade] ${skill.name}: no session found, skipping`);
|
|
705
|
-
continue;
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
// Derive expectations from SKILL.md
|
|
709
|
-
const derived = deriveExpectationsFromSkill(skill.name);
|
|
710
|
-
let transcriptExcerpt = "(no transcript)";
|
|
711
|
-
if (resolved.transcriptPath) {
|
|
712
|
-
try {
|
|
713
|
-
transcriptExcerpt = readExcerpt(resolved.transcriptPath);
|
|
714
|
-
} catch {
|
|
715
|
-
transcriptExcerpt = "(no transcript)";
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
console.error(` [auto-grade] Grading "${skill.name}" (session ${resolved.sessionId})...`);
|
|
720
|
-
|
|
721
|
-
const result = await gradeSession({
|
|
722
|
-
expectations: derived.expectations,
|
|
723
|
-
telemetry: resolved.telemetry,
|
|
724
|
-
sessionId: resolved.sessionId,
|
|
725
|
-
skillName: skill.name,
|
|
726
|
-
transcriptExcerpt,
|
|
727
|
-
transcriptPath: resolved.transcriptPath,
|
|
728
|
-
agent,
|
|
729
|
-
});
|
|
730
|
-
|
|
731
|
-
// Persist to SQLite — only count as graded if DB write succeeds
|
|
732
|
-
let persisted = false;
|
|
733
|
-
try {
|
|
734
|
-
persisted = writeGradingResultToDb(result);
|
|
735
|
-
} catch {
|
|
736
|
-
persisted = false;
|
|
737
|
-
}
|
|
738
|
-
if (!persisted) {
|
|
739
|
-
console.error(` [auto-grade] ${skill.name}: graded but failed to persist result`);
|
|
740
|
-
continue;
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
// Persist to file (fail-open, supplementary)
|
|
744
|
-
try {
|
|
745
|
-
const basePath = buildDefaultGradingOutputPath(resolved.sessionId);
|
|
746
|
-
const safeName = skill.name.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
747
|
-
const outputPath = basePath.replace(/\.json$/, `_${safeName}.json`);
|
|
748
|
-
const outputDir = dirname(outputPath);
|
|
749
|
-
mkdirSync(outputDir, { recursive: true });
|
|
750
|
-
writeFileSync(outputPath, JSON.stringify(result, null, 2), "utf-8");
|
|
751
|
-
} catch {
|
|
752
|
-
// fail-open: DB is authoritative, file is supplementary
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
const passRate = result.summary.pass_rate;
|
|
756
|
-
console.error(
|
|
757
|
-
` [auto-grade] ${skill.name}: ${result.summary.passed}/${result.summary.total} passed (${Math.round(passRate * 100)}%)`,
|
|
758
|
-
);
|
|
759
|
-
graded++;
|
|
760
|
-
} catch (err) {
|
|
761
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
762
|
-
console.error(
|
|
763
|
-
` [auto-grade] ${skill.name}: error — ${msg}. Retry with: selftune grade ${skill.name}`,
|
|
764
|
-
);
|
|
765
|
-
// fail-open: continue to next skill
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
return graded;
|
|
182
|
+
discoverWorkflowSkillProposals?: typeof discoverWorkflowSkillProposals;
|
|
183
|
+
persistWorkflowSkillProposal?: typeof persistWorkflowSkillProposal;
|
|
184
|
+
buildReplayOptions?: typeof buildReplayValidationOptions;
|
|
770
185
|
}
|
|
771
186
|
|
|
772
187
|
// ---------------------------------------------------------------------------
|
|
@@ -777,6 +192,8 @@ export async function orchestrate(
|
|
|
777
192
|
options: OrchestrateOptions,
|
|
778
193
|
deps: OrchestrateDeps = {},
|
|
779
194
|
): Promise<OrchestrateResult> {
|
|
195
|
+
const startTime = Date.now();
|
|
196
|
+
|
|
780
197
|
if (!acquireLock()) {
|
|
781
198
|
// Another orchestrate run is in progress
|
|
782
199
|
console.error("[orchestrate] Another run is in progress (lock held). Exiting.");
|
|
@@ -789,6 +206,7 @@ export async function orchestrate(
|
|
|
789
206
|
codex: { available: false, scanned: 0, synced: 0, skipped: 0 },
|
|
790
207
|
opencode: { available: false, scanned: 0, synced: 0, skipped: 0 },
|
|
791
208
|
openclaw: { available: false, scanned: 0, synced: 0, skipped: 0 },
|
|
209
|
+
pi: { available: false, scanned: 0, synced: 0, skipped: 0 },
|
|
792
210
|
},
|
|
793
211
|
repair: {
|
|
794
212
|
ran: false,
|
|
@@ -796,6 +214,12 @@ export async function orchestrate(
|
|
|
796
214
|
repaired_records: 0,
|
|
797
215
|
codex_repaired_records: 0,
|
|
798
216
|
},
|
|
217
|
+
creator_contributions: {
|
|
218
|
+
ran: false,
|
|
219
|
+
eligible_skills: 0,
|
|
220
|
+
built_signals: 0,
|
|
221
|
+
staged_signals: 0,
|
|
222
|
+
},
|
|
799
223
|
timings: [],
|
|
800
224
|
total_elapsed_ms: 0,
|
|
801
225
|
},
|
|
@@ -807,6 +231,7 @@ export async function orchestrate(
|
|
|
807
231
|
system: { healthy: true, pass: 0, fail: 0, warn: 0 },
|
|
808
232
|
},
|
|
809
233
|
candidates: [],
|
|
234
|
+
workflowProposals: [],
|
|
810
235
|
summary: {
|
|
811
236
|
totalSkills: 0,
|
|
812
237
|
evaluated: 0,
|
|
@@ -815,6 +240,7 @@ export async function orchestrate(
|
|
|
815
240
|
watched: 0,
|
|
816
241
|
skipped: 0,
|
|
817
242
|
autoGraded: 0,
|
|
243
|
+
freshlyWatchedSkills: [],
|
|
818
244
|
dryRun: options.dryRun,
|
|
819
245
|
approvalMode: options.approvalMode,
|
|
820
246
|
elapsedMs: 0,
|
|
@@ -823,420 +249,103 @@ export async function orchestrate(
|
|
|
823
249
|
}
|
|
824
250
|
|
|
825
251
|
try {
|
|
826
|
-
const
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
const _detectAgent = deps.detectAgent ?? detectAgent;
|
|
831
|
-
const _doctor = deps.doctor ?? doctor;
|
|
832
|
-
const _readTelemetry =
|
|
833
|
-
deps.readTelemetry ??
|
|
834
|
-
(() => {
|
|
835
|
-
const db = getDb();
|
|
836
|
-
return querySessionTelemetry(db) as SessionTelemetryRecord[];
|
|
837
|
-
});
|
|
838
|
-
const _readSkillRecords =
|
|
839
|
-
deps.readSkillRecords ??
|
|
840
|
-
(() => {
|
|
841
|
-
const db = getDb();
|
|
842
|
-
return querySkillUsageRecords(db) as SkillUsageRecord[];
|
|
843
|
-
});
|
|
844
|
-
const _readQueryRecords =
|
|
845
|
-
deps.readQueryRecords ??
|
|
846
|
-
(() => {
|
|
847
|
-
const db = getDb();
|
|
848
|
-
return queryQueryLog(db) as QueryLogRecord[];
|
|
849
|
-
});
|
|
850
|
-
const _readAuditEntries =
|
|
851
|
-
deps.readAuditEntries ??
|
|
852
|
-
(() => {
|
|
853
|
-
const db = getDb();
|
|
854
|
-
return queryEvolutionAudit(db) as EvolutionAuditEntry[];
|
|
855
|
-
});
|
|
856
|
-
const _resolveSkillPath = deps.resolveSkillPath ?? defaultResolveSkillPath;
|
|
857
|
-
const _readGradingResults = deps.readGradingResults ?? readGradingResultsForSkill;
|
|
858
|
-
const _readAlphaIdentity =
|
|
859
|
-
deps.readAlphaIdentity ?? (() => readAlphaIdentity(SELFTUNE_CONFIG_PATH));
|
|
860
|
-
|
|
861
|
-
// Lazy-load evolve and watch to avoid circular imports
|
|
862
|
-
const _evolve = deps.evolve ?? (await import("./evolution/evolve.js")).evolve;
|
|
863
|
-
const _watch = deps.watch ?? (await import("./monitoring/watch.js")).watch;
|
|
864
|
-
|
|
865
|
-
// -------------------------------------------------------------------------
|
|
866
|
-
// Step 1: Sync source-truth telemetry (mandatory)
|
|
867
|
-
// -------------------------------------------------------------------------
|
|
868
|
-
console.error("[orchestrate] Syncing source-truth telemetry...");
|
|
869
|
-
const syncResult = _syncSources(createDefaultSyncOptions({ force: options.syncForce }));
|
|
870
|
-
const sourceSynced = Object.values(syncResult.sources).reduce((sum, s) => sum + s.synced, 0);
|
|
871
|
-
console.error(
|
|
872
|
-
`[orchestrate] Sync complete: ${sourceSynced} sessions synced, ${syncResult.repair.repaired_records} repaired`,
|
|
873
|
-
);
|
|
874
|
-
|
|
875
|
-
// -------------------------------------------------------------------------
|
|
876
|
-
// Step 2: Compute status
|
|
877
|
-
// -------------------------------------------------------------------------
|
|
878
|
-
console.error("[orchestrate] Computing skill status...");
|
|
879
|
-
const telemetry = _readTelemetry();
|
|
880
|
-
const skillRecords = _readSkillRecords();
|
|
881
|
-
const queryRecords = _readQueryRecords();
|
|
882
|
-
const auditEntries = _readAuditEntries();
|
|
883
|
-
const doctorResult = await _doctor();
|
|
884
|
-
|
|
885
|
-
let statusResult = _computeStatus(
|
|
252
|
+
const runtime = await resolveOrchestrateRuntime(deps);
|
|
253
|
+
const {
|
|
254
|
+
syncResult,
|
|
255
|
+
statusResult,
|
|
886
256
|
telemetry,
|
|
887
257
|
skillRecords,
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
);
|
|
258
|
+
pendingSignals,
|
|
259
|
+
candidates,
|
|
260
|
+
evolveCandidates,
|
|
261
|
+
agent,
|
|
262
|
+
autoGradedCount,
|
|
263
|
+
} = await prepareOrchestrateRun(options, runtime);
|
|
895
264
|
|
|
896
265
|
// -------------------------------------------------------------------------
|
|
897
|
-
// Step
|
|
266
|
+
// Step 5: Evolve candidates
|
|
898
267
|
// -------------------------------------------------------------------------
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
if (gradeAgent) {
|
|
910
|
-
console.error(
|
|
911
|
-
`[orchestrate] Auto-grading ${Math.min(ungradedWithData.length, options.maxAutoGrade)} ungraded skill(s)...`,
|
|
912
|
-
);
|
|
913
|
-
autoGradedCount = await autoGradeTopUngraded(
|
|
914
|
-
scopedSkills,
|
|
915
|
-
options.maxAutoGrade,
|
|
916
|
-
gradeAgent,
|
|
917
|
-
{ readTelemetry: _readTelemetry, readSkillRecords: _readSkillRecords },
|
|
918
|
-
);
|
|
919
|
-
|
|
920
|
-
if (autoGradedCount > 0) {
|
|
921
|
-
// Recompute status so candidate selection sees updated grades
|
|
922
|
-
console.error(
|
|
923
|
-
`[orchestrate] Recomputing status after grading ${autoGradedCount} skill(s)...`,
|
|
924
|
-
);
|
|
925
|
-
try {
|
|
926
|
-
const freshTelemetry = _readTelemetry();
|
|
927
|
-
const freshSkillRecords = _readSkillRecords();
|
|
928
|
-
const freshQueryRecords = _readQueryRecords();
|
|
929
|
-
const freshAudit = _readAuditEntries();
|
|
930
|
-
const freshDoctor = doctorResult; // reuse — environment unchanged during grading
|
|
931
|
-
statusResult = _computeStatus(
|
|
932
|
-
freshTelemetry,
|
|
933
|
-
freshSkillRecords,
|
|
934
|
-
freshQueryRecords,
|
|
935
|
-
freshAudit,
|
|
936
|
-
freshDoctor,
|
|
937
|
-
);
|
|
938
|
-
} catch (recomputeErr) {
|
|
939
|
-
console.error(
|
|
940
|
-
`[orchestrate] Warning: failed to recompute status after grading — using pre-grade status. ${recomputeErr instanceof Error ? recomputeErr.message : String(recomputeErr)}`,
|
|
941
|
-
);
|
|
942
|
-
}
|
|
943
|
-
}
|
|
944
|
-
} else {
|
|
945
|
-
console.error(
|
|
946
|
-
"[orchestrate] No agent CLI found — skipping auto-grade. To disable, rerun with: selftune orchestrate --max-auto-grade 0",
|
|
947
|
-
);
|
|
948
|
-
}
|
|
949
|
-
}
|
|
268
|
+
const freshlyDeployedInThisRun = await runEvolutionPhase({
|
|
269
|
+
evolveCandidates,
|
|
270
|
+
agent,
|
|
271
|
+
options,
|
|
272
|
+
resolveSkillPath: runtime.resolveSkillPath,
|
|
273
|
+
readGradingResults: runtime.readGradingResults,
|
|
274
|
+
evolve: runtime.evolve,
|
|
275
|
+
buildReplayOptions: runtime.buildReplayOptions,
|
|
276
|
+
evolveDefaults: AUTONOMOUS_EVOLVE_DEFAULTS,
|
|
277
|
+
});
|
|
950
278
|
|
|
951
279
|
// -------------------------------------------------------------------------
|
|
952
|
-
// Step
|
|
280
|
+
// Step 5b: Auto-grade & write baselines for freshly deployed skills
|
|
953
281
|
// -------------------------------------------------------------------------
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
282
|
+
await autoGradeFreshDeploys({
|
|
283
|
+
freshlyDeployedCandidates: freshlyDeployedInThisRun,
|
|
284
|
+
dryRun: options.dryRun,
|
|
285
|
+
agent,
|
|
286
|
+
detectAgent: runtime.detectAgent,
|
|
287
|
+
readTelemetry: runtime.readTelemetry,
|
|
288
|
+
readSkillRecords: runtime.readSkillRecords,
|
|
289
|
+
});
|
|
961
290
|
|
|
962
291
|
// -------------------------------------------------------------------------
|
|
963
|
-
// Step
|
|
292
|
+
// Step 6: Watch recently evolved skills (including freshly deployed in this run)
|
|
964
293
|
// -------------------------------------------------------------------------
|
|
965
|
-
const
|
|
294
|
+
const { freshAuditEntries, freshlyWatchedSkills } = await watchRecentDeploys({
|
|
295
|
+
candidates,
|
|
296
|
+
freshlyDeployedCandidates: freshlyDeployedInThisRun,
|
|
966
297
|
skillFilter: options.skillFilter,
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
298
|
+
recentWindowHours: options.recentWindowHours,
|
|
299
|
+
readAuditEntries: runtime.readAuditEntries,
|
|
300
|
+
resolveSkillPath: runtime.resolveSkillPath,
|
|
301
|
+
watch: runtime.watch,
|
|
970
302
|
});
|
|
971
303
|
|
|
972
|
-
const evolveCandidates = candidates.filter((c) => c.action === "evolve");
|
|
973
|
-
const skipCount = candidates.filter((c) => c.action === "skip").length;
|
|
974
|
-
console.error(
|
|
975
|
-
`[orchestrate] Candidates: ${evolveCandidates.length} to evolve, ${skipCount} skipped`,
|
|
976
|
-
);
|
|
977
|
-
|
|
978
|
-
// Log each decision
|
|
979
|
-
for (const c of candidates) {
|
|
980
|
-
console.error(` ${c.action === "skip" ? "⊘" : "→"} ${c.skill}: ${c.reason}`);
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
// Cross-skill overlap detection (console-only, non-critical)
|
|
984
|
-
if (evolveCandidates.length >= 2) {
|
|
985
|
-
try {
|
|
986
|
-
const overlap = await detectCrossSkillOverlap(evolveCandidates, skillRecords, queryRecords);
|
|
987
|
-
if (overlap.length > 0) {
|
|
988
|
-
console.error("\n[orchestrate] Cross-skill eval overlap detected:");
|
|
989
|
-
for (const o of overlap) {
|
|
990
|
-
console.error(
|
|
991
|
-
` ⚠ ${o.skill_a} ↔ ${o.skill_b}: ${(o.overlap_pct * 100).toFixed(0)}% shared queries (${o.shared_queries.length} queries)`,
|
|
992
|
-
);
|
|
993
|
-
}
|
|
994
|
-
console.error("");
|
|
995
|
-
}
|
|
996
|
-
} catch {
|
|
997
|
-
// fail-open: overlap detection is non-critical
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
304
|
// -------------------------------------------------------------------------
|
|
1002
|
-
// Step
|
|
305
|
+
// Step 6b: Generate workflow-skill proposals from strong telemetry patterns
|
|
1003
306
|
// -------------------------------------------------------------------------
|
|
1004
|
-
const
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
}
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
// -------------------------------------------------------------------------
|
|
1014
|
-
// Step 5: Evolve candidates
|
|
1015
|
-
// -------------------------------------------------------------------------
|
|
1016
|
-
for (const candidate of evolveCandidates) {
|
|
1017
|
-
// Skip if agent detection marked this candidate as skip
|
|
1018
|
-
if (candidate.action === "skip") continue;
|
|
1019
|
-
|
|
1020
|
-
const skillPath = _resolveSkillPath(candidate.skill);
|
|
1021
|
-
if (!skillPath) {
|
|
1022
|
-
candidate.action = "skip";
|
|
1023
|
-
candidate.reason = `SKILL.md not found for "${candidate.skill}"`;
|
|
1024
|
-
console.error(` ⊘ ${candidate.skill}: ${candidate.reason}`);
|
|
1025
|
-
continue;
|
|
1026
|
-
}
|
|
307
|
+
const workflowProposals = runtime.discoverWorkflowSkillProposals(telemetry, skillRecords, {
|
|
308
|
+
cwd: process.cwd(),
|
|
309
|
+
skillFilter: options.skillFilter,
|
|
310
|
+
resolveSkillPath: runtime.resolveSkillPath,
|
|
311
|
+
existingAuditEntries: freshAuditEntries,
|
|
312
|
+
});
|
|
1027
313
|
|
|
1028
|
-
|
|
314
|
+
if (workflowProposals.length > 0) {
|
|
1029
315
|
console.error(
|
|
1030
|
-
`[orchestrate]
|
|
316
|
+
`[orchestrate] Workflow skill proposals: ${workflowProposals.length}${options.dryRun ? " (dry-run)" : ""}`,
|
|
1031
317
|
);
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
dryRun: effectiveDryRun,
|
|
1039
|
-
confidenceThreshold: 0.6,
|
|
1040
|
-
maxIterations: 3,
|
|
1041
|
-
gradingResults: _readGradingResults(candidate.skill),
|
|
1042
|
-
syncFirst: false, // We already synced
|
|
1043
|
-
...AUTONOMOUS_EVOLVE_DEFAULTS,
|
|
1044
|
-
});
|
|
1045
|
-
|
|
1046
|
-
candidate.evolveResult = evolveResult;
|
|
1047
|
-
|
|
1048
|
-
if (evolveResult.deployed) {
|
|
1049
|
-
console.error(` ✓ ${candidate.skill}: deployed (${evolveResult.reason})`);
|
|
1050
|
-
} else {
|
|
1051
|
-
console.error(` ✗ ${candidate.skill}: not deployed (${evolveResult.reason})`);
|
|
318
|
+
for (const proposal of workflowProposals) {
|
|
319
|
+
console.error(` + ${proposal.draft.skill_name}: ${proposal.summary}`);
|
|
320
|
+
if (!options.dryRun) {
|
|
321
|
+
runtime.persistWorkflowSkillProposal(proposal, {
|
|
322
|
+
sourceSkillPath: runtime.resolveSkillPath(proposal.source_skill_name),
|
|
323
|
+
});
|
|
1052
324
|
}
|
|
1053
|
-
} catch (err) {
|
|
1054
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1055
|
-
candidate.action = "skip";
|
|
1056
|
-
candidate.reason = `evolve error: ${msg}`;
|
|
1057
|
-
console.error(` ✗ ${candidate.skill}: error — ${msg}`);
|
|
1058
|
-
}
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
// -------------------------------------------------------------------------
|
|
1062
|
-
// Step 6: Watch recently evolved skills
|
|
1063
|
-
// -------------------------------------------------------------------------
|
|
1064
|
-
// Re-read audit entries to capture any newly-deployed entries from the evolve loop above.
|
|
1065
|
-
// evolve() writes audit entries synchronously, so a fresh read is needed.
|
|
1066
|
-
const freshAuditEntries = _readAuditEntries();
|
|
1067
|
-
const recentlyEvolved = findRecentlyDeployedSkills(
|
|
1068
|
-
freshAuditEntries,
|
|
1069
|
-
options.recentWindowHours,
|
|
1070
|
-
);
|
|
1071
|
-
|
|
1072
|
-
// O(1) lookup for skills already processed as evolve candidates
|
|
1073
|
-
const evolvedSkillNames = new Set(
|
|
1074
|
-
candidates.filter((c) => c.action === "evolve").map((c) => c.skill),
|
|
1075
|
-
);
|
|
1076
|
-
|
|
1077
|
-
for (const skillName of recentlyEvolved) {
|
|
1078
|
-
// Skip if already processed in this run as evolve candidate
|
|
1079
|
-
if (evolvedSkillNames.has(skillName)) {
|
|
1080
|
-
continue;
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
// Apply skill filter
|
|
1084
|
-
if (options.skillFilter && skillName !== options.skillFilter) continue;
|
|
1085
|
-
|
|
1086
|
-
const skillPath = _resolveSkillPath(skillName);
|
|
1087
|
-
if (!skillPath) continue;
|
|
1088
|
-
|
|
1089
|
-
console.error(`[orchestrate] Watching "${skillName}" (recently evolved)...`);
|
|
1090
|
-
|
|
1091
|
-
try {
|
|
1092
|
-
const watchResult = await _watch({
|
|
1093
|
-
skillName,
|
|
1094
|
-
skillPath,
|
|
1095
|
-
windowSessions: 20,
|
|
1096
|
-
regressionThreshold: 0.1,
|
|
1097
|
-
autoRollback: true,
|
|
1098
|
-
syncFirst: false,
|
|
1099
|
-
});
|
|
1100
|
-
|
|
1101
|
-
candidates.push({
|
|
1102
|
-
skill: skillName,
|
|
1103
|
-
action: "watch",
|
|
1104
|
-
reason: watchResult.alert ?? "stable",
|
|
1105
|
-
watchResult,
|
|
1106
|
-
});
|
|
1107
|
-
|
|
1108
|
-
console.error(
|
|
1109
|
-
` ${watchResult.alert ? "⚠" : "✓"} ${skillName}: ${watchResult.recommendation}`,
|
|
1110
|
-
);
|
|
1111
|
-
} catch (err) {
|
|
1112
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1113
|
-
console.error(` ✗ ${skillName}: watch error — ${msg}`);
|
|
1114
325
|
}
|
|
1115
326
|
}
|
|
1116
327
|
|
|
1117
328
|
// -------------------------------------------------------------------------
|
|
1118
329
|
// Step 7: Build summary (single source of truth for both CLI and dashboard)
|
|
1119
330
|
// -------------------------------------------------------------------------
|
|
1120
|
-
const
|
|
1121
|
-
totalSkills: statusResult.skills.length,
|
|
1122
|
-
evaluated: candidates.filter((c) => c.action === "evolve").length,
|
|
1123
|
-
evolved: candidates.filter((c) => c.action === "evolve" && c.evolveResult !== undefined)
|
|
1124
|
-
.length,
|
|
1125
|
-
deployed: candidates.filter((c) => c.evolveResult?.deployed).length,
|
|
1126
|
-
watched: candidates.filter((c) => c.action === "watch").length,
|
|
1127
|
-
skipped: candidates.filter((c) => c.action === "skip").length,
|
|
1128
|
-
autoGraded: autoGradedCount,
|
|
1129
|
-
};
|
|
1130
|
-
|
|
1131
|
-
const result: OrchestrateResult = {
|
|
331
|
+
const result = finalizeOrchestrateRun({
|
|
1132
332
|
syncResult,
|
|
1133
333
|
statusResult,
|
|
1134
334
|
candidates,
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
// -------------------------------------------------------------------------
|
|
1144
|
-
// Step 7b: Mark consumed signals
|
|
1145
|
-
// -------------------------------------------------------------------------
|
|
1146
|
-
const runId = `run_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
1147
|
-
if (pendingSignals.length > 0) {
|
|
1148
|
-
markSignalsConsumed(pendingSignals, runId);
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
// -------------------------------------------------------------------------
|
|
1152
|
-
// Step 8: Persist run report
|
|
1153
|
-
// -------------------------------------------------------------------------
|
|
1154
|
-
const runReport: OrchestrateRunReport = {
|
|
1155
|
-
run_id: runId,
|
|
1156
|
-
timestamp: new Date().toISOString(),
|
|
1157
|
-
elapsed_ms: result.summary.elapsedMs,
|
|
1158
|
-
dry_run: result.summary.dryRun,
|
|
1159
|
-
approval_mode: result.summary.approvalMode,
|
|
1160
|
-
total_skills: finalTotals.totalSkills,
|
|
1161
|
-
evaluated: finalTotals.evaluated,
|
|
1162
|
-
evolved: finalTotals.evolved,
|
|
1163
|
-
deployed: finalTotals.deployed,
|
|
1164
|
-
watched: finalTotals.watched,
|
|
1165
|
-
skipped: finalTotals.skipped,
|
|
1166
|
-
auto_graded: finalTotals.autoGraded,
|
|
1167
|
-
skill_actions: candidates.map(
|
|
1168
|
-
(c): OrchestrateRunSkillAction => ({
|
|
1169
|
-
skill: c.skill,
|
|
1170
|
-
action: c.action,
|
|
1171
|
-
reason: c.reason,
|
|
1172
|
-
deployed: c.evolveResult?.deployed,
|
|
1173
|
-
rolledBack: c.watchResult?.rolledBack,
|
|
1174
|
-
alert: c.watchResult?.alert,
|
|
1175
|
-
elapsed_ms: c.evolveResult?.elapsedMs,
|
|
1176
|
-
llm_calls: c.evolveResult?.llmCallCount,
|
|
1177
|
-
}),
|
|
1178
|
-
),
|
|
1179
|
-
};
|
|
1180
|
-
|
|
1181
|
-
try {
|
|
1182
|
-
writeOrchestrateRunToDb(runReport);
|
|
1183
|
-
} catch {
|
|
1184
|
-
/* fail-open */
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
// Also log to unified cron_runs timeline
|
|
1188
|
-
const totalLlmCalls = candidates.reduce(
|
|
1189
|
-
(sum, c) => sum + (c.evolveResult?.llmCallCount ?? 0),
|
|
1190
|
-
0,
|
|
1191
|
-
);
|
|
1192
|
-
try {
|
|
1193
|
-
writeCronRunToDb(getDb(), {
|
|
1194
|
-
jobName: "orchestrate",
|
|
1195
|
-
startedAt: runReport.timestamp,
|
|
1196
|
-
elapsedMs: runReport.elapsed_ms,
|
|
1197
|
-
status: "success",
|
|
1198
|
-
metrics: {
|
|
1199
|
-
total_skills: finalTotals.totalSkills,
|
|
1200
|
-
evaluated: finalTotals.evaluated,
|
|
1201
|
-
evolved: finalTotals.evolved,
|
|
1202
|
-
deployed: finalTotals.deployed,
|
|
1203
|
-
watched: finalTotals.watched,
|
|
1204
|
-
skipped: finalTotals.skipped,
|
|
1205
|
-
dry_run: result.summary.dryRun,
|
|
1206
|
-
total_llm_calls: totalLlmCalls,
|
|
1207
|
-
auto_graded: finalTotals.autoGraded,
|
|
1208
|
-
},
|
|
1209
|
-
});
|
|
1210
|
-
} catch {
|
|
1211
|
-
/* fail-open */
|
|
1212
|
-
}
|
|
335
|
+
workflowProposals,
|
|
336
|
+
dryRun: options.dryRun,
|
|
337
|
+
approvalMode: options.approvalMode,
|
|
338
|
+
autoGradedCount,
|
|
339
|
+
freshlyWatchedSkills,
|
|
340
|
+
pendingSignals,
|
|
341
|
+
elapsedMs: Date.now() - startTime,
|
|
342
|
+
});
|
|
1213
343
|
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
try {
|
|
1220
|
-
console.error("[orchestrate] Running alpha upload cycle...");
|
|
1221
|
-
const { runUploadCycle } = await import("./alpha-upload/index.js");
|
|
1222
|
-
const db = getDb();
|
|
1223
|
-
const uploadSummary = await runUploadCycle(db, {
|
|
1224
|
-
enrolled: true,
|
|
1225
|
-
userId: alphaIdentity.user_id,
|
|
1226
|
-
agentType: readConfiguredAgentType(SELFTUNE_CONFIG_PATH, "unknown"),
|
|
1227
|
-
selftuneVersion: getSelftuneVersion(),
|
|
1228
|
-
dryRun: options.dryRun,
|
|
1229
|
-
apiKey: alphaIdentity.api_key,
|
|
1230
|
-
});
|
|
1231
|
-
result.uploadSummary = uploadSummary;
|
|
1232
|
-
console.error(
|
|
1233
|
-
`[orchestrate] Alpha upload: prepared=${uploadSummary.prepared}, sent=${uploadSummary.sent}, failed=${uploadSummary.failed}, skipped=${uploadSummary.skipped}`,
|
|
1234
|
-
);
|
|
1235
|
-
} catch (err) {
|
|
1236
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1237
|
-
console.error(`[orchestrate] Alpha upload failed (non-blocking): ${msg}`);
|
|
1238
|
-
}
|
|
1239
|
-
}
|
|
344
|
+
await runPostOrchestrateSideEffects({
|
|
345
|
+
result,
|
|
346
|
+
dryRun: options.dryRun,
|
|
347
|
+
readAlphaIdentity: runtime.readAlphaIdentity,
|
|
348
|
+
});
|
|
1240
349
|
|
|
1241
350
|
return result;
|
|
1242
351
|
} catch (err) {
|
|
@@ -1264,113 +373,18 @@ export async function orchestrate(
|
|
|
1264
373
|
// ---------------------------------------------------------------------------
|
|
1265
374
|
|
|
1266
375
|
export async function cliMain(): Promise<void> {
|
|
1267
|
-
const
|
|
1268
|
-
options: {
|
|
1269
|
-
"dry-run": { type: "boolean", default: false },
|
|
1270
|
-
"review-required": { type: "boolean", default: false },
|
|
1271
|
-
"auto-approve": { type: "boolean", default: false },
|
|
1272
|
-
skill: { type: "string" },
|
|
1273
|
-
"max-skills": { type: "string", default: "5" },
|
|
1274
|
-
"recent-window": { type: "string", default: "48" },
|
|
1275
|
-
"sync-force": { type: "boolean", default: false },
|
|
1276
|
-
"max-auto-grade": { type: "string", default: "5" },
|
|
1277
|
-
loop: { type: "boolean", default: false },
|
|
1278
|
-
"loop-interval": { type: "string", default: "3600" },
|
|
1279
|
-
help: { type: "boolean", short: "h", default: false },
|
|
1280
|
-
},
|
|
1281
|
-
strict: true,
|
|
1282
|
-
});
|
|
1283
|
-
|
|
1284
|
-
if (values.help) {
|
|
1285
|
-
console.log(`selftune orchestrate — Autonomous core loop
|
|
1286
|
-
|
|
1287
|
-
Runs the full improvement cycle: sync → status → auto-grade → evolve → watch.
|
|
1288
|
-
|
|
1289
|
-
Usage:
|
|
1290
|
-
selftune orchestrate [options]
|
|
1291
|
-
|
|
1292
|
-
Options:
|
|
1293
|
-
--dry-run Preview actions without mutations
|
|
1294
|
-
--review-required Validate candidates but require human review before deploy
|
|
1295
|
-
--auto-approve Deprecated alias; autonomous mode is now the default
|
|
1296
|
-
--skill <name> Scope to a single skill
|
|
1297
|
-
--max-skills <n> Cap skills processed per run (default: 5)
|
|
1298
|
-
--recent-window <hrs> Hours to look back for watch targets (default: 48)
|
|
1299
|
-
--sync-force Force full rescan during sync
|
|
1300
|
-
--max-auto-grade <n> Max ungraded skills to auto-grade per run (default: 5, 0 to disable)
|
|
1301
|
-
--loop Run in continuous loop mode (never stops)
|
|
1302
|
-
--loop-interval <s> Seconds between iterations (default: 3600, min: 60)
|
|
1303
|
-
-h, --help Show this help message
|
|
1304
|
-
|
|
1305
|
-
Safety:
|
|
1306
|
-
By default, low-risk description evolution runs autonomously after
|
|
1307
|
-
validation. Use --review-required to keep a human in the loop, or
|
|
1308
|
-
--dry-run to preview the whole loop without mutations. Every deploy
|
|
1309
|
-
still passes validation gates first.
|
|
376
|
+
const cli = parseOrchestrateCliArgs();
|
|
1310
377
|
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
selftune orchestrate --review-required # validate but do not deploy
|
|
1314
|
-
selftune orchestrate --dry-run # preview only
|
|
1315
|
-
selftune orchestrate --skill Research # single skill
|
|
1316
|
-
selftune orchestrate --max-skills 3 # limit scope
|
|
1317
|
-
selftune orchestrate --loop # continuous loop (hourly)
|
|
1318
|
-
selftune orchestrate --loop --loop-interval 600 # every 10 minutes`);
|
|
378
|
+
if (cli.showHelp) {
|
|
379
|
+
console.log(renderOrchestrateHelp());
|
|
1319
380
|
process.exit(0);
|
|
1320
381
|
}
|
|
1321
382
|
|
|
1322
|
-
const
|
|
1323
|
-
|
|
1324
|
-
throw new CLIError(
|
|
1325
|
-
"--max-skills must be a positive integer",
|
|
1326
|
-
"INVALID_FLAG",
|
|
1327
|
-
"selftune orchestrate --max-skills 5",
|
|
1328
|
-
);
|
|
383
|
+
for (const warning of cli.warnings) {
|
|
384
|
+
console.error(warning);
|
|
1329
385
|
}
|
|
1330
|
-
const maxSkills = Number(maxSkillsRaw);
|
|
1331
386
|
|
|
1332
|
-
const
|
|
1333
|
-
if (!/^\d+$/.test(recentWindowRaw) || Number(recentWindowRaw) < 1) {
|
|
1334
|
-
throw new CLIError(
|
|
1335
|
-
"--recent-window must be a positive integer",
|
|
1336
|
-
"INVALID_FLAG",
|
|
1337
|
-
"selftune orchestrate --recent-window 48",
|
|
1338
|
-
);
|
|
1339
|
-
}
|
|
1340
|
-
const recentWindow = Number(recentWindowRaw);
|
|
1341
|
-
|
|
1342
|
-
const maxAutoGradeRaw = values["max-auto-grade"] ?? "5";
|
|
1343
|
-
if (!/^\d+$/.test(maxAutoGradeRaw)) {
|
|
1344
|
-
throw new CLIError(
|
|
1345
|
-
"--max-auto-grade must be a non-negative integer",
|
|
1346
|
-
"INVALID_FLAG",
|
|
1347
|
-
"selftune orchestrate --max-auto-grade 5",
|
|
1348
|
-
);
|
|
1349
|
-
}
|
|
1350
|
-
const maxAutoGrade = Number(maxAutoGradeRaw);
|
|
1351
|
-
|
|
1352
|
-
const loopIntervalRaw = values["loop-interval"] ?? "3600";
|
|
1353
|
-
if (!/^\d+$/.test(loopIntervalRaw) || (values.loop && Number(loopIntervalRaw) < 60)) {
|
|
1354
|
-
throw new CLIError(
|
|
1355
|
-
"--loop-interval must be an integer >= 60 (seconds)",
|
|
1356
|
-
"INVALID_FLAG",
|
|
1357
|
-
"selftune orchestrate --loop --loop-interval 3600",
|
|
1358
|
-
);
|
|
1359
|
-
}
|
|
1360
|
-
const loopInterval = Number(loopIntervalRaw);
|
|
1361
|
-
|
|
1362
|
-
const autoApprove = values["auto-approve"] ?? false;
|
|
1363
|
-
if (autoApprove) {
|
|
1364
|
-
console.error(
|
|
1365
|
-
"[orchestrate] --auto-approve is deprecated; autonomous mode is now the default.",
|
|
1366
|
-
);
|
|
1367
|
-
}
|
|
1368
|
-
|
|
1369
|
-
const reviewRequired = values["review-required"] ?? false;
|
|
1370
|
-
const dryRun = values["dry-run"] ?? false;
|
|
1371
|
-
const approvalMode: "auto" | "review" = reviewRequired ? "review" : "auto";
|
|
1372
|
-
|
|
1373
|
-
const isLoop = values.loop ?? false;
|
|
387
|
+
const isLoop = cli.loop;
|
|
1374
388
|
let stopRequested = false;
|
|
1375
389
|
let sleepTimer: ReturnType<typeof setTimeout> | null = null;
|
|
1376
390
|
let sleepResolve: (() => void) | null = null;
|
|
@@ -1400,54 +414,17 @@ Examples:
|
|
|
1400
414
|
}
|
|
1401
415
|
|
|
1402
416
|
const result = await orchestrate({
|
|
1403
|
-
|
|
1404
|
-
approvalMode,
|
|
1405
|
-
skillFilter: values.skill,
|
|
1406
|
-
maxSkills,
|
|
1407
|
-
recentWindowHours: recentWindow,
|
|
1408
|
-
syncForce: values["sync-force"] ?? false,
|
|
1409
|
-
maxAutoGrade,
|
|
417
|
+
...cli.runOptions,
|
|
1410
418
|
});
|
|
1411
419
|
|
|
1412
|
-
|
|
1413
|
-
const jsonOutput = {
|
|
1414
|
-
...result.summary,
|
|
1415
|
-
...(result.uploadSummary ? { upload: result.uploadSummary } : {}),
|
|
1416
|
-
decisions: result.candidates.map((c) => ({
|
|
1417
|
-
skill: c.skill,
|
|
1418
|
-
action: c.action,
|
|
1419
|
-
reason: c.reason,
|
|
1420
|
-
...(c.evolveResult
|
|
1421
|
-
? {
|
|
1422
|
-
deployed: c.evolveResult.deployed,
|
|
1423
|
-
evolveReason: c.evolveResult.reason,
|
|
1424
|
-
validation: c.evolveResult.validation
|
|
1425
|
-
? {
|
|
1426
|
-
before: c.evolveResult.validation.before_pass_rate,
|
|
1427
|
-
after: c.evolveResult.validation.after_pass_rate,
|
|
1428
|
-
improved: c.evolveResult.validation.improved,
|
|
1429
|
-
}
|
|
1430
|
-
: null,
|
|
1431
|
-
}
|
|
1432
|
-
: {}),
|
|
1433
|
-
...(c.watchResult
|
|
1434
|
-
? {
|
|
1435
|
-
alert: c.watchResult.alert,
|
|
1436
|
-
rolledBack: c.watchResult.rolledBack,
|
|
1437
|
-
passRate: c.watchResult.snapshot?.pass_rate ?? null,
|
|
1438
|
-
recommendation: c.watchResult.recommendation,
|
|
1439
|
-
}
|
|
1440
|
-
: {}),
|
|
1441
|
-
})),
|
|
1442
|
-
};
|
|
1443
|
-
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
420
|
+
console.log(JSON.stringify(buildOrchestrateJsonOutput(result), null, 2));
|
|
1444
421
|
|
|
1445
422
|
// Print human-readable decision report to stderr
|
|
1446
423
|
console.error(`\n${formatOrchestrateReport(result)}`);
|
|
1447
424
|
|
|
1448
425
|
if (!isLoop || stopRequested) break;
|
|
1449
426
|
|
|
1450
|
-
const nextMinutes = Math.round(
|
|
427
|
+
const nextMinutes = Math.round(cli.loopIntervalSeconds / 60);
|
|
1451
428
|
console.error(`\n[orchestrate] Next cycle in ${nextMinutes} minute(s)... (Ctrl+C to stop)`);
|
|
1452
429
|
await new Promise<void>((resolve) => {
|
|
1453
430
|
sleepResolve = resolve;
|
|
@@ -1455,7 +432,7 @@ Examples:
|
|
|
1455
432
|
sleepTimer = null;
|
|
1456
433
|
sleepResolve = null;
|
|
1457
434
|
resolve();
|
|
1458
|
-
},
|
|
435
|
+
}, cli.loopIntervalSeconds * 1000);
|
|
1459
436
|
});
|
|
1460
437
|
} while (isLoop && !stopRequested);
|
|
1461
438
|
|