@bli-cockpit/cli 0.2.242 → 0.2.244

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 (135) hide show
  1. package/dist/adapters/git-state.js +1 -0
  2. package/dist/adapters/raw-evidence-budget-admission.js +63 -0
  3. package/dist/adapters/raw-evidence-collection-state.js +6 -50
  4. package/dist/adapters/raw-evidence-git-diff.js +1 -0
  5. package/dist/agent-rules.js +8 -0
  6. package/dist/autostart-contract.js +12 -0
  7. package/dist/autostart-darwin-plist.js +2 -2
  8. package/dist/autostart-darwin.js +1 -1
  9. package/dist/autostart-remind.js +245 -0
  10. package/dist/autostart-self-heal.js +17 -1
  11. package/dist/autostart-windows-scripts.js +7 -7
  12. package/dist/autostart.js +16 -15
  13. package/dist/commands/approvals-check-where.js +14 -0
  14. package/dist/commands/approvals-render.js +8 -1
  15. package/dist/commands/approvals.js +3 -2
  16. package/dist/commands/autostart-command.js +10 -0
  17. package/dist/commands/autostart-heal.js +36 -1
  18. package/dist/commands/brain.js +1 -1
  19. package/dist/commands/cal-agenda.js +44 -0
  20. package/dist/commands/cal-availability-share.js +97 -0
  21. package/dist/commands/cal-booking.js +147 -0
  22. package/dist/commands/cal-join.js +62 -0
  23. package/dist/commands/cal-links.js +68 -0
  24. package/dist/commands/cal-people.js +136 -0
  25. package/dist/commands/cal-remind.js +176 -0
  26. package/dist/commands/cal-reminders.js +33 -0
  27. package/dist/commands/cal-suggest.js +89 -0
  28. package/dist/commands/cal.js +41 -1
  29. package/dist/commands/cycle-args.js +127 -0
  30. package/dist/commands/cycles.js +117 -0
  31. package/dist/commands/docs-comments-args.js +85 -0
  32. package/dist/commands/docs-comments.js +128 -0
  33. package/dist/commands/docs-files-args.js +49 -0
  34. package/dist/commands/docs-files.js +179 -0
  35. package/dist/commands/docs-formatting.js +45 -0
  36. package/dist/commands/docs-inbox-args.js +93 -0
  37. package/dist/commands/docs-inbox.js +126 -0
  38. package/dist/commands/docs-notion.js +5 -0
  39. package/dist/commands/docs-read.js +2 -1
  40. package/dist/commands/docs-recovery.js +173 -0
  41. package/dist/commands/docs-refs-args.js +104 -0
  42. package/dist/commands/docs-refs.js +153 -0
  43. package/dist/commands/docs-search-args.js +79 -0
  44. package/dist/commands/docs-search.js +64 -0
  45. package/dist/commands/docs-sync-args.js +82 -0
  46. package/dist/commands/docs-sync.js +197 -0
  47. package/dist/commands/docs-tree.js +136 -0
  48. package/dist/commands/docs.js +85 -51
  49. package/dist/commands/doctor-update.js +4 -1
  50. package/dist/commands/inbox-args.js +81 -0
  51. package/dist/commands/inbox.js +88 -0
  52. package/dist/commands/issue-batch.js +61 -0
  53. package/dist/commands/issue-relations.js +151 -0
  54. package/dist/commands/issue-write.js +38 -5
  55. package/dist/commands/issue.js +32 -1
  56. package/dist/commands/local-args-collector-sources.js +2 -1
  57. package/dist/commands/local-args-tower-approvals.js +16 -4
  58. package/dist/commands/local-args-tower-cal-booking.js +81 -0
  59. package/dist/commands/local-args-tower-cal-links.js +42 -0
  60. package/dist/commands/local-args-tower-cal-remind.js +74 -0
  61. package/dist/commands/local-args-tower-cal-share.js +41 -0
  62. package/dist/commands/local-args-tower-cal.js +81 -8
  63. package/dist/commands/local-args-tower-docs-msg.js +98 -9
  64. package/dist/commands/local-args-tower-msg.js +222 -10
  65. package/dist/commands/local-args-tower-search.js +32 -5
  66. package/dist/commands/local-args-tower-work.js +110 -34
  67. package/dist/commands/local-auth.js +1 -1
  68. package/dist/commands/local-help-commands-tower-ops.js +6 -4
  69. package/dist/commands/local-help-commands-tower.js +47 -31
  70. package/dist/commands/local-help-commands.js +3 -1
  71. package/dist/commands/local-help-cycles.js +9 -0
  72. package/dist/commands/local-help-docs-files.js +10 -0
  73. package/dist/commands/local-help-docs-formatting.js +7 -0
  74. package/dist/commands/local-help-docs-inbox.js +10 -0
  75. package/dist/commands/local-help-docs-recovery.js +10 -0
  76. package/dist/commands/local-help-docs-refs.js +11 -0
  77. package/dist/commands/local-help-docs-sync.js +9 -0
  78. package/dist/commands/local-help-inbox.js +9 -0
  79. package/dist/commands/local-help-project.js +21 -0
  80. package/dist/commands/local-help-templates.js +9 -0
  81. package/dist/commands/local-help-views.js +10 -0
  82. package/dist/commands/local-help-work-notify.js +6 -0
  83. package/dist/commands/local-help.js +3 -3
  84. package/dist/commands/manager-hints.js +4 -2
  85. package/dist/commands/manager-limits.json +3 -1
  86. package/dist/commands/manager-read-reasons.js +1 -0
  87. package/dist/commands/manager-render-home.js +13 -5
  88. package/dist/commands/manager-render.js +3 -0
  89. package/dist/commands/manager.js +3 -1
  90. package/dist/commands/mcp-stdio-probe.js +1 -0
  91. package/dist/commands/msg-activity.js +134 -0
  92. package/dist/commands/msg-channel-args.js +92 -0
  93. package/dist/commands/msg-channel.js +125 -0
  94. package/dist/commands/msg-context-args.js +67 -0
  95. package/dist/commands/msg-context.js +65 -0
  96. package/dist/commands/msg-dm.js +28 -0
  97. package/dist/commands/msg-file.js +185 -0
  98. package/dist/commands/msg-mark-args.js +78 -0
  99. package/dist/commands/msg-mark.js +148 -0
  100. package/dist/commands/msg-notify-args.js +100 -0
  101. package/dist/commands/msg-notify.js +122 -0
  102. package/dist/commands/msg-schedule-args.js +114 -0
  103. package/dist/commands/msg-schedule.js +226 -0
  104. package/dist/commands/msg-search-args.js +71 -0
  105. package/dist/commands/msg-search.js +48 -0
  106. package/dist/commands/msg-write.js +15 -1
  107. package/dist/commands/msg.js +62 -39
  108. package/dist/commands/project-args.js +82 -0
  109. package/dist/commands/project.js +71 -7
  110. package/dist/commands/public-root.js +1 -1
  111. package/dist/commands/search.js +23 -0
  112. package/dist/commands/session-sync-failures.js +17 -4
  113. package/dist/commands/session-sync-health.js +7 -2
  114. package/dist/commands/session-sync-hold.js +42 -1
  115. package/dist/commands/session-sync-outside-roots.js +72 -14
  116. package/dist/commands/session-sync-upload.js +2 -0
  117. package/dist/commands/session-sync.js +1 -0
  118. package/dist/commands/sources-check.js +63 -2
  119. package/dist/commands/sources-discovery.js +1 -1
  120. package/dist/commands/sources-reconciliation.js +87 -0
  121. package/dist/commands/sync-doctor.js +14 -1
  122. package/dist/commands/sync-report.js +1 -1
  123. package/dist/commands/template-args.js +122 -0
  124. package/dist/commands/templates.js +162 -0
  125. package/dist/commands/view-args.js +131 -0
  126. package/dist/commands/views.js +138 -0
  127. package/dist/commands/work-notify-args.js +67 -0
  128. package/dist/commands/work-notify.js +63 -0
  129. package/dist/delivery-hold-notice.js +34 -0
  130. package/dist/evidence-transcripts-client.js +1 -0
  131. package/dist/log-rotation.js +2 -2
  132. package/dist/notifier.js +83 -0
  133. package/dist/process-runner.js +4 -0
  134. package/dist/repo-identity-git.js +1 -0
  135. package/package.json +4 -4
@@ -131,6 +131,7 @@ function defaultGitCommandRunner() {
131
131
  const { stdout } = await execFileAsync("git", args, {
132
132
  cwd,
133
133
  timeout: timeoutMs,
134
+ windowsHide: true,
134
135
  maxBuffer: 1024 * 1024,
135
136
  });
136
137
  return stdout;
@@ -0,0 +1,63 @@
1
+ import { terminalDeliveryReason } from "../raw-evidence-staging.js";
2
+ import { admitToStaging } from "../staging-gate.js";
3
+ /**
4
+ * The one door a file passes before its bytes may be copied: the per-sync
5
+ * budget below, then the disk (BLI-12832). A refusal from the disk hands the
6
+ * budget back, because nothing was staged and the next tick re-reads the source.
7
+ * A copy already staged costs the disk nothing and is never refused by it.
8
+ */
9
+ export function admitEvidence(collection, byteLength, file) {
10
+ // Ticket 21952: a copy staged on an earlier tick is queued, not new bytes; re-charging it let a backlog use up the
11
+ // byte budget every tick. It still costs one object, and its upload resumes from the chunks the server holds.
12
+ const alreadyStaged = collection.staging.staged[file.contentHash] !== undefined;
13
+ const charged = alreadyStaged ? 0 : byteLength;
14
+ const budgetReason = admitToBudget(collection.budget, charged);
15
+ if (budgetReason)
16
+ return budgetReason;
17
+ const allowance = collection.budget.staging;
18
+ if (!allowance)
19
+ return null;
20
+ // BLI-12832: bytes the server refused for good are reclaimed by the prune
21
+ // (the refusal stays as a receipt), so copying them again would be churn.
22
+ if (!alreadyStaged &&
23
+ terminalDeliveryReason(collection.staging.delivery_attempts[file.contentHash]?.last_reason)) {
24
+ refundBudget(collection.budget, charged);
25
+ console.error("[staging-gate] not copying bytes the server refused for good", JSON.stringify({ reason: "delivery_terminal_refused", kind: file.kind, file_bytes: byteLength }));
26
+ return "delivery_terminal_refused";
27
+ }
28
+ const diskReason = admitToStaging(allowance, byteLength, {
29
+ alreadyStaged,
30
+ kind: file.kind,
31
+ });
32
+ if (!diskReason)
33
+ return null;
34
+ refundBudget(collection.budget, charged);
35
+ return `staging_deferred:${diskReason}`;
36
+ }
37
+ /** Hands back one admission that a later gate refused, so nothing was staged and nothing counts as sent. */
38
+ function refundBudget(budget, byteLength) {
39
+ budget.remainingObjects += 1;
40
+ budget.remainingBytes += byteLength;
41
+ budget.admittedBytes = Math.max(0, (budget.admittedBytes ?? 0) - byteLength);
42
+ budget.admittedObjects = Math.max(0, (budget.admittedObjects ?? 0) - 1);
43
+ }
44
+ /**
45
+ * Decrements the per-sync budget when a file fits, or returns a deferred-skip
46
+ * reason when it does not. The object budget bounds request count; the byte
47
+ * budget bounds buffered bytes.
48
+ */
49
+ export function admitToBudget(budget, byteLength) {
50
+ const single = budget.singleFileByteAllowance;
51
+ const firstLargeFile = single && budget.remainingBytes === single.normalBytes &&
52
+ byteLength <= single.maxFileBytes;
53
+ const reason = budget.remainingObjects <= 0 ? "deferred_object_budget" :
54
+ byteLength > 0 && byteLength > budget.remainingBytes && !firstLargeFile ? "deferred_byte_budget" : null;
55
+ if (reason) {
56
+ [budget.deferredObjects, budget.deferredBytes] = [(budget.deferredObjects ?? 0) + 1, (budget.deferredBytes ?? 0) + byteLength];
57
+ return reason;
58
+ }
59
+ budget.remainingObjects -= 1;
60
+ budget.remainingBytes -= byteLength;
61
+ [budget.admittedBytes, budget.admittedObjects] = [(budget.admittedBytes ?? 0) + byteLength, (budget.admittedObjects ?? 0) + 1];
62
+ return null;
63
+ }
@@ -7,13 +7,14 @@
7
7
  * contract, so `raw-evidence-completeness.ts` can read and write them without
8
8
  * knowing about packs, budgets or disks.
9
9
  *
10
- * Two mechanics live here rather than in any one reader, because the Codex
10
+ * Two mechanics are exposed here rather than in any one reader, because the Codex
11
11
  * reader, the Claude reader, the image reader and the git-diff reader all spend
12
12
  * them:
13
13
  *
14
14
  * - **`admitToBudget`** — the per-sync byte and object budget (D7b),
15
15
  * decremented in place so one `cockpit sync` across many worktrees honours
16
- * one cap.
16
+ * one cap. Admission and disk-refusal refunds live together in
17
+ * `raw-evidence-budget-admission.ts`; the exports here keep reader APIs stable.
17
18
  * - **`stageEvidenceBytes`** — bytes go on disk exactly once; content an
18
19
  * earlier sync already staged is adopted rather than copied again.
19
20
  *
@@ -26,8 +27,8 @@ import { RAW_EVIDENCE_UPLOAD_MAX_FILE_BYTES } from "@bli-cockpit/telemetry-core"
26
27
  import crypto from "node:crypto";
27
28
  import fs from "node:fs/promises";
28
29
  import path from "node:path";
29
- import { DELIVERY_BACKOFF_BYPASS_REASON, deliveryBackoffApplies, heldSourceKeys, readRawEvidenceStagingState, resolveStagedObject, terminalDeliveryReason, } from "../raw-evidence-staging.js";
30
- import { admitToStaging, openStagingAllowance, } from "../staging-gate.js";
30
+ import { DELIVERY_BACKOFF_BYPASS_REASON, deliveryBackoffApplies, heldSourceKeys, readRawEvidenceStagingState, resolveStagedObject, } from "../raw-evidence-staging.js";
31
+ import { openStagingAllowance, } from "../staging-gate.js";
31
32
  import { chmodPrivate } from "./raw-evidence-pack-store.js";
32
33
  import { GIT_DIFF_TIMEOUT_MS, MAX_GIT_DIFF_BYTES, } from "./raw-evidence-git-diff.js";
33
34
  export const DEFAULT_SINCE_MINUTES = 24 * 60;
@@ -201,49 +202,4 @@ export async function stageEvidenceBytes(collection, options) {
201
202
  collection.stagedNewCount += 1;
202
203
  return { local_path: destination, staged_in_pack: true };
203
204
  }
204
- /**
205
- * The one door a file passes before its bytes may be copied: the per-sync
206
- * budget below, then the disk (BLI-12832). A refusal from the disk hands the
207
- * budget back, because nothing was staged and the next tick re-reads the source.
208
- * A copy already staged costs the disk nothing and is never refused by it.
209
- */
210
- export function admitEvidence(collection, byteLength, file) {
211
- const budgetReason = admitToBudget(collection.budget, byteLength);
212
- if (budgetReason)
213
- return budgetReason;
214
- const allowance = collection.budget.staging;
215
- if (!allowance)
216
- return null;
217
- // BLI-12832: bytes the server refused for good are reclaimed by the prune
218
- // (the refusal stays as a receipt), so copying them again would be churn.
219
- if (collection.staging.staged[file.contentHash] === undefined &&
220
- terminalDeliveryReason(collection.staging.delivery_attempts[file.contentHash]?.last_reason)) {
221
- collection.budget.remainingObjects += 1;
222
- collection.budget.remainingBytes += byteLength;
223
- console.error("[staging-gate] not copying bytes the server refused for good", JSON.stringify({ reason: "delivery_terminal_refused", kind: file.kind, file_bytes: byteLength }));
224
- return "delivery_terminal_refused";
225
- }
226
- const diskReason = admitToStaging(allowance, byteLength, {
227
- alreadyStaged: collection.staging.staged[file.contentHash] !== undefined,
228
- kind: file.kind,
229
- });
230
- if (!diskReason)
231
- return null;
232
- collection.budget.remainingObjects += 1;
233
- collection.budget.remainingBytes += byteLength;
234
- return `staging_deferred:${diskReason}`;
235
- }
236
- /**
237
- * Decrements the per-sync budget when a file fits, or returns a deferred-skip
238
- * reason when it does not. The object budget bounds request count; the byte
239
- * budget bounds buffered bytes.
240
- */
241
- export function admitToBudget(budget, byteLength) {
242
- if (budget.remainingObjects <= 0)
243
- return "deferred_object_budget";
244
- if (byteLength > budget.remainingBytes)
245
- return "deferred_byte_budget";
246
- budget.remainingObjects -= 1;
247
- budget.remainingBytes -= byteLength;
248
- return null;
249
- }
205
+ export { admitEvidence, admitToBudget } from "./raw-evidence-budget-admission.js";
@@ -86,6 +86,7 @@ function captureGitDiff(args, repoRoot) {
86
86
  const child = spawn("git", [...args, ...SECRET_EXCLUDING_PATHSPEC], {
87
87
  cwd: repoRoot,
88
88
  stdio: ["ignore", "pipe", "pipe"],
89
+ windowsHide: true,
89
90
  });
90
91
  const stdoutChunks = [];
91
92
  const stderrChunks = [];
@@ -180,6 +180,14 @@ export function cockpitAgentRulesBlock(options = {}) {
180
180
  "- Computer use first, always. The human can take over the browser at any point; when that happens, stop driving, observe, and record what the human verified.",
181
181
  "- A reviewer or QA agent spawned by the builder does not count as independent verification by itself — the receipt is what counts, not the reviewer's opinion.",
182
182
  "- QA catches the cheap majority of bugs, not all of them. Do the pass, attach the receipts, ship; remaining bugs get fixed as normal bugs.",
183
+ "",
184
+ "## Ground rules",
185
+ "",
186
+ "- Database: raw SQL through the repo's one door. Never add Supabase JS `.from()` or `.rpc()` on the server. Every query under 500 ms through indexes. No caching to hide a slow read. Store totals, do not recount. Never OFFSET on serving paths. Never reset a database password.",
187
+ "- Migrations: additive ones merge on green. Stop for Edward if one can lose data or widens private reads.",
188
+ "- Plans: a large plan, database change or architecture change runs `plan-review` first and follows its verdict.",
189
+ "- Secrets: use them exec-only, never print them, and ask nobody to paste a token.",
190
+ "- Setups are zero-click for Edward.",
183
191
  MANAGED_BLOCK_END,
184
192
  ].join("\n");
185
193
  }
@@ -8,7 +8,19 @@ export const AUTOSTART_LABEL = "com.bli.cockpit.sync";
8
8
  /** The name Task Scheduler files the same job under on Windows. */
9
9
  export const WINDOWS_AUTOSTART_TASK_NAME = "BLI Cockpit Sync";
10
10
  export const DEFAULT_AUTOSTART_INTERVAL_SECONDS = 15 * 60;
11
+ /**
12
+ * BLI-21564: the second job of the one autostart contract. It runs `cockpit cal remind`
13
+ * every minute (the laptop meeting-reminder tick), is installed, repaired and removed
14
+ * with the sync agent, and is never a long-lived process.
15
+ */
16
+ export const REMIND_AUTOSTART_LABEL = "com.bli.cockpit.remind";
17
+ export const WINDOWS_REMIND_TASK_NAME = "BLI Cockpit Remind";
18
+ export const REMIND_INTERVAL_SECONDS = 60;
11
19
  export const UNSUPPORTED_MESSAGE = "autostart is supported on macOS and Windows only";
20
+ /** Where the reminder LaunchAgent lives for a given home. */
21
+ export function remindPlistPathFor(homeDir) {
22
+ return path.join(homeDir, "Library", "LaunchAgents", `${REMIND_AUTOSTART_LABEL}.plist`);
23
+ }
12
24
  /** Where the LaunchAgent lives for a given home; also the `plist_path` every
13
25
  * result carries, including the unsupported-platform one. */
14
26
  export function plistPathFor(homeDir) {
@@ -245,7 +245,7 @@ function renderDarwinSyncCommand(options) {
245
245
  * .zshrc) still fails — but now it fails with a reason in the log instead of a
246
246
  * bare 127.
247
247
  */
248
- function darwinNodeResolutionPreamble(nodeExecutable) {
248
+ export function darwinNodeResolutionPreamble(nodeExecutable) {
249
249
  return [
250
250
  `node_bin=${shellQuote(nodeExecutable)}`,
251
251
  `[ -x "$node_bin" ] || node_bin="$(${DARWIN_LOGIN_SHELL} -lc 'command -v node' 2>/dev/null)"`,
@@ -274,6 +274,6 @@ function darwinAgentRegistrationProblems(plist, expected) {
274
274
  }
275
275
  return problems;
276
276
  }
277
- function shellQuote(value) {
277
+ export function shellQuote(value) {
278
278
  return `'${value.replace(/'/g, "'\\''")}'`;
279
279
  }
@@ -151,7 +151,7 @@ export function launchdUserDomain(uid = process.getuid?.() ?? 0) {
151
151
  export async function reregisterDarwinAgent(options) {
152
152
  const domain = launchdUserDomain(options.uid);
153
153
  const bootout = await options
154
- .exec("launchctl", ["bootout", `${domain}/${AUTOSTART_LABEL}`])
154
+ .exec("launchctl", ["bootout", `${domain}/${options.label ?? AUTOSTART_LABEL}`])
155
155
  .catch(() => ({ code: 1, stdout: "", stderr: "" }));
156
156
  const bootstrap = await options
157
157
  .exec("launchctl", ["bootstrap", domain, options.plistPath])
@@ -0,0 +1,245 @@
1
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { fileExists, REMIND_AUTOSTART_LABEL, REMIND_INTERVAL_SECONDS, remindPlistPathFor, resolveAutostartSettings, schedulerNodeExecutable, WINDOWS_REMIND_TASK_NAME, } from "./autostart-contract.js";
5
+ import { darwinNodeResolutionPreamble, shellQuote } from "./autostart-darwin-plist.js";
6
+ import { describeDifference, powershellLiteral, renderedFileDifference, renderWindowsLauncherScript, renderWindowsRegistrationScript, UTF8_BOM, WINDOWS_SYNC_LOG_LIMIT_BYTES, windowsPowerShellPath, windowsWScriptPath, } from "./autostart-windows-scripts.js";
7
+ import { windowsTaskRegistrationProblems } from "./autostart-windows-task-xml.js";
8
+ import { xmlEscape } from "./autostart-xml.js";
9
+ import { isDefaultDashboardUrl, getCollectorRuntimePaths } from "./local-state.js";
10
+ /**
11
+ * The reminder job: the SECOND scheduled job of the one autostart contract (BLI-21564).
12
+ *
13
+ * `com.bli.cockpit.remind` (launchd) and `BLI Cockpit Remind` (Task Scheduler) run
14
+ * `cockpit cal remind --quiet` every minute. They are installed, repaired and removed together with
15
+ * the sync agent (`autostart.ts` calls these three functions from the same front doors), honour the
16
+ * same opt-out marker through the same callers, and never watch a path: after a sleep the next tick
17
+ * runs, and the 5-minute grace decides shown versus missed. There is no always-on process.
18
+ *
19
+ * Nothing here throws for a host problem: every function answers with a named status and a message,
20
+ * and every deciding branch logs its reason (metadata only, never a path).
21
+ */
22
+ const TAG = "[autostart-remind]";
23
+ const REMIND_LOG = "remind.log";
24
+ const REMIND_ERR_LOG = "remind.err.log";
25
+ const REMIND_SCRIPT = "autostart-remind.ps1";
26
+ const REMIND_LAUNCHER = "autostart-remind.vbs";
27
+ const REMIND_REGISTRATION = "autostart-remind-register.ps1";
28
+ const REMIND_COMMAND = ["cal", "remind", "--quiet"];
29
+ function stateFile(homeDir, name) {
30
+ return path.join(getCollectorRuntimePaths(homeDir ?? os.homedir()).state_dir, name);
31
+ }
32
+ function result(base) {
33
+ return { label: REMIND_AUTOSTART_LABEL, interval_seconds: REMIND_INTERVAL_SECONDS, ...base };
34
+ }
35
+ /** launchd's reminder plist: StartInterval 60, RunAtLoad, and deliberately no WatchPaths. */
36
+ export function renderRemindPlist(options) {
37
+ return [
38
+ '<?xml version="1.0" encoding="UTF-8"?>',
39
+ '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
40
+ '<plist version="1.0">',
41
+ "<dict>",
42
+ " <key>Label</key>",
43
+ ` <string>${xmlEscape(REMIND_AUTOSTART_LABEL)}</string>`,
44
+ ...remindProgramArguments(options),
45
+ " <key>StartInterval</key>",
46
+ ` <integer>${REMIND_INTERVAL_SECONDS}</integer>`,
47
+ " <key>RunAtLoad</key>",
48
+ " <true/>",
49
+ " <key>StandardOutPath</key>",
50
+ ` <string>${xmlEscape(options.stdoutPath)}</string>`,
51
+ " <key>StandardErrorPath</key>",
52
+ ` <string>${xmlEscape(options.stderrPath)}</string>`,
53
+ "</dict>",
54
+ "</plist>",
55
+ "",
56
+ ].join("\n");
57
+ }
58
+ function remindProgramArguments(options) {
59
+ const dashboardArg = isDefaultDashboardUrl(options.dashboardUrl) ? "" : ` --dashboard-url ${shellQuote(options.dashboardUrl)}`;
60
+ const command = [
61
+ ...darwinNodeResolutionPreamble(options.nodeExecutable),
62
+ `"$node_bin" ${shellQuote(options.cliEntryPoint)} ${REMIND_COMMAND.join(" ")}${dashboardArg}`,
63
+ ].join("; ");
64
+ return [" <key>ProgramArguments</key>", " <array>", " <string>/bin/zsh</string>", " <string>-c</string>", ` <string>${xmlEscape(command)}</string>`, " </array>"];
65
+ }
66
+ async function renderedDarwinPlist(options) {
67
+ const homeDir = options.homeDir ?? os.homedir();
68
+ const settings = resolveAutostartSettings(options, "darwin");
69
+ const node = await schedulerNodeExecutable(options, "darwin");
70
+ const paths = getCollectorRuntimePaths(homeDir);
71
+ return {
72
+ plistPath: remindPlistPathFor(homeDir),
73
+ plist: renderRemindPlist({
74
+ nodeExecutable: node.path,
75
+ cliEntryPoint: settings.cli_entry_point,
76
+ dashboardUrl: settings.dashboard_url,
77
+ stdoutPath: path.join(paths.state_dir, REMIND_LOG),
78
+ stderrPath: path.join(paths.state_dir, REMIND_ERR_LOG),
79
+ }),
80
+ };
81
+ }
82
+ /** Writes the reminder plist without touching launchd (the detached healer registers it with bootout/bootstrap). */
83
+ export async function writeDarwinRemindPlist(options) {
84
+ const { plist, plistPath } = await renderedDarwinPlist(options);
85
+ await mkdir(path.dirname(plistPath), { recursive: true });
86
+ await mkdir(getCollectorRuntimePaths(options.homeDir ?? os.homedir()).state_dir, { recursive: true });
87
+ await writeFile(plistPath, plist, "utf8");
88
+ return { plist_path: plistPath };
89
+ }
90
+ function windowsPaths(homeDir) {
91
+ return {
92
+ script: stateFile(homeDir, REMIND_SCRIPT),
93
+ launcher: stateFile(homeDir, REMIND_LAUNCHER),
94
+ registration: stateFile(homeDir, REMIND_REGISTRATION),
95
+ log: stateFile(homeDir, REMIND_LOG),
96
+ };
97
+ }
98
+ function renderWindowsRemindScript(options) {
99
+ const dashboardArgs = isDefaultDashboardUrl(options.dashboardUrl) ? "" : ` --dashboard-url ${powershellLiteral(options.dashboardUrl)}`;
100
+ return [
101
+ // Same reasons as the sync script: stderr is a log channel here, so 'Continue', and failure is the exit code.
102
+ "$ErrorActionPreference = 'Continue'",
103
+ "$exitCode = 0",
104
+ `$nodeExecutable = ${powershellLiteral(options.nodeExecutable)}`,
105
+ `$cliEntryPoint = ${powershellLiteral(options.cliEntryPoint)}`,
106
+ `$remindLogPath = ${powershellLiteral(options.logPath)}`,
107
+ "$remindLogFile = Get-Item -LiteralPath $remindLogPath -ErrorAction SilentlyContinue",
108
+ `if ($remindLogFile -and $remindLogFile.Length -gt ${WINDOWS_SYNC_LOG_LIMIT_BYTES}) { Set-Content -LiteralPath $remindLogPath -Value '' -Encoding UTF8 }`,
109
+ "try {",
110
+ ` & $nodeExecutable $cliEntryPoint ${REMIND_COMMAND.join(" ")}${dashboardArgs} 2>&1 | ForEach-Object { "$_" } | Add-Content -LiteralPath $remindLogPath -Encoding UTF8`,
111
+ " if ($LASTEXITCODE -ne 0) { $exitCode = $LASTEXITCODE }",
112
+ "} catch {",
113
+ " Add-Content -LiteralPath $remindLogPath -Encoding UTF8 -Value $_.Exception.Message",
114
+ " $exitCode = 1",
115
+ "}",
116
+ "exit $exitCode",
117
+ "",
118
+ ].join("\r\n");
119
+ }
120
+ async function expectedWindowsFiles(options) {
121
+ const settings = resolveAutostartSettings(options, "win32");
122
+ const node = await schedulerNodeExecutable(options, "win32");
123
+ const paths = windowsPaths(options.homeDir);
124
+ return {
125
+ paths,
126
+ script: `${UTF8_BOM}${renderWindowsRemindScript({ nodeExecutable: node.path, cliEntryPoint: settings.cli_entry_point, dashboardUrl: settings.dashboard_url, logPath: paths.log })}`,
127
+ launcher: renderWindowsLauncherScript({ powershellPath: windowsPowerShellPath(), scriptPath: paths.script }),
128
+ registration: `${UTF8_BOM}${renderWindowsRegistrationScript({ taskName: WINDOWS_REMIND_TASK_NAME, launcherPath: paths.launcher, intervalMinutes: REMIND_INTERVAL_SECONDS / 60 })}`,
129
+ };
130
+ }
131
+ /** Installs or repairs the reminder job for this host. Never throws. */
132
+ export async function installRemindAgent(options) {
133
+ const platform = options.platform ?? process.platform;
134
+ try {
135
+ if (platform === "darwin") {
136
+ const written = await writeDarwinRemindPlist(options);
137
+ await options.exec("launchctl", ["unload", written.plist_path]).catch(() => undefined);
138
+ const load = await options.exec("launchctl", ["load", written.plist_path]);
139
+ console.error(`${TAG} ${load.code === 0 ? "launchd job loaded" : "launchd job not loaded"}`, JSON.stringify({ label: REMIND_AUTOSTART_LABEL, exit_code: load.code, interval_seconds: REMIND_INTERVAL_SECONDS }));
140
+ return result({ status: "installed", path: written.plist_path, loaded: load.code === 0, ...(load.code === 0 ? {} : { message: `launchctl load exited ${load.code} for the reminder job` }) });
141
+ }
142
+ if (platform === "win32")
143
+ return await installWindowsRemind(options);
144
+ }
145
+ catch (error) {
146
+ console.error(`${TAG} install failed`, JSON.stringify({ reason: "install_threw", platform, error_name: error instanceof Error ? error.name : typeof error }));
147
+ return result({ status: "not_loaded", path: remindPlistPathFor(options.homeDir ?? os.homedir()), loaded: false, message: "the reminder job could not be written" });
148
+ }
149
+ return result({ status: "unsupported", path: remindPlistPathFor(options.homeDir ?? os.homedir()) });
150
+ }
151
+ async function installWindowsRemind(options) {
152
+ const expected = await expectedWindowsFiles(options);
153
+ await mkdir(path.dirname(expected.paths.script), { recursive: true });
154
+ await writeFile(expected.paths.script, expected.script, "utf8");
155
+ await writeFile(expected.paths.launcher, expected.launcher, "utf8");
156
+ await writeFile(expected.paths.registration, expected.registration, "utf8");
157
+ const created = await options.exec("powershell.exe", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", expected.paths.registration]);
158
+ console.error(`${TAG} ${created.code === 0 ? "scheduled task registered" : "scheduled task registration failed"}`, JSON.stringify({ task_name: WINDOWS_REMIND_TASK_NAME, exit_code: created.code, interval_minutes: REMIND_INTERVAL_SECONDS / 60 }));
159
+ return result({
160
+ status: "installed", path: expected.paths.script, task_name: WINDOWS_REMIND_TASK_NAME, loaded: created.code === 0,
161
+ ...(created.code === 0 ? {} : { message: `PowerShell registration of the reminder task exited ${created.code}` }),
162
+ });
163
+ }
164
+ /** Removes the reminder job and its files. `absent` when there was nothing, so it is safe to repeat. */
165
+ export async function uninstallRemindAgent(options) {
166
+ const platform = options.platform ?? process.platform;
167
+ if (platform === "darwin") {
168
+ const plistPath = remindPlistPathFor(options.homeDir ?? os.homedir());
169
+ if (!(await fileExists(plistPath)))
170
+ return result({ status: "absent", path: plistPath });
171
+ await options.exec("launchctl", ["unload", plistPath]).catch(() => undefined);
172
+ await rm(plistPath, { force: true });
173
+ console.error(`${TAG} launchd job removed`, JSON.stringify({ label: REMIND_AUTOSTART_LABEL }));
174
+ return result({ status: "uninstalled", path: plistPath });
175
+ }
176
+ if (platform === "win32") {
177
+ const paths = windowsPaths(options.homeDir);
178
+ const query = await options.exec("schtasks.exe", ["/Query", "/TN", WINDOWS_REMIND_TASK_NAME]);
179
+ const known = query.code === 0;
180
+ const removed = known ? await options.exec("schtasks.exe", ["/Delete", "/TN", WINDOWS_REMIND_TASK_NAME, "/F"]) : null;
181
+ if (removed && removed.code !== 0) {
182
+ console.error(`${TAG} scheduled task not removed`, JSON.stringify({ task_name: WINDOWS_REMIND_TASK_NAME, exit_code: removed.code }));
183
+ return result({ status: "not_loaded", path: paths.script, task_name: WINDOWS_REMIND_TASK_NAME, message: `schtasks /Delete exited ${removed.code} for the reminder task` });
184
+ }
185
+ for (const file of [paths.script, paths.launcher, paths.registration])
186
+ await rm(file, { force: true });
187
+ console.error(`${TAG} scheduled task ${known ? "removed" : "was not registered"}`, JSON.stringify({ task_name: WINDOWS_REMIND_TASK_NAME }));
188
+ return result({ status: known ? "uninstalled" : "absent", path: paths.script, task_name: WINDOWS_REMIND_TASK_NAME });
189
+ }
190
+ return result({ status: "unsupported", path: remindPlistPathFor(options.homeDir ?? os.homedir()) });
191
+ }
192
+ /**
193
+ * Whether the reminder job is registered and still says what this build would write. Content is only compared
194
+ * when the caller supplied roots, exactly like the sync agent (a bare `autostart status` stays permissive).
195
+ */
196
+ export async function remindAgentStatus(options) {
197
+ const platform = options.platform ?? process.platform;
198
+ const compare = Boolean(options.repoRoots && options.repoRoots.length > 0);
199
+ if (platform === "darwin")
200
+ return darwinRemindStatus(options, compare);
201
+ if (platform === "win32")
202
+ return windowsRemindStatus(options, compare);
203
+ return result({ status: "unsupported", path: remindPlistPathFor(options.homeDir ?? os.homedir()) });
204
+ }
205
+ async function darwinRemindStatus(options, compare) {
206
+ const plistPath = remindPlistPathFor(options.homeDir ?? os.homedir());
207
+ const onDisk = await readFile(plistPath, "utf8").catch(() => null);
208
+ if (onDisk === null)
209
+ return result({ status: "absent", path: plistPath });
210
+ const problems = [];
211
+ if (compare && onDisk !== (await renderedDarwinPlist(options)).plist)
212
+ problems.push("reminder plist does not match the current Tower runtime");
213
+ const list = await options.exec("launchctl", ["list", REMIND_AUTOSTART_LABEL]);
214
+ if (list.code !== 0)
215
+ problems.unshift("reminder job is not loaded");
216
+ console.error(`${TAG} status`, JSON.stringify({ label: REMIND_AUTOSTART_LABEL, problems: problems.length }));
217
+ return result({ status: problems.length === 0 ? "loaded" : "not_loaded", path: plistPath, loaded: problems.length === 0, ...(problems.length ? { message: problems.join("; ") } : {}) });
218
+ }
219
+ async function windowsRemindStatus(options, compare) {
220
+ const paths = windowsPaths(options.homeDir);
221
+ const query = await options.exec("schtasks.exe", ["/Query", "/TN", WINDOWS_REMIND_TASK_NAME, "/XML"]);
222
+ if (query.code !== 0) {
223
+ const absent = query.code === 1 && /cannot find|not found|does not exist/iu.test(`${query.stderr}\n${query.stdout}`);
224
+ console.error(`${TAG} task query failed`, JSON.stringify({ task_name: WINDOWS_REMIND_TASK_NAME, exit_code: query.code, status: absent ? "absent" : "not_loaded" }));
225
+ return result({ status: absent ? "absent" : "not_loaded", path: paths.script, task_name: WINDOWS_REMIND_TASK_NAME });
226
+ }
227
+ const problems = [
228
+ ...(/<Enabled>\s*false\s*<\/Enabled>/iu.test(query.stdout) ? ["reminder task is disabled"] : []),
229
+ ...windowsTaskRegistrationProblems(query.stdout, { launcherPath: paths.launcher, intervalMinutes: REMIND_INTERVAL_SECONDS / 60, wscriptPath: windowsWScriptPath() }),
230
+ ];
231
+ if (!(await fileExists(paths.script)))
232
+ problems.push("reminder script is missing");
233
+ else if (!(await fileExists(paths.launcher)))
234
+ problems.push("reminder launcher is missing");
235
+ else if (compare) {
236
+ const expected = await expectedWindowsFiles(options);
237
+ for (const [label, file, text] of [["script", paths.script, expected.script], ["launcher", paths.launcher, expected.launcher]]) {
238
+ const difference = renderedFileDifference(await readFile(file, "utf8").catch(() => null), text);
239
+ if (difference)
240
+ problems.push(`reminder ${label} does not match the current Tower runtime (${describeDifference(difference)})`);
241
+ }
242
+ }
243
+ console.error(`${TAG} status`, JSON.stringify({ task_name: WINDOWS_REMIND_TASK_NAME, problems: problems.length }));
244
+ return result({ status: problems.length === 0 ? "loaded" : "not_loaded", path: paths.script, task_name: WINDOWS_REMIND_TASK_NAME, loaded: problems.length === 0, ...(problems.length ? { message: problems.join("; ") } : {}) });
245
+ }
@@ -53,7 +53,13 @@ async function readAutostartProblem(paths, options, platform) {
53
53
  })
54
54
  : [];
55
55
  const marker = path.join(paths.state_dir, AUTOSTART_REPAIR_THROTTLE_MARKER);
56
- if (!absent && status.status !== "not_loaded" && runtimeProblems.length === 0) {
56
+ // BLI-21564: the reminder job is repaired with the sync agent. A missing or broken reminder job is a
57
+ // problem of its own (an existing install that predates it gets it created here), and the opt-out
58
+ // marker is honoured above because an operator uninstall removed both.
59
+ const remindProblem = status.status === "loaded" && status.remind && status.remind.status !== "loaded" && status.remind.status !== "unsupported"
60
+ ? [`reminder job ${status.remind.status === "absent" ? "absent" : status.remind.message ?? "not loaded"}`]
61
+ : [];
62
+ if (!absent && status.status !== "not_loaded" && runtimeProblems.length === 0 && remindProblem.length === 0) {
57
63
  // Healthy is the steady state and stays silent. A marker left by an
58
64
  // earlier attempt is spent: that repair worked, so the next break starts
59
65
  // fresh instead of waiting out a retry window.
@@ -64,6 +70,7 @@ async function readAutostartProblem(paths, options, platform) {
64
70
  ...(absent ? ["registration absent"] : []),
65
71
  ...(status.message ? [status.message] : []),
66
72
  ...runtimeProblems,
73
+ ...remindProblem,
67
74
  ]
68
75
  .join("; ")
69
76
  .slice(0, DETAIL_MAX_CHARS) || "registration not loaded";
@@ -111,6 +118,14 @@ async function repairWindowsAutostart(options, platform, absent, problem) {
111
118
  exec: options.exec,
112
119
  platform,
113
120
  });
121
+ if (repaired.loaded && repaired.remind?.loaded === false) {
122
+ return {
123
+ status: "fail",
124
+ reason: "remind_repair_failed",
125
+ detail: (repaired.remind.message ?? problem).slice(0, DETAIL_MAX_CHARS),
126
+ step: "autostart_repair",
127
+ };
128
+ }
114
129
  if (repaired.loaded) {
115
130
  return {
116
131
  status: "ok",
@@ -182,6 +197,7 @@ function defaultSpawnDetached(command, args) {
182
197
  const child = spawn(command, args, {
183
198
  detached: true,
184
199
  stdio: "ignore",
200
+ windowsHide: true,
185
201
  });
186
202
  child.unref();
187
203
  return { pid: child.pid ?? null };
@@ -29,8 +29,8 @@ const WINDOWS_AUTOSTART_LAUNCHER_NAME = "autostart-sync.vbs";
29
29
  const WINDOWS_SYNC_LOG_NAME = "sync.log";
30
30
  /** With the window hidden, the log is the only place output goes; cap it so an
31
31
  * always-on machine syncing every 15 minutes cannot grow it without bound. */
32
- const WINDOWS_SYNC_LOG_LIMIT_BYTES = 10 * 1024 * 1024;
33
- const UTF8_BOM = "\uFEFF";
32
+ export const WINDOWS_SYNC_LOG_LIMIT_BYTES = 10 * 1024 * 1024;
33
+ export const UTF8_BOM = "\uFEFF";
34
34
  /** Where each written file lives, so the flow can name them in one breath. */
35
35
  export function windowsAutostartScriptPath(homeDir) {
36
36
  return windowsStateFile(homeDir, WINDOWS_AUTOSTART_SCRIPT_NAME);
@@ -160,7 +160,7 @@ export async function syncLauncherProblem(options) {
160
160
  ? `sync launcher does not match the current Tower runtime (${describeDifference(difference)})`
161
161
  : null;
162
162
  }
163
- function renderedFileDifference(current, expected) {
163
+ export function renderedFileDifference(current, expected) {
164
164
  const expectedLines = normalizedScriptLines(expected);
165
165
  if (current === null) {
166
166
  return {
@@ -201,7 +201,7 @@ function renderedFileDifference(current, expected) {
201
201
  return null;
202
202
  }
203
203
  /** Safe to print: a reason label, a line index, two counts. No content. */
204
- function describeDifference(difference) {
204
+ export function describeDifference(difference) {
205
205
  if (difference.reason === "file_unreadable")
206
206
  return "file could not be read";
207
207
  if (difference.reason === "byte_order_mark")
@@ -218,7 +218,7 @@ function normalizedScriptLines(value) {
218
218
  function hasByteOrderMark(value) {
219
219
  return value.startsWith(UTF8_BOM);
220
220
  }
221
- function renderWindowsRegistrationScript(options) {
221
+ export function renderWindowsRegistrationScript(options) {
222
222
  return [
223
223
  "$ErrorActionPreference = 'Stop'",
224
224
  `$taskName = ${powershellLiteral(options.taskName)}`,
@@ -254,7 +254,7 @@ function renderWindowsRegistrationScript(options) {
254
254
  * doubling it; Windows paths cannot contain quotes, so the doubled quotes here
255
255
  * are only the fixed delimiters around the two paths.
256
256
  */
257
- function renderWindowsLauncherScript(options) {
257
+ export function renderWindowsLauncherScript(options) {
258
258
  const quotedPowershell = `""${options.powershellPath}""`;
259
259
  const quotedScript = `""${options.scriptPath}""`;
260
260
  return [
@@ -305,6 +305,6 @@ function renderWindowsSyncScript(options) {
305
305
  "",
306
306
  ].join("\r\n");
307
307
  }
308
- function powershellLiteral(value) {
308
+ export function powershellLiteral(value) {
309
309
  return `'${value.replace(/'/gu, "''")}'`;
310
310
  }
package/dist/autostart.js CHANGED
@@ -4,6 +4,7 @@ import { darwinAgentStatus, installDarwinAgent, uninstallDarwinAgent, } from "./
4
4
  import { readRegisteredDarwinRuntimePaths } from "./autostart-darwin-plist.js";
5
5
  import { installWindowsTask, uninstallWindowsTask, windowsTaskStatus, } from "./autostart-windows.js";
6
6
  import { readRegisteredWindowsRuntimePaths } from "./autostart-windows-scripts.js";
7
+ import { installRemindAgent, remindAgentStatus, uninstallRemindAgent } from "./autostart-remind.js";
7
8
  /**
8
9
  * Background collection's front door: install, uninstall, and status for the
9
10
  * OS scheduler, whichever OS this is.
@@ -20,7 +21,7 @@ import { readRegisteredWindowsRuntimePaths } from "./autostart-windows-scripts.j
20
21
  * autostart-contract.ts and is re-exported here because this module is the
21
22
  * import site the rest of the collector already knows (BLI-3571).
22
23
  */
23
- export { AUTOSTART_LABEL, DEFAULT_AUTOSTART_INTERVAL_SECONDS, WINDOWS_AUTOSTART_TASK_NAME, } from "./autostart-contract.js";
24
+ export { AUTOSTART_LABEL, DEFAULT_AUTOSTART_INTERVAL_SECONDS, WINDOWS_AUTOSTART_TASK_NAME, REMIND_AUTOSTART_LABEL, WINDOWS_REMIND_TASK_NAME, } from "./autostart-contract.js";
24
25
  export { DARWIN_NETWORK_CHANGE_SIGNAL, DARWIN_NODE_MISSING_MESSAGE, writeDarwinAutostartPlist, } from "./autostart-darwin-plist.js";
25
26
  export { launchdUserDomain, reregisterDarwinAgent, } from "./autostart-darwin.js";
26
27
  /**
@@ -30,35 +31,35 @@ export { launchdUserDomain, reregisterDarwinAgent, } from "./autostart-darwin.js
30
31
  */
31
32
  export async function installAutostartAgent(options) {
32
33
  const platform = options.platform ?? process.platform;
33
- if (platform === "win32")
34
- return installWindowsTask(options);
35
- if (platform !== "darwin")
34
+ if (platform !== "win32" && platform !== "darwin")
36
35
  return unsupportedResult(options.homeDir);
37
- return installDarwinAgent(options);
36
+ const sync = platform === "win32" ? await installWindowsTask(options) : await installDarwinAgent(options);
37
+ // BLI-21564: the reminder job is the second job of the same contract, installed and repaired with the sync agent.
38
+ return { ...sync, remind: await installRemindAgent(options) };
38
39
  }
39
40
  /**
40
- * Removes the scheduled sync. Reports `absent` when there was nothing to
41
- * remove, so the command is safe to run repeatedly.
41
+ * Removes the scheduled sync and the reminder job. Reports `absent` when there
42
+ * was nothing to remove, so the command is safe to run repeatedly.
42
43
  */
43
44
  export async function uninstallAutostartAgent(options) {
44
45
  const platform = options.platform ?? process.platform;
45
- if (platform === "win32")
46
- return uninstallWindowsTask(options);
47
- if (platform !== "darwin")
46
+ if (platform !== "win32" && platform !== "darwin")
48
47
  return unsupportedResult(options.homeDir);
49
- return uninstallDarwinAgent(options);
48
+ const sync = platform === "win32" ? await uninstallWindowsTask(options) : await uninstallDarwinAgent(options);
49
+ return { ...sync, remind: await uninstallRemindAgent(options) };
50
50
  }
51
51
  /**
52
52
  * Reports whether the scheduled sync is installed, registered, and still
53
53
  * matches the configured roots, cadence, dashboard URL, and Cockpit runtime.
54
+ * The reminder job's own reading rides beside it as `remind`; it never changes
55
+ * the sync agent's `status`, and the self-heal tick is what acts on it.
54
56
  */
55
57
  export async function autostartStatus(options) {
56
58
  const platform = options.platform ?? process.platform;
57
- if (platform === "win32")
58
- return windowsTaskStatus(options);
59
- if (platform !== "darwin")
59
+ if (platform !== "win32" && platform !== "darwin")
60
60
  return unsupportedResult(options.homeDir);
61
- return darwinAgentStatus(options);
61
+ const sync = platform === "win32" ? await windowsTaskStatus(options) : await darwinAgentStatus(options);
62
+ return { ...sync, remind: await remindAgentStatus(options) };
62
63
  }
63
64
  /**
64
65
  * What the platform says it will run, parsed out of the registration itself