@indigoai-us/hq-cli 5.345.55 → 5.345.57
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 +18 -0
- package/assets/scaffold/core/scripts/derive-trigger-facts.sh +10 -1
- package/assets/scaffold/core/scripts/jobs-validate.sh +2 -1
- package/assets/scaffold/core/scripts/session-title.sh +18 -15
- package/dist/command-catalog.generated.d.ts +124 -0
- package/dist/command-catalog.generated.js +155 -0
- package/dist/commands/core-delegate-w1b.js +18 -5
- package/dist/commands/core-derive-trigger-facts-command.js +14 -0
- package/dist/commands/core-derive-trigger-facts-native.d.ts +1 -0
- package/dist/commands/core-derive-trigger-facts-native.js +20 -1
- package/dist/commands/doctor.d.ts +1 -0
- package/dist/commands/doctor.js +15 -2
- package/dist/commands/lanes-checks.d.ts +3 -0
- package/dist/commands/lanes-checks.js +34 -0
- package/dist/commands/lanes-loop.d.ts +12 -0
- package/dist/commands/lanes-loop.js +214 -0
- package/dist/commands/lanes-questions.js +37 -11
- package/dist/commands/lanes-resume.js +19 -1
- package/dist/commands/lanes-session-start.d.ts +2 -7
- package/dist/commands/lanes-session-start.js +9 -29
- package/dist/commands/lanes-shared.d.ts +14 -2
- package/dist/commands/lanes.d.ts +3 -1
- package/dist/commands/lanes.js +376 -35
- package/dist/commands/members.d.ts +4 -0
- package/dist/commands/members.js +23 -2
- package/dist/lanes-drain-hook.js +6 -2
- package/dist/lib/doctor/checks/sync-health.d.ts +2 -0
- package/dist/lib/doctor/checks/sync-health.js +47 -23
- package/dist/lib/lanes/delivery.d.ts +3 -0
- package/dist/lib/lanes/delivery.js +5 -0
- package/dist/lib/lanes/dropbox.d.ts +7 -2
- package/dist/lib/lanes/dropbox.js +157 -10
- package/dist/lib/lanes/flag-keys.d.ts +12 -0
- package/dist/lib/lanes/flag-keys.js +12 -0
- package/dist/lib/lanes/hooks-install.d.ts +14 -11
- package/dist/lib/lanes/hooks-install.js +70 -50
- package/dist/lib/lanes/loop.d.ts +158 -0
- package/dist/lib/lanes/loop.js +743 -0
- package/dist/lib/lanes/outpost-dispatch.d.ts +9 -1
- package/dist/lib/lanes/outpost-dispatch.js +21 -7
- package/dist/lib/lanes/reaper.d.ts +58 -0
- package/dist/lib/lanes/reaper.js +403 -0
- package/dist/lib/lanes/reconcile.d.ts +19 -0
- package/dist/lib/lanes/reconcile.js +173 -6
- package/dist/lib/lanes/spawn.d.ts +4 -0
- package/dist/lib/lanes/spawn.js +41 -9
- package/dist/lib/lanes/status-fields.d.ts +12 -0
- package/dist/lib/lanes/status-fields.js +189 -0
- package/dist/lib/lanes/store.js +49 -2
- package/dist/lib/lanes/types.d.ts +18 -0
- package/dist/lib/lanes/types.js +3 -3
- package/dist/lib/lanes/watcher.js +3 -0
- package/dist/lib/lanes/worker-continuity.d.ts +18 -0
- package/dist/lib/lanes/worker-continuity.js +81 -0
- package/dist/lib/lanes/worker-process.d.ts +4 -0
- package/dist/lib/lanes/worker-process.js +77 -0
- package/dist/main.js +9 -3
- package/dist/utils/cli-telemetry.d.ts +4 -1
- package/dist/utils/cli-telemetry.js +8 -0
- package/dist/utils/jq-windows-compat.d.ts +13 -0
- package/dist/utils/jq-windows-compat.js +48 -0
- package/dist/utils/run-bundled-script.d.ts +3 -0
- package/dist/utils/run-bundled-script.js +9 -80
- package/dist/utils/windows-bash.d.ts +5 -0
- package/dist/utils/windows-bash.js +75 -0
- package/package.json +1 -1
|
@@ -48,6 +48,8 @@ export interface InviteOptions {
|
|
|
48
48
|
companyUid: string;
|
|
49
49
|
callerUid: string;
|
|
50
50
|
token: string;
|
|
51
|
+
/** Reports only whether the request returned a 2xx response. */
|
|
52
|
+
onRequestResult?: (ok: boolean) => void;
|
|
51
53
|
}
|
|
52
54
|
/**
|
|
53
55
|
* Outcome of `hq members invite`. Two shapes depending on server schema:
|
|
@@ -96,6 +98,8 @@ export interface ResendInviteOptions {
|
|
|
96
98
|
companyUid: string;
|
|
97
99
|
callerUid: string;
|
|
98
100
|
token: string;
|
|
101
|
+
/** Reports only whether the request returned a 2xx response. */
|
|
102
|
+
onRequestResult?: (ok: boolean) => void;
|
|
99
103
|
}
|
|
100
104
|
/**
|
|
101
105
|
* Outcome of `hq members invite --resend`. No new membership row was
|
package/dist/commands/members.js
CHANGED
|
@@ -3,6 +3,7 @@ import { resolveVaultCredential } from "../utils/resolve-vault-credential.js";
|
|
|
3
3
|
import { gateApiKeyCapabilities } from "../utils/api-key-command-gate.js";
|
|
4
4
|
import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
|
|
5
5
|
import { isPlanGateError } from "../utils/plan-gate-error.js";
|
|
6
|
+
import { queueCliFeatureUsed } from "../utils/cli-telemetry.js";
|
|
6
7
|
const EMAIL_PATTERN = /^[^\s]+@[^\s]+$/;
|
|
7
8
|
const PERSON_UID_PATTERN = /^prs_[A-Za-z0-9_-]+$/;
|
|
8
9
|
const AGENT_UID_PATTERN = /^agt_[A-Za-z0-9_-]+$/i;
|
|
@@ -107,6 +108,7 @@ export async function inviteMember(options) {
|
|
|
107
108
|
body,
|
|
108
109
|
});
|
|
109
110
|
if (!res.ok) {
|
|
111
|
+
options.onRequestResult?.(false);
|
|
110
112
|
const err = (await res.json().catch(() => ({})));
|
|
111
113
|
throw new InviteHttpError(res.status, err.message ?? err.error ?? res.statusText, err.code);
|
|
112
114
|
}
|
|
@@ -116,6 +118,7 @@ export async function inviteMember(options) {
|
|
|
116
118
|
throw new Error(`Invite endpoint returned 2xx with no membership row (response keys: ${keys}). ` +
|
|
117
119
|
"This is a server-side regression — file an issue.");
|
|
118
120
|
}
|
|
121
|
+
options.onRequestResult?.(true);
|
|
119
122
|
// Two server schemas in the wild:
|
|
120
123
|
// - Legacy (schemaVersion ≤ 1): response carries a random `inviteToken`
|
|
121
124
|
// the invitee redeems via `hq://accept/{token}`.
|
|
@@ -177,6 +180,7 @@ export async function resendInvite(options) {
|
|
|
177
180
|
},
|
|
178
181
|
});
|
|
179
182
|
if (!res.ok) {
|
|
183
|
+
options.onRequestResult?.(false);
|
|
180
184
|
const err = (await res.json().catch(() => ({})));
|
|
181
185
|
throw new InviteHttpError(res.status, err.message ?? err.error ?? res.statusText, err.code);
|
|
182
186
|
}
|
|
@@ -184,6 +188,7 @@ export async function resendInvite(options) {
|
|
|
184
188
|
if (data.resent !== true) {
|
|
185
189
|
throw new Error("Resend endpoint returned 2xx without `resent: true` — the connected hq-pro likely doesn't support `resend: true` yet. Upgrade hq-pro or re-invite without --resend.");
|
|
186
190
|
}
|
|
191
|
+
options.onRequestResult?.(true);
|
|
187
192
|
return {
|
|
188
193
|
resent: true,
|
|
189
194
|
emailSent: data.emailSent === true,
|
|
@@ -436,6 +441,7 @@ export function registerMembersCommand(program) {
|
|
|
436
441
|
let token;
|
|
437
442
|
let companyUid;
|
|
438
443
|
let companySlug;
|
|
444
|
+
let outcome = "failure";
|
|
439
445
|
try {
|
|
440
446
|
token = (await resolveVaultCredential()).token;
|
|
441
447
|
companySlug = members.opts().company;
|
|
@@ -448,6 +454,9 @@ export function registerMembersCommand(program) {
|
|
|
448
454
|
companyUid,
|
|
449
455
|
callerUid,
|
|
450
456
|
token,
|
|
457
|
+
onRequestResult: (ok) => {
|
|
458
|
+
outcome = ok ? "success" : "failure";
|
|
459
|
+
},
|
|
451
460
|
});
|
|
452
461
|
console.log(chalk.green(`Re-fired invite email for ${target.toLowerCase()} (existing pending row left untouched)`));
|
|
453
462
|
console.log();
|
|
@@ -472,6 +481,9 @@ export function registerMembersCommand(program) {
|
|
|
472
481
|
companyUid,
|
|
473
482
|
callerUid,
|
|
474
483
|
token,
|
|
484
|
+
onRequestResult: (ok) => {
|
|
485
|
+
outcome = ok ? "success" : "failure";
|
|
486
|
+
},
|
|
475
487
|
});
|
|
476
488
|
const agentTarget = detectTarget(target)?.isAgent === true;
|
|
477
489
|
console.log(chalk.green(`Invited ${target} as ${result.membership.role} (status: ${result.membership.status})`));
|
|
@@ -612,10 +624,19 @@ export function registerMembersCommand(program) {
|
|
|
612
624
|
}
|
|
613
625
|
if (err instanceof InviteHttpError) {
|
|
614
626
|
console.error(chalk.red(formatInviteHttpError(err.status, err.message, err.code)));
|
|
615
|
-
process.
|
|
627
|
+
process.exitCode = 1;
|
|
628
|
+
return;
|
|
616
629
|
}
|
|
617
630
|
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
618
|
-
process.
|
|
631
|
+
process.exitCode = 1;
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
finally {
|
|
635
|
+
queueCliFeatureUsed({
|
|
636
|
+
kind: "members_invite",
|
|
637
|
+
outcome,
|
|
638
|
+
...(companyUid ? { companyUid } : {}),
|
|
639
|
+
});
|
|
619
640
|
}
|
|
620
641
|
});
|
|
621
642
|
members
|
package/dist/lanes-drain-hook.js
CHANGED
|
@@ -17,7 +17,7 @@ function parseHookInput(text) {
|
|
|
17
17
|
throw new Error("invalid_hook_input_shape: expected a JSON object");
|
|
18
18
|
}
|
|
19
19
|
const value = parsed;
|
|
20
|
-
for (const key of ["transcript_path", "session_id", "hook_event_name"]) {
|
|
20
|
+
for (const key of ["transcript_path", "session_id", "hook_event_name", "hookEventName"]) {
|
|
21
21
|
if (value[key] !== undefined && value[key] !== null && typeof value[key] !== "string") {
|
|
22
22
|
throw new Error(`invalid_hook_input_field: ${key} must be a string`);
|
|
23
23
|
}
|
|
@@ -25,7 +25,11 @@ function parseHookInput(text) {
|
|
|
25
25
|
return {
|
|
26
26
|
...(typeof value.transcript_path === "string" ? { transcript_path: value.transcript_path } : {}),
|
|
27
27
|
...(typeof value.session_id === "string" ? { session_id: value.session_id } : {}),
|
|
28
|
-
...(typeof value.hook_event_name === "string"
|
|
28
|
+
...(typeof value.hook_event_name === "string"
|
|
29
|
+
? { hook_event_name: value.hook_event_name }
|
|
30
|
+
: typeof value.hookEventName === "string"
|
|
31
|
+
? { hookEventName: value.hookEventName }
|
|
32
|
+
: {}),
|
|
29
33
|
};
|
|
30
34
|
}
|
|
31
35
|
function laneIdIsSafe(laneId) {
|
|
@@ -59,6 +59,8 @@ export interface SyncManifestUploadStatus {
|
|
|
59
59
|
snapshotId: string | null;
|
|
60
60
|
sequence: number;
|
|
61
61
|
baseUsable: boolean;
|
|
62
|
+
/** Local company slug for company-scoped remediation; absent for personal. */
|
|
63
|
+
companySlug?: string;
|
|
62
64
|
}
|
|
63
65
|
/** Injectable dependencies — defaults are the real collectors. */
|
|
64
66
|
export interface SyncHealthDeps {
|
|
@@ -32,6 +32,7 @@ import * as path from "node:path";
|
|
|
32
32
|
// ../../hq-cloud-manifest.js.
|
|
33
33
|
import * as hqCloud from "@indigoai-us/hq-cloud";
|
|
34
34
|
import { loadManifestExports, MANIFEST_UNAVAILABLE_REASON, } from "../../hq-cloud-manifest.js";
|
|
35
|
+
import { readCompaniesManifestMap } from "../../work-context/repo-remote.js";
|
|
35
36
|
import { CLI_VERSION } from "../../../cli-version.js";
|
|
36
37
|
import { readSyncVersion } from "../../../utils/feedback-versions.js";
|
|
37
38
|
import { readHqVersion } from "../../../utils/pack-contributions.js";
|
|
@@ -52,9 +53,9 @@ function withDefaults(deps, hqRoot) {
|
|
|
52
53
|
return {
|
|
53
54
|
...deps,
|
|
54
55
|
runtime: deps.runtime ?? (() => []),
|
|
55
|
-
manifestStatuses: deps.manifestStatuses ?? defaultManifestStatuses,
|
|
56
|
+
manifestStatuses: deps.manifestStatuses ?? ((journals) => defaultManifestStatuses(hqRoot, journals)),
|
|
56
57
|
manifestAvailable: deps.manifestAvailable ?? (() => loadManifestExports() !== null),
|
|
57
|
-
unresolvedScopes: deps.unresolvedScopes ?? defaultUnresolvedScopes,
|
|
58
|
+
unresolvedScopes: deps.unresolvedScopes ?? ((journals) => defaultUnresolvedScopes(hqRoot, journals)),
|
|
58
59
|
localCompanyExists: deps.localCompanyExists ??
|
|
59
60
|
((slug) => defaultLocalCompanyExists(hqRoot, slug)),
|
|
60
61
|
};
|
|
@@ -78,15 +79,35 @@ function defaultLocalCompanyExists(hqRoot, slug) {
|
|
|
78
79
|
return false;
|
|
79
80
|
}
|
|
80
81
|
}
|
|
81
|
-
/**
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
82
|
+
/** Resolve locally-known company journal slugs to the UID used by snapshots. */
|
|
83
|
+
function resolvedCompanyScopes(hqRoot, journals) {
|
|
84
|
+
const companies = readCompaniesManifestMap(hqRoot);
|
|
85
|
+
const seen = new Set();
|
|
86
|
+
const scopes = [];
|
|
87
|
+
for (const { slug } of journals) {
|
|
88
|
+
if (NON_COMPANY_JOURNAL_SLUGS.has(slug) ||
|
|
89
|
+
!isSafeCompanySlug(slug) ||
|
|
90
|
+
!defaultLocalCompanyExists(hqRoot, slug) ||
|
|
91
|
+
seen.has(slug)) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
seen.add(slug);
|
|
95
|
+
const uid = companies?.[slug]?.cloud_uid;
|
|
96
|
+
if (typeof uid === "string" && /^cmp_[A-Za-z0-9]{3,128}$/.test(uid.trim())) {
|
|
97
|
+
scopes.push({ kind: "company", companyUid: uid.trim(), slug });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return scopes;
|
|
101
|
+
}
|
|
102
|
+
function defaultUnresolvedScopes(hqRoot, journals) {
|
|
103
|
+
const resolved = new Set(resolvedCompanyScopes(hqRoot, journals).map((scope) => scope.slug));
|
|
104
|
+
return [
|
|
105
|
+
...new Set(journals
|
|
106
|
+
.filter((entry) => !NON_COMPANY_JOURNAL_SLUGS.has(entry.slug) &&
|
|
107
|
+
defaultLocalCompanyExists(hqRoot, entry.slug) &&
|
|
108
|
+
!resolved.has(entry.slug))
|
|
109
|
+
.map((entry) => entry.slug)),
|
|
110
|
+
];
|
|
90
111
|
}
|
|
91
112
|
function defaultVersions(hqRoot) {
|
|
92
113
|
return {
|
|
@@ -119,21 +140,15 @@ const DEFAULT_DEPS = {
|
|
|
119
140
|
// than no check.
|
|
120
141
|
journals: () => hqCloud.listJournals(),
|
|
121
142
|
now: () => new Date(),
|
|
122
|
-
manifestStatuses: defaultManifestStatuses,
|
|
123
143
|
runtime: defaultSyncRuntimeResults,
|
|
124
144
|
};
|
|
125
145
|
/**
|
|
126
146
|
* Map the machine's journal shards onto the manifest scopes hq-cloud keys its
|
|
127
|
-
* snapshot store by, then read each one's last upload.
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
* company. Note the snapshot store keys company scopes by `companyUid`, which
|
|
131
|
-
* the journal listing does not carry — so a company whose uid we cannot name
|
|
132
|
-
* is simply not reported here rather than reported wrongly. That is the honest
|
|
133
|
-
* outcome: this check exists to tell an operator when a manifest STOPPED being
|
|
134
|
-
* uploaded, and inventing a scope key would make it lie in both directions.
|
|
147
|
+
* snapshot store by, then read each one's last upload. Company journal rows
|
|
148
|
+
* carry only slugs, so resolve their UIDs from the local companies manifest;
|
|
149
|
+
* a missing or malformed UID stays explicitly unresolved below.
|
|
135
150
|
*/
|
|
136
|
-
function defaultManifestStatuses(journals) {
|
|
151
|
+
function defaultManifestStatuses(hqRoot, journals) {
|
|
137
152
|
// Both the legacy `personal` shard and the current personal-vault
|
|
138
153
|
// pseudo-slug name the SAME personal tree; a machine mid-migration has both
|
|
139
154
|
// on disk, so collapse them to a single personal scope rather than asking
|
|
@@ -141,6 +156,7 @@ function defaultManifestStatuses(journals) {
|
|
|
141
156
|
const scopes = journals.some((entry) => NON_COMPANY_JOURNAL_SLUGS.has(entry.slug))
|
|
142
157
|
? [{ kind: "personal" }]
|
|
143
158
|
: [];
|
|
159
|
+
scopes.push(...resolvedCompanyScopes(hqRoot, journals));
|
|
144
160
|
if (scopes.length === 0)
|
|
145
161
|
return [];
|
|
146
162
|
// Absent on an older hq-cloud; the NA row is emitted by `manifestResults`
|
|
@@ -148,7 +164,15 @@ function defaultManifestStatuses(journals) {
|
|
|
148
164
|
const exports = loadManifestExports();
|
|
149
165
|
if (!exports)
|
|
150
166
|
return [];
|
|
151
|
-
|
|
167
|
+
const companySlugsByUid = new Map(scopes
|
|
168
|
+
.filter((scope) => scope.kind === "company")
|
|
169
|
+
.map((scope) => [scope.companyUid, scope.slug]));
|
|
170
|
+
const statuses = exports.readManifestUploadStatus(exports.getStateDir(), scopes);
|
|
171
|
+
return statuses.map((status) => {
|
|
172
|
+
const companyUid = /^company:(cmp_[A-Za-z0-9]{3,128})$/.exec(status.scopeKey)?.[1];
|
|
173
|
+
const companySlug = companyUid ? companySlugsByUid.get(companyUid) : undefined;
|
|
174
|
+
return companySlug ? { ...status, companySlug } : status;
|
|
175
|
+
});
|
|
152
176
|
}
|
|
153
177
|
/** The legacy journal slug that named the personal (non-company) tree. */
|
|
154
178
|
const PERSONAL_SCOPE_SLUG = "personal";
|
|
@@ -526,7 +550,7 @@ function manifestResults(deps, journals) {
|
|
|
526
550
|
checkId,
|
|
527
551
|
reasonCode: "stale-threshold",
|
|
528
552
|
message: `Scope '${entry.scopeKey}' last uploaded a sync manifest ${days} day${days === 1 ? "" : "s"} ago (${entry.lastUploadAt}) — the daily audit pass is not running.`,
|
|
529
|
-
remediation:
|
|
553
|
+
remediation: `Run \`hq sync manifest${entry.companySlug ? ` --scope ${entry.companySlug}` : ""}\` and check HQ_SYNC_MANIFEST_DISABLED and the sync runner.`,
|
|
530
554
|
};
|
|
531
555
|
}
|
|
532
556
|
return {
|
|
@@ -144,6 +144,8 @@ export type LaunchTrigger = {
|
|
|
144
144
|
kind: "resume";
|
|
145
145
|
/** Lane state at the moment of the command; recorded as `last_resume.resumed_from`. */
|
|
146
146
|
from: ResumeCommandState | "failed" | "awaiting_input";
|
|
147
|
+
/** Loop phases may deliberately start a fresh session at a story boundary. */
|
|
148
|
+
fresh?: true;
|
|
147
149
|
allowAwaitingInput?: true;
|
|
148
150
|
fromFailed?: true;
|
|
149
151
|
/** Senior that authorized a failed-lane resume, rechecked at the claim CAS. */
|
|
@@ -164,6 +166,7 @@ export type LaunchPlan = {
|
|
|
164
166
|
export declare function resumeCommandRefusal(lane: DeliveryLane, options?: {
|
|
165
167
|
allowFailed?: boolean;
|
|
166
168
|
allowAwaitingInput?: boolean;
|
|
169
|
+
allowFresh?: boolean;
|
|
167
170
|
}): {
|
|
168
171
|
error: "lane_not_resumable" | "resume_unsupported" | "no_provider_session";
|
|
169
172
|
message: string;
|
|
@@ -371,6 +371,8 @@ export function resumeCommandRefusal(lane, options = {}) {
|
|
|
371
371
|
: "a lane in this state is not resumed."}`,
|
|
372
372
|
};
|
|
373
373
|
}
|
|
374
|
+
if (options.allowFresh === true)
|
|
375
|
+
return null;
|
|
374
376
|
if (!providerSupportsSessionResume(lane.provider)) {
|
|
375
377
|
return {
|
|
376
378
|
error: "resume_unsupported",
|
|
@@ -411,7 +413,10 @@ export function planLaneLaunch(lane, trigger) {
|
|
|
411
413
|
const refusal = resumeCommandRefusal(lane, {
|
|
412
414
|
allowFailed: trigger.fromFailed === true || isFailedQuotaKeepAliveLane(lane),
|
|
413
415
|
allowAwaitingInput: trigger.allowAwaitingInput === true,
|
|
416
|
+
allowFresh: trigger.fresh === true,
|
|
414
417
|
});
|
|
418
|
+
if (trigger.fresh === true && refusal === null)
|
|
419
|
+
return { mode: "fresh" };
|
|
415
420
|
if (refusal !== null || !hasRecordedSessionId(lane)) {
|
|
416
421
|
throw new LanesError(refusal?.error ?? "no_provider_session", refusal?.message ?? `Lane is ${lane.state} and has no recorded provider session`, { state: lane.state, provider: lane.provider });
|
|
417
422
|
}
|
|
@@ -53,12 +53,14 @@ export interface DrainDeps {
|
|
|
53
53
|
}
|
|
54
54
|
export interface LaneDrainHookInput {
|
|
55
55
|
transcript_path?: string | null;
|
|
56
|
-
session_id?: string;
|
|
57
56
|
hook_event_name?: string;
|
|
57
|
+
hookEventName?: string;
|
|
58
|
+
session_id?: string;
|
|
58
59
|
}
|
|
59
60
|
export interface RunDrainHookDeps {
|
|
60
61
|
readLane?: typeof tryReadLane;
|
|
61
62
|
probeCompactionsSince?: typeof import("./compaction.js").probeProviderCompactionsSince;
|
|
63
|
+
readLifecycleDeliveryFlag?: () => Promise<boolean>;
|
|
62
64
|
}
|
|
63
65
|
export declare function mintClaimId(): string;
|
|
64
66
|
export declare function assertSafeLaneId(laneId: string): void;
|
|
@@ -88,8 +90,11 @@ export interface ToolActivityRecord {
|
|
|
88
90
|
export declare function recordToolActivity(hqRoot: string, laneId: string, at?: string): ToolActivityRecord;
|
|
89
91
|
export declare function readToolActivity(hqRoot: string, laneId: string): ToolActivityRecord | null;
|
|
90
92
|
export declare function appendDropboxMessage(hqRoot: string, laneId: string, message: DropboxMessage): DropboxMessage;
|
|
93
|
+
export declare function appendDropboxMessageOnce(hqRoot: string, laneId: string, message: DropboxMessage): DropboxMessage;
|
|
91
94
|
/** Append using the same on-disk layout for lane and monitor-session inboxes. */
|
|
92
95
|
export declare function appendDropboxMessageAtPath(box: string, message: DropboxMessage): DropboxMessage;
|
|
96
|
+
/** Append an idempotent message once, including retries after its inbox was archived. */
|
|
97
|
+
export declare function appendDropboxMessageOnceAtPath(box: string, message: DropboxMessage): DropboxMessage;
|
|
93
98
|
/**
|
|
94
99
|
* Drain this lane's inbox exactly once. Concurrent callers: one rename wins;
|
|
95
100
|
* the loser sees ENOENT and returns whatever it separately claimed (usually
|
|
@@ -102,7 +107,7 @@ export declare function formatOperatorInstruction(messages: DropboxMessage[]): s
|
|
|
102
107
|
export declare function formatMonitorEvent(message: DropboxMessage): string;
|
|
103
108
|
/** Keep event order while framing monitor output separately from operator messages. */
|
|
104
109
|
export declare function formatDropboxMessages(messages: DropboxMessage[]): string;
|
|
105
|
-
export declare function hookAdditionalContextJson(messages: DropboxMessage[], hookEventName?: "PreToolUse" | "UserPromptSubmit", leadingNote?: string): string;
|
|
110
|
+
export declare function hookAdditionalContextJson(messages: DropboxMessage[], hookEventName?: "PreToolUse" | "PostToolUse" | "UserPromptSubmit", leadingNote?: string): string;
|
|
106
111
|
export interface DrainHookResult {
|
|
107
112
|
exitCode: number;
|
|
108
113
|
stdout: string;
|
|
@@ -7,13 +7,15 @@
|
|
|
7
7
|
* A read-then-truncate is not this mechanism and loses or duplicates under
|
|
8
8
|
* overlap.
|
|
9
9
|
*/
|
|
10
|
-
import { randomBytes } from "node:crypto";
|
|
10
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
11
11
|
import * as fs from "node:fs";
|
|
12
12
|
import * as path from "node:path";
|
|
13
13
|
import { LanesError } from "./errors.js";
|
|
14
14
|
import { MESSAGE_COMPACTION_REPLAY_NOTE, MESSAGE_REPLAY_STATE_NAME, readMessageReplayState, updateMessageReplayTranscript, writeMessageReplayState, } from "./message-replay.js";
|
|
15
15
|
import { LANES_RUNS_REL, readProcStarttime } from "./spawn.js";
|
|
16
16
|
import { tryReadLane } from "./store.js";
|
|
17
|
+
import { readLanesRegistryFlagFailOpen } from "./registry-flag.js";
|
|
18
|
+
import { LANE_DROPBOX_LIFECYCLE_DELIVERY_FLAG } from "./flag-keys.js";
|
|
17
19
|
export const DROPBOX_DIR = "dropbox";
|
|
18
20
|
export const INBOX_NAME = "inbox.jsonl";
|
|
19
21
|
export const DRAINING_DIR = "draining";
|
|
@@ -177,6 +179,9 @@ export function readToolActivity(hqRoot, laneId) {
|
|
|
177
179
|
export function appendDropboxMessage(hqRoot, laneId, message) {
|
|
178
180
|
return appendDropboxMessageAtPath(dropboxDir(hqRoot, laneId), message);
|
|
179
181
|
}
|
|
182
|
+
export function appendDropboxMessageOnce(hqRoot, laneId, message) {
|
|
183
|
+
return appendDropboxMessageOnceAtPath(dropboxDir(hqRoot, laneId), message);
|
|
184
|
+
}
|
|
180
185
|
/** Append using the same on-disk layout for lane and monitor-session inboxes. */
|
|
181
186
|
export function appendDropboxMessageAtPath(box, message) {
|
|
182
187
|
const inbox = path.join(box, INBOX_NAME);
|
|
@@ -184,6 +189,105 @@ export function appendDropboxMessageAtPath(box, message) {
|
|
|
184
189
|
fs.appendFileSync(inbox, `${JSON.stringify(message)}\n`, "utf8");
|
|
185
190
|
return message;
|
|
186
191
|
}
|
|
192
|
+
/** Append an idempotent message once, including retries after its inbox was archived. */
|
|
193
|
+
export function appendDropboxMessageOnceAtPath(box, message) {
|
|
194
|
+
const messageId = message.message_id?.trim();
|
|
195
|
+
if (!messageId) {
|
|
196
|
+
throw new LanesError("dropbox_message_id_required", "Idempotent dropbox delivery requires a message id");
|
|
197
|
+
}
|
|
198
|
+
fs.mkdirSync(box, { recursive: true });
|
|
199
|
+
const digest = createHash("sha256").update(messageId).digest("hex");
|
|
200
|
+
const lockDir = path.join(box, `.message-${digest}.lock`);
|
|
201
|
+
const lockTimeoutAt = Date.now() + 5_000;
|
|
202
|
+
while (true) {
|
|
203
|
+
try {
|
|
204
|
+
fs.mkdirSync(lockDir);
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
if (error.code !== "EEXIST")
|
|
209
|
+
throw error;
|
|
210
|
+
try {
|
|
211
|
+
if (Date.now() - fs.statSync(lockDir).mtimeMs > 30_000) {
|
|
212
|
+
fs.rmSync(lockDir, { recursive: true, force: true });
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
catch (statError) {
|
|
217
|
+
if (statError.code === "ENOENT")
|
|
218
|
+
continue;
|
|
219
|
+
throw statError;
|
|
220
|
+
}
|
|
221
|
+
if (Date.now() >= lockTimeoutAt) {
|
|
222
|
+
throw new LanesError("dropbox_message_claim_busy", `Timed out claiming message ${messageId}`, { message_id: messageId });
|
|
223
|
+
}
|
|
224
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
try {
|
|
228
|
+
const inbox = path.join(box, INBOX_NAME);
|
|
229
|
+
const files = [inbox, ...messageHistoryFiles(box)];
|
|
230
|
+
for (const file of files) {
|
|
231
|
+
let content;
|
|
232
|
+
try {
|
|
233
|
+
content = fs.readFileSync(file, "utf8");
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
if (error.code === "ENOENT")
|
|
237
|
+
continue;
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
for (const line of content.split(/\r?\n/)) {
|
|
241
|
+
if (!line.trim())
|
|
242
|
+
continue;
|
|
243
|
+
let parsed;
|
|
244
|
+
try {
|
|
245
|
+
parsed = JSON.parse(line);
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
throw new LanesError("dropbox_message_history_invalid", `Cannot verify message ${messageId} against ${file}`, {
|
|
249
|
+
message_id: messageId,
|
|
250
|
+
path: file,
|
|
251
|
+
cause: error instanceof Error ? error.message : String(error),
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) &&
|
|
255
|
+
parsed.message_id === messageId)
|
|
256
|
+
return message;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
fs.appendFileSync(inbox, `${JSON.stringify(message)}\n`, "utf8");
|
|
260
|
+
return message;
|
|
261
|
+
}
|
|
262
|
+
finally {
|
|
263
|
+
fs.rmSync(lockDir, { recursive: true, force: true });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
function messageHistoryFiles(box) {
|
|
267
|
+
const files = [];
|
|
268
|
+
for (const directory of [DRAINING_DIR, INFLIGHT_DIR, DELIVERED_DIR]) {
|
|
269
|
+
const root = path.join(box, directory);
|
|
270
|
+
const visit = (current) => {
|
|
271
|
+
let entries;
|
|
272
|
+
try {
|
|
273
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
if (error.code === "ENOENT")
|
|
277
|
+
return;
|
|
278
|
+
throw error;
|
|
279
|
+
}
|
|
280
|
+
for (const entry of entries) {
|
|
281
|
+
if (entry.isDirectory())
|
|
282
|
+
visit(path.join(current, entry.name));
|
|
283
|
+
else if (entry.isFile())
|
|
284
|
+
files.push(path.join(current, entry.name));
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
visit(root);
|
|
288
|
+
}
|
|
289
|
+
return files;
|
|
290
|
+
}
|
|
187
291
|
function listNames(dir, deps) {
|
|
188
292
|
try {
|
|
189
293
|
return deps.readdirSync(dir).filter((name) => !name.startsWith("."));
|
|
@@ -426,6 +530,9 @@ export function hookAdditionalContextJson(messages, hookEventName = "PreToolUse"
|
|
|
426
530
|
},
|
|
427
531
|
})}\n`;
|
|
428
532
|
}
|
|
533
|
+
function hookStopBlockJson(reason) {
|
|
534
|
+
return `${JSON.stringify({ decision: "block", reason })}\n`;
|
|
535
|
+
}
|
|
429
536
|
function replayStatePath(hqRoot, laneId) {
|
|
430
537
|
return path.join(dropboxDir(hqRoot, laneId), MESSAGE_REPLAY_STATE_NAME);
|
|
431
538
|
}
|
|
@@ -465,14 +572,47 @@ export async function runDrainHook(env, input = {}, deps = {}) {
|
|
|
465
572
|
stderr: "hq-lanes-drain: HQ_ROOT is unset while HQ_LANE_ID is set\n",
|
|
466
573
|
};
|
|
467
574
|
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
try {
|
|
472
|
-
recordToolActivity(hqRoot, laneId);
|
|
575
|
+
const hookEvent = input.hook_event_name ?? input.hookEventName ?? "PreToolUse";
|
|
576
|
+
if (hookEvent !== "PreToolUse" && hookEvent !== "PostToolUse" && hookEvent !== "Stop") {
|
|
577
|
+
return { exitCode: 0, stdout: "", stderr: "" };
|
|
473
578
|
}
|
|
474
|
-
|
|
475
|
-
|
|
579
|
+
// Grok invokes lifecycle hooks, but their output cannot reach the model.
|
|
580
|
+
// Preserve queued messages for its next deliverable PreToolUse event.
|
|
581
|
+
if (env.HQ_LANE_PROVIDER === "grok" && hookEvent !== "PreToolUse") {
|
|
582
|
+
return { exitCode: 0, stdout: "", stderr: "" };
|
|
583
|
+
}
|
|
584
|
+
const hasQueuedMessages = dropboxInboxHasMessages(hqRoot, laneId);
|
|
585
|
+
if (hookEvent === "Stop" && !hasQueuedMessages) {
|
|
586
|
+
return { exitCode: 0, stdout: "", stderr: "" };
|
|
587
|
+
}
|
|
588
|
+
if ((hookEvent === "PostToolUse" || hookEvent === "Stop") && hasQueuedMessages) {
|
|
589
|
+
const lane = (deps.readLane ?? tryReadLane)(hqRoot, laneId);
|
|
590
|
+
const company = lane?.mesh_company ?? lane?.company ?? undefined;
|
|
591
|
+
let enabled = true;
|
|
592
|
+
try {
|
|
593
|
+
enabled = deps.readLifecycleDeliveryFlag
|
|
594
|
+
? await deps.readLifecycleDeliveryFlag()
|
|
595
|
+
: await readLanesRegistryFlagFailOpen(LANE_DROPBOX_LIFECYCLE_DELIVERY_FLAG, {
|
|
596
|
+
...(company ? { company } : {}),
|
|
597
|
+
env,
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
catch (err) {
|
|
601
|
+
process.stderr.write(`hq-lanes-drain: lifecycle delivery flag read failed open: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
602
|
+
}
|
|
603
|
+
if (!enabled)
|
|
604
|
+
return { exitCode: 0, stdout: "", stderr: "" };
|
|
605
|
+
}
|
|
606
|
+
// PreToolUse remains the tool-activity liveness touch used by watcher.ts.
|
|
607
|
+
// PostToolUse and Stop add delivery opportunities without extra liveness writes.
|
|
608
|
+
let stderr = "";
|
|
609
|
+
if (hookEvent === "PreToolUse") {
|
|
610
|
+
try {
|
|
611
|
+
recordToolActivity(hqRoot, laneId);
|
|
612
|
+
}
|
|
613
|
+
catch (err) {
|
|
614
|
+
stderr += `hq-lanes-drain: failed to record tool activity: ${err instanceof Error ? err.message : String(err)}\n`;
|
|
615
|
+
}
|
|
476
616
|
}
|
|
477
617
|
try {
|
|
478
618
|
const turnId = env.HQ_LANE_TURN_ID ?? "";
|
|
@@ -515,15 +655,22 @@ export async function runDrainHook(env, input = {}, deps = {}) {
|
|
|
515
655
|
}
|
|
516
656
|
}
|
|
517
657
|
if (replayNote && replayed && replayed.length > 0) {
|
|
658
|
+
const formatted = `${replayNote}\n\n${formatDropboxMessages(replayed)}`;
|
|
518
659
|
return {
|
|
519
660
|
exitCode: 0,
|
|
520
|
-
stdout:
|
|
661
|
+
stdout: hookEvent === "Stop"
|
|
662
|
+
? hookStopBlockJson(formatted)
|
|
663
|
+
: hookAdditionalContextJson(replayed, hookEvent, replayNote),
|
|
521
664
|
stderr,
|
|
522
665
|
};
|
|
523
666
|
}
|
|
524
667
|
if (messages.length === 0)
|
|
525
668
|
return { exitCode: 0, stdout: "", stderr };
|
|
526
|
-
|
|
669
|
+
const formatted = formatDropboxMessages(messages);
|
|
670
|
+
if (hookEvent === "Stop") {
|
|
671
|
+
return { exitCode: 0, stdout: hookStopBlockJson(formatted), stderr };
|
|
672
|
+
}
|
|
673
|
+
return { exitCode: 0, stdout: hookAdditionalContextJson(messages, hookEvent), stderr };
|
|
527
674
|
}
|
|
528
675
|
catch (err) {
|
|
529
676
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -16,6 +16,8 @@ export declare const LANE_HIRE_GRANT_FINISHED_LANES_FLAG = "lanes.hire-grant-cou
|
|
|
16
16
|
export declare const LANE_LIST_SKIP_REMOTE_REFRESH_FLAG = "lanes.list-skip-remote-refresh";
|
|
17
17
|
/** Enables missed-deadline lead nudges and second-miss operator escalation. */
|
|
18
18
|
export declare const LANE_AUTOPILOT_MISSED_DEADLINE_ESCALATION_FLAG = "lanes.autopilot-missed-deadline-escalation";
|
|
19
|
+
/** Enables durable six-hour escalation for lanes that remain blocked. */
|
|
20
|
+
export declare const LANE_BLOCKED_ESCALATION_FLAG = "lanes.blocked-escalation-6h";
|
|
19
21
|
/** Enables company-scoped lookup of a hostless lane ULID across Outpost indexes. */
|
|
20
22
|
export declare const LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG = "lanes.message-route-owner-host";
|
|
21
23
|
/** Enables stale manager-status KPI reporting and one-time lead nudges. */
|
|
@@ -30,6 +32,16 @@ export declare const LEAD_AUTOPILOT_OUTPOST_NATIVE_LEADS_FLAG = "lanes.autopilot
|
|
|
30
32
|
export declare const LANE_ORPHANED_BARE_ID_DROPBOX_REPAIR_FLAG = "lanes.orphaned-bare-id-dropbox-repair";
|
|
31
33
|
/** Allows a Claude lane to leave an exhausted profile account pin for an eligible account. */
|
|
32
34
|
export declare const LANE_PROFILE_PIN_EXHAUSTED_FALLBACK_FLAG = "lanes.profile-pin-exhausted-fallback";
|
|
35
|
+
/** Kill switch for PostToolUse and Stop drop-box delivery. Default on. */
|
|
36
|
+
export declare const LANE_DROPBOX_LIFECYCLE_DELIVERY_FLAG = "lanes.dropbox-lifecycle-delivery";
|
|
37
|
+
/** Kill switch for bounded local status fields in lane list/show JSON. Default on. */
|
|
38
|
+
export declare const LANE_STATUS_ROW_FIELDS_FLAG = "lanes.status-row-fields";
|
|
33
39
|
/** Enables conduct-mode SessionStart bootstrapping (defaults on; operator kill switch). */
|
|
34
40
|
export declare const LANE_CONDUCT_SESSION_START_FLAG = "lanes.conduct-session-start";
|
|
41
|
+
/** Kill switch for stopping local orphan lane process trees; off means report-only. */
|
|
42
|
+
export declare const LANE_REAP_STOP_FLAG = "lanes.reap-stop";
|
|
43
|
+
/** Enables same-senior worker lane continuity on create; keep ON as the kill switch default. */
|
|
44
|
+
export declare const LANE_WORKER_CONTINUITY_FLAG = "lanes.worker-continuity";
|
|
45
|
+
/** Kill switch for conduct loop lanes; absence or read failure keeps it enabled. */
|
|
46
|
+
export declare const LANE_CONDUCT_LOOP_FLAG = "lanes.conduct-loop";
|
|
35
47
|
//# sourceMappingURL=flag-keys.d.ts.map
|
|
@@ -16,6 +16,8 @@ export const LANE_HIRE_GRANT_FINISHED_LANES_FLAG = "lanes.hire-grant-count-live-
|
|
|
16
16
|
export const LANE_LIST_SKIP_REMOTE_REFRESH_FLAG = "lanes.list-skip-remote-refresh";
|
|
17
17
|
/** Enables missed-deadline lead nudges and second-miss operator escalation. */
|
|
18
18
|
export const LANE_AUTOPILOT_MISSED_DEADLINE_ESCALATION_FLAG = "lanes.autopilot-missed-deadline-escalation";
|
|
19
|
+
/** Enables durable six-hour escalation for lanes that remain blocked. */
|
|
20
|
+
export const LANE_BLOCKED_ESCALATION_FLAG = "lanes.blocked-escalation-6h";
|
|
19
21
|
/** Enables company-scoped lookup of a hostless lane ULID across Outpost indexes. */
|
|
20
22
|
export const LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG = "lanes.message-route-owner-host";
|
|
21
23
|
/** Enables stale manager-status KPI reporting and one-time lead nudges. */
|
|
@@ -30,6 +32,16 @@ export const LEAD_AUTOPILOT_OUTPOST_NATIVE_LEADS_FLAG = "lanes.autopilot-outpost
|
|
|
30
32
|
export const LANE_ORPHANED_BARE_ID_DROPBOX_REPAIR_FLAG = "lanes.orphaned-bare-id-dropbox-repair";
|
|
31
33
|
/** Allows a Claude lane to leave an exhausted profile account pin for an eligible account. */
|
|
32
34
|
export const LANE_PROFILE_PIN_EXHAUSTED_FALLBACK_FLAG = "lanes.profile-pin-exhausted-fallback";
|
|
35
|
+
/** Kill switch for PostToolUse and Stop drop-box delivery. Default on. */
|
|
36
|
+
export const LANE_DROPBOX_LIFECYCLE_DELIVERY_FLAG = "lanes.dropbox-lifecycle-delivery";
|
|
37
|
+
/** Kill switch for bounded local status fields in lane list/show JSON. Default on. */
|
|
38
|
+
export const LANE_STATUS_ROW_FIELDS_FLAG = "lanes.status-row-fields";
|
|
33
39
|
/** Enables conduct-mode SessionStart bootstrapping (defaults on; operator kill switch). */
|
|
34
40
|
export const LANE_CONDUCT_SESSION_START_FLAG = "lanes.conduct-session-start";
|
|
41
|
+
/** Kill switch for stopping local orphan lane process trees; off means report-only. */
|
|
42
|
+
export const LANE_REAP_STOP_FLAG = "lanes.reap-stop";
|
|
43
|
+
/** Enables same-senior worker lane continuity on create; keep ON as the kill switch default. */
|
|
44
|
+
export const LANE_WORKER_CONTINUITY_FLAG = "lanes.worker-continuity";
|
|
45
|
+
/** Kill switch for conduct loop lanes; absence or read failure keeps it enabled. */
|
|
46
|
+
export const LANE_CONDUCT_LOOP_FLAG = "lanes.conduct-loop";
|
|
35
47
|
//# sourceMappingURL=flag-keys.js.map
|