@indigoai-us/hq-cli 5.183.0 → 5.185.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 +19 -0
- package/dist/command-catalog.generated.d.ts +3 -0
- package/dist/command-catalog.generated.js +4 -0
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +100 -6
- package/dist/commands/mesh.js +18 -1
- package/dist/lib/bot/progress.d.ts +10 -0
- package/dist/lib/bot/progress.js +55 -0
- package/dist/lib/bot/prompt.js +3 -1
- package/dist/lib/bot/run.d.ts +4 -1
- package/dist/lib/bot/run.js +60 -5
- package/dist/lib/doctor/checks/sync-health.js +13 -0
- package/dist/lib/doctor/client-health-reason-codes-flag.d.ts +26 -0
- package/dist/lib/doctor/client-health-reason-codes-flag.js +94 -0
- package/dist/lib/doctor/fix/apply.d.ts +5 -0
- package/dist/lib/doctor/fix/apply.js +28 -3
- package/dist/lib/doctor/fix/json-output.d.ts +20 -0
- package/dist/lib/doctor/fix/json-output.js +22 -0
- package/dist/lib/doctor/fix/refresh-sync-outcome.d.ts +12 -0
- package/dist/lib/doctor/fix/refresh-sync-outcome.js +25 -0
- package/dist/lib/doctor/json-output.d.ts +7 -1
- package/dist/lib/doctor/json-output.js +6 -2
- package/dist/lib/doctor/types.d.ts +5 -0
- package/dist/lib/doctor/types.js +15 -0
- package/dist/lib/mesh/live/daemon/index.d.ts +1 -1
- package/dist/lib/mesh/live/daemon/index.js +1 -1
- package/dist/lib/mesh/live/daemon/transcript-watch.d.ts +7 -0
- package/dist/lib/mesh/live/daemon/transcript-watch.js +25 -3
- package/dist/lib/mesh/live/emit-client.d.ts +6 -0
- package/dist/lib/mesh/live/emit-client.js +8 -0
- package/dist/lib/mesh/live/enqueue.d.ts +5 -0
- package/dist/lib/mesh/live/enqueue.js +19 -0
- package/dist/lib/mesh/live/flush.js +5 -3
- package/dist/lib/mesh/live/format-spool-line.d.ts +5 -2
- package/dist/lib/mesh/live/format-spool-line.js +10 -0
- package/dist/lib/mesh/live/report-summary.d.ts +45 -0
- package/dist/lib/mesh/live/report-summary.js +222 -0
- package/dist/lib/mesh/live/session-event.schema.json +24 -1
- package/dist/lib/mesh/live/validate-session-event.d.ts +1 -1
- package/dist/lib/mesh/live/validate-session-event.js +1 -1
- package/dist/lib/search-index/collection-path-repair-flag.d.ts +4 -0
- package/dist/lib/search-index/collection-path-repair-flag.js +7 -0
- package/dist/lib/search-index/index.d.ts +4 -0
- package/dist/lib/search-index/index.js +106 -1
- package/package.json +1 -1
|
@@ -223,6 +223,7 @@ function versionResults(context, deps) {
|
|
|
223
223
|
results.push({
|
|
224
224
|
status: "WARN",
|
|
225
225
|
checkId: "sync.versions.core",
|
|
226
|
+
reasonCode: "core-version-unavailable",
|
|
226
227
|
target: path.join(context.hqRoot, "core", "core.yaml"),
|
|
227
228
|
message: "hq-core version could not be read from core/core.yaml — the scaffold may be missing or pre-v12.",
|
|
228
229
|
remediation: "Run /update-hq to (re)install the hq-core scaffold.",
|
|
@@ -282,6 +283,7 @@ function updateAvailabilityResult(context, deps) {
|
|
|
282
283
|
{
|
|
283
284
|
status: "WARN",
|
|
284
285
|
checkId: "sync.update.core",
|
|
286
|
+
reasonCode: "core-update-available",
|
|
285
287
|
message: `hq-core update available: local v${core}, latest v${latest}.`,
|
|
286
288
|
remediation: "Run /update-hq in a fresh session to upgrade.",
|
|
287
289
|
},
|
|
@@ -373,6 +375,7 @@ function journalResults(deps, journals) {
|
|
|
373
375
|
return {
|
|
374
376
|
status: "NA",
|
|
375
377
|
checkId,
|
|
378
|
+
reasonCode: "no-local-tree",
|
|
376
379
|
target: entry.path,
|
|
377
380
|
message: `Sync journal '${entry.slug}' is unused on this machine — there is no companies/${entry.slug} directory, so this journal is not a live sync target.`,
|
|
378
381
|
};
|
|
@@ -382,6 +385,7 @@ function journalResults(deps, journals) {
|
|
|
382
385
|
return {
|
|
383
386
|
status: "WARN",
|
|
384
387
|
checkId,
|
|
388
|
+
reasonCode: "never-synced",
|
|
385
389
|
target: entry.path,
|
|
386
390
|
message: `Sync journal '${entry.slug}' exists but has never recorded a successful sync.`,
|
|
387
391
|
remediation: "Run /hq-sync (or `hq sync`) to complete a first sync.",
|
|
@@ -392,6 +396,7 @@ function journalResults(deps, journals) {
|
|
|
392
396
|
return {
|
|
393
397
|
status: "UNKNOWN",
|
|
394
398
|
checkId,
|
|
399
|
+
reasonCode: "invalid-last-sync",
|
|
395
400
|
target: entry.path,
|
|
396
401
|
message: `Sync journal '${entry.slug}' has an unparseable lastSync (${lastSync}).`,
|
|
397
402
|
};
|
|
@@ -402,6 +407,7 @@ function journalResults(deps, journals) {
|
|
|
402
407
|
return {
|
|
403
408
|
status: "WARN",
|
|
404
409
|
checkId,
|
|
410
|
+
reasonCode: "stale-threshold",
|
|
405
411
|
target: entry.path,
|
|
406
412
|
message: `Sync journal '${entry.slug}' is stale: last successful sync ${days} day${days === 1 ? "" : "s"} ago (${lastSync}).`,
|
|
407
413
|
remediation: "Run /hq-sync (or `hq sync`) and check the sync runner.",
|
|
@@ -443,6 +449,7 @@ function manifestResults(deps, journals) {
|
|
|
443
449
|
{
|
|
444
450
|
status: "NA",
|
|
445
451
|
checkId: "sync.manifest",
|
|
452
|
+
reasonCode: "manifest-unavailable",
|
|
446
453
|
message: `${MANIFEST_UNAVAILABLE_REASON} — upgrade hq-cloud to audit manifest uploads.`,
|
|
447
454
|
},
|
|
448
455
|
];
|
|
@@ -456,6 +463,7 @@ function manifestResults(deps, journals) {
|
|
|
456
463
|
{
|
|
457
464
|
status: "UNKNOWN",
|
|
458
465
|
checkId: "sync.manifest",
|
|
466
|
+
reasonCode: "manifest-state-unavailable",
|
|
459
467
|
message: `Manifest upload state could not be read: ${error instanceof Error ? error.message : String(error)}`,
|
|
460
468
|
remediation: "Check that the HQ state directory is readable, then re-run `hq doctor`.",
|
|
461
469
|
},
|
|
@@ -469,6 +477,7 @@ function manifestResults(deps, journals) {
|
|
|
469
477
|
{
|
|
470
478
|
status: "NA",
|
|
471
479
|
checkId: "sync.manifest",
|
|
480
|
+
reasonCode: "no-manifest-scopes",
|
|
472
481
|
message: "No manifest-auditable sync scopes on this machine — nothing to reconcile.",
|
|
473
482
|
},
|
|
474
483
|
];
|
|
@@ -480,6 +489,7 @@ function manifestResults(deps, journals) {
|
|
|
480
489
|
return {
|
|
481
490
|
status: "UNTESTED",
|
|
482
491
|
checkId,
|
|
492
|
+
reasonCode: "never-uploaded",
|
|
483
493
|
message: `Scope '${entry.scopeKey}' has never uploaded a sync manifest — expected until the first pass runs.`,
|
|
484
494
|
remediation: "Run `hq sync manifest` to upload one now.",
|
|
485
495
|
};
|
|
@@ -489,6 +499,7 @@ function manifestResults(deps, journals) {
|
|
|
489
499
|
return {
|
|
490
500
|
status: "UNKNOWN",
|
|
491
501
|
checkId,
|
|
502
|
+
reasonCode: "invalid-upload-time",
|
|
492
503
|
message: `Scope '${entry.scopeKey}' has an unparseable last manifest upload time (${entry.lastUploadAt}).`,
|
|
493
504
|
};
|
|
494
505
|
}
|
|
@@ -501,6 +512,7 @@ function manifestResults(deps, journals) {
|
|
|
501
512
|
return {
|
|
502
513
|
status: "WARN",
|
|
503
514
|
checkId,
|
|
515
|
+
reasonCode: "stale-threshold",
|
|
504
516
|
message: `Scope '${entry.scopeKey}' last uploaded a sync manifest ${days} day${days === 1 ? "" : "s"} ago (${entry.lastUploadAt}) — the daily audit pass is not running.`,
|
|
505
517
|
remediation: "Run `hq sync manifest` and check HQ_SYNC_MANIFEST_DISABLED and the sync runner.",
|
|
506
518
|
};
|
|
@@ -532,6 +544,7 @@ function unresolvedResults(deps, journals) {
|
|
|
532
544
|
return slugs.map((slug) => ({
|
|
533
545
|
status: "UNTESTED",
|
|
534
546
|
checkId: `sync.manifest.unresolved.${slug}`,
|
|
547
|
+
reasonCode: "unresolved-company-uid",
|
|
535
548
|
message: `Company scope '${slug}' has a sync journal but its companyUid could not be resolved locally — its manifest uploads were not checked.`,
|
|
536
549
|
remediation: `Run \`hq sync manifest --scope ${slug}\` to audit it explicitly.`,
|
|
537
550
|
}));
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type FlagClient, type FlagClientOptions } from "@indigoai-us/hq-flags-client";
|
|
2
|
+
export declare const CLIENT_HEALTH_REASON_CODES_FLAG = "client-health-sync-reason-codes";
|
|
3
|
+
export declare const CLIENT_HEALTH_REASON_CODES_FLAG_FALLBACK = false;
|
|
4
|
+
export declare const CLIENT_HEALTH_REASON_CODES_FLAG_TIMEOUT_MS = 150;
|
|
5
|
+
type ClientHealthFlagClient = Pick<FlagClient, "ready" | "isEnabled" | "close">;
|
|
6
|
+
export interface ClientHealthReasonCodesFlagDependencies {
|
|
7
|
+
env?: NodeJS.ProcessEnv;
|
|
8
|
+
createClient?: (options: FlagClientOptions) => ClientHealthFlagClient;
|
|
9
|
+
getToken?: FlagClientOptions["getToken"];
|
|
10
|
+
fetch?: typeof fetch;
|
|
11
|
+
reportFailure?: (error: unknown) => void;
|
|
12
|
+
}
|
|
13
|
+
export interface ClientHealthReasonCodesCompany {
|
|
14
|
+
companyUid: string;
|
|
15
|
+
companySlug?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Resolve the privacy gate from hq-flags with a bounded request. Missing
|
|
19
|
+
* identity, service configuration, and every lookup failure keep diagnostics
|
|
20
|
+
* off so a flag outage cannot delay the doctor's health result.
|
|
21
|
+
*/
|
|
22
|
+
export declare function clientHealthReasonCodesEnabled(dependencies?: ClientHealthReasonCodesFlagDependencies, callerSignal?: AbortSignal): Promise<boolean>;
|
|
23
|
+
/** Resolve the same default-off gate for one explicitly identified company. */
|
|
24
|
+
export declare function clientHealthReasonCodesEnabledForCompany(company: ClientHealthReasonCodesCompany, dependencies?: ClientHealthReasonCodesFlagDependencies, callerSignal?: AbortSignal): Promise<boolean>;
|
|
25
|
+
export {};
|
|
26
|
+
//# sourceMappingURL=client-health-reason-codes-flag.d.ts.map
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { loadCachedTokens } from "@indigoai-us/hq-cloud";
|
|
2
|
+
import { createFlagClient, } from "@indigoai-us/hq-flags-client";
|
|
3
|
+
export const CLIENT_HEALTH_REASON_CODES_FLAG = "client-health-sync-reason-codes";
|
|
4
|
+
export const CLIENT_HEALTH_REASON_CODES_FLAG_FALLBACK = false;
|
|
5
|
+
export const CLIENT_HEALTH_REASON_CODES_FLAG_TIMEOUT_MS = 150;
|
|
6
|
+
function safeErrorClass(error) {
|
|
7
|
+
const name = error instanceof Error ? error.name : "UnknownError";
|
|
8
|
+
return /^[A-Za-z][A-Za-z0-9]{0,63}$/.test(name) ? name : "UnknownError";
|
|
9
|
+
}
|
|
10
|
+
function reportFlagFailure(error) {
|
|
11
|
+
process.stderr.write(`HQ client-health reason-code flag lookup failed (${safeErrorClass(error)}); using default off.\n`);
|
|
12
|
+
}
|
|
13
|
+
function combineSignals(first, second) {
|
|
14
|
+
return first ? AbortSignal.any([first, second]) : second;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Resolve the privacy gate from hq-flags with a bounded request. Missing
|
|
18
|
+
* identity, service configuration, and every lookup failure keep diagnostics
|
|
19
|
+
* off so a flag outage cannot delay the doctor's health result.
|
|
20
|
+
*/
|
|
21
|
+
export async function clientHealthReasonCodesEnabled(dependencies = {}, callerSignal) {
|
|
22
|
+
const env = dependencies.env ?? process.env;
|
|
23
|
+
const companyUid = env.HQ_COMPANY_UID?.trim() || "";
|
|
24
|
+
if (!/^cmp_[A-Za-z0-9]{3,128}$/.test(companyUid)) {
|
|
25
|
+
return CLIENT_HEALTH_REASON_CODES_FLAG_FALLBACK;
|
|
26
|
+
}
|
|
27
|
+
return clientHealthReasonCodesEnabledForCompany({
|
|
28
|
+
companyUid,
|
|
29
|
+
companySlug: env.HQ_COMPANY_SLUG?.trim() || undefined,
|
|
30
|
+
}, dependencies, callerSignal);
|
|
31
|
+
}
|
|
32
|
+
/** Resolve the same default-off gate for one explicitly identified company. */
|
|
33
|
+
export async function clientHealthReasonCodesEnabledForCompany(company, dependencies = {}, callerSignal) {
|
|
34
|
+
const env = dependencies.env ?? process.env;
|
|
35
|
+
const endpoint = env.HQ_FLAGS_API_URL?.trim() || "";
|
|
36
|
+
const companyUid = company.companyUid.trim();
|
|
37
|
+
if (!endpoint || !/^cmp_[A-Za-z0-9]{3,128}$/.test(companyUid)) {
|
|
38
|
+
return CLIENT_HEALTH_REASON_CODES_FLAG_FALLBACK;
|
|
39
|
+
}
|
|
40
|
+
const companySlug = company.companySlug?.trim() || "";
|
|
41
|
+
const deadline = AbortSignal.timeout(CLIENT_HEALTH_REASON_CODES_FLAG_TIMEOUT_MS);
|
|
42
|
+
const signal = callerSignal ? AbortSignal.any([callerSignal, deadline]) : deadline;
|
|
43
|
+
const doFetch = dependencies.fetch ?? globalThis.fetch;
|
|
44
|
+
let client;
|
|
45
|
+
let failed = false;
|
|
46
|
+
let failureReported = false;
|
|
47
|
+
let enabled = CLIENT_HEALTH_REASON_CODES_FLAG_FALLBACK;
|
|
48
|
+
const reportFailureOnce = (error) => {
|
|
49
|
+
failed = true;
|
|
50
|
+
if (failureReported)
|
|
51
|
+
return;
|
|
52
|
+
failureReported = true;
|
|
53
|
+
(dependencies.reportFailure ?? reportFlagFailure)(error);
|
|
54
|
+
};
|
|
55
|
+
try {
|
|
56
|
+
client = (dependencies.createClient ?? createFlagClient)({
|
|
57
|
+
endpoint,
|
|
58
|
+
companyUid,
|
|
59
|
+
...(companySlug && /^[A-Za-z0-9][A-Za-z0-9-]{0,63}$/.test(companySlug)
|
|
60
|
+
? { companyIdentifiers: [companySlug] }
|
|
61
|
+
: {}),
|
|
62
|
+
env: {},
|
|
63
|
+
getToken: dependencies.getToken ?? (() => loadCachedTokens()?.idToken ?? ""),
|
|
64
|
+
fetch: (input, init) => doFetch(input, {
|
|
65
|
+
...init,
|
|
66
|
+
signal: combineSignals(init?.signal ?? undefined, signal),
|
|
67
|
+
}),
|
|
68
|
+
refreshIntervalMs: 0,
|
|
69
|
+
requestTimeoutMs: CLIENT_HEALTH_REASON_CODES_FLAG_TIMEOUT_MS,
|
|
70
|
+
onError: reportFailureOnce,
|
|
71
|
+
});
|
|
72
|
+
await client.ready();
|
|
73
|
+
if (!failed && !signal.aborted) {
|
|
74
|
+
enabled = client.isEnabled(CLIENT_HEALTH_REASON_CODES_FLAG, {
|
|
75
|
+
fallback: CLIENT_HEALTH_REASON_CODES_FLAG_FALLBACK,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
reportFailureOnce(error);
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
try {
|
|
84
|
+
client?.close();
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
reportFailureOnce(error);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return failed || signal.aborted
|
|
91
|
+
? CLIENT_HEALTH_REASON_CODES_FLAG_FALLBACK
|
|
92
|
+
: enabled;
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=client-health-reason-codes-flag.js.map
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
import type { CheckResult, DoctorStatus } from "../types.js";
|
|
32
32
|
import { type GateProfile } from "../hook-gate-profiles.js";
|
|
33
33
|
import { type FixClass } from "./remediation.js";
|
|
34
|
+
import { type RefreshSyncOutcome } from "./refresh-sync-outcome.js";
|
|
34
35
|
/** The tree subtrees whose uncommitted changes block a `--fix` run. */
|
|
35
36
|
export declare const HOOK_CONFIG_DIRS: readonly [".claude", ".codex", ".grok"];
|
|
36
37
|
/** Options for {@link applyFixes}. Everything is injectable for hermetic tests. */
|
|
@@ -91,6 +92,8 @@ export interface RefreshSyncTarget {
|
|
|
91
92
|
export interface RefreshSyncResult {
|
|
92
93
|
/** Whether the pull exited 0. */
|
|
93
94
|
ok: boolean;
|
|
95
|
+
/** Closed enum for machine-readable client-health diagnostics. */
|
|
96
|
+
outcome: RefreshSyncOutcome;
|
|
94
97
|
/** One-line summary for the post-fix report. */
|
|
95
98
|
message: string;
|
|
96
99
|
}
|
|
@@ -108,6 +111,8 @@ export interface AppliedFix {
|
|
|
108
111
|
summary: string;
|
|
109
112
|
/** The status of the re-run check for this target, or null if not found. */
|
|
110
113
|
postStatus: DoctorStatus | null;
|
|
114
|
+
/** Present for refresh-sync plans; contains no command output or path data. */
|
|
115
|
+
refreshSyncOutcome?: RefreshSyncOutcome;
|
|
111
116
|
}
|
|
112
117
|
/** The outcome of an {@link applyFixes} run. */
|
|
113
118
|
export interface ApplyFixesResult {
|
|
@@ -40,6 +40,7 @@ import { deriveRemediation } from "./remediation.js";
|
|
|
40
40
|
import { botDir } from "../../bot/paths.js";
|
|
41
41
|
import { readBotConfig } from "../../bot/config.js";
|
|
42
42
|
import { buildBotDaemonPaths, rewriteBotDaemonNodePath } from "../../bot/daemon.js";
|
|
43
|
+
import { classifyRefreshSyncOutcome, } from "./refresh-sync-outcome.js";
|
|
43
44
|
/** The tree subtrees whose uncommitted changes block a `--fix` run. */
|
|
44
45
|
export const HOOK_CONFIG_DIRS = [".claude", ".codex", ".grok"];
|
|
45
46
|
/** Per-company bound for the default `hq sync pull` repair. */
|
|
@@ -130,6 +131,9 @@ export async function applyFixes(options) {
|
|
|
130
131
|
target: plan.target,
|
|
131
132
|
summary: plan.summary,
|
|
132
133
|
postStatus: plan.postStatus(after),
|
|
134
|
+
...(plan.refreshSyncOutcome?.()
|
|
135
|
+
? { refreshSyncOutcome: plan.refreshSyncOutcome() }
|
|
136
|
+
: {}),
|
|
133
137
|
}));
|
|
134
138
|
write("\nPost-fix status:\n");
|
|
135
139
|
for (const fix of applied) {
|
|
@@ -197,23 +201,41 @@ export function defaultRefreshSync(target) {
|
|
|
197
201
|
cwd: target.hqRoot,
|
|
198
202
|
});
|
|
199
203
|
if (result.error) {
|
|
200
|
-
const
|
|
204
|
+
const spawnError = result.error;
|
|
205
|
+
const outcome = classifyRefreshSyncOutcome({
|
|
206
|
+
errorCode: spawnError.code,
|
|
207
|
+
...(result.error.message.includes("ETIMEDOUT")
|
|
208
|
+
? { errorCode: "ETIMEDOUT" }
|
|
209
|
+
: {}),
|
|
210
|
+
signal: result.signal,
|
|
211
|
+
status: result.status,
|
|
212
|
+
stdout: result.stdout,
|
|
213
|
+
stderr: result.stderr,
|
|
214
|
+
});
|
|
201
215
|
return Promise.resolve({
|
|
202
216
|
ok: false,
|
|
203
|
-
|
|
217
|
+
outcome,
|
|
218
|
+
message: outcome === "pull-timeout"
|
|
204
219
|
? `timed out pulling '${target.slug}'`
|
|
205
220
|
: `could not start pull for '${target.slug}': ${result.error.message}`,
|
|
206
221
|
});
|
|
207
222
|
}
|
|
208
223
|
if (result.status !== 0) {
|
|
224
|
+
const outcome = classifyRefreshSyncOutcome({
|
|
225
|
+
status: result.status,
|
|
226
|
+
stdout: result.stdout,
|
|
227
|
+
stderr: result.stderr,
|
|
228
|
+
});
|
|
209
229
|
const detail = (result.stderr || result.stdout || "").trim().split("\n").at(-1) ?? "";
|
|
210
230
|
return Promise.resolve({
|
|
211
231
|
ok: false,
|
|
232
|
+
outcome,
|
|
212
233
|
message: `pull of '${target.slug}' exited ${result.status}${detail ? `: ${detail}` : ""}`,
|
|
213
234
|
});
|
|
214
235
|
}
|
|
215
236
|
return Promise.resolve({
|
|
216
237
|
ok: true,
|
|
238
|
+
outcome: "pull-succeeded",
|
|
217
239
|
message: `pulled '${target.slug}'`,
|
|
218
240
|
});
|
|
219
241
|
}
|
|
@@ -287,6 +309,7 @@ function planRefreshSync(hqRoot, result, rem, refreshSync) {
|
|
|
287
309
|
const slug = rem.fixTarget;
|
|
288
310
|
if (!slug)
|
|
289
311
|
return null;
|
|
312
|
+
let outcome;
|
|
290
313
|
return {
|
|
291
314
|
checkId: result.checkId,
|
|
292
315
|
fixClass: "refresh-sync",
|
|
@@ -297,8 +320,10 @@ function planRefreshSync(hqRoot, result, rem, refreshSync) {
|
|
|
297
320
|
` refresh the stale sync journal (keeps local conflict copies)`,
|
|
298
321
|
summary: `pull ${slug} to refresh its sync journal`,
|
|
299
322
|
apply: async () => {
|
|
300
|
-
await refreshSync({ slug, hqRoot });
|
|
323
|
+
const result = await refreshSync({ slug, hqRoot });
|
|
324
|
+
outcome = result.outcome;
|
|
301
325
|
},
|
|
326
|
+
refreshSyncOutcome: () => outcome,
|
|
302
327
|
postStatus: (results) => results.find((entry) => entry.checkId === result.checkId)?.status ?? null,
|
|
303
328
|
};
|
|
304
329
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { AppliedFix, ApplyFixesResult } from "./apply.js";
|
|
2
|
+
export declare const DOCTOR_FIX_JSON_SCHEMA_VERSION = 1;
|
|
3
|
+
export interface DoctorFixJsonResult {
|
|
4
|
+
schemaVersion: number;
|
|
5
|
+
exitCode: number;
|
|
6
|
+
refused: ApplyFixesResult["refused"] | "outside-hq-tree";
|
|
7
|
+
refreshSync: {
|
|
8
|
+
checkId: string;
|
|
9
|
+
outcome: NonNullable<AppliedFix["refreshSyncOutcome"]>;
|
|
10
|
+
postStatus: AppliedFix["postStatus"];
|
|
11
|
+
}[];
|
|
12
|
+
}
|
|
13
|
+
/** Build the intentionally narrow JSON surface for `hq doctor --fix --json`. */
|
|
14
|
+
export declare function buildDoctorFixJson(input: {
|
|
15
|
+
exitCode: number;
|
|
16
|
+
refused: ApplyFixesResult["refused"] | "outside-hq-tree";
|
|
17
|
+
applied: readonly AppliedFix[];
|
|
18
|
+
}): DoctorFixJsonResult;
|
|
19
|
+
export declare function renderDoctorFixJson(input: Parameters<typeof buildDoctorFixJson>[0]): string;
|
|
20
|
+
//# sourceMappingURL=json-output.d.ts.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export const DOCTOR_FIX_JSON_SCHEMA_VERSION = 1;
|
|
2
|
+
/** Build the intentionally narrow JSON surface for `hq doctor --fix --json`. */
|
|
3
|
+
export function buildDoctorFixJson(input) {
|
|
4
|
+
return {
|
|
5
|
+
schemaVersion: DOCTOR_FIX_JSON_SCHEMA_VERSION,
|
|
6
|
+
exitCode: input.exitCode,
|
|
7
|
+
refused: input.refused,
|
|
8
|
+
refreshSync: input.applied.flatMap((fix) => fix.fixClass === "refresh-sync" && fix.refreshSyncOutcome
|
|
9
|
+
? [
|
|
10
|
+
{
|
|
11
|
+
checkId: fix.checkId,
|
|
12
|
+
outcome: fix.refreshSyncOutcome,
|
|
13
|
+
postStatus: fix.postStatus,
|
|
14
|
+
},
|
|
15
|
+
]
|
|
16
|
+
: []),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function renderDoctorFixJson(input) {
|
|
20
|
+
return JSON.stringify(buildDoctorFixJson(input), null, 2) + "\n";
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=json-output.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Privacy-safe outcomes for a targeted refresh-sync attempt. */
|
|
2
|
+
export type RefreshSyncOutcome = "pull-succeeded" | "pull-timeout" | "pull-failed:4xx" | "pull-failed:5xx" | "pull-failed:network" | "pull-failed:other";
|
|
3
|
+
export interface RefreshSyncObservation {
|
|
4
|
+
errorCode?: string;
|
|
5
|
+
signal?: string | null;
|
|
6
|
+
status?: number | null;
|
|
7
|
+
stdout?: string;
|
|
8
|
+
stderr?: string;
|
|
9
|
+
}
|
|
10
|
+
/** Classify process/HTTP results without retaining their free-text details. */
|
|
11
|
+
export declare function classifyRefreshSyncOutcome(observation: RefreshSyncObservation): RefreshSyncOutcome;
|
|
12
|
+
//# sourceMappingURL=refresh-sync-outcome.d.ts.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Classify process/HTTP results without retaining their free-text details. */
|
|
2
|
+
export function classifyRefreshSyncOutcome(observation) {
|
|
3
|
+
const details = String(observation.stdout ?? "") + "\n" + String(observation.stderr ?? "");
|
|
4
|
+
const errorCode = observation.errorCode ??
|
|
5
|
+
/\b(ETIMEDOUT|ETIME|EAI_AGAIN|ENOTFOUND|ECONNRESET|ECONNREFUSED|EHOSTUNREACH|ENETUNREACH|EPIPE|ECONNABORTED)\b/.exec(details)?.[1];
|
|
6
|
+
if (errorCode === "ETIMEDOUT" ||
|
|
7
|
+
errorCode === "ETIME" ||
|
|
8
|
+
observation.signal === "SIGTERM") {
|
|
9
|
+
return "pull-timeout";
|
|
10
|
+
}
|
|
11
|
+
if (!errorCode && observation.status === 0) {
|
|
12
|
+
return "pull-succeeded";
|
|
13
|
+
}
|
|
14
|
+
const httpStatus = /(?:HTTP(?:\/\d(?:\.\d)?)?\s+|status(?:\s+code)?\s*[=: ]\s*)([45]\d\d)\b/i.exec(details)?.[1] ?? "";
|
|
15
|
+
if (httpStatus.startsWith("4"))
|
|
16
|
+
return "pull-failed:4xx";
|
|
17
|
+
if (httpStatus.startsWith("5"))
|
|
18
|
+
return "pull-failed:5xx";
|
|
19
|
+
if (errorCode &&
|
|
20
|
+
/^(?:EAI_AGAIN|ENOTFOUND|ECONNRESET|ECONNREFUSED|EHOSTUNREACH|ENETUNREACH|EPIPE|ECONNABORTED)$/.test(errorCode)) {
|
|
21
|
+
return "pull-failed:network";
|
|
22
|
+
}
|
|
23
|
+
return "pull-failed:other";
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=refresh-sync-outcome.js.map
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* per-item result shape where both are optional.
|
|
19
19
|
*/
|
|
20
20
|
import { type DoctorPlatform, type StatusCounts } from "./report.js";
|
|
21
|
-
import type { DoctorStatus, FamilyRun } from "./types.js";
|
|
21
|
+
import type { DoctorStatus, FamilyRun, SyncReasonCode } from "./types.js";
|
|
22
22
|
/**
|
|
23
23
|
* The `--json` schema version. Bump on any breaking change to the document
|
|
24
24
|
* shape; the snapshot test in report.test.ts fails until the bump is
|
|
@@ -50,6 +50,10 @@ export interface DoctorJsonResult {
|
|
|
50
50
|
family: string;
|
|
51
51
|
/** Stable machine id for the check, e.g. `hooks.settings-present`. */
|
|
52
52
|
checkId: string;
|
|
53
|
+
/** Stable classification for the sync family; contains no customer data. */
|
|
54
|
+
reasonCode?: SyncReasonCode;
|
|
55
|
+
/** Present only when this result's company opted in through hq-flags. */
|
|
56
|
+
clientHealthReasonCodesEnabled?: true;
|
|
53
57
|
/** What was inspected. Present only when the result carries one. */
|
|
54
58
|
target?: string;
|
|
55
59
|
/** Human-readable explanation. */
|
|
@@ -92,6 +96,8 @@ export interface BuildDoctorJsonInput {
|
|
|
92
96
|
platform?: DoctorPlatform;
|
|
93
97
|
/** Families this run was restricted to. Omitted on a full run. */
|
|
94
98
|
scope?: readonly string[];
|
|
99
|
+
/** Check ids whose own company enabled diagnostic reason codes in hq-flags. */
|
|
100
|
+
clientHealthReasonCodesEnabledCheckIds?: ReadonlySet<string>;
|
|
95
101
|
}
|
|
96
102
|
/** Assemble the machine-readable document from a completed run. */
|
|
97
103
|
export declare function buildDoctorJson(input: BuildDoctorJsonInput): DoctorJsonDocument;
|
|
@@ -34,7 +34,7 @@ export function buildDoctorJson(input) {
|
|
|
34
34
|
const results = [];
|
|
35
35
|
for (const run of input.families) {
|
|
36
36
|
for (const result of run.results) {
|
|
37
|
-
results.push(toJsonResult(run.family.id, result));
|
|
37
|
+
results.push(toJsonResult(run.family.id, result, input.clientHealthReasonCodesEnabledCheckIds));
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
return {
|
|
@@ -48,12 +48,16 @@ export function buildDoctorJson(input) {
|
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
50
|
/** Convert one {@link CheckResult} into its JSON form, omitting empty optionals. */
|
|
51
|
-
function toJsonResult(familyId, result) {
|
|
51
|
+
function toJsonResult(familyId, result, clientHealthReasonCodesEnabledCheckIds) {
|
|
52
52
|
const remediation = deriveRemediation(result);
|
|
53
53
|
return {
|
|
54
54
|
status: result.status,
|
|
55
55
|
family: familyId,
|
|
56
56
|
checkId: result.checkId,
|
|
57
|
+
...(result.reasonCode !== undefined ? { reasonCode: result.reasonCode } : {}),
|
|
58
|
+
...(clientHealthReasonCodesEnabledCheckIds?.has(result.checkId)
|
|
59
|
+
? { clientHealthReasonCodesEnabled: true }
|
|
60
|
+
: {}),
|
|
57
61
|
...(result.target !== undefined ? { target: result.target } : {}),
|
|
58
62
|
message: result.message,
|
|
59
63
|
...(result.remediation !== undefined
|
|
@@ -18,6 +18,9 @@ export declare const DOCTOR_STATUSES: readonly ["PASS", "FAIL", "WARN", "UNTESTE
|
|
|
18
18
|
* the type system rejects any other string at every call site.
|
|
19
19
|
*/
|
|
20
20
|
export type DoctorStatus = (typeof DOCTOR_STATUSES)[number];
|
|
21
|
+
/** Stable, privacy-safe diagnostics emitted by the sync health family. */
|
|
22
|
+
export declare const SYNC_REASON_CODES: readonly ["core-version-unavailable", "core-update-available", "no-local-tree", "never-synced", "invalid-last-sync", "stale-threshold", "manifest-unavailable", "manifest-state-unavailable", "no-manifest-scopes", "never-uploaded", "invalid-upload-time", "unresolved-company-uid"];
|
|
23
|
+
export type SyncReasonCode = (typeof SYNC_REASON_CODES)[number];
|
|
21
24
|
/** Runtime type guard: true only for a member of the fixed vocabulary. */
|
|
22
25
|
export declare function isDoctorStatus(value: unknown): value is DoctorStatus;
|
|
23
26
|
/** A single per-item result produced by a check family. */
|
|
@@ -26,6 +29,8 @@ export interface CheckResult {
|
|
|
26
29
|
status: DoctorStatus;
|
|
27
30
|
/** Stable machine id for this item, e.g. `hooks.settings-present`. */
|
|
28
31
|
checkId: string;
|
|
32
|
+
/** Closed diagnostic enum; never contains paths, company slugs, or prose. */
|
|
33
|
+
reasonCode?: SyncReasonCode;
|
|
29
34
|
/** What was inspected — a path, hook id, etc. Optional. */
|
|
30
35
|
target?: string;
|
|
31
36
|
/** Human-readable one-line explanation of the result. */
|
package/dist/lib/doctor/types.js
CHANGED
|
@@ -21,6 +21,21 @@ export const DOCTOR_STATUSES = [
|
|
|
21
21
|
"UNKNOWN",
|
|
22
22
|
"KNOWN-DEFECT",
|
|
23
23
|
];
|
|
24
|
+
/** Stable, privacy-safe diagnostics emitted by the sync health family. */
|
|
25
|
+
export const SYNC_REASON_CODES = [
|
|
26
|
+
"core-version-unavailable",
|
|
27
|
+
"core-update-available",
|
|
28
|
+
"no-local-tree",
|
|
29
|
+
"never-synced",
|
|
30
|
+
"invalid-last-sync",
|
|
31
|
+
"stale-threshold",
|
|
32
|
+
"manifest-unavailable",
|
|
33
|
+
"manifest-state-unavailable",
|
|
34
|
+
"no-manifest-scopes",
|
|
35
|
+
"never-uploaded",
|
|
36
|
+
"invalid-upload-time",
|
|
37
|
+
"unresolved-company-uid",
|
|
38
|
+
];
|
|
24
39
|
/** Runtime type guard: true only for a member of the fixed vocabulary. */
|
|
25
40
|
export function isDoctorStatus(value) {
|
|
26
41
|
return (typeof value === "string" &&
|
|
@@ -22,6 +22,6 @@ export type { DaemonDoctorDeps, DaemonDoctorReport } from "./doctor.js";
|
|
|
22
22
|
export { scopeBundleToAgentIdentity, } from "./credentials.js";
|
|
23
23
|
export { FLUSH_INTERVAL_MS, DAEMON_VERSION_DRIFT_EXIT_CODE, SPOOL_DEBOUNCE_MS, runMeshDaemon, } from "./run.js";
|
|
24
24
|
export type { DaemonHandle, DaemonRunDeps } from "./run.js";
|
|
25
|
-
export { TRANSCRIPT_ADAPTER_VERSION, TRANSCRIPT_EVENT_SOURCE, TRANSCRIPT_FRESH_MS, TRANSCRIPT_HOOK_COVER_MS, TRANSCRIPT_MARKER, TRANSCRIPT_SESSION_END_MS, TRANSCRIPT_TURN_QUIET_MS, TRANSCRIPT_WATCH_BACKOFF_DURATION_MS, TRANSCRIPT_WATCH_BACKOFF_ENTRIES, TRANSCRIPT_WATCH_ENTRY_CAP, TRANSCRIPT_WATCH_INTERVAL_MAX_MS, TRANSCRIPT_WATCH_INTERVAL_MS, TRANSCRIPT_WATCH_REVALIDATE_MS, TranscriptWatcher, createTranscriptDiscoverCache, decodeClaudeProjectDirName, defaultTranscriptFs, discoverTranscripts, discoverTranscriptsWithMeta, isTranscriptHookCovered, nextTranscriptWatchIntervalMs, noteHookSessionsFromSpoolFile, resolveTranscriptRegistration, sessionIdFromClaudeTranscriptPath, sessionIdFromCodexTranscriptPath, sortDiscoveries, } from "./transcript-watch.js";
|
|
25
|
+
export { TRANSCRIPT_ADAPTER_VERSION, TRANSCRIPT_EVENT_SOURCE, TRANSCRIPT_FRESH_MS, TRANSCRIPT_HOOK_COVER_MS, TRANSCRIPT_MARKER, TRANSCRIPT_SESSION_END_MS, TRANSCRIPT_HOOK_QUIET_ADOPT_MS, TRANSCRIPT_TURN_QUIET_MS, TRANSCRIPT_WATCH_BACKOFF_DURATION_MS, TRANSCRIPT_WATCH_BACKOFF_ENTRIES, TRANSCRIPT_WATCH_ENTRY_CAP, TRANSCRIPT_WATCH_INTERVAL_MAX_MS, TRANSCRIPT_WATCH_INTERVAL_MS, TRANSCRIPT_WATCH_REVALIDATE_MS, TranscriptWatcher, createTranscriptDiscoverCache, decodeClaudeProjectDirName, defaultTranscriptFs, discoverTranscripts, discoverTranscriptsWithMeta, isTranscriptHookCovered, nextTranscriptWatchIntervalMs, noteHookSessionsFromSpoolFile, resolveTranscriptRegistration, sessionIdFromClaudeTranscriptPath, sessionIdFromCodexTranscriptPath, sortDiscoveries, } from "./transcript-watch.js";
|
|
26
26
|
export type { DiscoveredTranscript, DiscoverScanMeta, DiscoverTranscriptsResult, TranscriptDiscoverCache, TranscriptFs, TranscriptKind, TranscriptWatchDeps, TranscriptWatchTickResult, } from "./transcript-watch.js";
|
|
27
27
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -11,5 +11,5 @@ export { daemonConfigPath, findAncestorHqRoot, isInstallableHqRoot, readDaemonCo
|
|
|
11
11
|
export { UNHEALTHY_SPOOL_AGE_MS, collectDaemonDoctor, formatCliIdentityLines, formatCliMismatchLine, formatDaemonDoctor, listHqBinariesOnPath, } from "./doctor.js";
|
|
12
12
|
export { scopeBundleToAgentIdentity, } from "./credentials.js";
|
|
13
13
|
export { FLUSH_INTERVAL_MS, DAEMON_VERSION_DRIFT_EXIT_CODE, SPOOL_DEBOUNCE_MS, runMeshDaemon, } from "./run.js";
|
|
14
|
-
export { TRANSCRIPT_ADAPTER_VERSION, TRANSCRIPT_EVENT_SOURCE, TRANSCRIPT_FRESH_MS, TRANSCRIPT_HOOK_COVER_MS, TRANSCRIPT_MARKER, TRANSCRIPT_SESSION_END_MS, TRANSCRIPT_TURN_QUIET_MS, TRANSCRIPT_WATCH_BACKOFF_DURATION_MS, TRANSCRIPT_WATCH_BACKOFF_ENTRIES, TRANSCRIPT_WATCH_ENTRY_CAP, TRANSCRIPT_WATCH_INTERVAL_MAX_MS, TRANSCRIPT_WATCH_INTERVAL_MS, TRANSCRIPT_WATCH_REVALIDATE_MS, TranscriptWatcher, createTranscriptDiscoverCache, decodeClaudeProjectDirName, defaultTranscriptFs, discoverTranscripts, discoverTranscriptsWithMeta, isTranscriptHookCovered, nextTranscriptWatchIntervalMs, noteHookSessionsFromSpoolFile, resolveTranscriptRegistration, sessionIdFromClaudeTranscriptPath, sessionIdFromCodexTranscriptPath, sortDiscoveries, } from "./transcript-watch.js";
|
|
14
|
+
export { TRANSCRIPT_ADAPTER_VERSION, TRANSCRIPT_EVENT_SOURCE, TRANSCRIPT_FRESH_MS, TRANSCRIPT_HOOK_COVER_MS, TRANSCRIPT_MARKER, TRANSCRIPT_SESSION_END_MS, TRANSCRIPT_HOOK_QUIET_ADOPT_MS, TRANSCRIPT_TURN_QUIET_MS, TRANSCRIPT_WATCH_BACKOFF_DURATION_MS, TRANSCRIPT_WATCH_BACKOFF_ENTRIES, TRANSCRIPT_WATCH_ENTRY_CAP, TRANSCRIPT_WATCH_INTERVAL_MAX_MS, TRANSCRIPT_WATCH_INTERVAL_MS, TRANSCRIPT_WATCH_REVALIDATE_MS, TranscriptWatcher, createTranscriptDiscoverCache, decodeClaudeProjectDirName, defaultTranscriptFs, discoverTranscripts, discoverTranscriptsWithMeta, isTranscriptHookCovered, nextTranscriptWatchIntervalMs, noteHookSessionsFromSpoolFile, resolveTranscriptRegistration, sessionIdFromClaudeTranscriptPath, sessionIdFromCodexTranscriptPath, sortDiscoveries, } from "./transcript-watch.js";
|
|
15
15
|
//# sourceMappingURL=index.js.map
|
|
@@ -62,6 +62,13 @@ export declare const TRANSCRIPT_START_RETRY_BASE_MS = 30000;
|
|
|
62
62
|
export declare const TRANSCRIPT_START_RETRY_MAX_MS = 120000;
|
|
63
63
|
/** Ignore cold transcripts whose mtime is older than this on first sight. */
|
|
64
64
|
export declare const TRANSCRIPT_FRESH_MS: number;
|
|
65
|
+
/**
|
|
66
|
+
* Hook-covered sessions skipped as "stale" on first sight never got a quiet
|
|
67
|
+
* session_end (live: skippedHook=411, ended=0). Adopt them for this long after
|
|
68
|
+
* last activity so a 10–120 min quiet window still reports after a daemon
|
|
69
|
+
* restart or a missed track. Older than this stays silent (the 6h reaper).
|
|
70
|
+
*/
|
|
71
|
+
export declare const TRANSCRIPT_HOOK_QUIET_ADOPT_MS: number;
|
|
65
72
|
export declare const TRANSCRIPT_MARKER: "from transcript";
|
|
66
73
|
export declare const TRANSCRIPT_ADAPTER_VERSION: string;
|
|
67
74
|
/** Network `source` stamped on every synthesized event (AMEND-001). */
|
|
@@ -67,6 +67,13 @@ export const TRANSCRIPT_START_RETRY_BASE_MS = 30_000;
|
|
|
67
67
|
export const TRANSCRIPT_START_RETRY_MAX_MS = 120_000;
|
|
68
68
|
/** Ignore cold transcripts whose mtime is older than this on first sight. */
|
|
69
69
|
export const TRANSCRIPT_FRESH_MS = TRANSCRIPT_SESSION_END_MS;
|
|
70
|
+
/**
|
|
71
|
+
* Hook-covered sessions skipped as "stale" on first sight never got a quiet
|
|
72
|
+
* session_end (live: skippedHook=411, ended=0). Adopt them for this long after
|
|
73
|
+
* last activity so a 10–120 min quiet window still reports after a daemon
|
|
74
|
+
* restart or a missed track. Older than this stays silent (the 6h reaper).
|
|
75
|
+
*/
|
|
76
|
+
export const TRANSCRIPT_HOOK_QUIET_ADOPT_MS = 2 * 60 * 60 * 1000;
|
|
70
77
|
export const TRANSCRIPT_MARKER = "from transcript";
|
|
71
78
|
export const TRANSCRIPT_ADAPTER_VERSION = CLI_VERSION;
|
|
72
79
|
/** Network `source` stamped on every synthesized event (AMEND-001). */
|
|
@@ -521,13 +528,24 @@ export function noteHookSessionsFromSpoolFile(spoolPath, lastHookEventAt, atMs,
|
|
|
521
528
|
if (!line.trim())
|
|
522
529
|
continue;
|
|
523
530
|
try {
|
|
524
|
-
const
|
|
531
|
+
const raw = JSON.parse(line);
|
|
532
|
+
const ev = raw.event && typeof raw.event === "object" ? raw.event : raw;
|
|
525
533
|
if (typeof ev.sessionId !== "string" || !ev.sessionId)
|
|
526
534
|
continue;
|
|
527
535
|
// Synthesized transcript events carry source=transcript — not hooks.
|
|
528
536
|
if (ev.source === TRANSCRIPT_EVENT_SOURCE)
|
|
529
537
|
continue;
|
|
530
|
-
|
|
538
|
+
let eventAt = atMs;
|
|
539
|
+
const atRaw = typeof ev.at === "string" ? ev.at : undefined;
|
|
540
|
+
if (atRaw) {
|
|
541
|
+
const parsed = Date.parse(atRaw);
|
|
542
|
+
if (Number.isFinite(parsed))
|
|
543
|
+
eventAt = parsed;
|
|
544
|
+
}
|
|
545
|
+
const prev = lastHookEventAt.get(ev.sessionId);
|
|
546
|
+
if (prev === undefined || eventAt > prev) {
|
|
547
|
+
lastHookEventAt.set(ev.sessionId, eventAt);
|
|
548
|
+
}
|
|
531
549
|
noted.push(ev.sessionId);
|
|
532
550
|
}
|
|
533
551
|
catch {
|
|
@@ -739,7 +757,10 @@ export class TranscriptWatcher {
|
|
|
739
757
|
const lastActivity = Math.max(d.mtimeMs, hookAt ?? 0);
|
|
740
758
|
let track = this.tracked.get(d.path);
|
|
741
759
|
if (!track) {
|
|
742
|
-
|
|
760
|
+
// Cold transcripts stay silent. Hook-covered sessions that already sat
|
|
761
|
+
// quiet past TRANSCRIPT_SESSION_END_MS still need one session_end — the
|
|
762
|
+
// live skip path returned here and never tracked them (US-051).
|
|
763
|
+
if (nowMs - lastActivity > TRANSCRIPT_HOOK_QUIET_ADOPT_MS)
|
|
743
764
|
return;
|
|
744
765
|
track = {
|
|
745
766
|
path: d.path,
|
|
@@ -857,6 +878,7 @@ export class TranscriptWatcher {
|
|
|
857
878
|
root: this.deps.workMeshRoot,
|
|
858
879
|
env: this.deps.env,
|
|
859
880
|
now: () => new Date(nowMs),
|
|
881
|
+
...(kind === "session_end" ? { transcriptPath: track.path } : {}),
|
|
860
882
|
};
|
|
861
883
|
this.enqueueFn(opts);
|
|
862
884
|
}
|
|
@@ -74,6 +74,9 @@ export interface MeshEmitEvent {
|
|
|
74
74
|
status?: "queued" | "in_progress" | "review" | "done";
|
|
75
75
|
reason?: string;
|
|
76
76
|
summary?: string;
|
|
77
|
+
repo?: string;
|
|
78
|
+
gitBranch?: string;
|
|
79
|
+
prUrls?: string[];
|
|
77
80
|
evidence?: EmitEvidence;
|
|
78
81
|
}
|
|
79
82
|
export interface BuildMeshEmitEventInput {
|
|
@@ -90,6 +93,9 @@ export interface BuildMeshEmitEventInput {
|
|
|
90
93
|
status?: "queued" | "in_progress" | "review" | "done";
|
|
91
94
|
reason?: string;
|
|
92
95
|
summary?: string;
|
|
96
|
+
repo?: string;
|
|
97
|
+
gitBranch?: string;
|
|
98
|
+
prUrls?: string[];
|
|
93
99
|
evidence?: EmitEvidence;
|
|
94
100
|
}
|
|
95
101
|
/**
|
|
@@ -192,6 +192,14 @@ export function buildMeshEmitEvent(input) {
|
|
|
192
192
|
const summary = keep(input.summary);
|
|
193
193
|
if (summary)
|
|
194
194
|
event.summary = summary;
|
|
195
|
+
const repo = keep(input.repo);
|
|
196
|
+
if (repo)
|
|
197
|
+
event.repo = repo;
|
|
198
|
+
const gitBranch = keep(input.gitBranch);
|
|
199
|
+
if (gitBranch)
|
|
200
|
+
event.gitBranch = gitBranch;
|
|
201
|
+
if (input.prUrls && input.prUrls.length > 0)
|
|
202
|
+
event.prUrls = input.prUrls;
|
|
195
203
|
if (input.evidence && Object.keys(input.evidence).length > 0) {
|
|
196
204
|
event.evidence = input.evidence;
|
|
197
205
|
}
|