@indigoai-us/hq-cli 5.273.0 → 5.274.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -0
- package/README.md +32 -1
- package/dist/command-catalog.generated.d.ts +72 -0
- package/dist/command-catalog.generated.js +92 -0
- package/dist/command-registration-plan.d.ts +12 -0
- package/dist/command-registration-plan.js +2 -0
- package/dist/commands/cloud.js +40 -0
- package/dist/commands/core-checkpoint.js +3 -1
- package/dist/commands/core-command-catalog.d.ts +32 -0
- package/dist/commands/core-command-catalog.js +58 -0
- package/dist/commands/core-ontology.js +9 -4
- package/dist/commands/core.d.ts +4 -0
- package/dist/commands/core.js +79 -75
- package/dist/commands/deploy-comments.d.ts +3 -0
- package/dist/commands/deploy-comments.js +159 -0
- package/dist/commands/lanes-move.d.ts +12 -0
- package/dist/commands/lanes-move.js +342 -0
- package/dist/commands/lanes-resume.js +3 -1
- package/dist/commands/lanes-shared.d.ts +36 -1
- package/dist/commands/lanes-shared.js +59 -0
- package/dist/commands/lanes.d.ts +1 -1
- package/dist/commands/lanes.js +204 -456
- package/dist/commands/report.js +2 -14
- package/dist/fast-core.js +1 -1
- package/dist/{commands/scaffold-fast.d.ts → fast-scaffold.d.ts} +1 -1
- package/dist/{commands/scaffold-fast.js → fast-scaffold.js} +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/lib/checkpoint-ontology/apply.d.ts +8 -0
- package/dist/lib/checkpoint-ontology/apply.js +33 -0
- package/dist/lib/checkpoint-ontology/plan.d.ts +15 -0
- package/dist/lib/checkpoint-ontology/plan.js +15 -0
- package/dist/lib/deploy-api.d.ts +6 -0
- package/dist/lib/deploy-api.js +21 -0
- package/dist/lib/doctor/fix/apply.d.ts +17 -2
- package/dist/lib/doctor/fix/apply.js +111 -57
- package/dist/lib/doctor/fix/json-output.d.ts +9 -1
- package/dist/lib/doctor/fix/json-output.js +11 -0
- package/dist/lib/first-sync-next-step.d.ts +37 -0
- package/dist/lib/first-sync-next-step.js +189 -0
- package/dist/main.js +4 -1
- package/dist/utils/cli-telemetry.d.ts +7 -1
- package/dist/utils/cli-telemetry.js +84 -8
- package/dist/utils/contribution-table.d.ts +1 -1
- package/dist/utils/scaffold-fast-import-guard.d.ts +3 -0
- package/dist/utils/scaffold-fast-import-guard.js +28 -0
- package/dist/utils/scaffold-packlist-guard.d.ts +7 -0
- package/dist/utils/scaffold-packlist-guard.js +23 -0
- package/dist/utils/scaffold-sibling-references.d.ts +21 -0
- package/dist/utils/scaffold-sibling-references.js +156 -0
- package/package.json +1 -1
package/dist/commands/report.js
CHANGED
|
@@ -5,9 +5,9 @@ import * as os from "node:os";
|
|
|
5
5
|
import * as path from "node:path";
|
|
6
6
|
import { ensureCognitoToken, } from "../utils/cognito-session.js";
|
|
7
7
|
import { vaultApiFetch } from "../utils/vault-api.js";
|
|
8
|
+
import { DEPLOY_API_URL, deployApiRequest, deployHeaders } from "../lib/deploy-api.js";
|
|
8
9
|
import { listReportTemplates } from "../lib/report-templates.js";
|
|
9
10
|
import { adoptReport, buildReportTarball, conventionReportDir, deployOrgContext, ReportPublishError, plainFailureReason, publishReport, } from "../lib/report-publish.js";
|
|
10
|
-
const DEPLOY_API_URL = process.env.HQ_DEPLOY_API_URL ?? "https://api.indigo-hq.com";
|
|
11
11
|
const REPORT_CHANNEL_STATE_FILE = "report-channels.json";
|
|
12
12
|
const MAX_REPORT_DISCOVERY_CHANNELS = 50;
|
|
13
13
|
const JOB_EXECUTION_DISABLED_MESSAGE = "Scheduled jobs are turned off for this company. A company owner can turn them back on.";
|
|
@@ -214,22 +214,10 @@ export async function patchReport(token, report, patch) {
|
|
|
214
214
|
throw new ReportPublishError("Report update response was incomplete.");
|
|
215
215
|
return body.report;
|
|
216
216
|
}
|
|
217
|
-
function deployHeaders(token, json = true, extra = {}) {
|
|
218
|
-
return {
|
|
219
|
-
Authorization: `Bearer ${token}`,
|
|
220
|
-
Accept: "application/json",
|
|
221
|
-
...(json ? { "Content-Type": "application/json" } : {}),
|
|
222
|
-
...extra,
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
217
|
async function deployJson(token, route, method, body, extraHeaders = {}) {
|
|
226
218
|
let res;
|
|
227
219
|
try {
|
|
228
|
-
res = await
|
|
229
|
-
method,
|
|
230
|
-
headers: deployHeaders(token, body !== undefined, extraHeaders),
|
|
231
|
-
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
232
|
-
});
|
|
220
|
+
res = await deployApiRequest(token, route, method, body, extraHeaders);
|
|
233
221
|
}
|
|
234
222
|
catch {
|
|
235
223
|
throw new ReportPublishError("Could not reach hq-deploy.");
|
package/dist/fast-core.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { resolveLiveRoot } from "./utils/hq-roots.js";
|
|
11
11
|
import { runBundledScript } from "./utils/run-bundled-script.js";
|
|
12
|
-
import { parseFastCore } from "./
|
|
12
|
+
import { parseFastCore } from "./fast-scaffold.js";
|
|
13
13
|
/**
|
|
14
14
|
* Run the fast-path scaffold script named in `argv` and exit with its code.
|
|
15
15
|
* Only call when {@link isFastCoreRequest} is true; a non-match returns without
|
|
@@ -38,4 +38,4 @@ export type ParsedFastCore = {
|
|
|
38
38
|
export declare function parseFastCore(argv: readonly string[]): ParsedFastCore | null;
|
|
39
39
|
/** Whether this invocation is a fast-path scaffold dispatch. */
|
|
40
40
|
export declare function isFastCoreRequest(argv: readonly string[]): boolean;
|
|
41
|
-
//# sourceMappingURL=scaffold
|
|
41
|
+
//# sourceMappingURL=fast-scaffold.d.ts.map
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import "./node-preflight.js";
|
|
3
3
|
import "./node-network-compat.js";
|
|
4
4
|
import { isVersionRequest } from "./version-request.js";
|
|
5
|
-
import { isFastCoreRequest } from "./
|
|
5
|
+
import { isFastCoreRequest } from "./fast-scaffold.js";
|
|
6
6
|
export declare const __test__: {
|
|
7
7
|
isVersionRequest: typeof isVersionRequest;
|
|
8
8
|
isFastCoreRequest: typeof isFastCoreRequest;
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import "./node-network-compat.js";
|
|
|
6
6
|
import { isVersionRequest, wantsLabelledVersionReport } from "./version-request.js";
|
|
7
7
|
// Dependency-light: a pure parser + table, no command graph. Safe to load on
|
|
8
8
|
// every path (including --version) without reintroducing the heavy startup.
|
|
9
|
-
import { isFastCoreRequest } from "./
|
|
9
|
+
import { isFastCoreRequest } from "./fast-scaffold.js";
|
|
10
10
|
// Also dependency-light: registers a process-level rejection boundary; its only
|
|
11
11
|
// heavy import (main.js) is lazy and reached only when a rejection fires.
|
|
12
12
|
import { installProcessRejectionBoundary } from "./unhandled-rejection-boundary.js";
|
|
@@ -33,7 +33,7 @@ if (isVersionRequest(process.argv)) {
|
|
|
33
33
|
else if (isFastCoreRequest(process.argv)) {
|
|
34
34
|
// Hot relocated plumbing (`hq core hq-session`, `hq core checkpoint-stop-gate`)
|
|
35
35
|
// dispatches straight to the bundled-script runner, so it never pays for the
|
|
36
|
-
// ~60-module command graph main.ts imports. See src/
|
|
36
|
+
// ~60-module command graph main.ts imports. See src/fast-scaffold.ts.
|
|
37
37
|
void import("./fast-core.js").then(({ runFastCore }) => runFastCore(process.argv));
|
|
38
38
|
}
|
|
39
39
|
else {
|
|
@@ -105,6 +105,14 @@ export declare class ApplyError extends Error {
|
|
|
105
105
|
}
|
|
106
106
|
/** Validate one raw candidate. Returns the candidate or a rejection reason. */
|
|
107
107
|
export declare function parseCandidate(raw: unknown): Candidate | string;
|
|
108
|
+
/**
|
|
109
|
+
* checkpoint.ontology.named-evidence: true when `evidence` holds one of
|
|
110
|
+
* `names` as a word, matching case. A name inside a path, domain, handle, tag
|
|
111
|
+
* or identifier does not count: `indigoai-us/hq`, `getindigo.ai`,
|
|
112
|
+
* `C:\\Indigo\\hq`, `scope: company:indigo`, `Indigo:internal`, `@indigo`,
|
|
113
|
+
* `` `indigo` ``.
|
|
114
|
+
*/
|
|
115
|
+
export declare function evidenceNames(evidence: string, names: string[]): boolean;
|
|
108
116
|
type Resolution = {
|
|
109
117
|
tier: 1;
|
|
110
118
|
target: string;
|
|
@@ -63,6 +63,25 @@ export function parseCandidate(raw) {
|
|
|
63
63
|
evidence: typeof c.evidence === "string" ? c.evidence : "",
|
|
64
64
|
};
|
|
65
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* checkpoint.ontology.named-evidence: true when `evidence` holds one of
|
|
68
|
+
* `names` as a word, matching case. A name inside a path, domain, handle, tag
|
|
69
|
+
* or identifier does not count: `indigoai-us/hq`, `getindigo.ai`,
|
|
70
|
+
* `C:\\Indigo\\hq`, `scope: company:indigo`, `Indigo:internal`, `@indigo`,
|
|
71
|
+
* `` `indigo` ``.
|
|
72
|
+
*/
|
|
73
|
+
export function evidenceNames(evidence, names) {
|
|
74
|
+
for (const raw of names) {
|
|
75
|
+
const name = raw.trim();
|
|
76
|
+
if (!name || !evidence)
|
|
77
|
+
continue;
|
|
78
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
79
|
+
const word = new RegExp(`(?<![\\p{L}\\p{N}_\\-./\\\\@:\`#])${escaped}(?![\\p{L}\\p{N}_\\-/\\\\@\`]|[.:#][\\p{L}\\p{N}])`, "u");
|
|
80
|
+
if (word.test(evidence))
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
66
85
|
/** Design section 5.3, tiers 1 to 4. */
|
|
67
86
|
export function resolveCandidate(index, type, canonical) {
|
|
68
87
|
const normalized = normalizeEntityName(canonical, type);
|
|
@@ -281,6 +300,9 @@ export function applyOntologyRun(options) {
|
|
|
281
300
|
if (types.size > 1)
|
|
282
301
|
crossTypeNames.add(name);
|
|
283
302
|
}
|
|
303
|
+
// checkpoint.ontology.named-evidence: the company's own company entity.
|
|
304
|
+
const ownCompanyRel = `ontology/entities/company/${company.slug}.md`;
|
|
305
|
+
const isOwnCompany = (entity, rel) => entity.type === "company" && (rel === ownCompanyRel || crossTypeKey(entity.canonical_name) === crossTypeKey(company.slug));
|
|
284
306
|
const inputs = liveInputs(options.hqRoot, company, plan);
|
|
285
307
|
const blocked = new Set();
|
|
286
308
|
const updates = new Map();
|
|
@@ -339,6 +361,13 @@ export function applyOntologyRun(options) {
|
|
|
339
361
|
note(report.rejected, "ambiguous_type");
|
|
340
362
|
continue;
|
|
341
363
|
}
|
|
364
|
+
if (plan.named_evidence === true &&
|
|
365
|
+
input.kind === "knowledge" &&
|
|
366
|
+
isOwnCompany(linked, resolved.target) &&
|
|
367
|
+
!evidenceNames(candidate.evidence, [candidate.name, ...candidate.aliases])) {
|
|
368
|
+
note(report.rejected, "unnamed_in_evidence");
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
342
371
|
report.matched += 1;
|
|
343
372
|
report.links.push({ input: candidate.input, action: "link", type: linked.type, canonical_name: linked.canonical_name, path: resolved.target });
|
|
344
373
|
if (resolved.tier === 1 && resolved.duplicates.length > 0) {
|
|
@@ -379,6 +408,10 @@ export function applyOntologyRun(options) {
|
|
|
379
408
|
? [...(index.canonical.get(entityKey("project", normalizeEntityName(canonical, "project"))) ?? [])]
|
|
380
409
|
: [];
|
|
381
410
|
if (projects.length === 1) {
|
|
411
|
+
if (plan.named_evidence === true && !evidenceNames(candidate.evidence, [candidate.name, ...candidate.aliases])) {
|
|
412
|
+
note(report.rejected, "unnamed_in_evidence");
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
382
415
|
const linked = index.byRel.get(projects[0]);
|
|
383
416
|
report.matched += 1;
|
|
384
417
|
report.links.push({ input: candidate.input, action: "link", type: linked.type, canonical_name: linked.canonical_name, path: projects[0] });
|
|
@@ -81,6 +81,19 @@ export declare const SKIP_AMBIGUOUS_CONCEPT_LINKS_FLAG = "checkpoint.ontology.sk
|
|
|
81
81
|
* to 0.709 at precision 0.747 to 0.731 (61 of the 63 added links are HQ).
|
|
82
82
|
*/
|
|
83
83
|
export declare const CONCEPT_PROJECT_LINK_FLAG = "checkpoint.ontology.concept-project-link";
|
|
84
|
+
/**
|
|
85
|
+
* A knowledge link to the company's own company entity, or a
|
|
86
|
+
* concept-project-link redirect, needs evidence that names the entity as a
|
|
87
|
+
* word: the candidate name or one of its aliases, not inside a path, domain,
|
|
88
|
+
* handle, tag or identifier (`indigoai-us/hq`, `getindigo.ai`,
|
|
89
|
+
* `scope: company:indigo`). The plan carries `named_evidence: true`, and apply
|
|
90
|
+
* rejects such a link as `unnamed_in_evidence`. CO-57: on the six CO-52 runs,
|
|
91
|
+
* 67 of 143 false positives were links to company/indigo and project/HQ, most
|
|
92
|
+
* on evidence that only held the name inside an identifier; a replay of this
|
|
93
|
+
* rule raised precision on the concept-project-link arm from 0.705 to 0.750
|
|
94
|
+
* and lost no gold link.
|
|
95
|
+
*/
|
|
96
|
+
export declare const NAMED_EVIDENCE_FLAG = "checkpoint.ontology.named-evidence";
|
|
84
97
|
export declare const PLAN_FILE = "ontology-plan.json";
|
|
85
98
|
export declare const CANDIDATES_FILE = "ontology-candidates.json";
|
|
86
99
|
export declare const REPORT_FILE = "ontology-report.json";
|
|
@@ -153,6 +166,8 @@ export interface OntologyPlan {
|
|
|
153
166
|
skip_ambiguous_concept_links?: true;
|
|
154
167
|
/** Set by checkpoint.ontology.concept-project-link: apply links a knowledge concept with an ambiguous alias to the one project of that canonical name. */
|
|
155
168
|
concept_project_link?: true;
|
|
169
|
+
/** Set by checkpoint.ontology.named-evidence: apply rejects a knowledge link to the company's own entity, or a concept-project-link redirect, whose evidence does not name it as a word. */
|
|
170
|
+
named_evidence?: true;
|
|
156
171
|
inputs: PlanInput[];
|
|
157
172
|
/** The session's summary, decisions and learnings, for the `session` input. */
|
|
158
173
|
session_text: string | null;
|
|
@@ -85,6 +85,19 @@ export const SKIP_AMBIGUOUS_CONCEPT_LINKS_FLAG = "checkpoint.ontology.skip-ambig
|
|
|
85
85
|
* to 0.709 at precision 0.747 to 0.731 (61 of the 63 added links are HQ).
|
|
86
86
|
*/
|
|
87
87
|
export const CONCEPT_PROJECT_LINK_FLAG = "checkpoint.ontology.concept-project-link";
|
|
88
|
+
/**
|
|
89
|
+
* A knowledge link to the company's own company entity, or a
|
|
90
|
+
* concept-project-link redirect, needs evidence that names the entity as a
|
|
91
|
+
* word: the candidate name or one of its aliases, not inside a path, domain,
|
|
92
|
+
* handle, tag or identifier (`indigoai-us/hq`, `getindigo.ai`,
|
|
93
|
+
* `scope: company:indigo`). The plan carries `named_evidence: true`, and apply
|
|
94
|
+
* rejects such a link as `unnamed_in_evidence`. CO-57: on the six CO-52 runs,
|
|
95
|
+
* 67 of 143 false positives were links to company/indigo and project/HQ, most
|
|
96
|
+
* on evidence that only held the name inside an identifier; a replay of this
|
|
97
|
+
* rule raised precision on the concept-project-link arm from 0.705 to 0.750
|
|
98
|
+
* and lost no gold link.
|
|
99
|
+
*/
|
|
100
|
+
export const NAMED_EVIDENCE_FLAG = "checkpoint.ontology.named-evidence";
|
|
88
101
|
export const PLAN_FILE = "ontology-plan.json";
|
|
89
102
|
export const CANDIDATES_FILE = "ontology-candidates.json";
|
|
90
103
|
export const REPORT_FILE = "ontology-report.json";
|
|
@@ -264,6 +277,7 @@ export async function planOntologyRun(options) {
|
|
|
264
277
|
const firstNamePeople = await options.readFlag(FIRST_NAME_PEOPLE_FLAG, company);
|
|
265
278
|
const skipAmbiguousConceptLinks = await options.readFlag(SKIP_AMBIGUOUS_CONCEPT_LINKS_FLAG, company);
|
|
266
279
|
const conceptProjectLink = await options.readFlag(CONCEPT_PROJECT_LINK_FLAG, company);
|
|
280
|
+
const namedEvidence = await options.readFlag(NAMED_EVIDENCE_FLAG, company);
|
|
267
281
|
const touched = sessionTouched(hqRoot, company, payload.files);
|
|
268
282
|
// A first run takes its boundary from the entity tree, so the ledger is
|
|
269
283
|
// refreshed before merge-back moves any `last_updated` forward.
|
|
@@ -366,6 +380,7 @@ export async function planOntologyRun(options) {
|
|
|
366
380
|
...(firstNamePeople ? { first_name_people: true } : {}),
|
|
367
381
|
...(skipAmbiguousConceptLinks ? { skip_ambiguous_concept_links: true } : {}),
|
|
368
382
|
...(conceptProjectLink ? { concept_project_link: true } : {}),
|
|
383
|
+
...(namedEvidence ? { named_evidence: true } : {}),
|
|
369
384
|
inputs,
|
|
370
385
|
session_text: sessionText,
|
|
371
386
|
pending_after_run: remaining,
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Shared authenticated request path for the hq-deploy control plane. */
|
|
2
|
+
export declare const DEPLOY_API_URL: string;
|
|
3
|
+
export declare function deployHeaders(token: string, json?: boolean, extra?: Record<string, string>): Record<string, string>;
|
|
4
|
+
export declare function deployApiUrl(route: string): string;
|
|
5
|
+
export declare function deployApiRequest(token: string, route: string, method: string, body?: unknown, extraHeaders?: Record<string, string>): Promise<Response>;
|
|
6
|
+
//# sourceMappingURL=deploy-api.d.ts.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Shared authenticated request path for the hq-deploy control plane. */
|
|
2
|
+
export const DEPLOY_API_URL = process.env.HQ_DEPLOY_API_URL?.trim() || "https://api.indigo-hq.com";
|
|
3
|
+
export function deployHeaders(token, json = true, extra = {}) {
|
|
4
|
+
return {
|
|
5
|
+
Authorization: `Bearer ${token}`,
|
|
6
|
+
Accept: "application/json",
|
|
7
|
+
...(json ? { "Content-Type": "application/json" } : {}),
|
|
8
|
+
...extra,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export function deployApiUrl(route) {
|
|
12
|
+
return `${DEPLOY_API_URL.replace(/\/+$/, "")}${route}`;
|
|
13
|
+
}
|
|
14
|
+
export async function deployApiRequest(token, route, method, body, extraHeaders = {}) {
|
|
15
|
+
return fetch(deployApiUrl(route), {
|
|
16
|
+
method,
|
|
17
|
+
headers: deployHeaders(token, body !== undefined, extraHeaders),
|
|
18
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=deploy-api.js.map
|
|
@@ -101,6 +101,8 @@ export interface RefreshSyncResult {
|
|
|
101
101
|
export declare const REFRESH_SYNC_TIMEOUT_MS = 90000;
|
|
102
102
|
/** One applied (or attempted) repair, with its post-fix re-check status. */
|
|
103
103
|
export interface AppliedFix {
|
|
104
|
+
/** One-based repair pass in this `hq doctor --fix` invocation. */
|
|
105
|
+
pass: number;
|
|
104
106
|
/** The check id of the finding that was repaired. */
|
|
105
107
|
checkId: string;
|
|
106
108
|
/** The safe class applied. */
|
|
@@ -114,13 +116,24 @@ export interface AppliedFix {
|
|
|
114
116
|
/** Present for refresh-sync plans; contains no command output or path data. */
|
|
115
117
|
refreshSyncOutcome?: RefreshSyncOutcome;
|
|
116
118
|
}
|
|
119
|
+
/** Backup and restore information for one pass that wrote files. */
|
|
120
|
+
export interface AppliedFixBackup {
|
|
121
|
+
/** One-based repair pass that created this backup. */
|
|
122
|
+
pass: number;
|
|
123
|
+
/** Absolute path of the backup directory. */
|
|
124
|
+
backupDir: string;
|
|
125
|
+
/** Command that restores this pass's pre-write files. */
|
|
126
|
+
restoreCommand: string;
|
|
127
|
+
}
|
|
128
|
+
/** A newly unlocked safe repair can be picked up for at most this many passes. */
|
|
129
|
+
export declare const MAX_FIX_PASSES = 3;
|
|
117
130
|
/** The outcome of an {@link applyFixes} run. */
|
|
118
131
|
export interface ApplyFixesResult {
|
|
119
|
-
/** 0 on success or a clean no-op; 1 when
|
|
132
|
+
/** 0 on success, decline, or a clean no-op; 1 when a dirty tree is refused. */
|
|
120
133
|
exitCode: number;
|
|
121
134
|
/** Whether any file was written. */
|
|
122
135
|
wrote: boolean;
|
|
123
|
-
/** Why
|
|
136
|
+
/** Why no further repairs were applied, when the run stopped early. */
|
|
124
137
|
refused: "dirty-tree" | "declined" | null;
|
|
125
138
|
/** How many findings were auto-fixable. */
|
|
126
139
|
fixableCount: number;
|
|
@@ -128,6 +141,8 @@ export interface ApplyFixesResult {
|
|
|
128
141
|
backupDir: string | null;
|
|
129
142
|
/** The restore command, or null when nothing was written. */
|
|
130
143
|
restoreCommand: string | null;
|
|
144
|
+
/** Every backup made, in pass order. */
|
|
145
|
+
backups: AppliedFixBackup[];
|
|
131
146
|
/** The applied repairs with their post-fix status. */
|
|
132
147
|
applied: AppliedFix[];
|
|
133
148
|
}
|
|
@@ -45,6 +45,8 @@ import { classifyRefreshSyncOutcome, } from "./refresh-sync-outcome.js";
|
|
|
45
45
|
export const HOOK_CONFIG_DIRS = [".claude", ".codex", ".grok"];
|
|
46
46
|
/** Per-company bound for the default `hq sync pull` repair. */
|
|
47
47
|
export const REFRESH_SYNC_TIMEOUT_MS = 90_000;
|
|
48
|
+
/** A newly unlocked safe repair can be picked up for at most this many passes. */
|
|
49
|
+
export const MAX_FIX_PASSES = 3;
|
|
48
50
|
/**
|
|
49
51
|
* Apply every auto-fixable finding, honouring the dirty-tree refusal, the
|
|
50
52
|
* preview/confirmation gate, the pre-write backup, and the post-fix re-check.
|
|
@@ -62,6 +64,7 @@ export async function applyFixes(options) {
|
|
|
62
64
|
fixableCount: 0,
|
|
63
65
|
backupDir: null,
|
|
64
66
|
restoreCommand: null,
|
|
67
|
+
backups: [],
|
|
65
68
|
applied: [],
|
|
66
69
|
...over,
|
|
67
70
|
});
|
|
@@ -80,72 +83,123 @@ export async function applyFixes(options) {
|
|
|
80
83
|
const collect = options.runChecks ?? defaultRunChecks;
|
|
81
84
|
const refreshSync = options.refreshSync ?? defaultRefreshSync;
|
|
82
85
|
const rewriteBotNodePath = options.rewriteBotNodePath ?? defaultRewriteBotNodePath;
|
|
83
|
-
// 2. Collect findings
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
86
|
+
// 2. Collect findings, then plan each newly unlocked allowlisted repair.
|
|
87
|
+
// Identity is deliberately the check id + target pair: an unsuccessful
|
|
88
|
+
// repair must not be applied repeatedly in one invocation.
|
|
89
|
+
let findings = await collect(hqRoot);
|
|
90
|
+
const appliedKeys = new Set();
|
|
91
|
+
const planned = [];
|
|
92
|
+
const backups = [];
|
|
93
|
+
let fixableCount = 0;
|
|
94
|
+
let refused = null;
|
|
95
|
+
let lastBackupMs = Number.NEGATIVE_INFINITY;
|
|
96
|
+
const backupClock = () => {
|
|
97
|
+
const suppliedMs = (options.now ?? (() => new Date()))().getTime();
|
|
98
|
+
lastBackupMs = Math.max(suppliedMs, lastBackupMs + 1);
|
|
99
|
+
return new Date(lastBackupMs);
|
|
100
|
+
};
|
|
101
|
+
const buildPlans = (results) => {
|
|
102
|
+
const keysThisPass = new Set();
|
|
103
|
+
const plans = [];
|
|
104
|
+
for (const result of results) {
|
|
105
|
+
const rem = deriveRemediation(result);
|
|
106
|
+
if (!rem?.autoFixable)
|
|
107
|
+
continue;
|
|
108
|
+
const plan = planFix(hqRoot, result, rem, refreshSync, rewriteBotNodePath);
|
|
109
|
+
if (!plan)
|
|
110
|
+
continue;
|
|
111
|
+
const key = JSON.stringify([plan.checkId, plan.target]);
|
|
112
|
+
if (appliedKeys.has(key) || keysThisPass.has(key))
|
|
113
|
+
continue;
|
|
114
|
+
keysThisPass.add(key);
|
|
115
|
+
plans.push(plan);
|
|
116
|
+
}
|
|
117
|
+
return plans;
|
|
118
|
+
};
|
|
119
|
+
for (let pass = 1; pass <= MAX_FIX_PASSES; pass += 1) {
|
|
120
|
+
const plans = buildPlans(findings);
|
|
121
|
+
if (plans.length === 0)
|
|
122
|
+
break;
|
|
123
|
+
fixableCount += plans.length;
|
|
124
|
+
// 3. Preview and require confirmation for every pass unless --yes is set.
|
|
125
|
+
write(`Pass ${pass}/${MAX_FIX_PASSES}: hq doctor --fix will apply ${plans.length} repair${plans.length === 1 ? "" : "s"}:\n\n`);
|
|
126
|
+
for (const plan of plans)
|
|
127
|
+
write(plan.preview + "\n");
|
|
128
|
+
if (!options.yes) {
|
|
129
|
+
const confirmed = await Promise.resolve((options.confirm ?? declineByDefault)());
|
|
130
|
+
if (!confirmed) {
|
|
131
|
+
refused = "declined";
|
|
132
|
+
if (planned.length === 0) {
|
|
133
|
+
write("Aborted — no changes written.\n");
|
|
134
|
+
return empty({ refused, fixableCount });
|
|
135
|
+
}
|
|
136
|
+
write(`Pass ${pass} declined — no further changes written.\n`);
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// 4. Back up this pass's files before its first write. The clock advances
|
|
141
|
+
// at least one millisecond per backup so fast consecutive passes cannot
|
|
142
|
+
// overwrite an earlier pass's restore point.
|
|
143
|
+
const affected = unique(plans.map((plan) => plan.relpath).filter((rel) => rel.length > 0));
|
|
144
|
+
const backup = affected.length > 0 ? createBackup(hqRoot, affected, backupClock) : null;
|
|
145
|
+
if (backup) {
|
|
146
|
+
backups.push({
|
|
147
|
+
pass,
|
|
148
|
+
backupDir: backup.dir,
|
|
149
|
+
restoreCommand: backup.restoreCommand,
|
|
150
|
+
});
|
|
151
|
+
write(`\nBacked up ${backup.files.length} file${backup.files.length === 1 ? "" : "s"} for pass ${pass} to ${backup.dir}\n`);
|
|
152
|
+
write(`To restore: ${backup.restoreCommand}\n`);
|
|
111
153
|
}
|
|
154
|
+
// 5. Apply each plan once. Later findings with this check id + target are
|
|
155
|
+
// excluded even if this repair did not make its check pass.
|
|
156
|
+
for (const plan of plans) {
|
|
157
|
+
appliedKeys.add(JSON.stringify([plan.checkId, plan.target]));
|
|
158
|
+
planned.push({ plan, pass });
|
|
159
|
+
await plan.apply();
|
|
160
|
+
}
|
|
161
|
+
// 6. Re-run checks. Newly surfaced repairs are considered by the next loop.
|
|
162
|
+
findings = await collect(hqRoot);
|
|
112
163
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const affected = unique(plans.map((plan) => plan.relpath).filter((rel) => rel.length > 0));
|
|
117
|
-
const backup = affected.length > 0 ? createBackup(hqRoot, affected, options.now) : null;
|
|
118
|
-
// 5. Apply. Each plan reads the current on-disk state, so multiple plans that
|
|
119
|
-
// touch the same file (two gate ids) compose correctly.
|
|
120
|
-
for (const plan of plans)
|
|
121
|
-
await plan.apply();
|
|
122
|
-
if (backup) {
|
|
123
|
-
write(`\nBacked up ${backup.files.length} file${backup.files.length === 1 ? "" : "s"} to ${backup.dir}\n`);
|
|
124
|
-
write(`To restore: ${backup.restoreCommand}\n`);
|
|
164
|
+
if (planned.length === 0) {
|
|
165
|
+
write("hq doctor --fix: nothing to repair — no auto-fixable findings.\n");
|
|
166
|
+
return empty({ refused, fixableCount });
|
|
125
167
|
}
|
|
126
|
-
//
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
: {}),
|
|
137
|
-
|
|
168
|
+
// Show each repair's final post-fix status, including work from earlier passes.
|
|
169
|
+
const applied = planned.map(({ plan, pass }) => {
|
|
170
|
+
const refreshSyncOutcome = plan.refreshSyncOutcome?.();
|
|
171
|
+
return {
|
|
172
|
+
pass,
|
|
173
|
+
checkId: plan.checkId,
|
|
174
|
+
fixClass: plan.fixClass,
|
|
175
|
+
target: plan.target,
|
|
176
|
+
summary: plan.summary,
|
|
177
|
+
postStatus: plan.postStatus(findings),
|
|
178
|
+
...(refreshSyncOutcome ? { refreshSyncOutcome } : {}),
|
|
179
|
+
};
|
|
180
|
+
});
|
|
138
181
|
write("\nPost-fix status:\n");
|
|
139
182
|
for (const fix of applied) {
|
|
140
|
-
write(` ${fix.postStatus ?? "UNKNOWN"} ${fix.checkId} [${fix.target}]\n`);
|
|
183
|
+
write(` ${fix.postStatus ?? "UNKNOWN"} ${fix.checkId} [${fix.target}] (pass ${fix.pass})\n`);
|
|
184
|
+
}
|
|
185
|
+
// Restore later pass snapshots first. This also returns every file to its
|
|
186
|
+
// pre-run state when multiple passes touched the same path.
|
|
187
|
+
const restoreCommand = backups.length > 0
|
|
188
|
+
? [...backups].reverse().map(({ restoreCommand: command }) => command).join(" && ")
|
|
189
|
+
: null;
|
|
190
|
+
if (restoreCommand)
|
|
191
|
+
write(`To restore all changes: ${restoreCommand}\n`);
|
|
192
|
+
if (planned.at(-1)?.pass === MAX_FIX_PASSES && buildPlans(findings).length > 0) {
|
|
193
|
+
write(`Stopped after ${MAX_FIX_PASSES} repair passes; new auto-fixable findings remain.\n`);
|
|
141
194
|
}
|
|
142
195
|
return {
|
|
143
196
|
exitCode: 0,
|
|
144
197
|
wrote: true,
|
|
145
|
-
refused
|
|
146
|
-
fixableCount
|
|
147
|
-
backupDir:
|
|
148
|
-
restoreCommand
|
|
198
|
+
refused,
|
|
199
|
+
fixableCount,
|
|
200
|
+
backupDir: backups[0]?.backupDir ?? null,
|
|
201
|
+
restoreCommand,
|
|
202
|
+
backups,
|
|
149
203
|
applied,
|
|
150
204
|
};
|
|
151
205
|
}
|
|
@@ -1,9 +1,16 @@
|
|
|
1
|
-
import type { AppliedFix, ApplyFixesResult } from "./apply.js";
|
|
1
|
+
import type { AppliedFix, AppliedFixBackup, ApplyFixesResult } from "./apply.js";
|
|
2
2
|
export declare const DOCTOR_FIX_JSON_SCHEMA_VERSION = 1;
|
|
3
3
|
export interface DoctorFixJsonResult {
|
|
4
4
|
schemaVersion: number;
|
|
5
5
|
exitCode: number;
|
|
6
6
|
refused: ApplyFixesResult["refused"] | "outside-hq-tree";
|
|
7
|
+
applied: {
|
|
8
|
+
checkId: string;
|
|
9
|
+
fixClass: AppliedFix["fixClass"];
|
|
10
|
+
pass: number;
|
|
11
|
+
postStatus: AppliedFix["postStatus"];
|
|
12
|
+
}[];
|
|
13
|
+
backups: Pick<AppliedFixBackup, "pass" | "backupDir" | "restoreCommand">[];
|
|
7
14
|
refreshSync: {
|
|
8
15
|
checkId: string;
|
|
9
16
|
outcome: NonNullable<AppliedFix["refreshSyncOutcome"]>;
|
|
@@ -15,6 +22,7 @@ export declare function buildDoctorFixJson(input: {
|
|
|
15
22
|
exitCode: number;
|
|
16
23
|
refused: ApplyFixesResult["refused"] | "outside-hq-tree";
|
|
17
24
|
applied: readonly AppliedFix[];
|
|
25
|
+
backups?: readonly AppliedFixBackup[];
|
|
18
26
|
}): DoctorFixJsonResult;
|
|
19
27
|
export declare function renderDoctorFixJson(input: Parameters<typeof buildDoctorFixJson>[0]): string;
|
|
20
28
|
//# sourceMappingURL=json-output.d.ts.map
|
|
@@ -5,6 +5,17 @@ export function buildDoctorFixJson(input) {
|
|
|
5
5
|
schemaVersion: DOCTOR_FIX_JSON_SCHEMA_VERSION,
|
|
6
6
|
exitCode: input.exitCode,
|
|
7
7
|
refused: input.refused,
|
|
8
|
+
applied: input.applied.map(({ checkId, fixClass, pass, postStatus }) => ({
|
|
9
|
+
checkId,
|
|
10
|
+
fixClass,
|
|
11
|
+
pass,
|
|
12
|
+
postStatus,
|
|
13
|
+
})),
|
|
14
|
+
backups: (input.backups ?? []).map(({ pass, backupDir, restoreCommand }) => ({
|
|
15
|
+
pass,
|
|
16
|
+
backupDir,
|
|
17
|
+
restoreCommand,
|
|
18
|
+
})),
|
|
8
19
|
refreshSync: input.applied.flatMap((fix) => fix.fixClass === "refresh-sync" && fix.refreshSyncOutcome
|
|
9
20
|
? [
|
|
10
21
|
{
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export declare const CLI_FIRST_SYNC_NEXT_STEP_FLAG = "cli.first-sync-next-step";
|
|
2
|
+
export declare const FIRST_SYNC_NEXT_STEP_COPY = "Save a file in your HQ folder, then run `hq sync push`.";
|
|
3
|
+
export declare const FIRST_SYNC_CONFIRMATION_COPY = "Your first work sync is complete.";
|
|
4
|
+
export type FirstSyncStepOutcome = "shown" | "followed";
|
|
5
|
+
export declare function shouldShowFirstSyncNextStep(input: {
|
|
6
|
+
flagEnabled: boolean;
|
|
7
|
+
interactive: boolean;
|
|
8
|
+
alreadyShown: boolean;
|
|
9
|
+
alreadyCompleted: boolean;
|
|
10
|
+
}): boolean;
|
|
11
|
+
export declare function shouldConfirmFirstWorkBearingSync(input: {
|
|
12
|
+
flagEnabled: boolean;
|
|
13
|
+
interactive: boolean;
|
|
14
|
+
alreadyCompleted: boolean;
|
|
15
|
+
workBearing: boolean;
|
|
16
|
+
}): boolean;
|
|
17
|
+
export declare function isWorkBearingSyncResult(result: Record<string, unknown>): boolean;
|
|
18
|
+
export declare function isHumanInteractiveCliSession(input: {
|
|
19
|
+
stdoutIsTTY: boolean;
|
|
20
|
+
argv: readonly string[];
|
|
21
|
+
env: NodeJS.ProcessEnv;
|
|
22
|
+
machineIdentity: boolean;
|
|
23
|
+
}): boolean;
|
|
24
|
+
export declare function firstSyncStepMarkerPaths(identity: string, home?: string): {
|
|
25
|
+
shown: string;
|
|
26
|
+
completed: string;
|
|
27
|
+
};
|
|
28
|
+
export declare function claimFirstSyncStepMarker(kind: "shown" | "completed", identity: string, home?: string): boolean;
|
|
29
|
+
export declare function firstSyncStepTelemetryProperties(outcome: FirstSyncStepOutcome): Record<string, unknown>;
|
|
30
|
+
export declare function isFirstSyncNextStepEnabled(): boolean;
|
|
31
|
+
/** Called once after the first completed CLI action, before process finalization. */
|
|
32
|
+
export declare function maybeShowFirstSyncNextStep(): boolean;
|
|
33
|
+
/** Capture the first positive CLI sync result without changing transfer logic. */
|
|
34
|
+
export declare function recordFirstWorkBearingSync(result: Record<string, unknown>): boolean;
|
|
35
|
+
/** Drain outcomes once so each shown/followed event is sent at most once per run. */
|
|
36
|
+
export declare function takeFirstSyncStepOutcomes(): FirstSyncStepOutcome[];
|
|
37
|
+
//# sourceMappingURL=first-sync-next-step.d.ts.map
|