@senpi/runtime 3.0.6-dev.runtime-phase-2.20260630134511 → 3.0.6-dev.runtime-phase-2.20260630182800
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/guide-commands.d.ts.map +1 -1
- package/dist/cli/guide-commands.js +61 -0
- package/dist/cli/guide-commands.js.map +1 -1
- package/dist/cli/senpi-commands.d.ts +14 -0
- package/dist/cli/senpi-commands.d.ts.map +1 -1
- package/dist/cli/senpi-commands.js +390 -2
- package/dist/cli/senpi-commands.js.map +1 -1
- package/dist/dsl/plugin/index.d.ts.map +1 -1
- package/dist/dsl/plugin/index.js +6 -0
- package/dist/dsl/plugin/index.js.map +1 -1
- package/dist/health/types.d.ts +14 -0
- package/dist/health/types.d.ts.map +1 -1
- package/dist/health/types.js.map +1 -1
- package/dist/index.d.ts +19 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +183 -5
- package/dist/index.js.map +1 -1
- package/dist/risk/risk-audit-log.d.ts +7 -0
- package/dist/risk/risk-audit-log.d.ts.map +1 -1
- package/dist/risk/risk-audit-log.js +48 -1
- package/dist/risk/risk-audit-log.js.map +1 -1
- package/dist/runtime/runtime.d.ts +2 -0
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +17 -3
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime-api/intake/scanner-registry.d.ts +3 -0
- package/dist/runtime-api/intake/scanner-registry.d.ts.map +1 -1
- package/dist/runtime-api/intake/scanner-registry.js +3 -0
- package/dist/runtime-api/intake/scanner-registry.js.map +1 -1
- package/dist/scanners/runtime-module.d.ts.map +1 -1
- package/dist/scanners/runtime-module.js +6 -0
- package/dist/scanners/runtime-module.js.map +1 -1
- package/dist/skills-manager/bootstrap.d.ts +12 -7
- package/dist/skills-manager/bootstrap.d.ts.map +1 -1
- package/dist/skills-manager/bootstrap.js +31 -20
- package/dist/skills-manager/bootstrap.js.map +1 -1
- package/dist/skills-manager/clone-groups.d.ts +20 -0
- package/dist/skills-manager/clone-groups.d.ts.map +1 -0
- package/dist/skills-manager/clone-groups.js +45 -0
- package/dist/skills-manager/clone-groups.js.map +1 -0
- package/dist/skills-manager/constants.d.ts +5 -33
- package/dist/skills-manager/constants.d.ts.map +1 -1
- package/dist/skills-manager/constants.js +11 -50
- package/dist/skills-manager/constants.js.map +1 -1
- package/dist/skills-manager/coordinator.d.ts +26 -119
- package/dist/skills-manager/coordinator.d.ts.map +1 -1
- package/dist/skills-manager/coordinator.js +145 -346
- package/dist/skills-manager/coordinator.js.map +1 -1
- package/dist/skills-manager/fetcher.d.ts +23 -2
- package/dist/skills-manager/fetcher.d.ts.map +1 -1
- package/dist/skills-manager/fetcher.js +36 -6
- package/dist/skills-manager/fetcher.js.map +1 -1
- package/dist/skills-manager/manifest.d.ts +160 -0
- package/dist/skills-manager/manifest.d.ts.map +1 -0
- package/dist/skills-manager/manifest.js +185 -0
- package/dist/skills-manager/manifest.js.map +1 -0
- package/dist/skills-manager/skills-manifest.json +16 -0
- package/dist/skills-manager/types.d.ts +61 -30
- package/dist/skills-manager/types.d.ts.map +1 -1
- package/dist/skills-manager/utils.d.ts +10 -26
- package/dist/skills-manager/utils.d.ts.map +1 -1
- package/dist/skills-manager/utils.js +18 -34
- package/dist/skills-manager/utils.js.map +1 -1
- package/dist/skills-manager/version-manager.d.ts +21 -91
- package/dist/skills-manager/version-manager.d.ts.map +1 -1
- package/dist/skills-manager/version-manager.js +48 -178
- package/dist/skills-manager/version-manager.js.map +1 -1
- package/dist/skills-manager/writer.d.ts +12 -1
- package/dist/skills-manager/writer.d.ts.map +1 -1
- package/dist/skills-manager/writer.js +50 -4
- package/dist/skills-manager/writer.js.map +1 -1
- package/dist/telemetry/event-store.d.ts +66 -0
- package/dist/telemetry/event-store.d.ts.map +1 -0
- package/dist/telemetry/event-store.js +202 -0
- package/dist/telemetry/event-store.js.map +1 -0
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +7 -15
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/wallet-address.d.ts +3 -0
- package/dist/utils/wallet-address.d.ts.map +1 -0
- package/dist/utils/wallet-address.js +17 -0
- package/dist/utils/wallet-address.js.map +1 -0
- package/openclaw.plugin.json +18 -6
- package/package.json +3 -2
|
@@ -16,17 +16,19 @@
|
|
|
16
16
|
* health as healthy. The first tick fires on the next external invocation
|
|
17
17
|
* of `runTickOnce()`.
|
|
18
18
|
*
|
|
19
|
-
* Each tick:
|
|
20
|
-
*
|
|
21
|
-
* (deduped by version),
|
|
19
|
+
* Each tick: take the managed skill set from the manifest
|
|
20
|
+
* (`skills-manifest.json`, the single source of truth), check remote versions
|
|
21
|
+
* in parallel, auto-apply patch/minor, notify on major (deduped by version),
|
|
22
|
+
* skip on none/unknown.
|
|
22
23
|
*/
|
|
23
|
-
import { readdir } from "node:fs/promises";
|
|
24
24
|
import { bootstrapSkillsIfNeeded } from "./bootstrap.js";
|
|
25
|
+
import { withClonedGroups } from "./clone-groups.js";
|
|
25
26
|
import { cloneSkillsRepo } from "./fetcher.js";
|
|
27
|
+
import { getManifestSkillNames, resolveSkillSource } from "./manifest.js";
|
|
28
|
+
import { parseSemver } from "../runtime/auto-update/policy.js";
|
|
26
29
|
import { SkillsManagerStateStore } from "./state-store.js";
|
|
27
|
-
import { checkSkillVersion } from "./version-manager.js";
|
|
30
|
+
import { checkSkillVersion, normalizeVersion } from "./version-manager.js";
|
|
28
31
|
import { installSkill } from "./writer.js";
|
|
29
|
-
import { NOTIFICATION_SKILL_LIST_TOP_N, SKILLS_RAW_BASE_URL, SKILLS_REPO_URL, TELEGRAM_NOTIFICATION_BUDGET_CHARS, } from "./constants.js";
|
|
30
32
|
import { createDefaultSkillState, createDefaultSkillsManagerState, formatErr } from "./utils.js";
|
|
31
33
|
export class SkillsCoordinatorUnavailableError extends Error {
|
|
32
34
|
constructor(message) {
|
|
@@ -34,6 +36,37 @@ export class SkillsCoordinatorUnavailableError extends Error {
|
|
|
34
36
|
this.name = "SkillsCoordinatorUnavailableError";
|
|
35
37
|
}
|
|
36
38
|
}
|
|
39
|
+
/** Parse the major-version number from a SKILL.md version string, or null. */
|
|
40
|
+
function majorOf(version) {
|
|
41
|
+
if (!version)
|
|
42
|
+
return null;
|
|
43
|
+
return parseSemver(normalizeVersion(version))?.major ?? null;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Whether a remote major is **gated** (not auto-applied by the tick).
|
|
47
|
+
*
|
|
48
|
+
* The effective ceiling is `max(maxMajor, installedMajor)` — flooring at the
|
|
49
|
+
* installed major matters because a manual `senpi skills update` applies any
|
|
50
|
+
* major, so a skill's installed major can legitimately exceed `maxMajor`; once
|
|
51
|
+
* it does, patch/minor *within* that already-installed major must keep
|
|
52
|
+
* auto-applying (you can't un-install it). Null handling:
|
|
53
|
+
* - `maxMajor` null → floor is just `installedMajor` (gate all true majors).
|
|
54
|
+
* - `installedMajor` null (unversioned/not-installed) → floor is `maxMajor`
|
|
55
|
+
* (so a null-local "minor" classification can't bypass the ceiling).
|
|
56
|
+
* - both null → no ceiling → not gated.
|
|
57
|
+
*/
|
|
58
|
+
function isMajorGated(remoteMajor, installedMajor, maxMajor) {
|
|
59
|
+
if (remoteMajor === null)
|
|
60
|
+
return false;
|
|
61
|
+
const candidates = [];
|
|
62
|
+
if (maxMajor !== null)
|
|
63
|
+
candidates.push(maxMajor);
|
|
64
|
+
if (installedMajor !== null)
|
|
65
|
+
candidates.push(installedMajor);
|
|
66
|
+
if (candidates.length === 0)
|
|
67
|
+
return false;
|
|
68
|
+
return remoteMajor > Math.max(...candidates);
|
|
69
|
+
}
|
|
37
70
|
export class SkillsUpdateCoordinator {
|
|
38
71
|
options;
|
|
39
72
|
store;
|
|
@@ -42,38 +75,11 @@ export class SkillsUpdateCoordinator {
|
|
|
42
75
|
runToken = 0;
|
|
43
76
|
state = createDefaultSkillsManagerState();
|
|
44
77
|
snapshot;
|
|
45
|
-
/** Latest remote version per skill name — populated during ticks. */
|
|
46
|
-
latestVersions = new Map();
|
|
47
|
-
/**
|
|
48
|
-
* Local (pre-install) version per skill name — populated during ticks
|
|
49
|
-
* and manual RPC paths from `SkillVersionInfo.localVersion`. Used by the
|
|
50
|
-
* update-summary and major-available notifications to render the
|
|
51
|
-
* `prev → new` transition. Captured *before* the install runs so the
|
|
52
|
-
* value still reflects the on-disk version at the time the user chose to
|
|
53
|
-
* update; reading `state.skills[name].installedVersion` instead would
|
|
54
|
-
* surface the *post-install* version (recorded by `recordSuccess`) and
|
|
55
|
-
* collapse the transition to `vX -> vX`.
|
|
56
|
-
*/
|
|
57
|
-
previousVersions = new Map();
|
|
58
78
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* version, not whatever's installed locally. Used to enrich
|
|
62
|
-
* update-summary and major-available notifications with a one-line
|
|
63
|
-
* "About:" so the user gets context for the version the message is
|
|
64
|
-
* actually announcing.
|
|
65
|
-
*
|
|
66
|
-
* Populated during tick / updateSkill / updateAll alongside
|
|
67
|
-
* `latestVersions`; absent / null entries simply omit the line from the
|
|
68
|
-
* notification.
|
|
69
|
-
*
|
|
70
|
-
* Using the remote description (rather than the local one) keeps the
|
|
71
|
-
* notification description in sync with the version number it sits next
|
|
72
|
-
* to — e.g. when notifying about a major bump v1 → v2, the "About:" line
|
|
73
|
-
* reflects the v2 description, even if the v1 local copy on disk is
|
|
74
|
-
* stale or worded differently.
|
|
79
|
+
* Latest remote version per skill name — populated during ticks. Read by
|
|
80
|
+
* `buildSnapshot` (status RPC) and used as the install `expectedVersion`.
|
|
75
81
|
*/
|
|
76
|
-
|
|
82
|
+
latestVersions = new Map();
|
|
77
83
|
constructor(options) {
|
|
78
84
|
this.options = options;
|
|
79
85
|
this.store = new SkillsManagerStateStore(options.stateDir, options.config.skillsDir, options.logger);
|
|
@@ -99,7 +105,7 @@ export class SkillsUpdateCoordinator {
|
|
|
99
105
|
logger.info("[senpi-skills-manager] disabled by config");
|
|
100
106
|
return;
|
|
101
107
|
}
|
|
102
|
-
logger.info(`[senpi-skills-manager] starting | skillsDir=${config.skillsDir.join(",")} skills=${
|
|
108
|
+
logger.info(`[senpi-skills-manager] starting | skillsDir=${config.skillsDir.join(",")} skills=${this.resolveSkillNames().join(",")} (manifest) cadence=external (driven by auto-update)`);
|
|
103
109
|
this.state = await this.store.load();
|
|
104
110
|
const bootstrapFn = this.options.deps?.bootstrapSkillsIfNeeded ?? bootstrapSkillsIfNeeded;
|
|
105
111
|
try {
|
|
@@ -196,9 +202,10 @@ export class SkillsUpdateCoordinator {
|
|
|
196
202
|
const { logger } = this.options;
|
|
197
203
|
logger.info(`[senpi-skills-manager] updateSkill invoked | skill=${skillName}`);
|
|
198
204
|
const checkVersionFn = this.resolveCheckFn();
|
|
205
|
+
const source = this.resolveSource(skillName);
|
|
199
206
|
let versionInfo;
|
|
200
207
|
try {
|
|
201
|
-
versionInfo = await checkVersionFn(this.options.config.skillsDir, skillName,
|
|
208
|
+
versionInfo = await checkVersionFn(this.options.config.skillsDir, skillName, source.rawBaseUrl, logger, source.path);
|
|
202
209
|
}
|
|
203
210
|
catch (err) {
|
|
204
211
|
// Version check threw (5xx, network error, timeout, body-parse failure
|
|
@@ -215,11 +222,8 @@ export class SkillsUpdateCoordinator {
|
|
|
215
222
|
}
|
|
216
223
|
// Record the remote version we just learned so the snapshot's latestVersion
|
|
217
224
|
// field reflects it on the next getSnapshot() call — even when no install
|
|
218
|
-
// happens (skip paths below).
|
|
219
|
-
// notifications (sendUpdateSummary / handleMajorAvailable) can include it.
|
|
225
|
+
// happens (skip paths below).
|
|
220
226
|
this.latestVersions.set(skillName, versionInfo.remoteVersion);
|
|
221
|
-
this.previousVersions.set(skillName, versionInfo.localVersion);
|
|
222
|
-
this.skillDescriptions.set(skillName, versionInfo.remoteDescription ?? null);
|
|
223
227
|
// Mirror runTick: every skill whose version we just checked gets its
|
|
224
228
|
// `lastCheckedAt` updated (and `installedVersion` refreshed from the local
|
|
225
229
|
// read) regardless of whether it proceeds to install. Without this, skills
|
|
@@ -287,7 +291,7 @@ export class SkillsUpdateCoordinator {
|
|
|
287
291
|
this.assertManualOperationAllowed("updateAll", token);
|
|
288
292
|
const { logger, config } = this.options;
|
|
289
293
|
logger.info("[senpi-skills-manager] updateAll invoked");
|
|
290
|
-
const skillNames =
|
|
294
|
+
const skillNames = this.resolveSkillNames();
|
|
291
295
|
if (skillNames.length === 0) {
|
|
292
296
|
logger.info("[senpi-skills-manager] updateAll — no skills to check");
|
|
293
297
|
return [];
|
|
@@ -296,10 +300,9 @@ export class SkillsUpdateCoordinator {
|
|
|
296
300
|
const checkVersionFn = this.resolveCheckFn();
|
|
297
301
|
// Check every skill's version in parallel — one rejection does not abort others.
|
|
298
302
|
const versionChecks = await Promise.allSettled(skillNames.map(async (skillName) => {
|
|
299
|
-
const
|
|
303
|
+
const source = this.resolveSource(skillName);
|
|
304
|
+
const versionInfo = await checkVersionFn(config.skillsDir, skillName, source.rawBaseUrl, logger, source.path);
|
|
300
305
|
this.latestVersions.set(skillName, versionInfo.remoteVersion);
|
|
301
|
-
this.previousVersions.set(skillName, versionInfo.localVersion);
|
|
302
|
-
this.skillDescriptions.set(skillName, versionInfo.remoteDescription ?? null);
|
|
303
306
|
return { name: skillName, ...versionInfo };
|
|
304
307
|
}));
|
|
305
308
|
const results = [];
|
|
@@ -440,7 +443,7 @@ export class SkillsUpdateCoordinator {
|
|
|
440
443
|
*/
|
|
441
444
|
async runTick() {
|
|
442
445
|
const { logger, config } = this.options;
|
|
443
|
-
const skillNames =
|
|
446
|
+
const skillNames = this.resolveSkillNames();
|
|
444
447
|
if (skillNames.length === 0) {
|
|
445
448
|
logger.info("[senpi-skills-manager] tick — no skills to check");
|
|
446
449
|
return;
|
|
@@ -449,11 +452,12 @@ export class SkillsUpdateCoordinator {
|
|
|
449
452
|
const checkVersionFn = this.resolveCheckFn();
|
|
450
453
|
// Check all versions in parallel — per-skill errors are captured, not thrown.
|
|
451
454
|
const versionChecks = await Promise.allSettled(skillNames.map(async (skillName) => {
|
|
452
|
-
const
|
|
455
|
+
const source = this.resolveSource(skillName);
|
|
456
|
+
const versionInfo = await checkVersionFn(config.skillsDir, skillName, source.rawBaseUrl, logger, source.path);
|
|
453
457
|
this.latestVersions.set(skillName, versionInfo.remoteVersion);
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
return { name: skillName, ...versionInfo };
|
|
458
|
+
// Carry the resolved major ceiling so the triage loop can decide whether
|
|
459
|
+
// a major bump is authorized without re-resolving the source.
|
|
460
|
+
return { name: skillName, ...versionInfo, maxMajor: source.maxMajor };
|
|
457
461
|
}));
|
|
458
462
|
const skillsToInstall = [];
|
|
459
463
|
let majorPendingCount = 0;
|
|
@@ -467,7 +471,7 @@ export class SkillsUpdateCoordinator {
|
|
|
467
471
|
logger.warn(`[senpi-skills-manager] tick — version check failed | skill=${skillName} err=${errorMessage}`);
|
|
468
472
|
continue;
|
|
469
473
|
}
|
|
470
|
-
const { localVersion, remoteVersion, updateKind } = checkResult.value;
|
|
474
|
+
const { localVersion, remoteVersion, updateKind, maxMajor } = checkResult.value;
|
|
471
475
|
const skillState = this.state.skills[skillName] ?? createDefaultSkillState(skillName);
|
|
472
476
|
skillState.lastCheckedAt = new Date().toISOString();
|
|
473
477
|
if (localVersion !== null)
|
|
@@ -486,14 +490,25 @@ export class SkillsUpdateCoordinator {
|
|
|
486
490
|
upToDateCount += 1;
|
|
487
491
|
continue;
|
|
488
492
|
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
493
|
+
// Enforce the `maxMajor` ceiling on the **remote major regardless of how
|
|
494
|
+
// the delta was classified** (a null local version classifies as "minor"
|
|
495
|
+
// but could still pull a remote major above the ceiling). The effective
|
|
496
|
+
// ceiling is `max(maxMajor, installedMajor)` — see `isMajorGated`.
|
|
497
|
+
const remoteMajor = majorOf(remoteVersion);
|
|
498
|
+
const ceilingExceeded = isMajorGated(remoteMajor, majorOf(localVersion), maxMajor);
|
|
499
|
+
if (ceilingExceeded) {
|
|
500
|
+
// Gated: record the newer `latestVersion` (surfaced via
|
|
501
|
+
// `senpi skills status`); the operator applies it with a manual
|
|
502
|
+
// `senpi skills update <name>` or by bumping `maxMajor` in the manifest.
|
|
503
|
+
logger.info(`[senpi-skills-manager] tick — major available, not auto-applied | skill=${skillName} local=${localVersion ?? "none"} remote=${remoteVersion ?? "unknown"} maxMajor=${maxMajor ?? "none"}`);
|
|
492
504
|
this.state.skills[skillName] = skillState;
|
|
493
505
|
majorPendingCount += 1;
|
|
494
506
|
continue;
|
|
495
507
|
}
|
|
496
|
-
|
|
508
|
+
if (updateKind === "major") {
|
|
509
|
+
logger.info(`[senpi-skills-manager] tick — major within maxMajor ceiling, auto-applying | skill=${skillName} local=${localVersion ?? "none"} remote=${remoteVersion ?? "unknown"} maxMajor=${maxMajor}`);
|
|
510
|
+
}
|
|
511
|
+
// patch / minor / authorized-major — queue for install
|
|
497
512
|
skillsToInstall.push(skillName);
|
|
498
513
|
this.state.skills[skillName] = skillState;
|
|
499
514
|
}
|
|
@@ -504,14 +519,12 @@ export class SkillsUpdateCoordinator {
|
|
|
504
519
|
}
|
|
505
520
|
/**
|
|
506
521
|
* Clone the repo once and install every skill in `skillNames`.
|
|
507
|
-
* Per-skill errors are recorded in state without aborting the others
|
|
508
|
-
*
|
|
509
|
-
* failures so the user can see exactly which skills need attention.
|
|
522
|
+
* Per-skill errors are recorded in state without aborting the others;
|
|
523
|
+
* outcomes are logged (no notification).
|
|
510
524
|
*
|
|
511
|
-
* If the clone itself fails we log and return without touching state
|
|
512
|
-
*
|
|
513
|
-
*
|
|
514
|
-
* per-skill would be misleading. The next tick retries.
|
|
525
|
+
* If the clone itself fails we log and return without touching state — a
|
|
526
|
+
* full-clone failure is almost always a transient network/git issue that
|
|
527
|
+
* affects every skill equally. The next tick retries.
|
|
515
528
|
*/
|
|
516
529
|
async applyUpdates(skillNames) {
|
|
517
530
|
const { logger } = this.options;
|
|
@@ -524,20 +537,19 @@ export class SkillsUpdateCoordinator {
|
|
|
524
537
|
logger.warn(`[senpi-skills-manager] applyUpdates — git clone failed, skipping batch: ${formatErr(err)}`);
|
|
525
538
|
return;
|
|
526
539
|
}
|
|
527
|
-
|
|
528
|
-
|
|
540
|
+
let succeeded = 0;
|
|
541
|
+
let failed = 0;
|
|
529
542
|
for (const skillName of skillNames) {
|
|
530
543
|
const outcome = this.processInstallOutcome(skillName, installOutcomes);
|
|
531
544
|
if (outcome.success) {
|
|
532
|
-
|
|
545
|
+
succeeded += 1;
|
|
533
546
|
logger.info(`[senpi-skills-manager] applyUpdates — installed | skill=${skillName} version=${outcome.version ?? "unknown"}`);
|
|
534
547
|
}
|
|
535
548
|
else {
|
|
536
|
-
|
|
549
|
+
failed += 1;
|
|
537
550
|
}
|
|
538
551
|
}
|
|
539
|
-
logger.info(`[senpi-skills-manager] applyUpdates complete | installed=${
|
|
540
|
-
await this.sendUpdateSummary(succeededSkillNames, failedSkills);
|
|
552
|
+
logger.info(`[senpi-skills-manager] applyUpdates complete | installed=${succeeded} failed=${failed}`);
|
|
541
553
|
}
|
|
542
554
|
/**
|
|
543
555
|
* Clone the remote skills repo exactly once and install every skill in
|
|
@@ -573,16 +585,29 @@ export class SkillsUpdateCoordinator {
|
|
|
573
585
|
}
|
|
574
586
|
return outcomes;
|
|
575
587
|
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
588
|
+
// Allowlist = the managed (manifest) set, NOT just this batch — otherwise a
|
|
589
|
+
// manual single-skill `updateSkill(name)` (batch `[name]`) would make the
|
|
590
|
+
// installSkill allowlist pass for any name, letting it install an arbitrary
|
|
591
|
+
// repo subdir outside the managed set.
|
|
592
|
+
const allowedSkills = this.resolveSkillNames();
|
|
593
|
+
// Group skills by their resolved (repo, branch, commit) and clone each group
|
|
594
|
+
// once (shared helper handles grouping + per-group clone resilience).
|
|
595
|
+
const sources = skillNames.map((name) => this.resolveSource(name));
|
|
596
|
+
await withClonedGroups(sources, cloneSkillsRepo, logger, async (cloneDir, groupSources) => {
|
|
597
|
+
logger.info(`[senpi-skills-manager] cloneAndInstallMany — clone ready, installing | count=${groupSources.length} cloneDir=${cloneDir}`);
|
|
598
|
+
for (const source of groupSources) {
|
|
599
|
+
const skillName = source.name;
|
|
581
600
|
try {
|
|
582
601
|
// Writes always target the *primary* dir (first entry in the
|
|
583
602
|
// search path). Secondary dirs are read-only mirrors — we'd never
|
|
584
603
|
// overwrite them on a tick or manual update.
|
|
585
|
-
await installSkill(cloneDir, skillName, config.skillsDir[0], logger
|
|
604
|
+
await installSkill(cloneDir, skillName, config.skillsDir[0], logger, {
|
|
605
|
+
sourcePath: source.path,
|
|
606
|
+
allowedSkills,
|
|
607
|
+
// Verify the freshly-installed SKILL.md matches the version the
|
|
608
|
+
// check decided to install — rolls back on mismatch.
|
|
609
|
+
expectedVersion: this.latestVersions.get(skillName) ?? null,
|
|
610
|
+
});
|
|
586
611
|
const newVersion = this.latestVersions.get(skillName) ?? null;
|
|
587
612
|
outcomes.set(skillName, { version: newVersion });
|
|
588
613
|
logger.info(`[senpi-skills-manager] cloneAndInstallMany — installed | skill=${skillName} version=${newVersion ?? "unknown"}`);
|
|
@@ -593,11 +618,15 @@ export class SkillsUpdateCoordinator {
|
|
|
593
618
|
logger.warn(`[senpi-skills-manager] cloneAndInstallMany — install failed | skill=${skillName} err=${errorMessage}`);
|
|
594
619
|
}
|
|
595
620
|
}
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
621
|
+
}, (groupSources, err) => {
|
|
622
|
+
// A clone failure for one group records that group's skills as failed
|
|
623
|
+
// and continues — earlier groups' installs are preserved.
|
|
624
|
+
const errorMessage = `clone failed: ${formatErr(err)}`;
|
|
625
|
+
for (const source of groupSources) {
|
|
626
|
+
outcomes.set(source.name, { version: null, error: errorMessage });
|
|
627
|
+
}
|
|
628
|
+
logger.warn(`[senpi-skills-manager] cloneAndInstallMany — clone failed, skipping group | skills=${groupSources.map((s) => s.name).join(",")} err=${formatErr(err)}`);
|
|
629
|
+
});
|
|
601
630
|
return outcomes;
|
|
602
631
|
}
|
|
603
632
|
/**
|
|
@@ -622,7 +651,6 @@ export class SkillsUpdateCoordinator {
|
|
|
622
651
|
*/
|
|
623
652
|
recordSuccess(skillState, newVersion) {
|
|
624
653
|
skillState.installedVersion = newVersion;
|
|
625
|
-
skillState.lastAppliedVersion = newVersion;
|
|
626
654
|
skillState.consecutiveFailures = 0;
|
|
627
655
|
skillState.lastFailureAt = null;
|
|
628
656
|
skillState.lastFailureReason = null;
|
|
@@ -710,208 +738,6 @@ export class SkillsUpdateCoordinator {
|
|
|
710
738
|
logger.info(`[senpi-skills-manager] applyUpdate complete | skill=${skillName} version=${installedVersion ?? "unknown"}`);
|
|
711
739
|
return installedVersion;
|
|
712
740
|
}
|
|
713
|
-
/**
|
|
714
|
-
* Format the per-skill nested-bullet block used in both notification
|
|
715
|
-
* shapes. Renders the structure suggested in PR #78 review feedback:
|
|
716
|
-
*
|
|
717
|
-
* **skill-name**
|
|
718
|
-
* • v1.0.0 → v2.0.0 (or "v1.0.0 (install failed)" when failed)
|
|
719
|
-
* • description: … (omitted when no remote description)
|
|
720
|
-
* • error: … (only on failure, replaces the description)
|
|
721
|
-
*
|
|
722
|
-
* **Why markdown, not HTML?**
|
|
723
|
-
* Openclaw's Telegram channel (`src/telegram/send.ts:sendMessageTelegram`)
|
|
724
|
-
* pipes outbound text through `renderTelegramHtmlText`, which defaults
|
|
725
|
-
* `textMode` to `"markdown"` — i.e. it expects the source to be markdown
|
|
726
|
-
* and runs `markdownToTelegramHtml` over it before handing the converted
|
|
727
|
-
* HTML to the Bot API with `parse_mode: "HTML"`. The `message` tool
|
|
728
|
-
* (which is what this coordinator's `notify` callback ultimately hits via
|
|
729
|
-
* `/tools/invoke`) does not expose `textMode`, so HTML written by us is
|
|
730
|
-
* treated as literal characters by the markdown step and rendered as
|
|
731
|
-
* visible `<b>` text in the chat (the actual production bug this docstring
|
|
732
|
-
* fixes — confirmed against a live notification screenshot).
|
|
733
|
-
*
|
|
734
|
-
* Using markdown source (`**bold**`, backticks) instead means the channel's
|
|
735
|
-
* markdown→HTML converter produces the correct `<b>` / `<code>` HTML
|
|
736
|
-
* payload and Telegram renders the formatting properly.
|
|
737
|
-
*
|
|
738
|
-
* @param skillName Skill identifier (rendered bold via `**name**`).
|
|
739
|
-
* @param toVersion Resolved upgraded-to version. `null` for failure rows
|
|
740
|
-
* where no install completed; renders as
|
|
741
|
-
* "vPREV (install failed)".
|
|
742
|
-
* @param errorReason Optional failure reason. When provided, replaces
|
|
743
|
-
* the description line with `error: …`.
|
|
744
|
-
*/
|
|
745
|
-
formatSkillBlock(skillName, toVersion, errorReason = null, options = {}) {
|
|
746
|
-
const prev = this.previousVersions.get(skillName) ?? null;
|
|
747
|
-
const versionLine = errorReason !== null
|
|
748
|
-
? ` • ${prev !== null ? `v${prev} ` : ""}(install failed)`
|
|
749
|
-
: ` • ${prev !== null ? `v${prev} → ` : ""}${toVersion !== null ? `v${toVersion}` : "(unknown)"}`;
|
|
750
|
-
// `omitDescription` is set when the caller is rebuilding a notification
|
|
751
|
-
// in compact mode after the verbose form exceeded Telegram's per-message
|
|
752
|
-
// byte budget (see `sendUpdateSummary`'s three-tier sizing). The error
|
|
753
|
-
// line for failures is NEVER omitted — it's the actionable bit and a
|
|
754
|
-
// single line, not the source of the overflow.
|
|
755
|
-
const description = options.omitDescription
|
|
756
|
-
? null
|
|
757
|
-
: (this.skillDescriptions.get(skillName) ?? null);
|
|
758
|
-
const trailingLine = errorReason !== null
|
|
759
|
-
? ` • error: ${errorReason}`
|
|
760
|
-
: description
|
|
761
|
-
? ` • description: ${description}`
|
|
762
|
-
: null;
|
|
763
|
-
// Skill name line: bold only, no leading bullet — the bold acts as the
|
|
764
|
-
// visual anchor for the block, and the nested bullets below already
|
|
765
|
-
// convey "these belong to this skill". A leading `•` on the heading
|
|
766
|
-
// looked redundant in production.
|
|
767
|
-
const lines = [`**${skillName}**`, versionLine];
|
|
768
|
-
if (trailingLine)
|
|
769
|
-
lines.push(trailingLine);
|
|
770
|
-
return lines.join("\n");
|
|
771
|
-
}
|
|
772
|
-
/**
|
|
773
|
-
* Send a one-way Telegram notification for a pending major update, deduped
|
|
774
|
-
* by version.
|
|
775
|
-
*
|
|
776
|
-
* The dedup key (`lastNotifiedMajorVersion`) is only set after the notify
|
|
777
|
-
* call resolves successfully. If the notification fails (Telegram API down,
|
|
778
|
-
* transient network error, etc.) the key is left unchanged so the next tick
|
|
779
|
-
* retries delivery instead of silently suppressing it forever.
|
|
780
|
-
*/
|
|
781
|
-
async handleMajorAvailable(skillState, skillName, remoteVersion) {
|
|
782
|
-
if (skillState.lastNotifiedMajorVersion === remoteVersion) {
|
|
783
|
-
this.options.logger.info(`[senpi-skills-manager] handleMajorAvailable — notification already sent, skipping | skill=${skillName} version=${remoteVersion}`);
|
|
784
|
-
return;
|
|
785
|
-
}
|
|
786
|
-
try {
|
|
787
|
-
const block = this.formatSkillBlock(skillName, remoteVersion);
|
|
788
|
-
await this.options.notify(`Senpi skills update — major available:\n\n` +
|
|
789
|
-
`${block}\n\n` +
|
|
790
|
-
`Major updates are not applied automatically.\n` +
|
|
791
|
-
// Inline code via markdown backticks — the channel's markdown→HTML
|
|
792
|
-
// step converts these to `<code>` so the install command renders
|
|
793
|
-
// in monospace. Skill names in the senpi-skills repo are
|
|
794
|
-
// kebab-case identifiers (no backtick chars), so no escaping is
|
|
795
|
-
// needed here in practice.
|
|
796
|
-
`Run: \`openclaw senpi skills update ${skillName}\`\n\n` +
|
|
797
|
-
// Yes/no CTA — operator-friendly framing, intentionally short.
|
|
798
|
-
// Replies are routed to the agent layer, not to this coordinator.
|
|
799
|
-
`Want help with the upgrade? Reply yes or no.`);
|
|
800
|
-
skillState.lastNotifiedMajorVersion = remoteVersion;
|
|
801
|
-
this.options.logger.info(`[senpi-skills-manager] handleMajorAvailable — major-update notification sent | skill=${skillName} version=${remoteVersion}`);
|
|
802
|
-
}
|
|
803
|
-
catch (err) {
|
|
804
|
-
this.options.logger.warn(`[senpi-skills-manager] failed to send major-update notification for "${skillName}": ${formatErr(err)}`);
|
|
805
|
-
// Leave dedup key unchanged — retry on next tick.
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
/**
|
|
809
|
-
* Send an aggregated notification summarising the outcome of this tick's
|
|
810
|
-
* updates. Successful installs and failures are shown in separate sections
|
|
811
|
-
* so the user can tell which skills still need attention. Sections with no
|
|
812
|
-
* entries are omitted. If neither section has entries, no notification is
|
|
813
|
-
* sent.
|
|
814
|
-
*
|
|
815
|
-
* Format (markdown source — see `formatSkillBlock` for why markdown
|
|
816
|
-
* rather than HTML):
|
|
817
|
-
*
|
|
818
|
-
* Senpi skills update:
|
|
819
|
-
*
|
|
820
|
-
* Updated:
|
|
821
|
-
*
|
|
822
|
-
* **skill-a**
|
|
823
|
-
* • v1.0.0 → v1.0.1
|
|
824
|
-
* • description: …
|
|
825
|
-
*
|
|
826
|
-
* **skill-b**
|
|
827
|
-
* • v2.0.0 → v2.0.1
|
|
828
|
-
* • description: …
|
|
829
|
-
*
|
|
830
|
-
* Failed to update:
|
|
831
|
-
*
|
|
832
|
-
* **skill-c**
|
|
833
|
-
* • v1.0.0 (install failed)
|
|
834
|
-
* • error: clone failed: …
|
|
835
|
-
*/
|
|
836
|
-
async sendUpdateSummary(succeededSkillNames, failedSkills) {
|
|
837
|
-
if (succeededSkillNames.length === 0 && failedSkills.length === 0)
|
|
838
|
-
return;
|
|
839
|
-
// ── Three-tier size budget ────────────────────────────────────────────
|
|
840
|
-
//
|
|
841
|
-
// Telegram caps a single message at 4096 chars; if we send more, the
|
|
842
|
-
// Bot API rejects with "MESSAGE_TOO_LONG" and the user sees nothing.
|
|
843
|
-
// Discovery-mode deployments with many skills + long descriptions can
|
|
844
|
-
// realistically blow past that. Lead's review comment
|
|
845
|
-
// (PR #78 #discussion_r3247822298): always render *something* useful,
|
|
846
|
-
// never silently drop the notification.
|
|
847
|
-
//
|
|
848
|
-
// Tier 1 (default — verbose): full per-skill blocks with descriptions.
|
|
849
|
-
// Tier 2 (compact): same blocks but with description lines omitted —
|
|
850
|
-
// keeps version transitions + error reasons, which are the
|
|
851
|
-
// actionable bits.
|
|
852
|
-
// Tier 3 (top-N cap): if even compact mode exceeds the budget, render
|
|
853
|
-
// only the first NOTIFICATION_SKILL_LIST_TOP_N of each section with a
|
|
854
|
-
// "… and N more" tail.
|
|
855
|
-
//
|
|
856
|
-
// The 4000-char default budget (vs Telegram's hard 4096) leaves headroom
|
|
857
|
-
// for the gateway's markdown→HTML conversion to add wrapping tags
|
|
858
|
-
// without pushing the payload over. Both the budget and the per-section
|
|
859
|
-
// cap are env-overridable (see `constants.ts`) so operators with
|
|
860
|
-
// alternative transports or different verbosity preferences can tune
|
|
861
|
-
// them without a code change.
|
|
862
|
-
const BUDGET_CHARS = TELEGRAM_NOTIFICATION_BUDGET_CHARS;
|
|
863
|
-
const TOP_N = NOTIFICATION_SKILL_LIST_TOP_N;
|
|
864
|
-
const build = (opts) => {
|
|
865
|
-
const succeededRendered = opts.cap !== undefined
|
|
866
|
-
? succeededSkillNames.slice(0, opts.cap)
|
|
867
|
-
: succeededSkillNames;
|
|
868
|
-
const failedRendered = opts.cap !== undefined
|
|
869
|
-
? failedSkills.slice(0, opts.cap)
|
|
870
|
-
: failedSkills;
|
|
871
|
-
const sections = [];
|
|
872
|
-
if (succeededSkillNames.length > 0) {
|
|
873
|
-
const blocks = succeededRendered.map((name) => this.formatSkillBlock(name, this.latestVersions.get(name) ?? null, null, {
|
|
874
|
-
omitDescription: opts.omitDescriptions,
|
|
875
|
-
}));
|
|
876
|
-
// Tail when capped: shows the omitted count so operators know the
|
|
877
|
-
// list isn't the complete picture (and can look at logs for full).
|
|
878
|
-
const tail = opts.cap !== undefined && succeededSkillNames.length > opts.cap
|
|
879
|
-
? `\n\n… and ${succeededSkillNames.length - opts.cap} more updated skill${succeededSkillNames.length - opts.cap === 1 ? "" : "s"} not shown`
|
|
880
|
-
: "";
|
|
881
|
-
sections.push(`Updated:\n\n${blocks.join("\n\n")}${tail}`);
|
|
882
|
-
}
|
|
883
|
-
if (failedSkills.length > 0) {
|
|
884
|
-
const blocks = failedRendered.map(({ name, reason }) => this.formatSkillBlock(name, null, reason, {
|
|
885
|
-
omitDescription: opts.omitDescriptions,
|
|
886
|
-
}));
|
|
887
|
-
const tail = opts.cap !== undefined && failedSkills.length > opts.cap
|
|
888
|
-
? `\n\n… and ${failedSkills.length - opts.cap} more failed skill${failedSkills.length - opts.cap === 1 ? "" : "s"} not shown`
|
|
889
|
-
: "";
|
|
890
|
-
sections.push(`Failed to update:\n\n${blocks.join("\n\n")}${tail}`);
|
|
891
|
-
}
|
|
892
|
-
return `Senpi skills update:\n\n${sections.join("\n\n")}`;
|
|
893
|
-
};
|
|
894
|
-
// Tier 1: verbose
|
|
895
|
-
let message = build({ omitDescriptions: false });
|
|
896
|
-
let tier = "verbose";
|
|
897
|
-
if (message.length > BUDGET_CHARS) {
|
|
898
|
-
// Tier 2: drop descriptions
|
|
899
|
-
message = build({ omitDescriptions: true });
|
|
900
|
-
tier = "compact (descriptions omitted)";
|
|
901
|
-
if (message.length > BUDGET_CHARS) {
|
|
902
|
-
// Tier 3: cap each section to top-N
|
|
903
|
-
message = build({ omitDescriptions: true, cap: TOP_N });
|
|
904
|
-
tier = `capped (top ${TOP_N} per section, descriptions omitted)`;
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
try {
|
|
908
|
-
await this.options.notify(message);
|
|
909
|
-
this.options.logger.info(`[senpi-skills-manager] sendUpdateSummary — notification sent | succeeded=${succeededSkillNames.length} failed=${failedSkills.length} tier=${tier} messageChars=${message.length}`);
|
|
910
|
-
}
|
|
911
|
-
catch (err) {
|
|
912
|
-
this.options.logger.warn(`[senpi-skills-manager] failed to send update summary notification: ${formatErr(err)}`);
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
741
|
/**
|
|
916
742
|
* Return the version-check function for this coordinator: either the
|
|
917
743
|
* injected `deps.checkSkillVersion` (unit tests) or the real
|
|
@@ -923,50 +749,29 @@ export class SkillsUpdateCoordinator {
|
|
|
923
749
|
*/
|
|
924
750
|
resolveCheckFn() {
|
|
925
751
|
return (this.options.deps?.checkSkillVersion ??
|
|
926
|
-
((skillsDirs, skillName, rawBaseUrl, logger) => checkSkillVersion(skillsDirs, skillName, rawBaseUrl, undefined, logger)));
|
|
752
|
+
((skillsDirs, skillName, rawBaseUrl, logger, remotePath) => checkSkillVersion(skillsDirs, skillName, rawBaseUrl, undefined, logger, remotePath)));
|
|
927
753
|
}
|
|
928
754
|
/**
|
|
929
|
-
* Resolve
|
|
930
|
-
*
|
|
931
|
-
*
|
|
932
|
-
*
|
|
933
|
-
*
|
|
934
|
-
* Per-dir read errors (other than ENOENT) are logged but do not abort the
|
|
935
|
-
* scan — a misconfigured secondary dir cannot mask skills present in the
|
|
936
|
-
* primary.
|
|
755
|
+
* Resolve a skill name to its concrete source (repo/branch/path/rawBaseUrl)
|
|
756
|
+
* from the manifest, applying any plugin-config per-skill overrides. The
|
|
757
|
+
* `SENPI_SKILLS_BRANCH` / `SENPI_SKILLS_REPO_URL` env testing knobs are
|
|
758
|
+
* applied inside {@link resolveSkillSource}.
|
|
937
759
|
*/
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
}
|
|
954
|
-
catch (err) {
|
|
955
|
-
// ENOENT for a dir that hasn't been created yet is benign — only
|
|
956
|
-
// log the noisier failure modes so operators can diagnose them.
|
|
957
|
-
if (err.code !== "ENOENT") {
|
|
958
|
-
this.options.logger.warn(`[senpi-skills-manager] resolveSkillNames — failed to read skillsDir, continuing | dir=${dir} err=${formatErr(err)}`);
|
|
959
|
-
}
|
|
960
|
-
}
|
|
961
|
-
}
|
|
962
|
-
if (!anySucceeded && seen.size === 0) {
|
|
963
|
-
// Every configured dir failed to read (or none exist). Previously the
|
|
964
|
-
// single-dir version warned in this case; preserve that signal so
|
|
965
|
-
// "no skills checked" is still distinguishable from "skillsDir
|
|
966
|
-
// configured wrong" in the logs.
|
|
967
|
-
this.options.logger.warn(`[senpi-skills-manager] resolveSkillNames — no readable skills dirs | dirs=${this.options.config.skillsDir.join(",")}`);
|
|
968
|
-
}
|
|
969
|
-
return Array.from(seen).sort();
|
|
760
|
+
resolveSource(skillName) {
|
|
761
|
+
return resolveSkillSource(skillName, { overrides: this.options.config.overrides });
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* The managed skill set for this tick — **always the manifest** set
|
|
765
|
+
* (`skills-manifest.json`), the single source of truth. `config.skills` and
|
|
766
|
+
* directory discovery no longer define the set: the manifest does, full stop
|
|
767
|
+
* (only `enabled: false` disables the manager entirely).
|
|
768
|
+
*
|
|
769
|
+
* `deps.listManagedSkills` is a test-only seam to substitute the set without
|
|
770
|
+
* a fixture manifest on disk; production always falls through to the
|
|
771
|
+
* manifest.
|
|
772
|
+
*/
|
|
773
|
+
resolveSkillNames() {
|
|
774
|
+
return this.options.deps?.listManagedSkills?.() ?? getManifestSkillNames();
|
|
970
775
|
}
|
|
971
776
|
/**
|
|
972
777
|
* Build the point-in-time snapshot returned by {@link getSnapshot}.
|
|
@@ -987,18 +792,26 @@ export class SkillsUpdateCoordinator {
|
|
|
987
792
|
* state rather than stale pre-action data.
|
|
988
793
|
*/
|
|
989
794
|
buildSnapshot(health, lastAction, lastError = null) {
|
|
990
|
-
const skillSnapshots = Object.values(this.state.skills).map((skillState) =>
|
|
991
|
-
name: skillState.name,
|
|
992
|
-
installedVersion: skillState.installedVersion,
|
|
795
|
+
const skillSnapshots = Object.values(this.state.skills).map((skillState) => {
|
|
993
796
|
// Prefer the in-memory map (freshest — populated synchronously inside
|
|
994
797
|
// the current tick before `persistState` runs) and fall back to the
|
|
995
798
|
// persisted field. The persisted fallback covers the post-restart
|
|
996
799
|
// window where the map is empty until the first external tick fires.
|
|
997
|
-
latestVersion
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
800
|
+
const latestVersion = this.latestVersions.get(skillState.name) ?? skillState.latestVersion ?? null;
|
|
801
|
+
return {
|
|
802
|
+
name: skillState.name,
|
|
803
|
+
installedVersion: skillState.installedVersion,
|
|
804
|
+
latestVersion,
|
|
805
|
+
lastCheckedAt: skillState.lastCheckedAt,
|
|
806
|
+
consecutiveFailures: skillState.consecutiveFailures,
|
|
807
|
+
lastFailureReason: skillState.lastFailureReason,
|
|
808
|
+
// A major is pending when the latest observed major is **gated** — i.e.
|
|
809
|
+
// it exceeds the effective ceiling (max(maxMajor, installedMajor)) and so
|
|
810
|
+
// the tick won't auto-apply it. A major within the ceiling is omitted
|
|
811
|
+
// (it auto-applies on the next tick).
|
|
812
|
+
majorPending: isMajorGated(majorOf(latestVersion), majorOf(skillState.installedVersion), this.resolveSource(skillState.name).maxMajor),
|
|
813
|
+
};
|
|
814
|
+
});
|
|
1002
815
|
return {
|
|
1003
816
|
enabled: this.options.config.enabled,
|
|
1004
817
|
// Snapshot is a serializable view consumed by gateway RPCs and the CLI;
|
|
@@ -1020,18 +833,4 @@ export class SkillsUpdateCoordinator {
|
|
|
1020
833
|
}
|
|
1021
834
|
}
|
|
1022
835
|
}
|
|
1023
|
-
// `escapeHtml` was removed alongside the switch from HTML to markdown
|
|
1024
|
-
// notification source — see `formatSkillBlock` for why. The openclaw
|
|
1025
|
-
// Telegram channel runs `markdownToTelegramHtml` over our text and
|
|
1026
|
-
// handles HTML escaping itself on the converter's output. Pre-escaping
|
|
1027
|
-
// at this layer would double-escape (e.g. `&` becomes `&amp;`)
|
|
1028
|
-
// and corrupt the rendered output.
|
|
1029
|
-
//
|
|
1030
|
-
// Markdown-special characters (`*`, `_`, `[`, `]`, backtick) in dynamic
|
|
1031
|
-
// values aren't escaped either — descriptions that legitimately contain
|
|
1032
|
-
// these would author them as markdown intentionally; skill names in the
|
|
1033
|
-
// senpi-skills repo are kebab-case identifiers; error messages from
|
|
1034
|
-
// `formatErr` are typically prose. If a future skill description ever
|
|
1035
|
-
// breaks the rendering, the fix is to author the SKILL.md frontmatter
|
|
1036
|
-
// using proper markdown.
|
|
1037
836
|
//# sourceMappingURL=coordinator.js.map
|