@indigoai-us/hq-cli 5.345.170 → 5.345.172

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 (142) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/assets/scaffold/core/scripts/hq-delegate-send.sh +1 -1
  3. package/dist/command-catalog.generated.d.ts +206 -7
  4. package/dist/command-catalog.generated.js +252 -8
  5. package/dist/command-registration-plan.d.ts +7 -0
  6. package/dist/command-registration-plan.js +1 -0
  7. package/dist/commands/access.d.ts +32 -1
  8. package/dist/commands/access.js +112 -3
  9. package/dist/commands/agent-kit.d.ts +1 -1
  10. package/dist/commands/agent-kit.js +3 -1
  11. package/dist/commands/agents.js +2 -1
  12. package/dist/commands/bot.d.ts +3 -0
  13. package/dist/commands/bot.js +179 -18
  14. package/dist/commands/cloud.d.ts +58 -1
  15. package/dist/commands/cloud.js +228 -10
  16. package/dist/commands/core-company-index-scope-flag.d.ts +8 -1
  17. package/dist/commands/core-company-index-scope-flag.js +9 -2
  18. package/dist/commands/core-delegate-w1b.js +5 -2
  19. package/dist/commands/core-dm-bind-native.js +19 -4
  20. package/dist/commands/core-rebuild-index-fast.js +24 -6
  21. package/dist/commands/core.js +18 -6
  22. package/dist/commands/daemon.js +16 -1
  23. package/dist/commands/dm.d.ts +17 -3
  24. package/dist/commands/dm.js +121 -26
  25. package/dist/commands/files.d.ts +1 -0
  26. package/dist/commands/files.js +22 -4
  27. package/dist/commands/integrations-api.d.ts +4 -0
  28. package/dist/commands/integrations-api.js +18 -2
  29. package/dist/commands/integrations-connect.d.ts +10 -3
  30. package/dist/commands/integrations-connect.js +70 -6
  31. package/dist/commands/integrations-core.d.ts +23 -0
  32. package/dist/commands/integrations-core.js +100 -0
  33. package/dist/commands/integrations-manage.js +8 -1
  34. package/dist/commands/integrations.js +49 -5
  35. package/dist/commands/lanes-loop.d.ts +20 -0
  36. package/dist/commands/lanes-loop.js +69 -11
  37. package/dist/commands/lanes.js +9 -1
  38. package/dist/commands/rescue.d.ts +9 -0
  39. package/dist/commands/rescue.js +31 -0
  40. package/dist/commands/run.js +115 -40
  41. package/dist/commands/scan.d.ts +5 -1
  42. package/dist/commands/scan.js +16 -4
  43. package/dist/commands/secrets.js +50 -9
  44. package/dist/commands/sync-core.d.ts +22 -0
  45. package/dist/commands/sync-core.js +124 -0
  46. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2447 -2135
  47. package/dist/hq-cloud-sync-runner/package.json +1 -1
  48. package/dist/lib/agent-kit/inbox-state.d.ts +17 -0
  49. package/dist/lib/agent-kit/inbox-state.js +41 -20
  50. package/dist/lib/agent-kit/kit-config.d.ts +2 -0
  51. package/dist/lib/agent-kit/kit-config.js +6 -0
  52. package/dist/lib/agent-kit/mcp/tools.js +12 -5
  53. package/dist/lib/agent-kit/run/heartbeat.d.ts +6 -3
  54. package/dist/lib/agent-kit/run/heartbeat.js +6 -3
  55. package/dist/lib/agent-kit/run/inbox.d.ts +8 -3
  56. package/dist/lib/agent-kit/run/inbox.js +42 -10
  57. package/dist/lib/agent-kit/run/request-timeout.d.ts +26 -0
  58. package/dist/lib/agent-kit/run/request-timeout.js +51 -0
  59. package/dist/lib/bot/api.d.ts +1 -1
  60. package/dist/lib/bot/config.d.ts +10 -0
  61. package/dist/lib/bot/config.js +17 -1
  62. package/dist/lib/bot/fleet-local.d.ts +142 -0
  63. package/dist/lib/bot/fleet-local.js +689 -0
  64. package/dist/lib/bot/fleet-python.d.ts +47 -0
  65. package/dist/lib/bot/fleet-python.js +218 -0
  66. package/dist/lib/bot/index.d.ts +2 -0
  67. package/dist/lib/bot/index.js +2 -0
  68. package/dist/lib/bot/local-config.d.ts +3 -2
  69. package/dist/lib/bot/local-config.js +5 -2
  70. package/dist/lib/bot/promote.d.ts +10 -0
  71. package/dist/lib/bot/promote.js +3 -2
  72. package/dist/lib/bot/prompt.d.ts +6 -1
  73. package/dist/lib/bot/prompt.js +8 -3
  74. package/dist/lib/bot/runtime/index.js +8 -4
  75. package/dist/lib/bot/runtime/scratch-dir.d.ts +24 -0
  76. package/dist/lib/bot/runtime/scratch-dir.js +41 -0
  77. package/dist/lib/caller-principal.d.ts +4 -0
  78. package/dist/lib/caller-principal.js +40 -0
  79. package/dist/lib/capacity/collectors/grok.js +8 -2
  80. package/dist/lib/cli-flag-keys.d.ts +2 -0
  81. package/dist/lib/cli-flag-keys.js +2 -0
  82. package/dist/lib/daemon/catalog.d.ts +12 -4
  83. package/dist/lib/daemon/catalog.js +70 -24
  84. package/dist/lib/daemon/config.d.ts +0 -7
  85. package/dist/lib/daemon/config.js +0 -32
  86. package/dist/lib/daemon/run.d.ts +5 -1
  87. package/dist/lib/daemon/run.js +48 -11
  88. package/dist/lib/daemon/supervisor.js +3 -1
  89. package/dist/lib/doctor/checks/release-links.d.ts +62 -1
  90. package/dist/lib/doctor/checks/release-links.js +188 -2
  91. package/dist/lib/doctor/fix/apply.js +54 -0
  92. package/dist/lib/doctor/fix/remediation.d.ts +8 -1
  93. package/dist/lib/doctor/fix/remediation.js +22 -0
  94. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  95. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  96. package/dist/lib/index-render/index.d.ts +9 -0
  97. package/dist/lib/index-render/index.js +65 -5
  98. package/dist/lib/index-render/session-scope.d.ts +69 -0
  99. package/dist/lib/index-render/session-scope.js +279 -0
  100. package/dist/lib/lanes/admission.js +7 -20
  101. package/dist/lib/lanes/reconcile.js +13 -15
  102. package/dist/lib/lanes/stale-lock.d.ts +50 -0
  103. package/dist/lib/lanes/stale-lock.js +105 -0
  104. package/dist/lib/lanes/store.js +11 -76
  105. package/dist/lib/notify-recipient.d.ts +10 -4
  106. package/dist/lib/notify-recipient.js +16 -4
  107. package/dist/lib/secret-input-link.d.ts +42 -0
  108. package/dist/lib/secret-input-link.js +93 -0
  109. package/dist/lib/secret-scan/range.d.ts +57 -0
  110. package/dist/lib/secret-scan/range.js +266 -0
  111. package/dist/lib/secret-scan/repo-config.d.ts +18 -3
  112. package/dist/lib/secret-scan/repo-config.js +135 -2
  113. package/dist/lib/sync-core/engine-home.d.ts +44 -0
  114. package/dist/lib/sync-core/engine-home.js +193 -0
  115. package/dist/lib/sync-core/flag.d.ts +17 -0
  116. package/dist/lib/sync-core/flag.js +47 -0
  117. package/dist/lib/sync-core/reconcile.d.ts +31 -0
  118. package/dist/lib/sync-core/reconcile.js +196 -0
  119. package/dist/lib/sync-core/release.d.ts +32 -0
  120. package/dist/lib/sync-core/release.js +140 -0
  121. package/dist/lib/sync-core/rollout-state.d.ts +24 -0
  122. package/dist/lib/sync-core/rollout-state.js +94 -0
  123. package/dist/lib/vault-sync-switches.d.ts +5 -0
  124. package/dist/lib/vault-sync-switches.js +43 -0
  125. package/dist/run/script-bound-schema.d.ts +27 -0
  126. package/dist/run/script-bound-schema.js +97 -0
  127. package/dist/secrets-env-action.d.ts +2 -0
  128. package/dist/secrets-env-action.js +7 -2
  129. package/dist/utils/cognito-session.d.ts +3 -30
  130. package/dist/utils/cognito-session.js +2 -100
  131. package/dist/utils/machine-principal-state.d.ts +38 -0
  132. package/dist/utils/machine-principal-state.js +110 -0
  133. package/dist/utils/remote-list-not-converging.d.ts +21 -0
  134. package/dist/utils/remote-list-not-converging.js +40 -0
  135. package/dist/utils/safe-link.js +9 -0
  136. package/dist/utils/script-bound-exec.d.ts +158 -0
  137. package/dist/utils/script-bound-exec.js +568 -0
  138. package/dist/utils/sync-watchdog.d.ts +92 -0
  139. package/dist/utils/sync-watchdog.js +250 -0
  140. package/package.json +8 -8
  141. package/dist/lib/daemon/sync-core-canary.d.ts +0 -9
  142. package/dist/lib/daemon/sync-core-canary.js +0 -56
@@ -6,11 +6,15 @@
6
6
  * user customization and is left alone.
7
7
  */
8
8
  import * as fs from "node:fs";
9
+ import * as os from "node:os";
9
10
  import * as path from "node:path";
11
+ import { removeLinkOnly } from "../../../utils/safe-link.js";
10
12
  export const RELEASE_LINKS_FAMILY_ID = "release-links";
11
13
  export const RELEASE_LINKS_FAMILY_TITLE = "Release links";
12
14
  export const RELEASE_LINKS_CHECK_ID = "release-links.resolve";
13
- export const RELEASE_LINK_REMEDIATION = "Run `hq rescue -y` again. On Windows without Developer Mode, a directory link is written as a junction and a file link as a copy.";
15
+ export const RELEASE_LINK_REMEDIATION = "Run `hq doctor --fix` (or `hq rescue -y`) to restore the shipped link. A dangling link that points outside the HQ root and the temp folder is treated as your own and left alone. On Windows, a directory link is written as a junction and a file link as a copy when symlinks are not allowed.";
16
+ /** Message prefix for a link whose target is missing; `--fix` keys off it. */
17
+ export const DANGLING_RELEASE_LINK_PROBLEM = "does not resolve.";
14
18
  const CYGWIN_SYMLINK_COOKIE = Buffer.from("!<symlink>", "latin1");
15
19
  /** Paths git records as symlinks in the hq-core release. */
16
20
  export const RELEASE_LINKS = [
@@ -80,7 +84,11 @@ function releaseLinkProblem(hqRoot, entry) {
80
84
  resolved = fs.realpathSync.native(destination);
81
85
  }
82
86
  catch {
83
- return "does not resolve. Its target is missing, often because an update linked it at a staging folder that was then deleted.";
87
+ const current = readlinkAbsOrNull(destination);
88
+ if (current !== null && !isRepairableDanglingTarget(hqRoot, current)) {
89
+ return `points at ${current}, which is missing and outside this HQ root. It is treated as your own link and left alone; fix or remove it yourself.`;
90
+ }
91
+ return `${DANGLING_RELEASE_LINK_PROBLEM} Its target is missing, often because an update linked it at a staging folder that was then deleted.`;
84
92
  }
85
93
  if (!pathIsInside(hqRoot, resolved)) {
86
94
  return "points outside this HQ root, so it does not resolve to the shipped target.";
@@ -108,6 +116,42 @@ function releaseLinkProblem(hqRoot, entry) {
108
116
  // A real directory in place of a directory link is a user replacement.
109
117
  return null;
110
118
  }
119
+ function readlinkAbsOrNull(destination) {
120
+ try {
121
+ return path.resolve(path.dirname(destination), fs.readlinkSync(destination));
122
+ }
123
+ catch {
124
+ return null;
125
+ }
126
+ }
127
+ /** Prefix of the folder hq-cloud's rescue creates with mkdtemp. */
128
+ const RESCUE_STAGING_SEGMENT_PREFIX = "hq-replace-rescue-";
129
+ /**
130
+ * The rescue staging roots, mirroring hq-cloud's `resolveRescueStagingRoot`:
131
+ * the system temp folder, or `tmp` beside the backup root (`~/.hq/tmp`, or
132
+ * next to `$HQ_BACKUP_DIR`) when the temp folder is on another filesystem.
133
+ */
134
+ function rescueStagingRoots(deps) {
135
+ const backupRoot = deps.backupRoot ?? process.env.HQ_BACKUP_DIR ?? path.join(os.homedir(), ".hq", "backups");
136
+ return [
137
+ ...tempRoots(deps.tmpDir ?? os.tmpdir()),
138
+ ...tempRoots(path.join(path.dirname(path.resolve(backupRoot)), "tmp")),
139
+ ];
140
+ }
141
+ /**
142
+ * A dangling link HQ may repair. Its current target is under the HQ root,
143
+ * under the system temp folder, under the rescue staging root beside the
144
+ * backup root, or has a path segment starting with `hq-replace-rescue-` (the
145
+ * rescue's mkdtemp folder, wherever it was staged). Anything else is the
146
+ * user's own link and is left alone.
147
+ */
148
+ function isRepairableDanglingTarget(hqRoot, current, deps = {}) {
149
+ const segments = path.resolve(current).split(path.sep);
150
+ if (segments.some((segment) => segment.startsWith(RESCUE_STAGING_SEGMENT_PREFIX)))
151
+ return true;
152
+ const roots = [...tempRoots(path.resolve(hqRoot)), ...rescueStagingRoots(deps)];
153
+ return roots.some((root) => isUnder(root, current));
154
+ }
111
155
  function containedTarget(hqRoot, entry) {
112
156
  const destination = path.join(hqRoot, entry.rel);
113
157
  const resolved = path.resolve(path.dirname(destination), entry.linkText);
@@ -149,4 +193,146 @@ function readFileOrNull(target) {
149
193
  return null;
150
194
  }
151
195
  }
196
+ /**
197
+ * Repair HQ-managed release links whose target no longer exists, such as a
198
+ * `.agents/skills` or `.codex/claude` link an earlier rescue pointed at its
199
+ * deleted temp extraction folder (feedback_2ab63e50).
200
+ *
201
+ * Only the fixed {@link RELEASE_LINKS} paths are considered, and only when the
202
+ * entry itself is a symlink or junction that does not resolve. A real file or
203
+ * directory is never touched, a link that resolves anywhere is left alone, and
204
+ * the link's parent folder must sit inside the HQ root, so a `.agents` folder
205
+ * that is itself a link out of the tree is skipped.
206
+ *
207
+ * Ownership rule: a dangling link is repaired only when its current target is
208
+ * under the HQ root or under the system temp folder (where a rescue extracts
209
+ * its staging copy). A dangling link anywhere else, such as `/Volumes/X`, is
210
+ * the user's own link to a drive that is not mounted; it is left alone and
211
+ * reported as `skipped`.
212
+ *
213
+ * The repaired link carries the text HQ ships (`../.claude/skills`), so the
214
+ * git-tracked link is unchanged. On Windows a directory link is an absolute
215
+ * junction and a file link an absolute symlink, falling back to a copy. When
216
+ * the shipped target is missing too, the dangling link is removed and nothing
217
+ * is created. The old entry is removed with {@link removeLinkOnly}, which never
218
+ * follows it. A failure on one link is reported as `failed` and the remaining
219
+ * links are still repaired.
220
+ */
221
+ export function repairDanglingReleaseLinks(hqRoot, deps = {}) {
222
+ const platform = deps.platform ?? process.platform;
223
+ let rootReal;
224
+ try {
225
+ rootReal = fs.realpathSync.native(path.resolve(hqRoot));
226
+ }
227
+ catch {
228
+ return [];
229
+ }
230
+ const repairs = [];
231
+ for (const entry of RELEASE_LINKS) {
232
+ if (deps.only !== undefined && entry.rel !== deps.only)
233
+ continue;
234
+ const destination = path.join(rootReal, entry.rel);
235
+ const lst = lstatOrNull(destination);
236
+ if (!lst || !lst.isSymbolicLink())
237
+ continue;
238
+ if (resolvesOrNull(destination) !== null)
239
+ continue;
240
+ const parentReal = resolvesOrNull(path.dirname(destination));
241
+ if (parentReal === null || !isUnder(rootReal, parentReal))
242
+ continue;
243
+ const target = path.resolve(path.dirname(destination), entry.linkText);
244
+ if (!isUnder(rootReal, target))
245
+ continue;
246
+ let current;
247
+ try {
248
+ current = path.resolve(path.dirname(destination), fs.readlinkSync(destination));
249
+ }
250
+ catch (err) {
251
+ repairs.push({ rel: entry.rel, action: "failed", reason: errorMessage(err) });
252
+ continue;
253
+ }
254
+ if (!isRepairableDanglingTarget(rootReal, current, { tmpDir: deps.tmpDir, backupRoot: deps.backupRoot })) {
255
+ repairs.push({
256
+ rel: entry.rel,
257
+ action: "skipped",
258
+ reason: `it points at ${current}, outside the HQ root, so it is left as your own link`,
259
+ });
260
+ continue;
261
+ }
262
+ const targetStat = statOrNull(target);
263
+ const targetUsable = targetStat !== null &&
264
+ (entry.kind === "dir" ? targetStat.isDirectory() : targetStat.isFile()) &&
265
+ isUnder(rootReal, resolvesOrNull(target) ?? "");
266
+ try {
267
+ removeLinkOnly(destination);
268
+ if (!targetUsable) {
269
+ repairs.push({ rel: entry.rel, action: "removed" });
270
+ continue;
271
+ }
272
+ const linkText = writeReleaseLink(entry, target, destination, platform);
273
+ repairs.push({ rel: entry.rel, action: "repointed", target, linkText });
274
+ }
275
+ catch (err) {
276
+ repairs.push({ rel: entry.rel, action: "failed", reason: errorMessage(err) });
277
+ }
278
+ }
279
+ return repairs;
280
+ }
281
+ /** Write the link and return the text it carries. */
282
+ function writeReleaseLink(entry, target, destination, platform) {
283
+ if (platform !== "win32") {
284
+ fs.symlinkSync(entry.linkText, destination, entry.kind);
285
+ return entry.linkText;
286
+ }
287
+ if (entry.kind === "dir") {
288
+ fs.symlinkSync(target, destination, "junction");
289
+ return target;
290
+ }
291
+ try {
292
+ fs.symlinkSync(target, destination, "file");
293
+ return target;
294
+ }
295
+ catch (err) {
296
+ const code = err.code;
297
+ if (code !== "EPERM" && code !== "EACCES")
298
+ throw err;
299
+ // Windows without the symlink privilege: the check accepts a copy whose
300
+ // bytes equal the target.
301
+ fs.copyFileSync(target, destination);
302
+ return target;
303
+ }
304
+ }
305
+ function tempRoots(tmpDir) {
306
+ const roots = [path.resolve(tmpDir)];
307
+ const real = resolvesOrNull(tmpDir);
308
+ if (real && !roots.includes(real))
309
+ roots.push(real);
310
+ return roots;
311
+ }
312
+ function errorMessage(err) {
313
+ return err instanceof Error ? err.message : String(err);
314
+ }
315
+ function isUnder(rootReal, candidate) {
316
+ if (!candidate)
317
+ return false;
318
+ const abs = path.resolve(candidate);
319
+ const base = rootReal.endsWith(path.sep) ? rootReal.slice(0, -1) : rootReal;
320
+ return abs === base || abs.startsWith(base + path.sep);
321
+ }
322
+ function resolvesOrNull(target) {
323
+ try {
324
+ return fs.realpathSync.native(target);
325
+ }
326
+ catch {
327
+ return null;
328
+ }
329
+ }
330
+ function statOrNull(target) {
331
+ try {
332
+ return fs.statSync(target);
333
+ }
334
+ catch {
335
+ return null;
336
+ }
337
+ }
152
338
  //# sourceMappingURL=release-links.js.map
@@ -44,6 +44,7 @@ import { getStateDir } from "@indigoai-us/hq-cloud";
44
44
  import { isPersonalJournalSlug } from "../checks/sync-health.js";
45
45
  import { createDefaultRegistry } from "../registry.js";
46
46
  import { flattenFamilies } from "../report.js";
47
+ import { repairDanglingReleaseLinks } from "../checks/release-links.js";
47
48
  import { parseHookGateProfiles, requiredMissingProfiles, } from "../hook-gate-profiles.js";
48
49
  import { createBackup } from "./backup.js";
49
50
  import { removeStrayGateEntries } from "../stray-gate-entries.js";
@@ -905,6 +906,8 @@ function planFix(hqRoot, result, rem, refreshSync, rewriteBotNodePath = defaultR
905
906
  return planRefreshSync(hqRoot, result, rem, refreshSync);
906
907
  case "refresh-manifest":
907
908
  return planRefreshManifest(hqRoot, result, rem, refreshManifest);
909
+ case "repair-release-link":
910
+ return planRepairReleaseLink(hqRoot, result, rem);
908
911
  default:
909
912
  return null;
910
913
  }
@@ -1075,6 +1078,57 @@ function planRefreshManifest(hqRoot, result, rem, refreshManifest) {
1075
1078
  failureReason: () => failureReason,
1076
1079
  };
1077
1080
  }
1081
+ /**
1082
+ * Repoint (or remove) one dangling HQ-managed release link. The repair writes
1083
+ * only the link entry, never a hook body, so it needs no backup: a link that
1084
+ * does not resolve has no content to restore.
1085
+ */
1086
+ function planRepairReleaseLink(hqRoot, result, rem) {
1087
+ const rel = rem.fixTarget;
1088
+ if (!rel)
1089
+ return null;
1090
+ let failure;
1091
+ return {
1092
+ checkId: result.checkId,
1093
+ fixClass: "repair-release-link",
1094
+ target: rel,
1095
+ relpath: "",
1096
+ preview: ` ${rel}\n` +
1097
+ ` - dangling link\n` +
1098
+ ` + the shipped link to its target under the HQ root (removed if that target is missing)`,
1099
+ summary: `repair dangling link ${rel}`,
1100
+ apply: () => {
1101
+ // Each plan repairs only its own link, and a throw is recorded as this
1102
+ // plan's failure so the remaining plans still run.
1103
+ try {
1104
+ const own = repairDanglingReleaseLinks(hqRoot, { only: rel });
1105
+ const entry = own.find((item) => item.rel === rel);
1106
+ if (!entry) {
1107
+ failure = `${rel} was not repaired`;
1108
+ }
1109
+ else if (entry.action === "skipped" || entry.action === "failed") {
1110
+ failure = `${rel} was not repaired: ${entry.reason ?? entry.action}`;
1111
+ }
1112
+ else {
1113
+ failure = undefined;
1114
+ }
1115
+ }
1116
+ catch (err) {
1117
+ failure = `${rel} was not repaired: ${err instanceof Error ? err.message : String(err)}`;
1118
+ }
1119
+ },
1120
+ postStatus: (results) => {
1121
+ const own = results.filter((entry) => entry.checkId === result.checkId);
1122
+ if (own.some((entry) => entry.target === rel))
1123
+ return "FAIL";
1124
+ // Another link's failure is that plan's status, not this one's.
1125
+ if (own.some((entry) => entry.status === "FAIL"))
1126
+ return "PASS";
1127
+ return own[0]?.status ?? null;
1128
+ },
1129
+ failureReason: () => failure,
1130
+ };
1131
+ }
1078
1132
  /** Restore the execute bit on a hook script. */
1079
1133
  function planExecutableBit(hqRoot, result, rem) {
1080
1134
  const abs = rem.fixTarget;
@@ -43,6 +43,13 @@
43
43
  * hq-cli shim (see `lib/windows/ps1-shims.ts`); the
44
44
  * execution policy is never changed. This is the one
45
45
  * scoped exception to the no-delete rule below.
46
+ * - `repair-release-link` — repoint an HQ-managed release link
47
+ * (`.agents/skills`, `.codex/claude`, ...) whose target
48
+ * is missing, using the link text HQ ships (absolute junction on Windows).
49
+ * When that target is missing too, the dangling link
50
+ * entry is removed. Only a link that does not resolve
51
+ * is touched; a real file or folder never is, and the
52
+ * link is removed without following it.
46
53
  *
47
54
  * EVERYTHING else — content drift between platform copies, a missing script, a
48
55
  * missing Codex counterpart, an unquoted `$CLAUDE_PROJECT_DIR`, a stale
@@ -54,7 +61,7 @@
54
61
  */
55
62
  import type { CheckResult } from "../types.js";
56
63
  /** The allowlisted safe repair classes `--fix` is permitted to apply. */
57
- export type FixClass = "executable-bit" | "gate-profile" | "remove-stray-gate-entries" | "refresh-sync" | "refresh-manifest" | "bot-node-path" | "bot-hq-binary" | "remove-ps1-shims";
64
+ export type FixClass = "executable-bit" | "gate-profile" | "remove-stray-gate-entries" | "refresh-sync" | "refresh-manifest" | "bot-node-path" | "bot-hq-binary" | "remove-ps1-shims" | "repair-release-link";
58
65
  /**
59
66
  * A finding's structured remediation. `autoFixable`, `action`, and `command`
60
67
  * are the `--json` surface (AC1); `fixTarget`/`fixClass` are the internal handle
@@ -43,6 +43,13 @@
43
43
  * hq-cli shim (see `lib/windows/ps1-shims.ts`); the
44
44
  * execution policy is never changed. This is the one
45
45
  * scoped exception to the no-delete rule below.
46
+ * - `repair-release-link` — repoint an HQ-managed release link
47
+ * (`.agents/skills`, `.codex/claude`, ...) whose target
48
+ * is missing, using the link text HQ ships (absolute junction on Windows).
49
+ * When that target is missing too, the dangling link
50
+ * entry is removed. Only a link that does not resolve
51
+ * is touched; a real file or folder never is, and the
52
+ * link is removed without following it.
46
53
  *
47
54
  * EVERYTHING else — content drift between platform copies, a missing script, a
48
55
  * missing Codex counterpart, an unquoted `$CLAUDE_PROJECT_DIR`, a stale
@@ -53,6 +60,7 @@
53
60
  * deciding which of two diverged copies is correct needs human judgement.
54
61
  */
55
62
  import * as path from "node:path";
63
+ import { DANGLING_RELEASE_LINK_PROBLEM, RELEASE_LINKS, RELEASE_LINKS_CHECK_ID, } from "../checks/release-links.js";
56
64
  import { isCompanyJournalSlug, isSafeCompanySlug, isPersonalJournalSlug, journalSlugFromCheckId, } from "../checks/sync-health.js";
57
65
  /** Matches an exec-bit remediation command, quoted or not: `chmod +x <path>`. */
58
66
  const CHMOD_EXEC = /^chmod\s+\+x\s+(.+)$/;
@@ -165,6 +173,20 @@ export function deriveRemediation(result) {
165
173
  command: "hq.cmd install --global",
166
174
  };
167
175
  }
176
+ // repair-release-link — a shipped link whose target is missing, such as one
177
+ // a rescue pointed at its deleted temp folder (feedback_2ab63e50).
178
+ if (checkId === RELEASE_LINKS_CHECK_ID &&
179
+ result.status === "FAIL" &&
180
+ RELEASE_LINKS.some((entry) => entry.rel === target) &&
181
+ result.message.startsWith(target + " " + DANGLING_RELEASE_LINK_PROBLEM)) {
182
+ return {
183
+ autoFixable: true,
184
+ fixClass: "repair-release-link",
185
+ fixTarget: target,
186
+ action: `Repoint the dangling ${target} link at its target under the HQ root (or remove the link when that target is missing).`,
187
+ command: "hq doctor --fix",
188
+ };
189
+ }
168
190
  // Available hq-core upgrade. WARN is intentional in `hq doctor` and is
169
191
  // not a broken sync; --fix has no mechanical repair for it.
170
192
  if (checkId === "sync.update.core") {