selftune 0.2.31 → 0.2.32

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.
Files changed (95) hide show
  1. package/README.md +83 -56
  2. package/apps/local-dashboard/dist/assets/index-B-ut4w0B.js +15 -0
  3. package/apps/local-dashboard/dist/assets/index-BFGfCVrL.css +1 -0
  4. package/apps/local-dashboard/dist/assets/vendor-ui-DfowE3Hu.js +1 -0
  5. package/apps/local-dashboard/dist/index.html +3 -3
  6. package/cli/selftune/command-surface.ts +613 -2
  7. package/cli/selftune/create/baseline.ts +429 -0
  8. package/cli/selftune/create/check.ts +35 -0
  9. package/cli/selftune/create/init.ts +115 -0
  10. package/cli/selftune/create/package-candidate-state.ts +771 -0
  11. package/cli/selftune/create/package-evaluator.ts +710 -0
  12. package/cli/selftune/create/package-fingerprint.ts +142 -0
  13. package/cli/selftune/create/package-search.ts +377 -0
  14. package/cli/selftune/create/publish.ts +431 -0
  15. package/cli/selftune/create/readiness.ts +495 -0
  16. package/cli/selftune/create/replay.ts +330 -0
  17. package/cli/selftune/create/report.ts +74 -0
  18. package/cli/selftune/create/scaffold.ts +121 -0
  19. package/cli/selftune/create/skills-ref-adapter.ts +177 -0
  20. package/cli/selftune/create/status.ts +33 -0
  21. package/cli/selftune/create/templates.ts +249 -0
  22. package/cli/selftune/cron/setup.ts +1 -1
  23. package/cli/selftune/dashboard-action-events.ts +4 -1
  24. package/cli/selftune/dashboard-action-result.ts +789 -24
  25. package/cli/selftune/dashboard-action-stream.ts +80 -0
  26. package/cli/selftune/dashboard-contract.ts +146 -3
  27. package/cli/selftune/dashboard-server.ts +5 -4
  28. package/cli/selftune/eval/hooks-to-evals.ts +58 -35
  29. package/cli/selftune/eval/synthetic-evals.ts +145 -17
  30. package/cli/selftune/evolution/bounded-mutations.ts +1045 -0
  31. package/cli/selftune/evolution/evolve-body.ts +9 -36
  32. package/cli/selftune/evolution/evolve.ts +8 -72
  33. package/cli/selftune/evolution/stopping-criteria.ts +5 -13
  34. package/cli/selftune/evolution/unblock-suggestions.ts +0 -16
  35. package/cli/selftune/evolution/validate-host-replay.ts +115 -15
  36. package/cli/selftune/improve.ts +206 -0
  37. package/cli/selftune/index.ts +123 -6
  38. package/cli/selftune/init.ts +1 -1
  39. package/cli/selftune/localdb/queries/dashboard.ts +30 -0
  40. package/cli/selftune/localdb/schema.ts +52 -0
  41. package/cli/selftune/monitoring/watch.ts +257 -23
  42. package/cli/selftune/orchestrate/execute.ts +300 -1
  43. package/cli/selftune/orchestrate/finalize.ts +14 -0
  44. package/cli/selftune/orchestrate/plan.ts +22 -5
  45. package/cli/selftune/orchestrate/prepare.ts +59 -4
  46. package/cli/selftune/orchestrate/report.ts +1 -1
  47. package/cli/selftune/orchestrate.ts +34 -1
  48. package/cli/selftune/publish.ts +35 -0
  49. package/cli/selftune/routes/actions.ts +81 -15
  50. package/cli/selftune/routes/overview.ts +1 -1
  51. package/cli/selftune/routes/skill-report.ts +147 -2
  52. package/cli/selftune/run.ts +18 -0
  53. package/cli/selftune/schedule.ts +3 -3
  54. package/cli/selftune/search-run.ts +703 -0
  55. package/cli/selftune/status.ts +35 -11
  56. package/cli/selftune/testing-readiness.ts +431 -40
  57. package/cli/selftune/types.ts +316 -0
  58. package/cli/selftune/utils/eval-readiness.ts +1 -0
  59. package/cli/selftune/utils/json-output.ts +11 -0
  60. package/cli/selftune/utils/lifecycle-surface.ts +48 -0
  61. package/cli/selftune/utils/query-filter.ts +82 -1
  62. package/cli/selftune/utils/tui.ts +85 -2
  63. package/cli/selftune/verify.ts +205 -0
  64. package/cli/selftune/workflows/proposals.ts +1 -1
  65. package/cli/selftune/workflows/skill-scaffold.ts +141 -63
  66. package/cli/selftune/workflows/workflows.ts +4 -4
  67. package/package.json +1 -1
  68. package/skill/SKILL.md +148 -85
  69. package/skill/references/cli-quick-reference.md +16 -1
  70. package/skill/references/creator-playbook.md +31 -10
  71. package/skill/workflows/Baseline.md +8 -9
  72. package/skill/workflows/Contributions.md +4 -4
  73. package/skill/workflows/Create.md +173 -0
  74. package/skill/workflows/CreateTestDeploy.md +34 -30
  75. package/skill/workflows/Cron.md +2 -2
  76. package/skill/workflows/Dashboard.md +3 -3
  77. package/skill/workflows/Evals.md +13 -7
  78. package/skill/workflows/Evolve.md +75 -32
  79. package/skill/workflows/EvolveBody.md +22 -15
  80. package/skill/workflows/Hook.md +1 -1
  81. package/skill/workflows/Improve.md +168 -0
  82. package/skill/workflows/Initialize.md +3 -3
  83. package/skill/workflows/Orchestrate.md +49 -12
  84. package/skill/workflows/Publish.md +100 -0
  85. package/skill/workflows/Run.md +72 -0
  86. package/skill/workflows/Schedule.md +2 -2
  87. package/skill/workflows/SearchRun.md +89 -0
  88. package/skill/workflows/SignalsDashboard.md +2 -2
  89. package/skill/workflows/UnitTest.md +13 -4
  90. package/skill/workflows/Verify.md +136 -0
  91. package/skill/workflows/Watch.md +114 -47
  92. package/skill/workflows/Workflows.md +13 -8
  93. package/apps/local-dashboard/dist/assets/index-B7v_o1WC.js +0 -15
  94. package/apps/local-dashboard/dist/assets/index-CrO77SVi.css +0 -1
  95. package/apps/local-dashboard/dist/assets/vendor-ui-B0H8s1mP.js +0 -1
@@ -0,0 +1,431 @@
1
+ import { join } from "node:path";
2
+ import { parseArgs } from "node:util";
3
+
4
+ import { PUBLIC_COMMAND_SURFACES, renderCommandHelp } from "../command-surface.js";
5
+ import type { WatchResult } from "../monitoring/watch.js";
6
+ import { computeWatchTrustScore } from "../monitoring/watch.js";
7
+ import {
8
+ readCanonicalPackageEvaluationArtifact,
9
+ writeCanonicalPackageEvaluation,
10
+ } from "../testing-readiness.js";
11
+ import type {
12
+ CreatePackageEvaluationSummary,
13
+ CreatePackageEvaluationWatchSummary,
14
+ } from "../types.js";
15
+ import { CLIError, handleCLIError } from "../utils/cli-error.js";
16
+ import { extractJsonObject } from "../utils/json-output.js";
17
+ import { refreshPackageCandidateEvaluationObservation } from "./package-candidate-state.js";
18
+ import {
19
+ attachCreatePackageWatchSummary,
20
+ runCreatePackageEvaluation,
21
+ } from "./package-evaluator.js";
22
+ import { computeCreateCheckResult } from "./readiness.js";
23
+
24
+ export interface CreatePublishResult {
25
+ skill: string;
26
+ skill_path: string;
27
+ published: boolean;
28
+ watch_started: boolean;
29
+ watch_gate_blocked: boolean;
30
+ next_command: string | null;
31
+ package_evaluation: CreatePackageEvaluationSummary | null;
32
+ replay_exit_code: number | null;
33
+ baseline_exit_code: number | null;
34
+ watch_exit_code: number | null;
35
+ watch_result: WatchResult | null;
36
+ watch_stdout: string;
37
+ watch_stderr: string;
38
+ watch_gate_passed: boolean | null;
39
+ watch_gate_warnings: string[];
40
+ watch_trust_score: number | null;
41
+ watch_gate_bypassed: boolean;
42
+ }
43
+
44
+ export interface CreatePublishDeps {
45
+ spawnSync?: typeof Bun.spawnSync;
46
+ computeCreateCheckResult?: typeof computeCreateCheckResult;
47
+ runCreatePackageEvaluation?: typeof runCreatePackageEvaluation;
48
+ refreshPackageCandidateEvaluationObservation?: typeof refreshPackageCandidateEvaluationObservation;
49
+ writeCanonicalPackageEvaluation?: typeof writeCanonicalPackageEvaluation;
50
+ }
51
+
52
+ function hydrateWatchResult(summary: CreatePackageEvaluationWatchSummary): WatchResult {
53
+ return {
54
+ snapshot: summary.snapshot,
55
+ alert: summary.alert,
56
+ rolledBack: summary.rolled_back,
57
+ recommendation: summary.recommendation,
58
+ recommended_command: summary.recommended_command,
59
+ gradeAlert: summary.grade_alert,
60
+ gradeRegression: summary.grade_regression,
61
+ ...(summary.efficiency_alert || summary.efficiency_regression
62
+ ? {
63
+ efficiencyAlert: summary.efficiency_alert ?? null,
64
+ efficiencyRegression: summary.efficiency_regression ?? null,
65
+ }
66
+ : {}),
67
+ };
68
+ }
69
+
70
+ function runSelftuneCommand(
71
+ command: string[],
72
+ deps: CreatePublishDeps = {},
73
+ ): { exitCode: number | null; stdout: string; stderr: string } {
74
+ const spawnSync = deps.spawnSync ?? Bun.spawnSync;
75
+ const indexPath = join(import.meta.dir, "..", "index.ts");
76
+ const result = spawnSync(["bun", "run", indexPath, ...command], {
77
+ stdout: "pipe",
78
+ stderr: "pipe",
79
+ env: process.env,
80
+ });
81
+ return {
82
+ exitCode: result.exitCode,
83
+ stdout: Buffer.from(result.stdout).toString("utf-8"),
84
+ stderr: Buffer.from(result.stderr).toString("utf-8"),
85
+ };
86
+ }
87
+
88
+ function parseWatchResult(stdout: string): WatchResult | null {
89
+ const parsed = extractJsonObject(stdout);
90
+ if (!parsed) return null;
91
+ if (!parsed["snapshot"] || typeof parsed["snapshot"] !== "object") return null;
92
+ return parsed as WatchResult;
93
+ }
94
+
95
+ export async function runCreatePublish(
96
+ options: { skillPath: string; watch?: boolean; ignoreWatchAlerts?: boolean },
97
+ deps: CreatePublishDeps = {},
98
+ ): Promise<CreatePublishResult> {
99
+ const check = await (deps.computeCreateCheckResult ?? computeCreateCheckResult)(
100
+ options.skillPath,
101
+ );
102
+ if (check.state !== "ready_to_publish") {
103
+ throw new CLIError(
104
+ `Draft package "${check.skill}" is not ready to publish (${check.state}).`,
105
+ "INVALID_STATUS",
106
+ check.next_command ?? `selftune create check --skill-path ${check.skill_path}`,
107
+ );
108
+ }
109
+
110
+ const evaluation = await (deps.runCreatePackageEvaluation ?? runCreatePackageEvaluation)({
111
+ skillPath: check.skill_path,
112
+ });
113
+ const replayExitCode = evaluation.replay.failed === 0 ? 0 : 1;
114
+ const baselineExitCode =
115
+ evaluation.summary.status === "replay_failed" ? null : evaluation.baseline.adds_value ? 0 : 1;
116
+
117
+ if (!evaluation.summary.evaluation_passed) {
118
+ return {
119
+ skill: check.skill,
120
+ skill_path: check.skill_path,
121
+ published: false,
122
+ watch_started: false,
123
+ watch_gate_blocked: false,
124
+ next_command: evaluation.summary.next_command,
125
+ package_evaluation: evaluation.summary,
126
+ replay_exit_code: replayExitCode,
127
+ baseline_exit_code: baselineExitCode,
128
+ watch_exit_code: null,
129
+ watch_result: null,
130
+ watch_stdout: "",
131
+ watch_stderr: "",
132
+ watch_gate_passed: null,
133
+ watch_gate_warnings: [],
134
+ watch_trust_score: null,
135
+ watch_gate_bypassed: false,
136
+ };
137
+ }
138
+
139
+ if (!options.watch) {
140
+ return {
141
+ skill: check.skill,
142
+ skill_path: check.skill_path,
143
+ published: true,
144
+ watch_started: false,
145
+ watch_gate_blocked: false,
146
+ next_command: `selftune watch --skill ${check.skill} --skill-path ${check.skill_path}`,
147
+ package_evaluation: evaluation.summary,
148
+ replay_exit_code: replayExitCode,
149
+ baseline_exit_code: baselineExitCode,
150
+ watch_exit_code: null,
151
+ watch_result: null,
152
+ watch_stdout: "",
153
+ watch_stderr: "",
154
+ watch_gate_passed: null,
155
+ watch_gate_warnings: [],
156
+ watch_trust_score: null,
157
+ watch_gate_bypassed: false,
158
+ };
159
+ }
160
+
161
+ const priorWatch = readCanonicalPackageEvaluationArtifact(check.skill)?.summary.watch;
162
+ const watch = runSelftuneCommand(
163
+ ["watch", "--skill", check.skill, "--skill-path", check.skill_path, "--sync-first"],
164
+ deps,
165
+ );
166
+ const watchResult = parseWatchResult(watch.stdout);
167
+ const packageEvaluationResult = watchResult
168
+ ? {
169
+ ...evaluation,
170
+ summary: attachCreatePackageWatchSummary(evaluation.summary, watchResult),
171
+ }
172
+ : evaluation;
173
+ const packageEvaluation = packageEvaluationResult.summary;
174
+
175
+ if (watchResult) {
176
+ try {
177
+ (
178
+ deps.refreshPackageCandidateEvaluationObservation ??
179
+ refreshPackageCandidateEvaluationObservation
180
+ )(packageEvaluationResult);
181
+ } catch {
182
+ // Fail-open: candidate observation refresh should not block publish/watch results.
183
+ }
184
+ }
185
+
186
+ const recentWatchResults = [
187
+ ...(priorWatch ? [hydrateWatchResult(priorWatch)] : []),
188
+ ...(watchResult ? [watchResult] : []),
189
+ ];
190
+ const baseWatchGate = checkPublishWatchGate({
191
+ skillName: check.skill,
192
+ recentWatchResults,
193
+ ignoreWatchAlerts: options.ignoreWatchAlerts,
194
+ });
195
+ const watchGate =
196
+ watchResult == null
197
+ ? {
198
+ passed: options.ignoreWatchAlerts === true,
199
+ warnings: [
200
+ `Watch for "${check.skill}" did not return structured JSON output${watch.exitCode != null ? ` (exit ${watch.exitCode})` : ""}. Re-run watch before publishing.`,
201
+ ...baseWatchGate.warnings,
202
+ ],
203
+ trustScore: baseWatchGate.trustScore,
204
+ bypassed: options.ignoreWatchAlerts === true,
205
+ }
206
+ : baseWatchGate;
207
+
208
+ try {
209
+ (deps.writeCanonicalPackageEvaluation ?? writeCanonicalPackageEvaluation)(
210
+ check.skill,
211
+ packageEvaluation,
212
+ );
213
+ } catch {
214
+ // Fail-open: post-watch persistence should not block publish/watch results.
215
+ }
216
+
217
+ const watchGateBlocked = !watchGate.passed;
218
+ const watchRemediationCommand = `selftune watch --skill ${check.skill} --skill-path ${check.skill_path}`;
219
+
220
+ return {
221
+ skill: check.skill,
222
+ skill_path: check.skill_path,
223
+ published: !watchGateBlocked,
224
+ watch_started: watch.exitCode === 0 && watchResult != null,
225
+ watch_gate_blocked: watchGateBlocked,
226
+ next_command: watchGateBlocked
227
+ ? watchRemediationCommand
228
+ : watch.exitCode === 0
229
+ ? null
230
+ : (watchResult?.recommended_command ?? watchRemediationCommand),
231
+ package_evaluation: packageEvaluation,
232
+ replay_exit_code: replayExitCode,
233
+ baseline_exit_code: baselineExitCode,
234
+ watch_exit_code: watch.exitCode,
235
+ watch_result: watchResult,
236
+ watch_stdout: watch.stdout,
237
+ watch_stderr: watch.stderr,
238
+ watch_gate_passed: watchGate.passed,
239
+ watch_gate_warnings: watchGate.warnings,
240
+ watch_trust_score: watchGate.trustScore,
241
+ watch_gate_bypassed: watchGate.bypassed,
242
+ };
243
+ }
244
+
245
+ function formatCreatePublishResult(result: CreatePublishResult): string {
246
+ const replayStatus =
247
+ result.package_evaluation == null
248
+ ? "not run"
249
+ : result.package_evaluation.replay.failed === 0
250
+ ? "passed"
251
+ : "failed";
252
+ const baselineStatus =
253
+ result.package_evaluation == null
254
+ ? "not run"
255
+ : result.package_evaluation.baseline.adds_value
256
+ ? "passed"
257
+ : "failed";
258
+
259
+ return [
260
+ `Skill: ${result.skill}`,
261
+ result.package_evaluation
262
+ ? `Evaluation source: ${
263
+ result.package_evaluation.evaluation_source === "artifact_cache"
264
+ ? "cached artifact"
265
+ : result.package_evaluation.evaluation_source === "candidate_cache"
266
+ ? "accepted candidate cache"
267
+ : "fresh"
268
+ }`
269
+ : "Evaluation source: n/a",
270
+ result.package_evaluation?.candidate_id
271
+ ? `Package candidate: ${result.package_evaluation.candidate_id} (generation ${result.package_evaluation.candidate_generation ?? 0}, parent ${result.package_evaluation.parent_candidate_id ?? "root"})`
272
+ : "Package candidate: n/a",
273
+ result.package_evaluation?.candidate_acceptance
274
+ ? `Candidate acceptance: ${result.package_evaluation.candidate_acceptance.decision} vs ${result.package_evaluation.candidate_acceptance.compared_to_candidate_id ?? "root"} | ${result.package_evaluation.candidate_acceptance.rationale}`
275
+ : "Candidate acceptance: n/a",
276
+ `Package replay: ${replayStatus}`,
277
+ `Package baseline: ${baselineStatus}`,
278
+ result.package_evaluation
279
+ ? `Package lift: ${result.package_evaluation.baseline.lift.toFixed(3)}`
280
+ : "Package lift: n/a",
281
+ result.package_evaluation?.evidence
282
+ ? `Package wins: ${result.package_evaluation.evidence.baseline_wins} | Replay failures: ${result.package_evaluation.evidence.replay_failures}`
283
+ : "Package wins: n/a",
284
+ result.package_evaluation?.efficiency
285
+ ? `Package runtime: ${(result.package_evaluation.efficiency.with_skill.total_duration_ms / 1000).toFixed(1)}s with skill / ${(result.package_evaluation.efficiency.without_skill.total_duration_ms / 1000).toFixed(1)}s without skill`
286
+ : "Package runtime: n/a",
287
+ result.watch_result
288
+ ? `Watch pass rate: ${result.watch_result.snapshot.pass_rate.toFixed(3)} vs baseline ${result.watch_result.snapshot.baseline_pass_rate.toFixed(3)}`
289
+ : "Watch pass rate: n/a",
290
+ result.watch_result?.alert
291
+ ? `Watch alert: ${result.watch_result.alert}`
292
+ : result.watch_result
293
+ ? `Watch alert: none (${result.watch_result.recommendation})`
294
+ : "Watch alert: n/a",
295
+ result.watch_gate_passed != null
296
+ ? `Watch gate: ${result.watch_gate_passed ? "passed" : result.watch_gate_bypassed ? "alert bypassed" : "alert"}`
297
+ : "Watch gate: n/a",
298
+ result.watch_trust_score != null
299
+ ? `Watch trust score: ${result.watch_trust_score.toFixed(2)}`
300
+ : "Watch trust score: n/a",
301
+ ...result.watch_gate_warnings.map(
302
+ (warning, index) => `Watch gate warning ${index + 1}: ${warning}`,
303
+ ),
304
+ ...(result.watch_gate_blocked ? ["Watch gate blocked publish: yes"] : []),
305
+ `Published: ${result.published ? "yes" : "no"}`,
306
+ `Watch started: ${result.watch_started ? "yes" : "no"}`,
307
+ result.next_command ? `Next command: ${result.next_command}` : "Next command: none",
308
+ ].join("\n");
309
+ }
310
+
311
+ export async function cliMain(): Promise<void> {
312
+ const { values } = parseArgs({
313
+ options: {
314
+ "skill-path": { type: "string" },
315
+ watch: { type: "boolean", default: false },
316
+ "ignore-watch-alerts": { type: "boolean", default: false },
317
+ json: { type: "boolean", default: false },
318
+ help: { type: "boolean", short: "h", default: false },
319
+ },
320
+ strict: true,
321
+ });
322
+
323
+ if (values.help) {
324
+ console.log(renderCommandHelp(PUBLIC_COMMAND_SURFACES.createPublish));
325
+ process.exit(0);
326
+ }
327
+
328
+ const result = await runCreatePublish({
329
+ skillPath: values["skill-path"] ?? "",
330
+ watch: values.watch,
331
+ ignoreWatchAlerts: values["ignore-watch-alerts"],
332
+ });
333
+
334
+ if (values.json || !process.stdout.isTTY) {
335
+ console.log(JSON.stringify(result, null, 2));
336
+ } else {
337
+ console.log(formatCreatePublishResult(result));
338
+ }
339
+
340
+ process.exit(result.published ? 0 : 1);
341
+ }
342
+
343
+ if (import.meta.main) {
344
+ cliMain().catch(handleCLIError);
345
+ }
346
+
347
+ // ---------------------------------------------------------------------------
348
+ // Publish watch gate — advisory gate before publish
349
+ // ---------------------------------------------------------------------------
350
+
351
+ export interface PublishWatchGateOptions {
352
+ skillName: string;
353
+ /** Recent watch results to evaluate. Empty array means no watch data. */
354
+ recentWatchResults: WatchResult[];
355
+ /** Bypass watch alerts (expert use). */
356
+ ignoreWatchAlerts?: boolean;
357
+ }
358
+
359
+ export interface PublishWatchGateResult {
360
+ /** Whether the gate passed (true = safe to publish). */
361
+ passed: boolean;
362
+ /** Warning messages for the user (empty if passed cleanly). */
363
+ warnings: string[];
364
+ /** The computed trust score from the most recent watch, or null if no data. */
365
+ trustScore: number | null;
366
+ /** Whether the user bypassed alerts with --ignore-watch-alerts. */
367
+ bypassed: boolean;
368
+ }
369
+
370
+ /** Skills below this trust score get a publish warning. */
371
+ const PUBLISH_TRUST_WARNING_THRESHOLD = 0.7;
372
+
373
+ /**
374
+ * Check whether recent watch results indicate safe-to-publish.
375
+ *
376
+ * This is the publish-time watch safety gate. Active watch alerts produce
377
+ * warnings that block publish unless --ignore-watch-alerts is set. Missing or
378
+ * malformed watch output is handled by the caller as a hard publish failure.
379
+ */
380
+ export function checkPublishWatchGate(options: PublishWatchGateOptions): PublishWatchGateResult {
381
+ const { skillName, recentWatchResults, ignoreWatchAlerts = false } = options;
382
+
383
+ // No watch data — pass with advisory
384
+ if (recentWatchResults.length === 0) {
385
+ return {
386
+ passed: true,
387
+ warnings: [
388
+ `No watch data for "${skillName}". Consider running "selftune watch" before publishing.`,
389
+ ],
390
+ trustScore: null,
391
+ bypassed: false,
392
+ };
393
+ }
394
+
395
+ const latestWatch = recentWatchResults[recentWatchResults.length - 1]!;
396
+ const trustScore = computeWatchTrustScore(latestWatch);
397
+ const warnings: string[] = [];
398
+
399
+ // Check for active alerts in any recent watch result
400
+ const activeAlerts = recentWatchResults.filter((r) => r.alert != null).map((r) => r.alert!);
401
+
402
+ if (activeAlerts.length > 0) {
403
+ warnings.push(
404
+ `Active watch alerts for "${skillName}":\n${activeAlerts.map((a) => ` - ${a}`).join("\n")}`,
405
+ );
406
+ }
407
+
408
+ // Check trust score threshold
409
+ if (trustScore < PUBLISH_TRUST_WARNING_THRESHOLD) {
410
+ warnings.push(
411
+ `Watch trust score for "${skillName}" is ${trustScore.toFixed(2)}, below threshold ${PUBLISH_TRUST_WARNING_THRESHOLD.toFixed(2)}.`,
412
+ );
413
+ }
414
+
415
+ // Check for recent rollbacks
416
+ const hasRollback = recentWatchResults.some((r) => r.rolledBack);
417
+ if (hasRollback) {
418
+ warnings.push(
419
+ `"${skillName}" was recently rolled back. Ensure the issue is resolved before publishing.`,
420
+ );
421
+ }
422
+
423
+ const passed = warnings.length === 0 || ignoreWatchAlerts;
424
+
425
+ return {
426
+ passed,
427
+ warnings,
428
+ trustScore,
429
+ bypassed: ignoreWatchAlerts && warnings.length > 0,
430
+ };
431
+ }