@indigoai-us/hq-cli 5.345.147 → 5.345.148
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 +30 -0
- package/assets/bot-workers/setup/worker.yaml +11 -0
- package/dist/command-catalog.generated.d.ts +297 -3
- package/dist/command-catalog.generated.js +368 -3
- package/dist/commands/agent-inbox.js +2 -1
- package/dist/commands/cloud.js +31 -1
- package/dist/commands/core-refresh-vault-access-native.d.ts +17 -0
- package/dist/commands/core-refresh-vault-access-native.js +214 -19
- package/dist/commands/core-work-mesh-bind-trusted-native.d.ts +9 -0
- package/dist/commands/core-work-mesh-bind-trusted-native.js +26 -19
- package/dist/commands/core.js +6 -0
- package/dist/commands/daemon.d.ts +7 -0
- package/dist/commands/daemon.js +37 -3
- package/dist/commands/dm.js +9 -2
- package/dist/commands/doctor.d.ts +11 -1
- package/dist/commands/doctor.js +56 -1
- package/dist/commands/install-global.d.ts +7 -0
- package/dist/commands/install-global.js +129 -35
- package/dist/commands/lanes-shared.d.ts +3 -1
- package/dist/commands/lanes.d.ts +4 -4
- package/dist/commands/lanes.js +380 -25
- package/dist/commands/pack-install.d.ts +2 -2
- package/dist/commands/pack-install.js +133 -35
- package/dist/commands/sync-manifest.d.ts +2 -9
- package/dist/commands/sync-manifest.js +4 -15
- package/dist/hook-engine-path.d.ts +4 -0
- package/dist/hook-engine-path.js +36 -0
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2783 -2354
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/index.js +6 -0
- package/dist/lib/agent-kit/inbox-redact.d.ts +44 -0
- package/dist/lib/agent-kit/inbox-redact.js +361 -0
- package/dist/lib/agent-kit/inbox-state.js +11 -2
- package/dist/lib/agent-kit/mcp/tools.js +2 -1
- package/dist/lib/agent-kit/run/inbox.js +36 -7
- package/dist/lib/anywhere-runtime-flag-key.d.ts +3 -0
- package/dist/lib/anywhere-runtime-flag-key.js +9 -0
- package/dist/lib/company-skill-wrapper.js +6 -4
- package/dist/lib/daemon/install.d.ts +47 -1
- package/dist/lib/daemon/install.js +181 -15
- package/dist/lib/daemon/lock.d.ts +32 -0
- package/dist/lib/daemon/lock.js +55 -2
- package/dist/lib/doctor/__testing__/run-doctor.js +4 -0
- package/dist/lib/doctor/checks/anywhere.d.ts +3 -0
- package/dist/lib/doctor/checks/anywhere.js +23 -8
- package/dist/lib/doctor/checks/cli-path.d.ts +11 -0
- package/dist/lib/doctor/checks/cli-path.js +45 -0
- package/dist/lib/doctor/checks/hook-engine.d.ts +4 -0
- package/dist/lib/doctor/checks/hook-engine.js +16 -0
- package/dist/lib/doctor/checks/sync-health.d.ts +7 -0
- package/dist/lib/doctor/checks/sync-health.js +38 -2
- package/dist/lib/doctor/checks/temp-homes.d.ts +19 -0
- package/dist/lib/doctor/checks/temp-homes.js +54 -0
- package/dist/lib/doctor/fix/apply.d.ts +90 -1
- package/dist/lib/doctor/fix/apply.js +176 -10
- package/dist/lib/doctor/fix/json-output.d.ts +4 -1
- package/dist/lib/doctor/fix/json-output.js +6 -0
- package/dist/lib/doctor/fix/remediation.d.ts +7 -1
- package/dist/lib/doctor/fix/remediation.js +23 -1
- package/dist/lib/doctor/registry.js +10 -0
- package/dist/lib/doctor/temp-home-sweeper.d.ts +149 -0
- package/dist/lib/doctor/temp-home-sweeper.js +418 -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/flag-registry.js +9 -1
- package/dist/lib/flag-snapshot-file.js +8 -3
- package/dist/lib/hook-engine/resolve.d.ts +31 -0
- package/dist/lib/hook-engine/resolve.js +134 -0
- package/dist/lib/lanes/admission-host.d.ts +1 -0
- package/dist/lib/lanes/admission-host.js +8 -1
- package/dist/lib/lanes/admission.d.ts +4 -0
- package/dist/lib/lanes/admission.js +6 -2
- package/dist/lib/lanes/envelope-repair-dispatch.d.ts +5 -0
- package/dist/lib/lanes/envelope-repair-dispatch.js +12 -1
- package/dist/lib/lanes/index.d.ts +2 -2
- package/dist/lib/lanes/index.js +1 -1
- package/dist/lib/lanes/keep-alive.js +4 -1
- package/dist/lib/lanes/local-host-identity.d.ts +40 -0
- package/dist/lib/lanes/local-host-identity.js +314 -0
- package/dist/lib/lanes/reaper.d.ts +1 -1
- package/dist/lib/lanes/reaper.js +7 -0
- package/dist/lib/lanes/reconcile.d.ts +4 -0
- package/dist/lib/lanes/reconcile.js +16 -8
- package/dist/lib/lanes/spawn.d.ts +20 -0
- package/dist/lib/lanes/spawn.js +48 -2
- package/dist/lib/lanes/status-fields.js +6 -1
- package/dist/lib/lanes/store.js +8 -1
- package/dist/lib/lanes/throttle.d.ts +49 -6
- package/dist/lib/lanes/throttle.js +154 -17
- package/dist/lib/lanes/types.d.ts +22 -1
- package/dist/lib/lanes/types.js +27 -3
- package/dist/lib/lanes/watcher.js +27 -2
- package/dist/lib/lanes/worker-continuity.d.ts +2 -0
- package/dist/lib/lanes/worker-continuity.js +18 -11
- package/dist/lib/manifest-disabled.d.ts +13 -0
- package/dist/lib/manifest-disabled.js +19 -0
- package/dist/lib/service-manager/launchd.d.ts +1 -0
- package/dist/lib/service-manager/launchd.js +1 -1
- package/dist/lib/windows/user-path.d.ts +48 -0
- package/dist/lib/windows/user-path.js +114 -0
- package/dist/lib/work-context/auto-bind.d.ts +1 -1
- package/dist/lib/work-context/auto-bind.js +22 -3
- package/dist/lib/work-context/lane-bind.d.ts +31 -0
- package/dist/lib/work-context/lane-bind.js +101 -0
- package/dist/lib/work-context/organize.js +1 -0
- package/dist/lib/work-context/prd-sync.js +7 -0
- package/dist/lib/work-context/project.d.ts +36 -10
- package/dist/lib/work-context/project.js +130 -41
- package/dist/lib/work-context/reconcile.d.ts +6 -0
- package/dist/lib/work-context/reconcile.js +25 -5
- package/dist/lib/work-context/state.d.ts +6 -0
- package/dist/lib/work-context/state.js +29 -1
- package/dist/utils/copy-tree.d.ts +61 -0
- package/dist/utils/copy-tree.js +249 -0
- package/dist/utils/environmental-error.js +7 -3
- package/dist/utils/npm-invocation.d.ts +27 -0
- package/dist/utils/npm-invocation.js +54 -0
- package/dist/utils/safe-link.d.ts +50 -0
- package/dist/utils/safe-link.js +300 -0
- package/package.json +17 -5
- package/scripts/hook-engine-manifest.mjs +22 -0
- package/scripts/install-hook-engine.mjs +10 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { findHookEngine, hookEngineRequired, pinnedHookEngineMatches, HOOK_ENGINE_REPAIR, HOOK_ENGINE_REPAIR_PIN } from '../../hook-engine/resolve.js';
|
|
2
|
+
export function checkHookEngine() {
|
|
3
|
+
const required = hookEngineRequired();
|
|
4
|
+
const fallback = required ? 'Native engine is required; repair before running hooks.' : 'Existing bash hooks remain active.';
|
|
5
|
+
try {
|
|
6
|
+
const engine = findHookEngine();
|
|
7
|
+
if (engine && pinnedHookEngineMatches(engine))
|
|
8
|
+
return { checkId: 'hooks.native-engine', status: 'PASS', target: engine.pinned, message: `Native hook engine ${engine.version} installed at its version-pinned path.` };
|
|
9
|
+
return { checkId: 'hooks.native-engine', status: required ? 'FAIL' : 'WARN', message: `Native hook engine is missing or damaged. ${fallback}`, remediation: engine ? `Close HQ sessions, then run ${HOOK_ENGINE_REPAIR_PIN}` : HOOK_ENGINE_REPAIR };
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
return { checkId: 'hooks.native-engine', status: required ? 'FAIL' : 'WARN', message: `Native hook engine unavailable: ${error.message}. ${fallback}`, remediation: HOOK_ENGINE_REPAIR };
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export const hookEngineFamily = { id: 'hook-engine', title: 'Native hook engine', run: async () => [checkHookEngine()] };
|
|
16
|
+
//# sourceMappingURL=hook-engine.js.map
|
|
@@ -100,6 +100,13 @@ export interface SyncHealthDeps {
|
|
|
100
100
|
* unhealthy-sync signal, and `hq doctor --fix` has nothing local to refresh.
|
|
101
101
|
*/
|
|
102
102
|
localCompanyExists?: (slug: string) => boolean;
|
|
103
|
+
/**
|
|
104
|
+
* Whether HQ_SYNC_MANIFEST_DISABLED turns manifest uploads off on this
|
|
105
|
+
* machine. Defaults to reading the environment the way `hq sync manifest`
|
|
106
|
+
* does. When true a stale row names the switch and is not auto-fixable,
|
|
107
|
+
* since a re-upload would exit 0 without uploading.
|
|
108
|
+
*/
|
|
109
|
+
manifestUploadsDisabled?: () => boolean;
|
|
103
110
|
}
|
|
104
111
|
/**
|
|
105
112
|
* True when `slug` is a filesystem-safe company folder name. Rejects empty
|
|
@@ -33,6 +33,7 @@ import * as path from "node:path";
|
|
|
33
33
|
import * as hqCloud from "@indigoai-us/hq-cloud";
|
|
34
34
|
import { loadManifestExports, MANIFEST_UNAVAILABLE_REASON, } from "../../hq-cloud-manifest.js";
|
|
35
35
|
import { readCompaniesManifestMap } from "../../work-context/repo-remote.js";
|
|
36
|
+
import { MANIFEST_DISABLED_ENV, manifestUploadsDisabled, } from "../../manifest-disabled.js";
|
|
36
37
|
import { CLI_VERSION } from "../../../cli-version.js";
|
|
37
38
|
import { readSyncVersion } from "../../../utils/feedback-versions.js";
|
|
38
39
|
import { readHqVersion } from "../../../utils/pack-contributions.js";
|
|
@@ -58,6 +59,7 @@ function withDefaults(deps, hqRoot) {
|
|
|
58
59
|
unresolvedScopes: deps.unresolvedScopes ?? ((journals) => defaultUnresolvedScopes(hqRoot, journals)),
|
|
59
60
|
localCompanyExists: deps.localCompanyExists ??
|
|
60
61
|
((slug) => defaultLocalCompanyExists(hqRoot, slug)),
|
|
62
|
+
manifestUploadsDisabled: deps.manifestUploadsDisabled ?? (() => manifestUploadsDisabled()),
|
|
61
63
|
};
|
|
62
64
|
}
|
|
63
65
|
/**
|
|
@@ -216,6 +218,22 @@ function boundInvalidScopeToken(token) {
|
|
|
216
218
|
return token;
|
|
217
219
|
return token.slice(0, maxSuffix);
|
|
218
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* The `hq sync manifest --scope` value that re-uploads this status's scope:
|
|
223
|
+
* `personal`, the company slug, or undefined when the scope cannot be named
|
|
224
|
+
* safely (a company row whose slug was not resolved locally).
|
|
225
|
+
*/
|
|
226
|
+
function manifestUploadScope(entry) {
|
|
227
|
+
if (entry.scopeKey === "personal")
|
|
228
|
+
return "personal";
|
|
229
|
+
if (entry.scopeKey.startsWith("company:") &&
|
|
230
|
+
entry.companySlug &&
|
|
231
|
+
isSafeCompanySlug(entry.companySlug) &&
|
|
232
|
+
!NON_COMPANY_JOURNAL_SLUGS.has(entry.companySlug)) {
|
|
233
|
+
return entry.companySlug;
|
|
234
|
+
}
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
219
237
|
/**
|
|
220
238
|
* Doctor JSON `checkId` for a manifest scope. hq-cloud keys company rows
|
|
221
239
|
* `company:cmp_…`; the colon is replaced so SessionStart health can report
|
|
@@ -629,12 +647,30 @@ function manifestResults(deps, journals) {
|
|
|
629
647
|
: "next pass will send a full manifest";
|
|
630
648
|
if (ageMs > STALE_MANIFEST_THRESHOLD_MS) {
|
|
631
649
|
const days = Math.floor(ageMs / 86_400_000);
|
|
650
|
+
// `target` is the `hq sync manifest --scope` value `--fix` re-uploads.
|
|
651
|
+
// Without it the row stays manual-only. Personal must be explicit: a
|
|
652
|
+
// bare `hq sync manifest` uploads the active company instead.
|
|
653
|
+
const scope = manifestUploadScope(entry);
|
|
654
|
+
const command = `hq sync manifest${scope ? ` --scope ${scope}` : ""}`;
|
|
655
|
+
const message = `Scope '${entry.scopeKey}' last uploaded a sync manifest ${days} day${days === 1 ? "" : "s"} ago (${entry.lastUploadAt}) — the daily audit pass is not running.`;
|
|
656
|
+
// With the kill switch set the upload exits 0 and sends nothing, so the
|
|
657
|
+
// row carries no `--fix` target and names the switch instead.
|
|
658
|
+
if (deps.manifestUploadsDisabled()) {
|
|
659
|
+
return {
|
|
660
|
+
status: "WARN",
|
|
661
|
+
checkId,
|
|
662
|
+
reasonCode: "stale-threshold",
|
|
663
|
+
message: `${message} Manifest uploads are turned off on this machine by ${MANIFEST_DISABLED_ENV}.`,
|
|
664
|
+
remediation: `Unset ${MANIFEST_DISABLED_ENV}, then run \`${command}\`.`,
|
|
665
|
+
};
|
|
666
|
+
}
|
|
632
667
|
return {
|
|
633
668
|
status: "WARN",
|
|
634
669
|
checkId,
|
|
635
670
|
reasonCode: "stale-threshold",
|
|
636
|
-
|
|
637
|
-
|
|
671
|
+
...(scope ? { target: scope } : {}),
|
|
672
|
+
message,
|
|
673
|
+
remediation: `Run \`${command}\` and check ${MANIFEST_DISABLED_ENV} and the sync runner.`,
|
|
638
674
|
};
|
|
639
675
|
}
|
|
640
676
|
return {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Doctor family: abandoned throwaway `$HOME` folders.
|
|
3
|
+
*
|
|
4
|
+
* Read-only and cheap. It runs the sweeper in shallow mode: no size walk and
|
|
5
|
+
* no process probe, so plain `hq doctor` stays fast. It reports how many idle
|
|
6
|
+
* temp homes it found; `hq doctor --sweep-temp-homes --dry-run` measures them,
|
|
7
|
+
* and `hq doctor --sweep-temp-homes` removes them.
|
|
8
|
+
*/
|
|
9
|
+
import { type TempHomeSweepOptions } from "../temp-home-sweeper.js";
|
|
10
|
+
import type { CheckFamily, CheckResult } from "../types.js";
|
|
11
|
+
export declare const TEMP_HOMES_FAMILY_ID = "temp-homes";
|
|
12
|
+
export declare const TEMP_HOMES_FAMILY_TITLE = "Temp homes";
|
|
13
|
+
type TempHomesCheckOptions = Omit<TempHomeSweepOptions, "dryRun" | "shallow">;
|
|
14
|
+
/** Build the family; tests pass fake roots and clock through `options`. */
|
|
15
|
+
export declare function createTempHomesFamily(options?: TempHomesCheckOptions): CheckFamily;
|
|
16
|
+
export declare const tempHomesFamily: CheckFamily;
|
|
17
|
+
export declare function checkTempHomes(options?: TempHomesCheckOptions): CheckResult[];
|
|
18
|
+
export {};
|
|
19
|
+
//# sourceMappingURL=temp-homes.d.ts.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Doctor family: abandoned throwaway `$HOME` folders.
|
|
3
|
+
*
|
|
4
|
+
* Read-only and cheap. It runs the sweeper in shallow mode: no size walk and
|
|
5
|
+
* no process probe, so plain `hq doctor` stays fast. It reports how many idle
|
|
6
|
+
* temp homes it found; `hq doctor --sweep-temp-homes --dry-run` measures them,
|
|
7
|
+
* and `hq doctor --sweep-temp-homes` removes them.
|
|
8
|
+
*/
|
|
9
|
+
import { sweepTempHomes, } from "../temp-home-sweeper.js";
|
|
10
|
+
export const TEMP_HOMES_FAMILY_ID = "temp-homes";
|
|
11
|
+
export const TEMP_HOMES_FAMILY_TITLE = "Temp homes";
|
|
12
|
+
/** Build the family; tests pass fake roots and clock through `options`. */
|
|
13
|
+
export function createTempHomesFamily(options = {}) {
|
|
14
|
+
return {
|
|
15
|
+
id: TEMP_HOMES_FAMILY_ID,
|
|
16
|
+
title: TEMP_HOMES_FAMILY_TITLE,
|
|
17
|
+
run: () => Promise.resolve(checkTempHomes(options)),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export const tempHomesFamily = createTempHomesFamily();
|
|
21
|
+
export function checkTempHomes(options = {}) {
|
|
22
|
+
let result;
|
|
23
|
+
try {
|
|
24
|
+
result = sweepTempHomes({ ...options, shallow: true });
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
return [{
|
|
28
|
+
status: "UNKNOWN",
|
|
29
|
+
checkId: "temp-homes.scan-error",
|
|
30
|
+
message: `Could not scan temp folders: ${error instanceof Error ? error.message : String(error)}`,
|
|
31
|
+
}];
|
|
32
|
+
}
|
|
33
|
+
const stale = result.entries.filter((entry) => entry.action === "would-remove");
|
|
34
|
+
if (stale.length === 0) {
|
|
35
|
+
return [{
|
|
36
|
+
status: "PASS",
|
|
37
|
+
checkId: "temp-homes.none",
|
|
38
|
+
message: "No idle temp homes to sweep.",
|
|
39
|
+
details: { roots: result.roots.length },
|
|
40
|
+
}];
|
|
41
|
+
}
|
|
42
|
+
return [{
|
|
43
|
+
status: "WARN",
|
|
44
|
+
checkId: "temp-homes.stale",
|
|
45
|
+
message: `${stale.length} idle temp home${stale.length === 1 ? "" : "s"} left in the temp folder. ` +
|
|
46
|
+
"Run hq doctor --sweep-temp-homes --dry-run to see the disk space they use.",
|
|
47
|
+
details: {
|
|
48
|
+
count: stale.length,
|
|
49
|
+
paths: stale.map((entry) => entry.path),
|
|
50
|
+
},
|
|
51
|
+
remediation: "hq doctor --sweep-temp-homes --dry-run, then hq doctor --sweep-temp-homes",
|
|
52
|
+
}];
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=temp-homes.js.map
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
* (the command resolves it once), keeping this module decoupled from the CLI and
|
|
35
35
|
* trivially testable against a fake tree.
|
|
36
36
|
*/
|
|
37
|
+
import { type SpawnSyncReturns } from "node:child_process";
|
|
37
38
|
import type { CheckResult, DoctorStatus } from "../types.js";
|
|
38
39
|
import { type GateProfile } from "../hook-gate-profiles.js";
|
|
39
40
|
import { type FixClass } from "./remediation.js";
|
|
@@ -78,6 +79,12 @@ export interface ApplyFixesOptions {
|
|
|
78
79
|
* Tests inject a stub so `--fix` never hits the network.
|
|
79
80
|
*/
|
|
80
81
|
refreshSync?: (target: RefreshSyncTarget) => Promise<RefreshSyncResult>;
|
|
82
|
+
/**
|
|
83
|
+
* Manifest upload used by the `refresh-manifest` class. Default shells out
|
|
84
|
+
* to `hq sync manifest --scope <slug|personal>`. Tests inject a stub so
|
|
85
|
+
* `--fix` never hits the network.
|
|
86
|
+
*/
|
|
87
|
+
refreshManifest?: (target: RefreshManifestTarget) => Promise<RefreshManifestResult>;
|
|
81
88
|
/**
|
|
82
89
|
* File-only rewrite used by the `bot-node-path` class. Default rewrites
|
|
83
90
|
* ~/Library/LaunchAgents/ai.getindigo.hq-bot.<name>.plist and never calls
|
|
@@ -117,6 +124,42 @@ export interface RefreshSyncResult {
|
|
|
117
124
|
}
|
|
118
125
|
/** Per-company bound for the default `hq sync pull` repair. */
|
|
119
126
|
export declare const REFRESH_SYNC_TIMEOUT_MS = 90000;
|
|
127
|
+
/** One manifest scope the `refresh-manifest` class will upload. */
|
|
128
|
+
export interface RefreshManifestTarget {
|
|
129
|
+
/** `personal` or a company slug, as `hq sync manifest --scope` takes it. */
|
|
130
|
+
scope: string;
|
|
131
|
+
/** HQ root the manifest is built from. */
|
|
132
|
+
hqRoot: string;
|
|
133
|
+
}
|
|
134
|
+
/** Outcome of one {@link ApplyFixesOptions.refreshManifest} call. */
|
|
135
|
+
export interface RefreshManifestResult {
|
|
136
|
+
/** Whether the upload command exited 0. The re-check is the real verdict. */
|
|
137
|
+
ok: boolean;
|
|
138
|
+
/** One-line summary. */
|
|
139
|
+
message: string;
|
|
140
|
+
/**
|
|
141
|
+
* Why the upload failed, in plain words with no paths or slugs, for the
|
|
142
|
+
* unresolved-check next step. Absent when the upload exited 0.
|
|
143
|
+
*/
|
|
144
|
+
reason?: string;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Why a sync check is still FAIL or WARN after `--fix`:
|
|
148
|
+
*
|
|
149
|
+
* - `manual-only` — no safe automatic repair exists.
|
|
150
|
+
* - `repair-did-not-clear` — `--fix` ran its repair and the re-check still fails.
|
|
151
|
+
* - `not-attempted` — a repair exists but this run did not get to it
|
|
152
|
+
* (per-run cap, declined, dirty tree, pass limit).
|
|
153
|
+
*/
|
|
154
|
+
export type UnresolvedSyncReason = "manual-only" | "repair-did-not-clear" | "not-attempted";
|
|
155
|
+
/** A sync check `--fix` left FAIL or WARN, with the next step in plain words. */
|
|
156
|
+
export interface UnresolvedSyncCheck {
|
|
157
|
+
checkId: string;
|
|
158
|
+
status: DoctorStatus;
|
|
159
|
+
reason: UnresolvedSyncReason;
|
|
160
|
+
/** What the user should do next. Never the finding message (it can hold paths). */
|
|
161
|
+
nextStep: string;
|
|
162
|
+
}
|
|
120
163
|
/** One applied (or attempted) repair, with its post-fix re-check status. */
|
|
121
164
|
export interface AppliedFix {
|
|
122
165
|
/** One-based repair pass in this `hq doctor --fix` invocation. */
|
|
@@ -145,7 +188,11 @@ export interface AppliedFixBackup {
|
|
|
145
188
|
}
|
|
146
189
|
/** A newly unlocked safe repair can be picked up for at most this many passes. */
|
|
147
190
|
export declare const MAX_FIX_PASSES = 3;
|
|
148
|
-
/**
|
|
191
|
+
/**
|
|
192
|
+
* Two 90s network repairs leave 120s for startup and check overhead under the
|
|
193
|
+
* hook's 300s ceiling. Journal pulls and manifest uploads share this budget;
|
|
194
|
+
* pulls go first and uploads take any slot left.
|
|
195
|
+
*/
|
|
149
196
|
export declare const MAX_REFRESH_SYNC_PLANS_PER_RUN = 2;
|
|
150
197
|
/** The outcome of an {@link applyFixes} run. */
|
|
151
198
|
export interface ApplyFixesResult {
|
|
@@ -165,6 +212,27 @@ export interface ApplyFixesResult {
|
|
|
165
212
|
backups: AppliedFixBackup[];
|
|
166
213
|
/** The applied repairs with their post-fix status. */
|
|
167
214
|
applied: AppliedFix[];
|
|
215
|
+
/** Sync checks still FAIL or WARN when the run ended, with the next step. */
|
|
216
|
+
unresolved: UnresolvedSyncCheck[];
|
|
217
|
+
}
|
|
218
|
+
/** A concrete, planned change: its preview, the file it touches, and its apply. */
|
|
219
|
+
interface FixPlan {
|
|
220
|
+
checkId: string;
|
|
221
|
+
fixClass: FixClass;
|
|
222
|
+
target: string;
|
|
223
|
+
/** HQ-root-relative path of the single file this plan writes. Empty when none. */
|
|
224
|
+
relpath: string;
|
|
225
|
+
/** Diff-style preview lines. */
|
|
226
|
+
preview: string;
|
|
227
|
+
/** One-line summary for the post-fix report. */
|
|
228
|
+
summary: string;
|
|
229
|
+
/** Apply the change. Reads the current on-disk state each time. */
|
|
230
|
+
apply: () => void | Promise<void>;
|
|
231
|
+
/** The status of `results` this target maps to after the fix. */
|
|
232
|
+
postStatus: (results: CheckResult[]) => DoctorStatus | null;
|
|
233
|
+
refreshSyncOutcome?: () => RefreshSyncOutcome | undefined;
|
|
234
|
+
/** Why the last apply's command failed (path-free), when it reported one. */
|
|
235
|
+
failureReason?: () => string | undefined;
|
|
168
236
|
}
|
|
169
237
|
/**
|
|
170
238
|
* Apply every auto-fixable finding, honouring the dirty-tree refusal, the
|
|
@@ -173,6 +241,13 @@ export interface ApplyFixesResult {
|
|
|
173
241
|
* assert on it.
|
|
174
242
|
*/
|
|
175
243
|
export declare function applyFixes(options: ApplyFixesOptions): Promise<ApplyFixesResult>;
|
|
244
|
+
/**
|
|
245
|
+
* The sync checks still FAIL or WARN in `findings`, each with why `--fix` left
|
|
246
|
+
* it and what to do next. Reported on every run, independent of the
|
|
247
|
+
* manual-only flag: the SessionStart health hook files a bug for any sync row
|
|
248
|
+
* left after `--fix`, so the user must see the same rows and a next step.
|
|
249
|
+
*/
|
|
250
|
+
export declare function unresolvedSyncChecks(findings: readonly CheckResult[], attempted: readonly Pick<FixPlan, "checkId" | "fixClass" | "summary" | "failureReason">[]): UnresolvedSyncCheck[];
|
|
176
251
|
/**
|
|
177
252
|
* Default `refresh-sync` repair: a bounded, pull-only, keep-conflicts
|
|
178
253
|
* `hq sync pull --company <slug>` (or `--personal`) against the tree
|
|
@@ -180,6 +255,19 @@ export declare function applyFixes(options: ApplyFixesOptions): Promise<ApplyFix
|
|
|
180
255
|
* non-zero exit is reported rather than thrown so later plans still run.
|
|
181
256
|
*/
|
|
182
257
|
export declare function defaultRefreshSync(target: RefreshSyncTarget): Promise<RefreshSyncResult>;
|
|
258
|
+
/**
|
|
259
|
+
* Default `refresh-manifest` repair: a bounded `hq sync manifest --scope <s>`
|
|
260
|
+
* against the tree `--fix` is repairing. Exit 0 also covers throttled,
|
|
261
|
+
* disabled, and skipped passes, so the post-fix re-check decides whether the
|
|
262
|
+
* row cleared. A timeout or non-zero exit is reported, not thrown.
|
|
263
|
+
*/
|
|
264
|
+
export declare function defaultRefreshManifest(target: RefreshManifestTarget): Promise<RefreshManifestResult>;
|
|
265
|
+
/**
|
|
266
|
+
* Map one `hq sync manifest` spawn result to a {@link RefreshManifestResult}.
|
|
267
|
+
* A spawn killed by the timeout has a null status, so it is reported as a
|
|
268
|
+
* timeout rather than as an exit code.
|
|
269
|
+
*/
|
|
270
|
+
export declare function manifestUploadResult(scope: string, result: Pick<SpawnSyncReturns<string>, "error" | "status" | "signal">): RefreshManifestResult;
|
|
183
271
|
/**
|
|
184
272
|
* The uncommitted changes under {@link HOOK_CONFIG_DIRS}, one porcelain line
|
|
185
273
|
* each. Returns [] when the tree is clean OR when `hqRoot` is not a git repo
|
|
@@ -218,4 +306,5 @@ export declare function addHookIdToGateProfiles(source: string, hookId: string,
|
|
|
218
306
|
* non-object `hooks` value — it is replaced rather than trusted.
|
|
219
307
|
*/
|
|
220
308
|
export declare function registerHookInSettings(settings: unknown, event: string, command: string): Record<string, unknown>;
|
|
309
|
+
export {};
|
|
221
310
|
//# sourceMappingURL=apply.d.ts.map
|
|
@@ -56,7 +56,11 @@ export const HOOK_CONFIG_DIRS = [".claude", ".codex", ".grok"];
|
|
|
56
56
|
export const REFRESH_SYNC_TIMEOUT_MS = 90_000;
|
|
57
57
|
/** A newly unlocked safe repair can be picked up for at most this many passes. */
|
|
58
58
|
export const MAX_FIX_PASSES = 3;
|
|
59
|
-
/**
|
|
59
|
+
/**
|
|
60
|
+
* Two 90s network repairs leave 120s for startup and check overhead under the
|
|
61
|
+
* hook's 300s ceiling. Journal pulls and manifest uploads share this budget;
|
|
62
|
+
* pulls go first and uploads take any slot left.
|
|
63
|
+
*/
|
|
60
64
|
export const MAX_REFRESH_SYNC_PLANS_PER_RUN = 2;
|
|
61
65
|
function rotateRefreshSyncPlans(plans, afterSlug) {
|
|
62
66
|
const ordered = [...plans].sort((a, b) => a.target < b.target ? -1 : a.target > b.target ? 1 : 0);
|
|
@@ -85,6 +89,7 @@ export async function applyFixes(options) {
|
|
|
85
89
|
restoreCommand: null,
|
|
86
90
|
backups: [],
|
|
87
91
|
applied: [],
|
|
92
|
+
unresolved: [],
|
|
88
93
|
...over,
|
|
89
94
|
});
|
|
90
95
|
// 1. Dirty-tree refusal (AC8). Never write over uncommitted security-layer
|
|
@@ -99,7 +104,9 @@ export async function applyFixes(options) {
|
|
|
99
104
|
// Journal refreshes never write hook files. Allow them by class, not
|
|
100
105
|
// only by empty relpath, so a personal-vault pull and a company pull
|
|
101
106
|
// still run when SessionStart `--fix --yes` cannot pass `--force`.
|
|
102
|
-
const allowed = plans.filter((plan) => plan.fixClass === "refresh-sync" ||
|
|
107
|
+
const allowed = plans.filter((plan) => plan.fixClass === "refresh-sync" ||
|
|
108
|
+
plan.fixClass === "refresh-manifest" ||
|
|
109
|
+
plan.relpath.length === 0);
|
|
103
110
|
if (allowed.length === plans.length)
|
|
104
111
|
return plans;
|
|
105
112
|
if (allowed.length === 0)
|
|
@@ -109,12 +116,14 @@ export async function applyFixes(options) {
|
|
|
109
116
|
`re-run with --force to repair hook files.\n`);
|
|
110
117
|
return allowed;
|
|
111
118
|
};
|
|
112
|
-
const refuseDirtyTree = () => {
|
|
119
|
+
const refuseDirtyTree = (results) => {
|
|
113
120
|
writeErr(`hq doctor --fix: refusing to run — uncommitted changes under ` +
|
|
114
121
|
`${HOOK_CONFIG_DIRS.join(", ")}:\n` +
|
|
115
122
|
dirty.map((line) => ` ${line}`).join("\n") +
|
|
116
123
|
`\nCommit or stash them, or re-run with --force.\n`);
|
|
117
|
-
|
|
124
|
+
const unresolved = unresolvedSyncChecks(results, []);
|
|
125
|
+
writeUnresolvedSyncChecks(unresolved, write);
|
|
126
|
+
return empty({ exitCode: 1, refused: "dirty-tree", unresolved });
|
|
118
127
|
};
|
|
119
128
|
// Resolve the customer-visible manual-only output gate once per fix run.
|
|
120
129
|
// Fail open to the pre-change output if the identity is absent or the
|
|
@@ -129,6 +138,7 @@ export async function applyFixes(options) {
|
|
|
129
138
|
}
|
|
130
139
|
const collect = options.runChecks ?? defaultRunChecks;
|
|
131
140
|
const refreshSync = options.refreshSync ?? defaultRefreshSync;
|
|
141
|
+
const refreshManifest = options.refreshManifest ?? defaultRefreshManifest;
|
|
132
142
|
const rewriteBotNodePath = options.rewriteBotNodePath ?? defaultRewriteBotNodePath;
|
|
133
143
|
const repairBotHqBinary = options.repairBotHqBinary ?? defaultRepairBotHqBinary;
|
|
134
144
|
// 2. Collect findings, then plan each newly unlocked allowlisted repair.
|
|
@@ -137,6 +147,7 @@ export async function applyFixes(options) {
|
|
|
137
147
|
let findings = await collect(hqRoot);
|
|
138
148
|
let lastAttemptedRefreshSyncSlug = readRefreshSyncProgress(hqRoot)?.lastAttemptedSlug ?? null;
|
|
139
149
|
let refreshSyncAppliedThisRun = 0;
|
|
150
|
+
let refreshManifestAppliedThisRun = 0;
|
|
140
151
|
const appliedKeys = new Set();
|
|
141
152
|
const planned = [];
|
|
142
153
|
const backups = [];
|
|
@@ -155,7 +166,7 @@ export async function applyFixes(options) {
|
|
|
155
166
|
const rem = deriveRemediation(result);
|
|
156
167
|
if (!rem?.autoFixable)
|
|
157
168
|
continue;
|
|
158
|
-
const plan = planFix(hqRoot, result, rem, refreshSync, rewriteBotNodePath, repairBotHqBinary);
|
|
169
|
+
const plan = planFix(hqRoot, result, rem, refreshSync, rewriteBotNodePath, repairBotHqBinary, refreshManifest);
|
|
159
170
|
if (!plan)
|
|
160
171
|
continue;
|
|
161
172
|
const key = JSON.stringify([plan.checkId, plan.target]);
|
|
@@ -171,16 +182,25 @@ export async function applyFixes(options) {
|
|
|
171
182
|
seenRefreshTargets.add(plan.target);
|
|
172
183
|
return true;
|
|
173
184
|
}).slice(0, Math.max(0, MAX_REFRESH_SYNC_PLANS_PER_RUN - refreshSyncAppliedThisRun));
|
|
185
|
+
// Manifest uploads take whatever network budget the pulls left.
|
|
186
|
+
const manifestPlans = plans
|
|
187
|
+
.filter((plan) => plan.fixClass === "refresh-manifest")
|
|
188
|
+
.sort((a, b) => (a.target < b.target ? -1 : a.target > b.target ? 1 : 0))
|
|
189
|
+
.slice(0, Math.max(0, MAX_REFRESH_SYNC_PLANS_PER_RUN -
|
|
190
|
+
refreshSyncAppliedThisRun -
|
|
191
|
+
refreshManifestAppliedThisRun -
|
|
192
|
+
refreshPlans.length));
|
|
174
193
|
return [
|
|
175
|
-
...plans.filter((plan) => plan.fixClass !== "refresh-sync"),
|
|
194
|
+
...plans.filter((plan) => plan.fixClass !== "refresh-sync" && plan.fixClass !== "refresh-manifest"),
|
|
176
195
|
...refreshPlans,
|
|
196
|
+
...manifestPlans,
|
|
177
197
|
];
|
|
178
198
|
};
|
|
179
199
|
for (let pass = 1; pass <= MAX_FIX_PASSES; pass += 1) {
|
|
180
200
|
const plannedThisPass = refuseDirtyHookWrites(buildPlans(findings));
|
|
181
201
|
if (plannedThisPass === "refuse") {
|
|
182
202
|
if (planned.length === 0)
|
|
183
|
-
return refuseDirtyTree();
|
|
203
|
+
return refuseDirtyTree(findings);
|
|
184
204
|
write(`Pass ${pass}/${MAX_FIX_PASSES}: remaining repairs write hook files; ` +
|
|
185
205
|
`stopped because the tree is dirty.\n`);
|
|
186
206
|
refused = "dirty-tree";
|
|
@@ -202,7 +222,9 @@ export async function applyFixes(options) {
|
|
|
202
222
|
write("Aborted — no changes written.\n");
|
|
203
223
|
if (showManualOnlyRemediations)
|
|
204
224
|
writeManualOnlyRemediations(findings, write);
|
|
205
|
-
|
|
225
|
+
const unresolved = unresolvedSyncChecks(findings, []);
|
|
226
|
+
writeUnresolvedSyncChecks(unresolved, write);
|
|
227
|
+
return empty({ refused, fixableCount, unresolved });
|
|
206
228
|
}
|
|
207
229
|
write(`Pass ${pass} declined — no further changes written.\n`);
|
|
208
230
|
break;
|
|
@@ -233,6 +255,8 @@ export async function applyFixes(options) {
|
|
|
233
255
|
lastAttemptedRefreshSyncSlug = plan.target;
|
|
234
256
|
refreshSyncAppliedThisRun += 1;
|
|
235
257
|
}
|
|
258
|
+
if (plan.fixClass === "refresh-manifest")
|
|
259
|
+
refreshManifestAppliedThisRun += 1;
|
|
236
260
|
appliedKeys.add(JSON.stringify([plan.checkId, plan.target]));
|
|
237
261
|
planned.push({ plan, pass });
|
|
238
262
|
await plan.apply();
|
|
@@ -244,7 +268,9 @@ export async function applyFixes(options) {
|
|
|
244
268
|
write("hq doctor --fix: nothing to repair — no auto-fixable findings.\n");
|
|
245
269
|
if (showManualOnlyRemediations)
|
|
246
270
|
writeManualOnlyRemediations(findings, write);
|
|
247
|
-
|
|
271
|
+
const unresolved = unresolvedSyncChecks(findings, []);
|
|
272
|
+
writeUnresolvedSyncChecks(unresolved, write);
|
|
273
|
+
return empty({ refused, fixableCount, unresolved });
|
|
248
274
|
}
|
|
249
275
|
// Show each repair's final post-fix status, including work from earlier passes.
|
|
250
276
|
const applied = planned.map(({ plan, pass }) => {
|
|
@@ -283,6 +309,8 @@ export async function applyFixes(options) {
|
|
|
283
309
|
}
|
|
284
310
|
if (showManualOnlyRemediations)
|
|
285
311
|
writeManualOnlyRemediations(findings, write);
|
|
312
|
+
const unresolved = unresolvedSyncChecks(findings, planned.map(({ plan }) => plan));
|
|
313
|
+
writeUnresolvedSyncChecks(unresolved, write);
|
|
286
314
|
return {
|
|
287
315
|
exitCode: 0,
|
|
288
316
|
wrote: true,
|
|
@@ -292,11 +320,81 @@ export async function applyFixes(options) {
|
|
|
292
320
|
restoreCommand,
|
|
293
321
|
backups,
|
|
294
322
|
applied,
|
|
323
|
+
unresolved,
|
|
295
324
|
};
|
|
296
325
|
}
|
|
326
|
+
/**
|
|
327
|
+
* The sync checks still FAIL or WARN in `findings`, each with why `--fix` left
|
|
328
|
+
* it and what to do next. Reported on every run, independent of the
|
|
329
|
+
* manual-only flag: the SessionStart health hook files a bug for any sync row
|
|
330
|
+
* left after `--fix`, so the user must see the same rows and a next step.
|
|
331
|
+
*/
|
|
332
|
+
export function unresolvedSyncChecks(findings, attempted) {
|
|
333
|
+
return findings.flatMap((result) => {
|
|
334
|
+
if (!result.checkId.startsWith("sync."))
|
|
335
|
+
return [];
|
|
336
|
+
const remediation = deriveRemediation(result);
|
|
337
|
+
if (!remediation)
|
|
338
|
+
return [];
|
|
339
|
+
const tried = attempted.find((plan) => plan.checkId === result.checkId);
|
|
340
|
+
const command = remediation.command.trim();
|
|
341
|
+
if (tried) {
|
|
342
|
+
const failure = tried.failureReason?.();
|
|
343
|
+
const outcome = failure
|
|
344
|
+
? `--fix tried to ${tried.summary}, but ${failure}, so the check still shows ${result.status}.`
|
|
345
|
+
: `--fix tried to ${tried.summary}, but the check still shows ${result.status}.`;
|
|
346
|
+
return [
|
|
347
|
+
{
|
|
348
|
+
checkId: result.checkId,
|
|
349
|
+
status: result.status,
|
|
350
|
+
reason: "repair-did-not-clear",
|
|
351
|
+
nextStep: command
|
|
352
|
+
? `${outcome} Run \`${command}\` yourself and read its error.`
|
|
353
|
+
: `${outcome} Run \`hq doctor --only sync\` for details.`,
|
|
354
|
+
},
|
|
355
|
+
];
|
|
356
|
+
}
|
|
357
|
+
if (remediation.autoFixable) {
|
|
358
|
+
return [
|
|
359
|
+
{
|
|
360
|
+
checkId: result.checkId,
|
|
361
|
+
status: result.status,
|
|
362
|
+
reason: "not-attempted",
|
|
363
|
+
nextStep: `--fix did not get to this one in this run. Run \`hq doctor --fix\` again, or run \`${command}\` now.`,
|
|
364
|
+
},
|
|
365
|
+
];
|
|
366
|
+
}
|
|
367
|
+
// The action is the check's remediation (or a classifier override). When
|
|
368
|
+
// it is only the finding message, give a generic step: messages can hold
|
|
369
|
+
// paths, and the JSON surface carries none.
|
|
370
|
+
const nextStep = remediation.action && remediation.action !== result.message
|
|
371
|
+
? remediation.action
|
|
372
|
+
: "Run `hq doctor --only sync` to see the details, then fix what it describes.";
|
|
373
|
+
return [
|
|
374
|
+
{
|
|
375
|
+
checkId: result.checkId,
|
|
376
|
+
status: result.status,
|
|
377
|
+
reason: "manual-only",
|
|
378
|
+
nextStep: `--fix cannot repair this automatically. ${nextStep}`,
|
|
379
|
+
},
|
|
380
|
+
];
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
/** Print the sync checks `--fix` left unresolved, or nothing when there are none. */
|
|
384
|
+
function writeUnresolvedSyncChecks(unresolved, write) {
|
|
385
|
+
if (unresolved.length === 0)
|
|
386
|
+
return;
|
|
387
|
+
write("\nSync checks still not resolved after --fix:\n");
|
|
388
|
+
for (const entry of unresolved) {
|
|
389
|
+
write(` ${entry.status} ${entry.checkId}\n Next step: ${entry.nextStep}\n`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
297
392
|
/** Print the available user action for findings that `--fix` deliberately skips. */
|
|
298
393
|
function writeManualOnlyRemediations(findings, write) {
|
|
299
394
|
const manual = findings.flatMap((result) => {
|
|
395
|
+
// Sync rows are always listed by writeUnresolvedSyncChecks.
|
|
396
|
+
if (result.checkId.startsWith("sync."))
|
|
397
|
+
return [];
|
|
300
398
|
const remediation = deriveRemediation(result);
|
|
301
399
|
return remediation && !remediation.autoFixable
|
|
302
400
|
? [{ result, remediation }]
|
|
@@ -403,6 +501,49 @@ export function defaultRefreshSync(target) {
|
|
|
403
501
|
message: `pulled '${target.slug}'`,
|
|
404
502
|
});
|
|
405
503
|
}
|
|
504
|
+
/**
|
|
505
|
+
* Default `refresh-manifest` repair: a bounded `hq sync manifest --scope <s>`
|
|
506
|
+
* against the tree `--fix` is repairing. Exit 0 also covers throttled,
|
|
507
|
+
* disabled, and skipped passes, so the post-fix re-check decides whether the
|
|
508
|
+
* row cleared. A timeout or non-zero exit is reported, not thrown.
|
|
509
|
+
*/
|
|
510
|
+
export function defaultRefreshManifest(target) {
|
|
511
|
+
const hqBin = process.argv[1];
|
|
512
|
+
const args = ["sync", "manifest", "--scope", target.scope, "--hq-root", target.hqRoot];
|
|
513
|
+
const result = spawnSync(hqBin ? process.execPath : "hq", hqBin ? [hqBin, ...args] : args, {
|
|
514
|
+
encoding: "utf8",
|
|
515
|
+
timeout: REFRESH_SYNC_TIMEOUT_MS,
|
|
516
|
+
cwd: target.hqRoot,
|
|
517
|
+
});
|
|
518
|
+
return Promise.resolve(manifestUploadResult(target.scope, result));
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Map one `hq sync manifest` spawn result to a {@link RefreshManifestResult}.
|
|
522
|
+
* A spawn killed by the timeout has a null status, so it is reported as a
|
|
523
|
+
* timeout rather than as an exit code.
|
|
524
|
+
*/
|
|
525
|
+
export function manifestUploadResult(scope, result) {
|
|
526
|
+
const errorCode = result.error?.code;
|
|
527
|
+
if (errorCode === "ETIMEDOUT" || (result.status === null && result.signal)) {
|
|
528
|
+
const reason = `the upload timed out after ${REFRESH_SYNC_TIMEOUT_MS / 1000}s`;
|
|
529
|
+
return { ok: false, reason, message: `manifest upload for '${scope}' timed out` };
|
|
530
|
+
}
|
|
531
|
+
if (result.error) {
|
|
532
|
+
return {
|
|
533
|
+
ok: false,
|
|
534
|
+
reason: `the upload could not start${errorCode ? ` (${errorCode})` : ""}`,
|
|
535
|
+
message: `could not upload the manifest for '${scope}': ${result.error.message}`,
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
if (result.status !== 0) {
|
|
539
|
+
return {
|
|
540
|
+
ok: false,
|
|
541
|
+
reason: `the upload exited with code ${result.status}`,
|
|
542
|
+
message: `manifest upload for '${scope}' exited ${result.status}`,
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
return { ok: true, message: `uploaded the manifest for '${scope}'` };
|
|
546
|
+
}
|
|
406
547
|
// --- Dirty-tree probe ----------------------------------------------------------
|
|
407
548
|
/**
|
|
408
549
|
* The uncommitted changes under {@link HOOK_CONFIG_DIRS}, one porcelain line
|
|
@@ -422,7 +563,7 @@ export function uncommittedHookConfigChanges(hqRoot) {
|
|
|
422
563
|
}
|
|
423
564
|
// --- Fix planning --------------------------------------------------------------
|
|
424
565
|
/** Build the concrete plan for one auto-fixable finding, or null if unplannable. */
|
|
425
|
-
function planFix(hqRoot, result, rem, refreshSync, rewriteBotNodePath = defaultRewriteBotNodePath, repairBotHqBinary = defaultRepairBotHqBinary) {
|
|
566
|
+
function planFix(hqRoot, result, rem, refreshSync, rewriteBotNodePath = defaultRewriteBotNodePath, repairBotHqBinary = defaultRepairBotHqBinary, refreshManifest = defaultRefreshManifest) {
|
|
426
567
|
switch (rem.fixClass) {
|
|
427
568
|
case "bot-node-path":
|
|
428
569
|
return planBotNodePath(result, rem, rewriteBotNodePath);
|
|
@@ -436,6 +577,8 @@ function planFix(hqRoot, result, rem, refreshSync, rewriteBotNodePath = defaultR
|
|
|
436
577
|
return planRemoveStrayGateEntries(hqRoot, result, rem);
|
|
437
578
|
case "refresh-sync":
|
|
438
579
|
return planRefreshSync(hqRoot, result, rem, refreshSync);
|
|
580
|
+
case "refresh-manifest":
|
|
581
|
+
return planRefreshManifest(hqRoot, result, rem, refreshManifest);
|
|
439
582
|
default:
|
|
440
583
|
return null;
|
|
441
584
|
}
|
|
@@ -555,6 +698,29 @@ function planRefreshSync(hqRoot, result, rem, refreshSync) {
|
|
|
555
698
|
postStatus: (results) => results.find((entry) => entry.checkId === result.checkId)?.status ?? null,
|
|
556
699
|
};
|
|
557
700
|
}
|
|
701
|
+
/** Re-upload one stale sync manifest scope. Writes nothing under the HQ tree. */
|
|
702
|
+
function planRefreshManifest(hqRoot, result, rem, refreshManifest) {
|
|
703
|
+
const scope = rem.fixTarget;
|
|
704
|
+
if (!scope)
|
|
705
|
+
return null;
|
|
706
|
+
let failureReason;
|
|
707
|
+
return {
|
|
708
|
+
checkId: result.checkId,
|
|
709
|
+
fixClass: "refresh-manifest",
|
|
710
|
+
target: scope,
|
|
711
|
+
relpath: "",
|
|
712
|
+
preview: ` sync manifest (${scope === "personal" ? "personal vault" : `company ${scope}`})\n` +
|
|
713
|
+
` ~ hq sync manifest --scope ${scope}\n` +
|
|
714
|
+
` upload a fresh file manifest for the sync audit (no local files change)`,
|
|
715
|
+
summary: `upload a fresh sync manifest for ${scope}`,
|
|
716
|
+
apply: async () => {
|
|
717
|
+
const outcome = await refreshManifest({ scope, hqRoot });
|
|
718
|
+
failureReason = outcome.ok ? undefined : outcome.reason;
|
|
719
|
+
},
|
|
720
|
+
postStatus: (results) => results.find((entry) => entry.checkId === result.checkId)?.status ?? null,
|
|
721
|
+
failureReason: () => failureReason,
|
|
722
|
+
};
|
|
723
|
+
}
|
|
558
724
|
/** Restore the execute bit on a hook script. */
|
|
559
725
|
function planExecutableBit(hqRoot, result, rem) {
|
|
560
726
|
const abs = rem.fixTarget;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AppliedFix, AppliedFixBackup, ApplyFixesResult } from "./apply.js";
|
|
1
|
+
import type { AppliedFix, AppliedFixBackup, ApplyFixesResult, UnresolvedSyncCheck } from "./apply.js";
|
|
2
2
|
export interface DoctorFixJsonResult {
|
|
3
3
|
schemaVersion: number;
|
|
4
4
|
exitCode: number;
|
|
@@ -15,6 +15,8 @@ export interface DoctorFixJsonResult {
|
|
|
15
15
|
outcome: NonNullable<AppliedFix["refreshSyncOutcome"]>;
|
|
16
16
|
postStatus: AppliedFix["postStatus"];
|
|
17
17
|
}[];
|
|
18
|
+
/** Sync checks still FAIL or WARN after the run, with the next step. */
|
|
19
|
+
unresolved: Pick<UnresolvedSyncCheck, "checkId" | "status" | "reason" | "nextStep">[];
|
|
18
20
|
}
|
|
19
21
|
/** Build the intentionally narrow JSON surface for `hq doctor --fix --json`. */
|
|
20
22
|
export declare function buildDoctorFixJson(input: {
|
|
@@ -22,6 +24,7 @@ export declare function buildDoctorFixJson(input: {
|
|
|
22
24
|
refused: ApplyFixesResult["refused"] | "outside-hq-tree";
|
|
23
25
|
applied: readonly AppliedFix[];
|
|
24
26
|
backups?: readonly AppliedFixBackup[];
|
|
27
|
+
unresolved?: readonly UnresolvedSyncCheck[];
|
|
25
28
|
}): DoctorFixJsonResult;
|
|
26
29
|
export declare function renderDoctorFixJson(input: Parameters<typeof buildDoctorFixJson>[0]): string;
|
|
27
30
|
//# sourceMappingURL=json-output.d.ts.map
|