@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.
- package/CHANGELOG.md +84 -0
- package/assets/scaffold/core/scripts/hq-delegate-send.sh +1 -1
- package/dist/command-catalog.generated.d.ts +206 -7
- package/dist/command-catalog.generated.js +252 -8
- package/dist/command-registration-plan.d.ts +7 -0
- package/dist/command-registration-plan.js +1 -0
- package/dist/commands/access.d.ts +32 -1
- package/dist/commands/access.js +112 -3
- package/dist/commands/agent-kit.d.ts +1 -1
- package/dist/commands/agent-kit.js +3 -1
- package/dist/commands/agents.js +2 -1
- package/dist/commands/bot.d.ts +3 -0
- package/dist/commands/bot.js +179 -18
- package/dist/commands/cloud.d.ts +58 -1
- package/dist/commands/cloud.js +228 -10
- package/dist/commands/core-company-index-scope-flag.d.ts +8 -1
- package/dist/commands/core-company-index-scope-flag.js +9 -2
- package/dist/commands/core-delegate-w1b.js +5 -2
- package/dist/commands/core-dm-bind-native.js +19 -4
- package/dist/commands/core-rebuild-index-fast.js +24 -6
- package/dist/commands/core.js +18 -6
- package/dist/commands/daemon.js +16 -1
- package/dist/commands/dm.d.ts +17 -3
- package/dist/commands/dm.js +121 -26
- package/dist/commands/files.d.ts +1 -0
- package/dist/commands/files.js +22 -4
- package/dist/commands/integrations-api.d.ts +4 -0
- package/dist/commands/integrations-api.js +18 -2
- package/dist/commands/integrations-connect.d.ts +10 -3
- package/dist/commands/integrations-connect.js +70 -6
- package/dist/commands/integrations-core.d.ts +23 -0
- package/dist/commands/integrations-core.js +100 -0
- package/dist/commands/integrations-manage.js +8 -1
- package/dist/commands/integrations.js +49 -5
- package/dist/commands/lanes-loop.d.ts +20 -0
- package/dist/commands/lanes-loop.js +69 -11
- package/dist/commands/lanes.js +9 -1
- package/dist/commands/rescue.d.ts +9 -0
- package/dist/commands/rescue.js +31 -0
- package/dist/commands/run.js +115 -40
- package/dist/commands/scan.d.ts +5 -1
- package/dist/commands/scan.js +16 -4
- package/dist/commands/secrets.js +50 -9
- package/dist/commands/sync-core.d.ts +22 -0
- package/dist/commands/sync-core.js +124 -0
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2447 -2135
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/agent-kit/inbox-state.d.ts +17 -0
- package/dist/lib/agent-kit/inbox-state.js +41 -20
- package/dist/lib/agent-kit/kit-config.d.ts +2 -0
- package/dist/lib/agent-kit/kit-config.js +6 -0
- package/dist/lib/agent-kit/mcp/tools.js +12 -5
- package/dist/lib/agent-kit/run/heartbeat.d.ts +6 -3
- package/dist/lib/agent-kit/run/heartbeat.js +6 -3
- package/dist/lib/agent-kit/run/inbox.d.ts +8 -3
- package/dist/lib/agent-kit/run/inbox.js +42 -10
- package/dist/lib/agent-kit/run/request-timeout.d.ts +26 -0
- package/dist/lib/agent-kit/run/request-timeout.js +51 -0
- package/dist/lib/bot/api.d.ts +1 -1
- package/dist/lib/bot/config.d.ts +10 -0
- package/dist/lib/bot/config.js +17 -1
- package/dist/lib/bot/fleet-local.d.ts +142 -0
- package/dist/lib/bot/fleet-local.js +689 -0
- package/dist/lib/bot/fleet-python.d.ts +47 -0
- package/dist/lib/bot/fleet-python.js +218 -0
- package/dist/lib/bot/index.d.ts +2 -0
- package/dist/lib/bot/index.js +2 -0
- package/dist/lib/bot/local-config.d.ts +3 -2
- package/dist/lib/bot/local-config.js +5 -2
- package/dist/lib/bot/promote.d.ts +10 -0
- package/dist/lib/bot/promote.js +3 -2
- package/dist/lib/bot/prompt.d.ts +6 -1
- package/dist/lib/bot/prompt.js +8 -3
- package/dist/lib/bot/runtime/index.js +8 -4
- package/dist/lib/bot/runtime/scratch-dir.d.ts +24 -0
- package/dist/lib/bot/runtime/scratch-dir.js +41 -0
- package/dist/lib/caller-principal.d.ts +4 -0
- package/dist/lib/caller-principal.js +40 -0
- package/dist/lib/capacity/collectors/grok.js +8 -2
- package/dist/lib/cli-flag-keys.d.ts +2 -0
- package/dist/lib/cli-flag-keys.js +2 -0
- package/dist/lib/daemon/catalog.d.ts +12 -4
- package/dist/lib/daemon/catalog.js +70 -24
- package/dist/lib/daemon/config.d.ts +0 -7
- package/dist/lib/daemon/config.js +0 -32
- package/dist/lib/daemon/run.d.ts +5 -1
- package/dist/lib/daemon/run.js +48 -11
- package/dist/lib/daemon/supervisor.js +3 -1
- package/dist/lib/doctor/checks/release-links.d.ts +62 -1
- package/dist/lib/doctor/checks/release-links.js +188 -2
- package/dist/lib/doctor/fix/apply.js +54 -0
- package/dist/lib/doctor/fix/remediation.d.ts +8 -1
- package/dist/lib/doctor/fix/remediation.js +22 -0
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/index-render/index.d.ts +9 -0
- package/dist/lib/index-render/index.js +65 -5
- package/dist/lib/index-render/session-scope.d.ts +69 -0
- package/dist/lib/index-render/session-scope.js +279 -0
- package/dist/lib/lanes/admission.js +7 -20
- package/dist/lib/lanes/reconcile.js +13 -15
- package/dist/lib/lanes/stale-lock.d.ts +50 -0
- package/dist/lib/lanes/stale-lock.js +105 -0
- package/dist/lib/lanes/store.js +11 -76
- package/dist/lib/notify-recipient.d.ts +10 -4
- package/dist/lib/notify-recipient.js +16 -4
- package/dist/lib/secret-input-link.d.ts +42 -0
- package/dist/lib/secret-input-link.js +93 -0
- package/dist/lib/secret-scan/range.d.ts +57 -0
- package/dist/lib/secret-scan/range.js +266 -0
- package/dist/lib/secret-scan/repo-config.d.ts +18 -3
- package/dist/lib/secret-scan/repo-config.js +135 -2
- package/dist/lib/sync-core/engine-home.d.ts +44 -0
- package/dist/lib/sync-core/engine-home.js +193 -0
- package/dist/lib/sync-core/flag.d.ts +17 -0
- package/dist/lib/sync-core/flag.js +47 -0
- package/dist/lib/sync-core/reconcile.d.ts +31 -0
- package/dist/lib/sync-core/reconcile.js +196 -0
- package/dist/lib/sync-core/release.d.ts +32 -0
- package/dist/lib/sync-core/release.js +140 -0
- package/dist/lib/sync-core/rollout-state.d.ts +24 -0
- package/dist/lib/sync-core/rollout-state.js +94 -0
- package/dist/lib/vault-sync-switches.d.ts +5 -0
- package/dist/lib/vault-sync-switches.js +43 -0
- package/dist/run/script-bound-schema.d.ts +27 -0
- package/dist/run/script-bound-schema.js +97 -0
- package/dist/secrets-env-action.d.ts +2 -0
- package/dist/secrets-env-action.js +7 -2
- package/dist/utils/cognito-session.d.ts +3 -30
- package/dist/utils/cognito-session.js +2 -100
- package/dist/utils/machine-principal-state.d.ts +38 -0
- package/dist/utils/machine-principal-state.js +110 -0
- package/dist/utils/remote-list-not-converging.d.ts +21 -0
- package/dist/utils/remote-list-not-converging.js +40 -0
- package/dist/utils/safe-link.js +9 -0
- package/dist/utils/script-bound-exec.d.ts +158 -0
- package/dist/utils/script-bound-exec.js +568 -0
- package/dist/utils/sync-watchdog.d.ts +92 -0
- package/dist/utils/sync-watchdog.js +250 -0
- package/package.json +8 -8
- package/dist/lib/daemon/sync-core-canary.d.ts +0 -9
- 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`
|
|
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
|
-
|
|
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") {
|