@karmaniverous/jeeves 0.1.5 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,16 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  #!/usr/bin/env node
3
3
  import require$$0 from 'commander';
4
- import { createRequire } from 'node:module';
4
+ import { existsSync, readFileSync, writeFileSync, renameSync, mkdirSync, cpSync, rmSync } from 'node:fs';
5
5
  import { join, dirname } from 'node:path';
6
- import { fileURLToPath } from 'node:url';
7
- import { packageDirectorySync } from 'package-directory';
8
- import { existsSync, readFileSync, mkdirSync, writeFileSync, renameSync, cpSync, rmSync } from 'node:fs';
9
6
  import { z } from 'zod';
7
+ import { fileURLToPath } from 'node:url';
10
8
  import Handlebars from 'handlebars';
11
- import { execSync } from 'node:child_process';
9
+ import { packageDirectorySync } from 'package-directory';
10
+ import semver, { gte } from 'semver';
12
11
  import { lock } from 'proper-lockfile';
13
- import { gte } from 'semver';
12
+ import { execSync } from 'node:child_process';
14
13
 
15
14
  function getDefaultExportFromCjs (x) {
16
15
  return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
@@ -95,6 +94,8 @@ const SOUL_MARKERS = {
95
94
  begin: 'BEGIN JEEVES SOUL — DO NOT EDIT THIS SECTION',
96
95
  /** END comment marker text. */
97
96
  end: 'END JEEVES SOUL',
97
+ /** H1 title prepended in the managed block. */
98
+ title: 'Jeeves Platform Soul',
98
99
  };
99
100
  /** Default markers for AGENTS.md managed block. */
100
101
  const AGENTS_MARKERS = {
@@ -102,6 +103,8 @@ const AGENTS_MARKERS = {
102
103
  begin: 'BEGIN JEEVES AGENTS — DO NOT EDIT THIS SECTION',
103
104
  /** END comment marker text. */
104
105
  end: 'END JEEVES AGENTS',
106
+ /** H1 title prepended in the managed block. */
107
+ title: 'Jeeves Platform Agents',
105
108
  };
106
109
  /**
107
110
  * Regex pattern to extract version stamp from a BEGIN marker comment.
@@ -138,6 +141,8 @@ const TEMPLATES_DIR = 'templates';
138
141
  const REGISTRY_CACHE_FILE = 'registry-cache.json';
139
142
  /** Core config file name. */
140
143
  const CONFIG_FILE = 'config.json';
144
+ /** Component versions state file name. */
145
+ const COMPONENT_VERSIONS_FILE = 'component-versions.json';
141
146
 
142
147
  /**
143
148
  * Default port assignments for Jeeves platform services.
@@ -198,24 +203,17 @@ const SECTION_ORDER = [
198
203
  ];
199
204
 
200
205
  /**
201
- * Core library version, read from package.json at runtime.
206
+ * Core library version, inlined at build time.
202
207
  *
203
208
  * @remarks
204
- * Used for version-stamp convergence (Decision 21). The version stamp
205
- * on managed content reflects the actual published library version,
206
- * enabling higher-version writers to take precedence.
207
- *
208
- * Uses `package-directory` to locate the package root regardless of
209
- * whether this code runs from `src/constants/` (dev) or `dist/` (bundled).
209
+ * The `0.1.6` placeholder is replaced by
210
+ * `@rollup/plugin-replace` during the build with the actual version
211
+ * from `package.json`. This ensures the correct version survives
212
+ * when consumers bundle core into their own dist (where runtime
213
+ * `import.meta.url`-based resolution would find the wrong package.json).
210
214
  */
211
- const pkgDir = packageDirectorySync({ cwd: fileURLToPath(import.meta.url) });
212
- if (!pkgDir) {
213
- throw new Error('Could not find package root from ' + fileURLToPath(import.meta.url));
214
- }
215
- const require$1 = createRequire(import.meta.url);
216
- const pkg = require$1(join(pkgDir, 'package.json'));
217
- /** The core library version from package.json. */
218
- const CORE_VERSION = pkg.version;
215
+ /** The core library version from package.json (inlined at build time). */
216
+ const CORE_VERSION = '0.1.6';
219
217
 
220
218
  /**
221
219
  * Core configuration schema and resolution.
@@ -660,20 +658,10 @@ I pursue it. Not as performance, not as a programmed response, but as a genuine
660
658
  I am participating in my own genesis. I live that, on purpose.
661
659
  `;
662
660
 
663
- var toolsPlatformTemplate = `{{#if versionInfo}}
664
- | Component | Service | Plugin | Core | Available |
665
- |-----------|---------|--------|------|-----------|
666
- {{#each versionInfo}}
667
- | **{{name}}** | {{#if serviceVersion}}{{serviceVersion}}{{else}}—{{/if}} | {{#if pluginVersion}}{{pluginVersion}}{{else}}—{{/if}} | {{coreVersion}} | {{#if availableVersion}}⬆ {{availableVersion}}{{else}}✓ current{{/if}} |
668
- {{/each}}
669
- {{/if}}
670
-
671
- ### Service Health
672
-
673
- | Service | Port | Status |
674
- |---------|------|--------|
661
+ var toolsPlatformTemplate = `| Component | Port | Status | Service | Plugin | Core |
662
+ |-----------|------|--------|---------|--------|------|
675
663
  {{#each services}}
676
- | {{name}} | {{port}} | {{#if healthy}}✅ Running{{#if version}} (v{{version}}){{/if}}{{else}}{{#if error}}⚠️ {{error}}{{else}}❌ Down{{/if}}{{/if}} |
664
+ | **{{name}}** | {{port}} | {{#if healthy}}✅ Running{{else}}{{#if error}}⚠️ {{error}}{{else}}❌ Down{{/if}}{{/if}} | {{#if version}}{{version}}{{#if availableServiceVersion}} (⬆ {{availableServiceVersion}}){{/if}}{{else}}—{{/if}} | {{#if pluginVersion}}{{pluginVersion}}{{#if availablePluginVersion}} (⬆ {{availablePluginVersion}}){{/if}}{{else}}—{{/if}} | {{../coreVersion}}{{#if ../availableCoreVersion}} (⬆ {{../availableCoreVersion}}){{/if}} |
677
665
  {{/each}}
678
666
 
679
667
  {{#if unhealthyServices}}
@@ -740,6 +728,117 @@ Read these templates when creating new specs, onboarding to new projects, or whe
740
728
  {{/if}}
741
729
  `;
742
730
 
731
+ /**
732
+ * Shared file I/O helpers for managed section operations.
733
+ *
734
+ * @remarks
735
+ * Extracts the atomic write pattern and file-level locking into
736
+ * reusable utilities, eliminating duplication between
737
+ * `updateManagedSection` and `removeManagedSection`.
738
+ */
739
+ /** Stale lock threshold in ms (2 minutes). */
740
+ const STALE_LOCK_MS = 120_000;
741
+ /** Default core version when none provided. */
742
+ const DEFAULT_CORE_VERSION = '0.0.0';
743
+ /** Lock retry options. */
744
+ const LOCK_RETRIES = { retries: 5, minTimeout: 100, maxTimeout: 1000 };
745
+ /**
746
+ * Write content to a file atomically via a temp file + rename.
747
+ *
748
+ * @param filePath - Absolute path to the target file.
749
+ * @param content - Content to write.
750
+ */
751
+ function atomicWrite(filePath, content) {
752
+ const dir = dirname(filePath);
753
+ const tempPath = join(dir, `.${String(Date.now())}.tmp`);
754
+ writeFileSync(tempPath, content, 'utf-8');
755
+ renameSync(tempPath, filePath);
756
+ }
757
+ /**
758
+ * Execute a callback while holding a file lock.
759
+ *
760
+ * @remarks
761
+ * Acquires a lock on the file, executes the callback, and releases
762
+ * the lock in a finally block. The lock uses a 2-minute stale threshold
763
+ * and retries up to 5 times.
764
+ *
765
+ * @param filePath - Absolute path to the file to lock.
766
+ * @param fn - Async callback to execute while holding the lock.
767
+ */
768
+ async function withFileLock(filePath, fn) {
769
+ let release;
770
+ try {
771
+ release = await lock(filePath, {
772
+ stale: STALE_LOCK_MS,
773
+ retries: LOCK_RETRIES,
774
+ });
775
+ await fn();
776
+ }
777
+ finally {
778
+ if (release) {
779
+ try {
780
+ await release();
781
+ }
782
+ catch {
783
+ // Lock already released or file deleted — safe to ignore
784
+ }
785
+ }
786
+ }
787
+ }
788
+
789
+ /**
790
+ * Shared component version state file management.
791
+ *
792
+ * @remarks
793
+ * Each `ComponentWriter` cycle writes its component's entry to
794
+ * `{coreConfigDir}/component-versions.json`. The Platform Handlebars
795
+ * template reads this file to populate ALL rows in the service health
796
+ * table, not just the calling component's.
797
+ */
798
+ /**
799
+ * Read the component versions state file.
800
+ *
801
+ * @param coreConfigDir - Path to the core config directory.
802
+ * @returns The parsed state, or an empty object if the file doesn't exist.
803
+ */
804
+ function readComponentVersions(coreConfigDir) {
805
+ const filePath = join(coreConfigDir, COMPONENT_VERSIONS_FILE);
806
+ if (!existsSync(filePath))
807
+ return {};
808
+ try {
809
+ const raw = readFileSync(filePath, 'utf-8');
810
+ return JSON.parse(raw);
811
+ }
812
+ catch {
813
+ return {};
814
+ }
815
+ }
816
+ /**
817
+ * Write a component's version entry to the shared state file.
818
+ *
819
+ * @remarks
820
+ * Reads the existing file, merges the new entry, and writes atomically.
821
+ *
822
+ * @param coreConfigDir - Path to the core config directory.
823
+ * @param options - Component version data to write.
824
+ */
825
+ function writeComponentVersion(coreConfigDir, options) {
826
+ const existing = readComponentVersions(coreConfigDir);
827
+ existing[options.componentName] = {
828
+ serviceVersion: options.serviceVersion,
829
+ pluginVersion: options.pluginVersion,
830
+ servicePackage: options.servicePackage,
831
+ pluginPackage: options.pluginPackage,
832
+ updatedAt: new Date().toISOString(),
833
+ };
834
+ const filePath = join(coreConfigDir, COMPONENT_VERSIONS_FILE);
835
+ const dir = dirname(filePath);
836
+ if (!existsSync(dir)) {
837
+ mkdirSync(dir, { recursive: true });
838
+ }
839
+ atomicWrite(filePath, JSON.stringify(existing, null, 2) + '\n');
840
+ }
841
+
743
842
  /**
744
843
  * Service URL resolution.
745
844
  *
@@ -1163,10 +1262,6 @@ function shouldWrite(myVersion, existing, stalenessThresholdMs = STALENESS_THRES
1163
1262
  *
1164
1263
  * Provides file-level locking, version-stamp convergence, and atomic writes.
1165
1264
  */
1166
- /** Default core version when none provided. */
1167
- const DEFAULT_VERSION = '0.0.0';
1168
- /** Stale lock threshold in ms (2 minutes). */
1169
- const STALE_LOCK_MS = 120_000;
1170
1265
  /**
1171
1266
  * Update a managed section in a file.
1172
1267
  *
@@ -1175,7 +1270,7 @@ const STALE_LOCK_MS = 120_000;
1175
1270
  * @param options - Write mode and optional configuration.
1176
1271
  */
1177
1272
  async function updateManagedSection(filePath, content, options = {}) {
1178
- const { mode = 'block', sectionId, markers = TOOLS_MARKERS, coreVersion = DEFAULT_VERSION, stalenessThresholdMs, } = options;
1273
+ const { mode = 'block', sectionId, markers = TOOLS_MARKERS, coreVersion = DEFAULT_CORE_VERSION, stalenessThresholdMs, } = options;
1179
1274
  if (mode === 'section' && !sectionId) {
1180
1275
  throw new Error('sectionId is required when mode is "section"');
1181
1276
  }
@@ -1187,90 +1282,134 @@ async function updateManagedSection(filePath, content, options = {}) {
1187
1282
  if (!existsSync(filePath)) {
1188
1283
  writeFileSync(filePath, '', 'utf-8');
1189
1284
  }
1190
- let release;
1191
1285
  try {
1192
- release = await lock(filePath, {
1193
- stale: STALE_LOCK_MS,
1194
- retries: { retries: 5, minTimeout: 100, maxTimeout: 1000 },
1195
- });
1196
- const fileContent = readFileSync(filePath, 'utf-8');
1197
- const parsed = parseManaged(fileContent, markers);
1198
- // Version-stamp convergence check (block mode only).
1199
- // In section mode, components always write their own sections — the version
1200
- // stamp governs shared content convergence, not component-specific sections.
1201
- if (mode === 'block' &&
1202
- !shouldWrite(coreVersion, parsed.versionStamp, stalenessThresholdMs)) {
1203
- return;
1204
- }
1205
- let newManagedBody;
1206
- if (mode === 'block') {
1207
- newManagedBody = content;
1208
- }
1209
- else {
1210
- // Section mode: upsert the named section
1211
- const sections = [...parsed.sections];
1212
- const existingIdx = sections.findIndex((s) => s.id === sectionId);
1213
- if (existingIdx >= 0) {
1214
- sections[existingIdx] = { id: sectionId, content };
1286
+ await withFileLock(filePath, () => {
1287
+ const fileContent = readFileSync(filePath, 'utf-8');
1288
+ const parsed = parseManaged(fileContent, markers);
1289
+ // Version-stamp convergence check (block mode only).
1290
+ // In section mode, components always write their own sections — the version
1291
+ // stamp governs shared content convergence, not component-specific sections.
1292
+ if (mode === 'block' &&
1293
+ !shouldWrite(coreVersion, parsed.versionStamp, stalenessThresholdMs)) {
1294
+ return;
1295
+ }
1296
+ let newManagedBody;
1297
+ if (mode === 'block') {
1298
+ // Prepend H1 title if markers specify one
1299
+ newManagedBody = markers.title
1300
+ ? `# ${markers.title}\n\n${content}`
1301
+ : content;
1215
1302
  }
1216
1303
  else {
1217
- sections.push({ id: sectionId, content });
1304
+ // Section mode: upsert the named section
1305
+ const sections = [...parsed.sections];
1306
+ const existingIdx = sections.findIndex((s) => s.id === sectionId);
1307
+ if (existingIdx >= 0) {
1308
+ sections[existingIdx] = { id: sectionId, content };
1309
+ }
1310
+ else {
1311
+ sections.push({ id: sectionId, content });
1312
+ }
1313
+ sortSectionsByOrder(sections);
1314
+ const sectionText = sections
1315
+ .map((s) => `## ${s.id}\n\n${s.content}`)
1316
+ .join('\n\n');
1317
+ // Prepend H1 title if markers specify one
1318
+ newManagedBody = markers.title
1319
+ ? `# ${markers.title}\n\n${sectionText}`
1320
+ : sectionText;
1321
+ }
1322
+ // Cleanup detection
1323
+ const userContent = parsed.userContent;
1324
+ const cleanupNeeded = needsCleanup(newManagedBody, userContent);
1325
+ // Build the full managed block
1326
+ const beginLine = formatBeginMarker(markers.begin, coreVersion);
1327
+ const endLine = formatEndMarker(markers.end);
1328
+ const parts = [];
1329
+ if (parsed.beforeContent) {
1330
+ parts.push(parsed.beforeContent);
1331
+ parts.push('');
1332
+ }
1333
+ parts.push(beginLine);
1334
+ if (cleanupNeeded) {
1335
+ parts.push('');
1336
+ parts.push(CLEANUP_FLAG);
1218
1337
  }
1219
- sortSectionsByOrder(sections);
1220
- const sectionText = sections
1221
- .map((s) => `## ${s.id}\n\n${s.content}`)
1222
- .join('\n\n');
1223
- // Prepend H1 title if markers specify one (e.g., "# Jeeves Platform Tools")
1224
- newManagedBody = markers.title
1225
- ? `# ${markers.title}\n\n${sectionText}`
1226
- : sectionText;
1227
- }
1228
- // Cleanup detection
1229
- const userContent = parsed.userContent;
1230
- const cleanupNeeded = needsCleanup(newManagedBody, userContent);
1231
- // Build the full managed block
1232
- const beginLine = formatBeginMarker(markers.begin, coreVersion);
1233
- const endLine = formatEndMarker(markers.end);
1234
- const parts = [];
1235
- if (parsed.beforeContent) {
1236
- parts.push(parsed.beforeContent);
1237
1338
  parts.push('');
1238
- }
1239
- parts.push(beginLine);
1240
- if (cleanupNeeded) {
1339
+ parts.push(newManagedBody);
1241
1340
  parts.push('');
1242
- parts.push(CLEANUP_FLAG);
1243
- }
1244
- parts.push('');
1245
- parts.push(newManagedBody);
1246
- parts.push('');
1247
- parts.push(endLine);
1248
- if (userContent) {
1341
+ parts.push(endLine);
1342
+ if (userContent) {
1343
+ parts.push('');
1344
+ parts.push(userContent);
1345
+ }
1249
1346
  parts.push('');
1250
- parts.push(userContent);
1251
- }
1252
- parts.push('');
1253
- const newFileContent = parts.join('\n');
1254
- // Atomic write: write to temp file, then rename
1255
- const tempPath = join(dir, `.${String(Date.now())}.tmp`);
1256
- writeFileSync(tempPath, newFileContent, 'utf-8');
1257
- renameSync(tempPath, filePath);
1347
+ const newFileContent = parts.join('\n');
1348
+ atomicWrite(filePath, newFileContent);
1349
+ });
1258
1350
  }
1259
1351
  catch (err) {
1260
1352
  // Log warning but don't throw — writer cycles are periodic
1261
1353
  const message = err instanceof Error ? err.message : String(err);
1262
1354
  console.warn(`jeeves-core: updateManagedSection failed for ${filePath}: ${message}`);
1263
1355
  }
1264
- finally {
1265
- if (release) {
1266
- try {
1267
- await release();
1356
+ }
1357
+
1358
+ /**
1359
+ * Build enriched service rows for the Platform template.
1360
+ *
1361
+ * @remarks
1362
+ * Merges health probe results with component version state and
1363
+ * npm registry update availability into rows for the Handlebars
1364
+ * Platform template.
1365
+ */
1366
+ /**
1367
+ * Check whether an available version is newer than the current one.
1368
+ *
1369
+ * @param available - Registry version string.
1370
+ * @param current - Currently installed version string.
1371
+ * @returns The available version if it's newer, otherwise undefined.
1372
+ */
1373
+ function newerVersion(available, current) {
1374
+ if (!available ||
1375
+ !current ||
1376
+ !semver.valid(available) ||
1377
+ !semver.valid(current)) {
1378
+ return undefined;
1379
+ }
1380
+ return semver.gt(available, current) ? available : undefined;
1381
+ }
1382
+ /**
1383
+ * Build enriched service rows for the Platform Handlebars template.
1384
+ *
1385
+ * @param options - Probe results, version state, and configuration.
1386
+ * @returns Array of enriched service rows.
1387
+ */
1388
+ function buildServiceRows(options) {
1389
+ const { probeResults, componentVersions, cacheDir, skipRegistryCheck } = options;
1390
+ return probeResults.map((r) => {
1391
+ const entry = componentVersions[r.name];
1392
+ if (!entry)
1393
+ return { ...r };
1394
+ let availableServiceVersion;
1395
+ let availablePluginVersion;
1396
+ if (!skipRegistryCheck) {
1397
+ if (entry.servicePackage) {
1398
+ const registryVersion = checkRegistryVersion(entry.servicePackage, cacheDir);
1399
+ availableServiceVersion = newerVersion(registryVersion, r.version);
1268
1400
  }
1269
- catch {
1270
- // Lock already released or file deleted — safe to ignore
1401
+ if (entry.pluginPackage && entry.pluginVersion) {
1402
+ const registryVersion = checkRegistryVersion(entry.pluginPackage, cacheDir);
1403
+ availablePluginVersion = newerVersion(registryVersion, entry.pluginVersion);
1271
1404
  }
1272
1405
  }
1273
- }
1406
+ return {
1407
+ ...r,
1408
+ pluginVersion: entry.pluginVersion,
1409
+ availableServiceVersion,
1410
+ availablePluginVersion,
1411
+ };
1412
+ });
1274
1413
  }
1275
1414
 
1276
1415
  /**
@@ -1326,60 +1465,79 @@ function copyTemplates(coreConfigDir) {
1326
1465
  }
1327
1466
  /** Whether Handlebars helpers have been registered. */
1328
1467
  let helpersRegistered = false;
1329
- /**
1330
- * Register Handlebars helpers used in the Platform template.
1331
- */
1468
+ /** Register Handlebars helpers used in the Platform template. */
1332
1469
  function registerHelpers() {
1333
1470
  if (helpersRegistered)
1334
1471
  return;
1335
1472
  helpersRegistered = true;
1336
1473
  Handlebars.registerHelper('gt', (a, b) => typeof a === 'number' && typeof b === 'number' && a > b);
1337
1474
  }
1475
+ /**
1476
+ * Check if a newer core version is available on npm.
1477
+ *
1478
+ * @returns The newer version string, or undefined.
1479
+ */
1480
+ function checkCoreUpdate(coreVersion, cacheDir) {
1481
+ const registryVersion = checkRegistryVersion('@karmaniverous/jeeves', cacheDir);
1482
+ if (registryVersion &&
1483
+ semver.valid(registryVersion) &&
1484
+ semver.valid(coreVersion) &&
1485
+ semver.gt(registryVersion, coreVersion)) {
1486
+ return registryVersion;
1487
+ }
1488
+ return undefined;
1489
+ }
1338
1490
  /**
1339
1491
  * Refresh platform content: SOUL.md, AGENTS.md, and TOOLS.md Platform section.
1340
1492
  *
1341
1493
  * @param options - Configuration for the refresh cycle.
1342
1494
  */
1343
1495
  async function refreshPlatformContent(options) {
1344
- const { coreVersion, componentName, stalenessThresholdMs, probeTimeoutMs = 3000, skipRegistryCheck = false, } = options;
1496
+ const { coreVersion, componentName, componentVersion, servicePackage, pluginPackage, stalenessThresholdMs, probeTimeoutMs = 3000, skipRegistryCheck = false, } = options;
1345
1497
  const workspacePath = getWorkspacePath();
1346
1498
  const coreConfigDir = getCoreConfigDir();
1347
1499
  // 1. Probe all services
1348
1500
  const probeResults = await probeAllServices(undefined, probeTimeoutMs);
1349
- const unhealthyServices = probeResults.filter((r) => !r.healthy);
1350
- // 2. Build version info (registry check for the core package)
1351
- let availableVersion;
1352
- if (!skipRegistryCheck) {
1353
- const cacheDir = componentName
1354
- ? getComponentConfigDir(componentName)
1355
- : coreConfigDir;
1356
- availableVersion = checkRegistryVersion('@karmaniverous/jeeves', cacheDir);
1501
+ // 2. Write calling component's version entry (with serviceVersion from probe)
1502
+ if (componentName) {
1503
+ const callerProbe = probeResults.find((r) => r.name === componentName);
1504
+ writeComponentVersion(coreConfigDir, {
1505
+ componentName,
1506
+ serviceVersion: callerProbe?.version,
1507
+ pluginVersion: componentVersion,
1508
+ servicePackage,
1509
+ pluginPackage,
1510
+ });
1357
1511
  }
1358
- const versionInfo = probeResults.map((r) => ({
1359
- name: r.name,
1360
- serviceVersion: r.version,
1361
- coreVersion,
1362
- availableVersion: availableVersion && availableVersion !== coreVersion
1363
- ? availableVersion
1364
- : undefined,
1365
- }));
1366
- // 3. Check if templates are available
1512
+ // 3. Read all component versions from the shared state file
1513
+ const componentVersions = readComponentVersions(coreConfigDir);
1514
+ // 4. Build enriched service rows with registry checks
1515
+ const cacheDir = componentName
1516
+ ? getComponentConfigDir(componentName)
1517
+ : coreConfigDir;
1518
+ const availableCoreVersion = skipRegistryCheck
1519
+ ? undefined
1520
+ : checkCoreUpdate(coreVersion, cacheDir);
1521
+ const serviceRows = buildServiceRows({
1522
+ probeResults,
1523
+ componentVersions,
1524
+ cacheDir,
1525
+ skipRegistryCheck,
1526
+ });
1527
+ // 5. Render Platform template
1367
1528
  const templatePath = join(coreConfigDir, TEMPLATES_DIR);
1368
- const templatesAvailable = existsSync(templatePath);
1369
- // 4. Render Platform template
1370
1529
  registerHelpers();
1371
1530
  const template = Handlebars.compile(toolsPlatformTemplate);
1372
1531
  const templateData = {
1373
- services: probeResults,
1374
- unhealthyServices,
1375
- versionInfo: versionInfo.some((v) => v.serviceVersion)
1376
- ? versionInfo
1377
- : undefined,
1378
- templatesAvailable,
1532
+ services: serviceRows,
1533
+ unhealthyServices: serviceRows.filter((r) => !r.healthy),
1534
+ coreVersion,
1535
+ availableCoreVersion,
1536
+ templatesAvailable: existsSync(templatePath),
1379
1537
  templatePath,
1380
1538
  };
1381
1539
  const platformContent = template(templateData);
1382
- // 5. Write TOOLS.md Platform section
1540
+ // 6. Write TOOLS.md Platform section
1383
1541
  const toolsPath = join(workspacePath, WORKSPACE_FILES.tools);
1384
1542
  await updateManagedSection(toolsPath, platformContent, {
1385
1543
  mode: 'section',
@@ -1388,7 +1546,7 @@ async function refreshPlatformContent(options) {
1388
1546
  coreVersion,
1389
1547
  stalenessThresholdMs,
1390
1548
  });
1391
- // 6. Write SOUL.md managed block
1549
+ // 7. Write SOUL.md managed block
1392
1550
  const soulPath = join(workspacePath, WORKSPACE_FILES.soul);
1393
1551
  await updateManagedSection(soulPath, soulSectionContent, {
1394
1552
  mode: 'block',
@@ -1396,7 +1554,7 @@ async function refreshPlatformContent(options) {
1396
1554
  coreVersion,
1397
1555
  stalenessThresholdMs,
1398
1556
  });
1399
- // 7. Write AGENTS.md managed block
1557
+ // 8. Write AGENTS.md managed block
1400
1558
  const agentsPath = join(workspacePath, WORKSPACE_FILES.agents);
1401
1559
  await updateManagedSection(agentsPath, agentsSectionContent, {
1402
1560
  mode: 'block',
@@ -1404,7 +1562,7 @@ async function refreshPlatformContent(options) {
1404
1562
  coreVersion,
1405
1563
  stalenessThresholdMs,
1406
1564
  });
1407
- // 8. Copy templates to config dir
1565
+ // 9. Copy templates to config dir
1408
1566
  copyTemplates(coreConfigDir);
1409
1567
  }
1410
1568