@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
package/dist/lib/lanes/store.js
CHANGED
|
@@ -537,6 +537,7 @@ export function parseLaneRecord(raw, filePath) {
|
|
|
537
537
|
});
|
|
538
538
|
}
|
|
539
539
|
for (const field of [
|
|
540
|
+
"blocked_since",
|
|
540
541
|
"keep_alive_opted_in_at",
|
|
541
542
|
"keep_alive_last_resume_at",
|
|
542
543
|
"keep_alive_next_eligible_at",
|
|
@@ -547,6 +548,24 @@ export function parseLaneRecord(raw, filePath) {
|
|
|
547
548
|
if (raw[field] !== undefined)
|
|
548
549
|
requireTimestamp(raw[field], field, filePath);
|
|
549
550
|
}
|
|
551
|
+
if (raw.escalations !== undefined) {
|
|
552
|
+
if (!Array.isArray(raw.escalations)) {
|
|
553
|
+
throw new LaneSchemaError("escalations must be an array", { path: filePath, field: "escalations" });
|
|
554
|
+
}
|
|
555
|
+
for (const [index, value] of raw.escalations.entries()) {
|
|
556
|
+
const field = `escalations[${index}]`;
|
|
557
|
+
if (!isPlainObject(value) || Object.keys(value).some((key) => !["at", "to", "hours"].includes(key))) {
|
|
558
|
+
throw new LaneSchemaError(`${field} must contain only at, to, and hours`, { path: filePath, field });
|
|
559
|
+
}
|
|
560
|
+
requireTimestamp(value.at, `${field}.at`, filePath);
|
|
561
|
+
if (typeof value.to !== "string" || value.to.trim() === "") {
|
|
562
|
+
throw new LaneSchemaError(`${field}.to must be a non-empty string`, { path: filePath, field: `${field}.to` });
|
|
563
|
+
}
|
|
564
|
+
if (typeof value.hours !== "number" || !Number.isFinite(value.hours) || value.hours < 6) {
|
|
565
|
+
throw new LaneSchemaError(`${field}.hours must be a finite number of at least 6`, { path: filePath, field: `${field}.hours` });
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
550
569
|
requireString(raw.project_id, "project_id");
|
|
551
570
|
if (typeof raw.story_id !== "string" ||
|
|
552
571
|
(raw.story_id.trim() === "" && raw.no_story_reason === undefined)) {
|
|
@@ -1165,9 +1184,27 @@ export function parseQuestionRecord(raw, filePath) {
|
|
|
1165
1184
|
value: delivered_at,
|
|
1166
1185
|
});
|
|
1167
1186
|
}
|
|
1187
|
+
const seen_at = raw.seen_at === undefined ? null : raw.seen_at;
|
|
1188
|
+
if (seen_at !== null &&
|
|
1189
|
+
(typeof seen_at !== "string" || !Number.isFinite(Date.parse(seen_at)))) {
|
|
1190
|
+
throw new LaneSchemaError("seen_at must be a valid timestamp or null", {
|
|
1191
|
+
path: filePath,
|
|
1192
|
+
value: seen_at,
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
const seen_session_id = raw.seen_session_id === undefined ? null : raw.seen_session_id;
|
|
1196
|
+
if (seen_session_id !== null &&
|
|
1197
|
+
(typeof seen_session_id !== "string" || !seen_session_id.trim())) {
|
|
1198
|
+
throw new LaneSchemaError("seen_session_id must be a non-empty string or null", {
|
|
1199
|
+
path: filePath,
|
|
1200
|
+
value: seen_session_id,
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1168
1203
|
return {
|
|
1169
1204
|
...raw,
|
|
1170
1205
|
delivered_at,
|
|
1206
|
+
seen_at,
|
|
1207
|
+
seen_session_id,
|
|
1171
1208
|
...(raw.priority === null ? { priority: undefined } : {}),
|
|
1172
1209
|
};
|
|
1173
1210
|
}
|
|
@@ -1362,12 +1399,15 @@ export function mergeQuestionRecords(live, sidecars) {
|
|
|
1362
1399
|
const all = [live, ...sidecars];
|
|
1363
1400
|
const base = higherVersionRecord(live, sidecars);
|
|
1364
1401
|
const deliveredAt = all
|
|
1402
|
+
.filter((record) => record.status === "answered")
|
|
1365
1403
|
.map((record) => record.delivered_at)
|
|
1366
1404
|
.filter((value) => typeof value === "string" && Number.isFinite(Date.parse(value)))
|
|
1367
1405
|
.sort((a, b) => Date.parse(a) - Date.parse(b))[0] ?? null;
|
|
1368
1406
|
return {
|
|
1369
1407
|
...base,
|
|
1370
1408
|
delivered_at: deliveredAt,
|
|
1409
|
+
seen_at: base.seen_at ?? null,
|
|
1410
|
+
seen_session_id: base.seen_session_id ?? null,
|
|
1371
1411
|
answer_history: unionHistory(all.map(questionAnswerHistory), entryIdentity, questionAnswerHistory(base)),
|
|
1372
1412
|
routing_history: unionHistory(all.map((record) => record.routing_history), entryIdentity, base.routing_history),
|
|
1373
1413
|
};
|
|
@@ -1631,14 +1671,19 @@ export function updateLane(hqRoot, laneId, expectedVersion, writer, updater, dep
|
|
|
1631
1671
|
// `current` is the pre-update snapshot and stays untouched: the updater
|
|
1632
1672
|
// receives a deep clone, so mutating its argument in place cannot reach
|
|
1633
1673
|
// the record the ownership check compares against.
|
|
1634
|
-
const
|
|
1674
|
+
const proposed = updater(structuredClone(current));
|
|
1675
|
+
const updated = current.state === "blocked" && proposed.state !== "blocked"
|
|
1676
|
+
? { ...proposed, blocked_since: undefined }
|
|
1677
|
+
: proposed;
|
|
1635
1678
|
for (const key of Object.keys(updated)) {
|
|
1636
1679
|
if (key === "version" || key === "updated_at" || fieldUnchanged(current[key], updated[key]))
|
|
1637
1680
|
continue;
|
|
1638
1681
|
const allowedWriters = LANE_FIELD_WRITERS[key];
|
|
1682
|
+
const transitionClearedBlockedSince = key === "blocked_since" &&
|
|
1683
|
+
current.state === "blocked" && updated.state !== "blocked" && updated.blocked_since === undefined;
|
|
1639
1684
|
// Unknown forward-version fields are preserved, never assigned an
|
|
1640
1685
|
// ownership rule by a v1 reader.
|
|
1641
|
-
if (allowedWriters && !allowedWriters.includes(writer)) {
|
|
1686
|
+
if (allowedWriters && !allowedWriters.includes(writer) && !transitionClearedBlockedSince) {
|
|
1642
1687
|
throw new LanesError("lane_writer_unauthorized", `Writer ${writer} cannot write lane field ${key}`, { writer, field: key });
|
|
1643
1688
|
}
|
|
1644
1689
|
}
|
|
@@ -1909,6 +1954,8 @@ export function createQuestion(hqRoot, init, deps = defaultStoreDeps()) {
|
|
|
1909
1954
|
schema_version: init.schema_version ?? QUESTION_SCHEMA_VERSION,
|
|
1910
1955
|
question_id,
|
|
1911
1956
|
delivered_at: init.delivered_at ?? null,
|
|
1957
|
+
seen_at: init.seen_at ?? null,
|
|
1958
|
+
seen_session_id: init.seen_session_id ?? null,
|
|
1912
1959
|
version: 1,
|
|
1913
1960
|
routing_history: init.routing_history ?? [],
|
|
1914
1961
|
};
|
|
@@ -546,6 +546,10 @@ export interface LaneRecord {
|
|
|
546
546
|
senior: SeniorRef;
|
|
547
547
|
senior_history: SeniorHistoryEntry[];
|
|
548
548
|
state: LaneState;
|
|
549
|
+
/** Start of the current uninterrupted blocked episode; never inferred from an envelope. */
|
|
550
|
+
blocked_since?: string;
|
|
551
|
+
/** Durable six-hour blocked-lane escalation history. */
|
|
552
|
+
escalations?: LaneEscalationEntry[];
|
|
549
553
|
queued_reason?: QueuedReason;
|
|
550
554
|
queued_details?: QueuedDetails;
|
|
551
555
|
/** Time the current queue turn began; old records fall back to created_at. */
|
|
@@ -573,6 +577,8 @@ export interface LaneRecord {
|
|
|
573
577
|
keep_alive?: boolean;
|
|
574
578
|
/** Whether completed keep-alive stories wait for queued work after a done turn. */
|
|
575
579
|
keep_alive_standing?: boolean;
|
|
580
|
+
/** Conduct loop lanes keep one registered worker identity while phases are queued. */
|
|
581
|
+
loop_mode?: true;
|
|
576
582
|
/** Time this lane most recently explicitly opted into keep-alive. */
|
|
577
583
|
keep_alive_opted_in_at?: string;
|
|
578
584
|
/** Why reconcile declined to resume this lane most recently. */
|
|
@@ -765,6 +771,11 @@ export interface LaneRecord {
|
|
|
765
771
|
/** Last successful lane_progress mesh delivery, preserved across watcher restarts. */
|
|
766
772
|
last_progress_event_at?: string;
|
|
767
773
|
}
|
|
774
|
+
export interface LaneEscalationEntry {
|
|
775
|
+
at: string;
|
|
776
|
+
to: string;
|
|
777
|
+
hours: number;
|
|
778
|
+
}
|
|
768
779
|
/** The v1 fields that must exist on every durable lane record (spec §3).
|
|
769
780
|
* `company` is normalized from absence for pre-attribution legacy records. */
|
|
770
781
|
export declare const REQUIRED_LANE_RECORD_FIELDS: readonly ["schema_version", "lane_id", "project_id", "story_id", "depth", "senior", "senior_history", "state", "priority", "provider", "host", "version", "created_at", "updated_at", "attribution_history"];
|
|
@@ -809,6 +820,10 @@ export interface PendingResume {
|
|
|
809
820
|
trigger: "answered_questions" | "message" | "monitor_event" | "system_notice" | "resume" | "watcher";
|
|
810
821
|
/** True when this queued resume was initiated automatically by keep-alive. */
|
|
811
822
|
keep_alive?: true;
|
|
823
|
+
/** Working directory for a deferred loop phase. */
|
|
824
|
+
working_directory?: string;
|
|
825
|
+
/** Start this deferred conduct phase with a fresh provider session. */
|
|
826
|
+
fresh_thread?: true;
|
|
812
827
|
resumed_from?: "interrupted" | "done" | "blocked" | "failed" | "awaiting_input";
|
|
813
828
|
from_failed?: true;
|
|
814
829
|
/** Senior that authorized a failed-lane resume, rechecked at the claim CAS. */
|
|
@@ -930,6 +945,9 @@ export interface QuestionRecord {
|
|
|
930
945
|
version: number;
|
|
931
946
|
/** Null until the owner starts a resume carrying this answer; absent on old records. */
|
|
932
947
|
delivered_at?: string | null;
|
|
948
|
+
/** When the current session audience last saw this pending question. */
|
|
949
|
+
seen_at?: string | null;
|
|
950
|
+
seen_session_id?: string | null;
|
|
933
951
|
body: QuestionBodyRef;
|
|
934
952
|
routing_history: RoutingHistoryEntry[];
|
|
935
953
|
answer?: QuestionAnswer;
|
package/dist/lib/lanes/types.js
CHANGED
|
@@ -230,15 +230,15 @@ export const LANE_FIELD_WRITERS = {
|
|
|
230
230
|
depth: ["command"], senior: ["command"], senior_history: ["command"],
|
|
231
231
|
hire_grant: ["creator", "command"],
|
|
232
232
|
released_hire_grant: ["command"],
|
|
233
|
-
state: ["creator", "command", "watcher", "reconcile", "spawn"], queued_reason: ["creator", "command", "spawn", "watcher", "reconcile"], queued_details: ["command", "reconcile", "spawn", "watcher"], queued_at: ["creator", "command", "spawn", "watcher", "reconcile"], admission_reserved_at: ["creator", "command", "reconcile", "spawn", "watcher"],
|
|
233
|
+
state: ["creator", "command", "watcher", "reconcile", "spawn"], blocked_since: ["watcher", "reconcile"], escalations: ["reconcile"], queued_reason: ["creator", "command", "spawn", "watcher", "reconcile"], queued_details: ["command", "reconcile", "spawn", "watcher"], queued_at: ["creator", "command", "spawn", "watcher", "reconcile"], admission_reserved_at: ["creator", "command", "reconcile", "spawn", "watcher"],
|
|
234
234
|
pickup_validation_pending: ["creator", "command", "spawn", "reconcile"],
|
|
235
235
|
pickup_validation_pending_since: ["creator", "command", "spawn", "reconcile"],
|
|
236
236
|
priority: ["command"], worker: ["creator", "command"], worker_path: ["creator", "command"], worker_pack: ["creator", "command"], worker_profile: ["creator", "command"], worker_profile_hash: ["creator", "command", "spawn"], registry_worker: ["creator", "command", "spawn"], registry_worker_approval_history: ["command", "watcher", "reconcile"], worker_profile_repin_history: ["command"], worker_profile_ignored: ["creator", "command", "spawn"], worker_policy_delivery: ["command", "spawn"],
|
|
237
|
-
keep_alive: ["creator", "command"], keep_alive_standing: ["command"], keep_alive_opted_in_at: ["creator", "command"], keep_alive_blocked_reason: ["command", "reconcile"], keep_alive_resume_count: ["command", "reconcile", "spawn"], keep_alive_history: ["reconcile"], keep_alive_blocked_resume_streak: ["reconcile", "spawn", "watcher"], keep_alive_last_resume_at: ["command", "reconcile", "spawn"], keep_alive_next_eligible_at: ["command", "reconcile", "spawn"], keep_alive_exhausted_at: ["command", "reconcile", "spawn"],
|
|
237
|
+
keep_alive: ["creator", "command"], keep_alive_standing: ["command"], keep_alive_opted_in_at: ["creator", "command"], keep_alive_blocked_reason: ["command", "reconcile"], keep_alive_resume_count: ["command", "reconcile", "spawn"], keep_alive_history: ["reconcile"], keep_alive_blocked_resume_streak: ["reconcile", "spawn", "watcher"], keep_alive_last_resume_at: ["command", "reconcile", "spawn"], keep_alive_next_eligible_at: ["command", "reconcile", "spawn"], keep_alive_exhausted_at: ["command", "reconcile", "spawn"], loop_mode: ["creator"],
|
|
238
238
|
provider: ["creator", "command"], provider_source: ["creator", "command"], account: ["creator", "spawn", "command", "watcher", "reconcile"], account_source: ["creator", "command"], profile_settings_pinned: ["command"], account_selection: ["creator", "command"], admission_accounts: ["creator", "command"], account_history: ["command", "spawn", "watcher", "reconcile"], failed_resume_history: ["command"], fallback_question_cancellation_failures: ["command"], pending_resume: ["command", "spawn", "watcher", "reconcile"], placement_default: ["creator"], host: ["creator", "command"], origin_host: ["creator"], origin_host_source: ["creator"],
|
|
239
239
|
version: ["creator"], created_at: ["creator"], updated_at: ["creator"], ended_at: ["reconcile"], envelope_summary: ["reconcile"], attribution_history: ["command", "watcher", "reconcile"], story_switch_pending: ["command", "watcher", "reconcile"], story_switch_unacknowledged: ["watcher", "reconcile"],
|
|
240
240
|
last_envelope: ["watcher", "reconcile"], model: ["creator", "spawn", "command"], model_source: ["creator", "spawn", "command"], effort: ["creator", "spawn", "command"], effort_source: ["creator", "spawn", "command"], fast_mode: ["spawn"],
|
|
241
|
-
last_message_at: ["command"], status_gate: ["spawn", "watcher"], status_gate_history: ["watcher", "reconcile"], blocker_freshness_state: ["watcher"], kr_freshness_state: ["watcher"], last_compaction: ["spawn"], story_complete: ["watcher", "reconcile"], story_complete_reason: ["watcher", "reconcile"], story_status: ["watcher", "reconcile"],
|
|
241
|
+
last_message_at: ["creator", "command"], status_gate: ["spawn", "watcher"], status_gate_history: ["watcher", "reconcile"], blocker_freshness_state: ["watcher"], kr_freshness_state: ["watcher"], last_compaction: ["spawn"], story_complete: ["watcher", "reconcile"], story_complete_reason: ["watcher", "reconcile"], story_status: ["watcher", "reconcile"],
|
|
242
242
|
overdue: ["reconcile"], overdue_at: ["reconcile"], last_host_report_at: ["watcher", "spawn", "reconcile"], stalled: ["watcher"], stalled_at: ["watcher"], stalled_reason: ["watcher"],
|
|
243
243
|
admitted_reason: ["command", "reconcile"], terminal_reason: ["command", "watcher", "spawn", "reconcile"], stop_history: ["command", "reconcile"], settings_history: ["command"], pending_settings: ["command", "spawn"], interrupted_at: ["command", "watcher", "reconcile", "spawn"], terminal_details: ["command", "spawn", "watcher", "reconcile"],
|
|
244
244
|
quota_reset_at: ["watcher", "reconcile", "spawn", "command"], model_quota_marker: ["watcher", "reconcile"], outpost_command_id: ["command"],
|
|
@@ -469,6 +469,9 @@ function applyTerminalToLaneWithResult(hqRoot, laneId, classification, opts = {}
|
|
|
469
469
|
...current,
|
|
470
470
|
...runResultFields(hqRoot, current, at),
|
|
471
471
|
state: effectiveClassification.state,
|
|
472
|
+
blocked_since: effectiveClassification.state === "blocked"
|
|
473
|
+
? current.state === "blocked" ? current.blocked_since ?? at : at
|
|
474
|
+
: undefined,
|
|
472
475
|
queued_reason: undefined,
|
|
473
476
|
queued_details: undefined,
|
|
474
477
|
stalled: undefined,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type LaneRecord, type LaneProvider, type SeniorRef } from "./types.js";
|
|
2
|
+
export interface WorkerContinuityLookup {
|
|
3
|
+
senior: SeniorRef;
|
|
4
|
+
worker: string;
|
|
5
|
+
provider: LaneProvider;
|
|
6
|
+
company: LaneRecord["company"];
|
|
7
|
+
freshThread?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface WorkerContinuityResult {
|
|
10
|
+
previous?: LaneRecord;
|
|
11
|
+
resumable: boolean;
|
|
12
|
+
fallbackReason?: "provider_unsupported" | "session_id_missing" | "state_unsafe" | "host_mismatch";
|
|
13
|
+
}
|
|
14
|
+
export declare function sameLaneHost(left: LaneRecord["host"], right: LaneRecord["host"]): boolean;
|
|
15
|
+
/** Select the prior worker lane within exactly one senior/company/provider scope. */
|
|
16
|
+
export declare function findWorkerContinuityLane(lanes: readonly LaneRecord[], lookup: WorkerContinuityLookup): WorkerContinuityResult;
|
|
17
|
+
export declare function workerContinuityPrompt(previous: LaneRecord, resultSummary?: string, resultRef?: string): string;
|
|
18
|
+
//# sourceMappingURL=worker-continuity.d.ts.map
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { LanesError } from "./errors.js";
|
|
2
|
+
import { providerSupportsSessionResume } from "./delivery.js";
|
|
3
|
+
import { isTerminalLaneState } from "./types.js";
|
|
4
|
+
function seniorKey(senior) {
|
|
5
|
+
return `${senior.kind}:${senior.id ?? ""}`;
|
|
6
|
+
}
|
|
7
|
+
function sameCompany(left, right) {
|
|
8
|
+
if (left?.uid && right?.uid)
|
|
9
|
+
return left.uid === right.uid;
|
|
10
|
+
if (left?.slug && right?.slug)
|
|
11
|
+
return left.slug === right.slug;
|
|
12
|
+
return left == null && right == null;
|
|
13
|
+
}
|
|
14
|
+
function compareNewest(left, right) {
|
|
15
|
+
return right.created_at.localeCompare(left.created_at) || right.lane_id.localeCompare(left.lane_id);
|
|
16
|
+
}
|
|
17
|
+
export function sameLaneHost(left, right) {
|
|
18
|
+
return left.kind === right.kind && left.id === right.id;
|
|
19
|
+
}
|
|
20
|
+
/** Select the prior worker lane within exactly one senior/company/provider scope. */
|
|
21
|
+
export function findWorkerContinuityLane(lanes, lookup) {
|
|
22
|
+
const scoped = lanes
|
|
23
|
+
.filter((lane) => lane.worker === lookup.worker && lane.provider === lookup.provider &&
|
|
24
|
+
seniorKey(lane.senior) === seniorKey(lookup.senior) && sameCompany(lane.company ?? lane.mesh_company, lookup.company))
|
|
25
|
+
.sort(compareNewest);
|
|
26
|
+
const live = scoped.find((lane) => !isTerminalLaneState(lane.state));
|
|
27
|
+
if (live) {
|
|
28
|
+
throw new LanesError("worker_lane_busy", `Worker ${lookup.worker} already has live lane ${live.lane_id}`, {
|
|
29
|
+
worker: lookup.worker,
|
|
30
|
+
lane_id: live.lane_id,
|
|
31
|
+
state: live.state,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
const previous = scoped[0];
|
|
35
|
+
if (!previous)
|
|
36
|
+
return { resumable: false };
|
|
37
|
+
if (lookup.freshThread)
|
|
38
|
+
return { previous, resumable: false };
|
|
39
|
+
const previousSessionId = previous.provider_session_id?.trim();
|
|
40
|
+
if (previousSessionId) {
|
|
41
|
+
const liveDescendant = lanes
|
|
42
|
+
.filter((lane) => lane.lane_id !== previous.lane_id && lane.provider === lookup.provider &&
|
|
43
|
+
sameCompany(lane.company ?? lane.mesh_company, lookup.company) &&
|
|
44
|
+
lane.provider_session_id === previousSessionId &&
|
|
45
|
+
(lane.state === "queued" || lane.state === "running"))
|
|
46
|
+
.sort(compareNewest)[0];
|
|
47
|
+
if (liveDescendant) {
|
|
48
|
+
throw new LanesError("worker_lane_busy", `Provider thread lineage is active in lane ${liveDescendant.lane_id}`, {
|
|
49
|
+
worker: lookup.worker,
|
|
50
|
+
lane_id: liveDescendant.lane_id,
|
|
51
|
+
state: liveDescendant.state,
|
|
52
|
+
provider_session_id: previousSessionId,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Only clean, resumable terminal outcomes are safe for automatic reuse.
|
|
57
|
+
if (previous.state !== "done" && previous.state !== "interrupted") {
|
|
58
|
+
return { previous, resumable: false, fallbackReason: "state_unsafe" };
|
|
59
|
+
}
|
|
60
|
+
if (!providerSupportsSessionResume(lookup.provider)) {
|
|
61
|
+
return { previous, resumable: false, fallbackReason: "provider_unsupported" };
|
|
62
|
+
}
|
|
63
|
+
if (!previous.provider_session_id?.trim()) {
|
|
64
|
+
return { previous, resumable: false, fallbackReason: "session_id_missing" };
|
|
65
|
+
}
|
|
66
|
+
return { previous, resumable: true };
|
|
67
|
+
}
|
|
68
|
+
export function workerContinuityPrompt(previous, resultSummary, resultRef) {
|
|
69
|
+
const summary = resultSummary?.trim();
|
|
70
|
+
const resultPath = resultRef?.trim()
|
|
71
|
+
? `workspace/lanes-runs/${previous.lane_id}/${resultRef.trim()}`
|
|
72
|
+
: `workspace/lanes-runs/${previous.lane_id}/envelopes/`;
|
|
73
|
+
return [
|
|
74
|
+
"## Previous worker lane",
|
|
75
|
+
`Continue the provider session from lane ${previous.lane_id}. Its latest result is recorded at ${resultPath}; read that result before acting.`,
|
|
76
|
+
...(summary ? [`Previous result summary: ${summary}`] : []),
|
|
77
|
+
"The current brief below is the instruction for this turn.",
|
|
78
|
+
"",
|
|
79
|
+
].join("\n");
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=worker-continuity.js.map
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const LANE_WORKER_EXIT_PATH_ENV = "HQ_LANE_WORKER_EXIT_PATH";
|
|
2
|
+
/** Runs one lane worker process and persists its terminal process status. */
|
|
3
|
+
export declare function runLaneWorkerProcess(command: string, args: string[], env?: NodeJS.ProcessEnv, cwd?: string): Promise<number>;
|
|
4
|
+
//# sourceMappingURL=worker-process.d.ts.map
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import * as childProcess from "node:child_process";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
export const LANE_WORKER_EXIT_PATH_ENV = "HQ_LANE_WORKER_EXIT_PATH";
|
|
7
|
+
function exitCodeFor(code, signal) {
|
|
8
|
+
if (code !== null)
|
|
9
|
+
return code;
|
|
10
|
+
return signal ? 128 + (os.constants.signals[signal] ?? 0) : 1;
|
|
11
|
+
}
|
|
12
|
+
function recordExit(pathname, code, signal) {
|
|
13
|
+
const tempPath = `${pathname}.${process.pid}.tmp`;
|
|
14
|
+
fs.writeFileSync(tempPath, `${JSON.stringify({
|
|
15
|
+
exit_code: exitCodeFor(code, signal),
|
|
16
|
+
...(signal ? { signal } : {}),
|
|
17
|
+
ended_at: new Date().toISOString(),
|
|
18
|
+
})}\n`, { mode: 0o600 });
|
|
19
|
+
fs.renameSync(tempPath, pathname);
|
|
20
|
+
}
|
|
21
|
+
/** Runs one lane worker process and persists its terminal process status. */
|
|
22
|
+
export function runLaneWorkerProcess(command, args, env = process.env, cwd = process.cwd()) {
|
|
23
|
+
const exitPath = env[LANE_WORKER_EXIT_PATH_ENV];
|
|
24
|
+
const childEnv = { ...env };
|
|
25
|
+
delete childEnv[LANE_WORKER_EXIT_PATH_ENV];
|
|
26
|
+
const child = childProcess.spawn(command, args, { cwd, env: childEnv, stdio: "inherit" });
|
|
27
|
+
let settled = false;
|
|
28
|
+
const finish = (code, signal) => {
|
|
29
|
+
if (!settled && exitPath) {
|
|
30
|
+
try {
|
|
31
|
+
recordExit(exitPath, code, signal);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
process.stderr.write(`hq lanes: could not persist worker exit status: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
settled = true;
|
|
38
|
+
return exitCodeFor(code, signal);
|
|
39
|
+
};
|
|
40
|
+
return new Promise((resolve) => {
|
|
41
|
+
const signals = ["SIGINT", "SIGTERM", "SIGHUP", "SIGQUIT"];
|
|
42
|
+
const handlers = new Map();
|
|
43
|
+
const cleanup = () => {
|
|
44
|
+
for (const [signal, handler] of handlers)
|
|
45
|
+
process.removeListener(signal, handler);
|
|
46
|
+
};
|
|
47
|
+
for (const signal of signals) {
|
|
48
|
+
const handler = () => child.kill(signal);
|
|
49
|
+
handlers.set(signal, handler);
|
|
50
|
+
process.on(signal, handler);
|
|
51
|
+
}
|
|
52
|
+
child.once("error", (error) => {
|
|
53
|
+
process.stderr.write(`hq lanes: worker process could not start: ${error.message}\n`);
|
|
54
|
+
if (!settled) {
|
|
55
|
+
cleanup();
|
|
56
|
+
resolve(finish(127, null));
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
child.once("exit", (code, signal) => {
|
|
60
|
+
if (!settled) {
|
|
61
|
+
cleanup();
|
|
62
|
+
resolve(finish(code, signal));
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
68
|
+
const [command, ...args] = process.argv.slice(2);
|
|
69
|
+
if (!command) {
|
|
70
|
+
process.stderr.write("hq lanes: worker process command is missing\n");
|
|
71
|
+
process.exitCode = 2;
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
process.exitCode = await runLaneWorkerProcess(command, args);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=worker-process.js.map
|
package/dist/main.js
CHANGED
|
@@ -47,7 +47,7 @@ import { enforceVersionGate, getVersionGatePhase, isLocalDependencyInstall, reso
|
|
|
47
47
|
import { autoUpdateAndReexec } from "./utils/self-update.js";
|
|
48
48
|
import { CLI_VERSION } from "./cli-version.js";
|
|
49
49
|
import { findLazyCommand, registerCommandCatalog } from "./lazy-commands.js";
|
|
50
|
-
import { emitCliFirstSyncNextStepOutcome, emitCliLimitNoticeEngagement, emitCliLimitNoticeExposure, emitCliInviteNudgeEvent, emitCliSessionStarted, emitPlanLimitInviteTeammateExposure, isCliLimitResourceKind, queueCliLimitNoticeEngagement, queueCliLimitNoticeExposure, takeFirstSyncStepOutcomes, takeCliLimitNoticeEngagements, takeCliLimitNoticeExposures, takeCliInviteNudgeEvents, takePlanLimitInviteTeammateExposures, maybeShowFirstSyncNextStep, } from "./utils/cli-telemetry.js";
|
|
50
|
+
import { emitCliFirstSyncNextStepOutcome, emitCliFeatureUsed, emitCliLimitNoticeEngagement, emitCliLimitNoticeExposure, emitCliInviteNudgeEvent, emitCliSessionStarted, emitPlanLimitInviteTeammateExposure, isCliLimitResourceKind, queueCliLimitNoticeEngagement, queueCliLimitNoticeExposure, takeFirstSyncStepOutcomes, takeCliFeatureUsages, takeCliLimitNoticeEngagements, takeCliLimitNoticeExposures, takeCliInviteNudgeEvents, takePlanLimitInviteTeammateExposures, maybeShowFirstSyncNextStep, } from "./utils/cli-telemetry.js";
|
|
51
51
|
import { reportCliClientHealthInvocation } from "./utils/client-health.js";
|
|
52
52
|
import { settleWithin } from "./utils/settle-with-timeout.js";
|
|
53
53
|
import { isVersionRequest } from "./version-request.js";
|
|
@@ -298,7 +298,9 @@ export async function runCli() {
|
|
|
298
298
|
await releasePackageUseLeaseAfterCommandLoad(process.argv);
|
|
299
299
|
await program.parseAsync();
|
|
300
300
|
commandParsed = true;
|
|
301
|
-
|
|
301
|
+
if (process.exitCode === undefined || process.exitCode === 0) {
|
|
302
|
+
maybeShowFirstSyncNextStep();
|
|
303
|
+
}
|
|
302
304
|
}
|
|
303
305
|
catch (err) {
|
|
304
306
|
await handleTopLevelError(err);
|
|
@@ -316,7 +318,10 @@ export async function runCli() {
|
|
|
316
318
|
// lifecycle boundary for this invocation. The budget reserves this 500 ms
|
|
317
319
|
// slice even when the command itself was long-running.
|
|
318
320
|
const releaseHealthTimeoutMs = foregroundNetworkBudget.timeoutFor(FINALIZATION_FOREGROUND_WAIT_MS);
|
|
319
|
-
const firstSyncOutcomeFlushes =
|
|
321
|
+
const firstSyncOutcomeFlushes = process.exitCode === undefined || process.exitCode === 0
|
|
322
|
+
? takeFirstSyncStepOutcomes().map((outcome) => emitCliFirstSyncNextStepOutcome(outcome, releaseHealthTimeoutMs))
|
|
323
|
+
: [];
|
|
324
|
+
const featureUsageFlushes = takeCliFeatureUsages().map((usage) => emitCliFeatureUsed(usage, releaseHealthTimeoutMs));
|
|
320
325
|
const inviteNudgeOutcomeFlushes = takeCliInviteNudgeEvents().map((event) => emitCliInviteNudgeEvent(event, releaseHealthTimeoutMs));
|
|
321
326
|
const inviteTeammateExposureFlushes = takePlanLimitInviteTeammateExposures().map((companyUid) => emitPlanLimitInviteTeammateExposure(companyUid, releaseHealthTimeoutMs));
|
|
322
327
|
const limitNoticeExposureFlushes = takeCliLimitNoticeExposures().map((exposure) => emitCliLimitNoticeExposure(exposure, releaseHealthTimeoutMs));
|
|
@@ -343,6 +348,7 @@ export async function runCli() {
|
|
|
343
348
|
...(cachedVersionNudge ? [cachedVersionNudge] : []),
|
|
344
349
|
Sentry.flush(releaseHealthTimeoutMs),
|
|
345
350
|
...firstSyncOutcomeFlushes,
|
|
351
|
+
...featureUsageFlushes,
|
|
346
352
|
...inviteNudgeOutcomeFlushes,
|
|
347
353
|
...inviteTeammateExposureFlushes,
|
|
348
354
|
...limitNoticeExposureFlushes,
|
|
@@ -46,7 +46,7 @@ export declare function queueCliInviteNudgeEvent(companyUid: string, outcome: Cl
|
|
|
46
46
|
export declare function takeCliInviteNudgeEvents(): CliInviteNudgeEvent[];
|
|
47
47
|
/** Emit a bounded CLI session fact through the existing product-telemetry path. */
|
|
48
48
|
export declare function emitCliInviteNudgeEvent(event: CliInviteNudgeEvent, timeoutMs?: number): Promise<void>;
|
|
49
|
-
export type CliFeatureKind = "secret_read" | "lane_create" | "lane_resume";
|
|
49
|
+
export type CliFeatureKind = "secret_read" | "lane_create" | "lane_resume" | "members_invite";
|
|
50
50
|
export type CliFeatureOutcome = "success" | "failure";
|
|
51
51
|
export interface CliFeatureUsage {
|
|
52
52
|
kind: CliFeatureKind;
|
|
@@ -54,6 +54,9 @@ export interface CliFeatureUsage {
|
|
|
54
54
|
/** Only company UIDs are accepted; names and slugs are never telemetry fields. */
|
|
55
55
|
companyUid?: string;
|
|
56
56
|
}
|
|
57
|
+
/** Queue privacy-limited feature telemetry for the bounded CLI finalization flush. */
|
|
58
|
+
export declare function queueCliFeatureUsed(usage: CliFeatureUsage): void;
|
|
59
|
+
export declare function takeCliFeatureUsages(): CliFeatureUsage[];
|
|
57
60
|
export declare function isCliLimitResourceKind(value: string): value is CliLimitResourceKind;
|
|
58
61
|
/**
|
|
59
62
|
* Emit one bounded, privacy-limited feature-use event with a cached identity.
|
|
@@ -142,6 +142,14 @@ export async function emitCliInviteNudgeEvent(event, timeoutMs = TELEMETRY_TIMEO
|
|
|
142
142
|
console.error(`hq: Could not prepare invite prompt telemetry (${errorClass}).`);
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
|
+
const pendingCliFeatureUsages = [];
|
|
146
|
+
/** Queue privacy-limited feature telemetry for the bounded CLI finalization flush. */
|
|
147
|
+
export function queueCliFeatureUsed(usage) {
|
|
148
|
+
pendingCliFeatureUsages.push({ ...usage });
|
|
149
|
+
}
|
|
150
|
+
export function takeCliFeatureUsages() {
|
|
151
|
+
return pendingCliFeatureUsages.splice(0, pendingCliFeatureUsages.length);
|
|
152
|
+
}
|
|
145
153
|
const COMPANY_UID_PATTERN = /^cmp_[A-Za-z0-9_-]{1,128}$/;
|
|
146
154
|
export function isCliLimitResourceKind(value) {
|
|
147
155
|
return ["users", "integrations", "secrets", "agents", "deployments", "storageBytes"].includes(value);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type JqResult = {
|
|
2
|
+
code: number;
|
|
3
|
+
stdout?: string;
|
|
4
|
+
stderr?: string;
|
|
5
|
+
stdoutBytes?: Buffer;
|
|
6
|
+
stderrBytes?: Buffer;
|
|
7
|
+
};
|
|
8
|
+
export type JqInvoke = (args: string[], input: Buffer | string | undefined, cwd: string, options: {
|
|
9
|
+
stdout?: "capture" | "ignore";
|
|
10
|
+
stderr?: "pipe" | "ignore";
|
|
11
|
+
}) => JqResult;
|
|
12
|
+
export declare function createShellCompatibleJqInvoker(invoke: JqInvoke, platform: NodeJS.Platform, env: NodeJS.ProcessEnv, resolveShellType: (cwd: string) => string | undefined): JqInvoke;
|
|
13
|
+
//# sourceMappingURL=jq-windows-compat.d.ts.map
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
function stripCarriageReturnBeforeLineFeed(bytes) {
|
|
2
|
+
const output = Buffer.allocUnsafe(bytes.length);
|
|
3
|
+
let length = 0;
|
|
4
|
+
for (let index = 0; index < bytes.length; index += 1) {
|
|
5
|
+
const byte = bytes[index];
|
|
6
|
+
if (byte === 0x0d && bytes[index + 1] === 0x0a)
|
|
7
|
+
continue;
|
|
8
|
+
output[length] = byte ?? 0;
|
|
9
|
+
length += 1;
|
|
10
|
+
}
|
|
11
|
+
return output.subarray(0, length);
|
|
12
|
+
}
|
|
13
|
+
function normalizeResult(result) {
|
|
14
|
+
const stdoutBytes = stripCarriageReturnBeforeLineFeed(result.stdoutBytes ?? Buffer.from(result.stdout ?? ""));
|
|
15
|
+
const stderrBytes = stripCarriageReturnBeforeLineFeed(result.stderrBytes ?? Buffer.from(result.stderr ?? ""));
|
|
16
|
+
return {
|
|
17
|
+
...result,
|
|
18
|
+
stdoutBytes,
|
|
19
|
+
stderrBytes,
|
|
20
|
+
stdout: stdoutBytes.toString("utf8"),
|
|
21
|
+
stderr: stderrBytes.toString("utf8"),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function createShellCompatibleJqInvoker(invoke, platform, env, resolveShellType) {
|
|
25
|
+
let binaryOptionSupported;
|
|
26
|
+
let shellTypeResolved = false;
|
|
27
|
+
let shellType;
|
|
28
|
+
return (args, input, cwd, options) => {
|
|
29
|
+
if (platform !== "win32")
|
|
30
|
+
return invoke(args, input, cwd, options);
|
|
31
|
+
if (!shellTypeResolved) {
|
|
32
|
+
shellType = resolveShellType(cwd);
|
|
33
|
+
shellTypeResolved = true;
|
|
34
|
+
}
|
|
35
|
+
const msysMode = /^msys/i.test(shellType ?? "") || /^MINGW/i.test(env.MSYSTEM ?? "");
|
|
36
|
+
if (!msysMode)
|
|
37
|
+
return invoke(args, input, cwd, options);
|
|
38
|
+
if (binaryOptionSupported === undefined) {
|
|
39
|
+
binaryOptionSupported = invoke(["--binary", "-n", "null"], undefined, cwd, {
|
|
40
|
+
stdout: "ignore",
|
|
41
|
+
stderr: "ignore",
|
|
42
|
+
}).code === 0;
|
|
43
|
+
}
|
|
44
|
+
const result = invoke(binaryOptionSupported ? ["--binary", ...args] : args, input, cwd, options);
|
|
45
|
+
return binaryOptionSupported ? result : normalizeResult(result);
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=jq-windows-compat.js.map
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
* - signals are surfaced as the conventional 128+n. A caller that kills a
|
|
30
30
|
* script needs to see termination, not a success code.
|
|
31
31
|
*/
|
|
32
|
+
export { resolveWindowsBash } from "./windows-bash.js";
|
|
32
33
|
/**
|
|
33
34
|
* Which interpreter runs a bundled asset, chosen by extension.
|
|
34
35
|
*
|
|
@@ -57,6 +58,8 @@ export type RunBundledScriptOptions = {
|
|
|
57
58
|
captureOutput?: boolean;
|
|
58
59
|
/** Bytes supplied on stdin. With no value, stdin is inherited as before. */
|
|
59
60
|
stdin?: string | Buffer;
|
|
61
|
+
/** Start the bundled Bash script with descriptor 0 closed. */
|
|
62
|
+
closeStdin?: boolean;
|
|
60
63
|
/** Optional child-process deadline for callers that need bounded script execution. */
|
|
61
64
|
timeoutMs?: number;
|
|
62
65
|
};
|
|
@@ -34,6 +34,8 @@ import * as fs from "node:fs";
|
|
|
34
34
|
import * as os from "node:os";
|
|
35
35
|
import * as path from "path";
|
|
36
36
|
import { resolveBundledAsset } from "./hq-roots-core.js";
|
|
37
|
+
import { resolveWindowsBash } from "./windows-bash.js";
|
|
38
|
+
export { resolveWindowsBash } from "./windows-bash.js";
|
|
37
39
|
/**
|
|
38
40
|
* Which interpreter runs a bundled asset, chosen by extension.
|
|
39
41
|
*
|
|
@@ -52,85 +54,6 @@ function scriptPathForBash(script) {
|
|
|
52
54
|
const drivePath = normalized.match(/^([A-Za-z]):\/(.*)$/);
|
|
53
55
|
return drivePath ? `/${drivePath[1].toLowerCase()}/${drivePath[2]}` : normalized;
|
|
54
56
|
}
|
|
55
|
-
function envValue(env, name) {
|
|
56
|
-
const key = Object.keys(env).find((candidate) => candidate.toLowerCase() === name.toLowerCase());
|
|
57
|
-
return key ? env[key] : undefined;
|
|
58
|
-
}
|
|
59
|
-
function isFile(file, lookupErrors) {
|
|
60
|
-
try {
|
|
61
|
-
return fs.statSync(file).isFile();
|
|
62
|
-
}
|
|
63
|
-
catch (error) {
|
|
64
|
-
// Missing PATH candidates are expected during lookup; retain the cause so
|
|
65
|
-
// the final actionable error can expose why no interpreter was resolved.
|
|
66
|
-
lookupErrors.push(error);
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
function isWithinWindowsDirectory(file, directory) {
|
|
71
|
-
const normalizedFile = path.win32.resolve(file).toLowerCase();
|
|
72
|
-
const normalizedDirectory = path.win32.resolve(directory).toLowerCase();
|
|
73
|
-
return (normalizedFile === normalizedDirectory ||
|
|
74
|
-
normalizedFile.startsWith(`${normalizedDirectory}${path.win32.sep}`));
|
|
75
|
-
}
|
|
76
|
-
function isExcludedWindowsBash(file, env) {
|
|
77
|
-
if (path.win32.basename(file).toLowerCase() !== "bash.exe")
|
|
78
|
-
return false;
|
|
79
|
-
const windowsDir = envValue(env, "SystemRoot") ?? envValue(env, "WINDIR") ?? "C:\\Windows";
|
|
80
|
-
const systemBash = path.win32.resolve(windowsDir, "System32", "bash.exe");
|
|
81
|
-
if (path.win32.resolve(file).toLowerCase() === systemBash.toLowerCase())
|
|
82
|
-
return true;
|
|
83
|
-
const localAppData = envValue(env, "LOCALAPPDATA");
|
|
84
|
-
return Boolean(localAppData &&
|
|
85
|
-
isWithinWindowsDirectory(file, path.win32.resolve(localAppData, "Microsoft", "WindowsApps")));
|
|
86
|
-
}
|
|
87
|
-
function findOnWindowsPath(executable, env, lookupErrors) {
|
|
88
|
-
const pathValue = envValue(env, "PATH") ?? "";
|
|
89
|
-
const extensions = executable.includes(".")
|
|
90
|
-
? [""]
|
|
91
|
-
: (envValue(env, "PATHEXT") ?? ".COM;.EXE;.BAT;.CMD")
|
|
92
|
-
.split(";")
|
|
93
|
-
.filter(Boolean)
|
|
94
|
-
.map((ext) => ext.toLowerCase());
|
|
95
|
-
for (const entry of pathValue.split(";").filter(Boolean)) {
|
|
96
|
-
const directory = entry.trim().replace(/^"|"$/g, "");
|
|
97
|
-
for (const extension of extensions) {
|
|
98
|
-
const candidate = path.win32.join(directory, `${executable}${extension}`);
|
|
99
|
-
if (!isExcludedWindowsBash(candidate, env) && isFile(candidate, lookupErrors))
|
|
100
|
-
return candidate;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
return undefined;
|
|
104
|
-
}
|
|
105
|
-
function resolveWindowsBash(env) {
|
|
106
|
-
const lookupErrors = [];
|
|
107
|
-
const configured = envValue(env, "CLAUDE_CODE_GIT_BASH_PATH");
|
|
108
|
-
if (configured && !isExcludedWindowsBash(configured, env) && isFile(configured, lookupErrors)) {
|
|
109
|
-
return { executable: configured, cause: lookupErrors.at(-1) };
|
|
110
|
-
}
|
|
111
|
-
const pathBash = findOnWindowsPath("bash", env, lookupErrors);
|
|
112
|
-
if (pathBash)
|
|
113
|
-
return { executable: pathBash, cause: lookupErrors.at(-1) };
|
|
114
|
-
const gitExe = findOnWindowsPath("git.exe", env, lookupErrors);
|
|
115
|
-
if (gitExe) {
|
|
116
|
-
const derived = path.win32.resolve(path.win32.dirname(gitExe), "..", "bin", "bash.exe");
|
|
117
|
-
if (!isExcludedWindowsBash(derived, env) && isFile(derived, lookupErrors)) {
|
|
118
|
-
return { executable: derived, cause: lookupErrors.at(-1) };
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
const programFiles = envValue(env, "ProgramFiles") ?? "C:\\Program Files";
|
|
122
|
-
const localAppData = envValue(env, "LOCALAPPDATA");
|
|
123
|
-
const standardPaths = [
|
|
124
|
-
path.win32.join(programFiles, "Git", "bin", "bash.exe"),
|
|
125
|
-
...(localAppData ? [path.win32.join(localAppData, "Programs", "Git", "bin", "bash.exe")] : []),
|
|
126
|
-
];
|
|
127
|
-
const executable = standardPaths.find((candidate) => !isExcludedWindowsBash(candidate, env) && isFile(candidate, lookupErrors));
|
|
128
|
-
return {
|
|
129
|
-
executable,
|
|
130
|
-
cause: lookupErrors.at(-1) ??
|
|
131
|
-
Object.assign(new Error("Bash was not found (ENOENT)"), { code: "ENOENT" }),
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
57
|
function bashNotFoundError(asset, cause) {
|
|
135
58
|
const isHandoff = asset.replaceAll("\\", "/").endsWith("core/scripts/handoff-finalize.sh");
|
|
136
59
|
const recovery = isHandoff
|
|
@@ -185,7 +108,13 @@ export function runBundledScript(opts) {
|
|
|
185
108
|
? [stdinStdio, "inherit", "inherit"]
|
|
186
109
|
: "inherit";
|
|
187
110
|
const scriptArg = interpreter === "bash" && opts.msysScriptPath ? scriptPathForBash(script) : script;
|
|
188
|
-
|
|
111
|
+
if (opts.closeStdin && (interpreter !== "bash" || inputDescriptor !== undefined)) {
|
|
112
|
+
throw new Error("closeStdin requires a Bash asset without supplied stdin");
|
|
113
|
+
}
|
|
114
|
+
const commandArgs = opts.closeStdin
|
|
115
|
+
? ["-c", 'exec 0<&-; exec "$BASH" "$@"', "hq-bundled-script", scriptArg, ...(opts.args ?? [])]
|
|
116
|
+
: [scriptArg, ...(opts.args ?? [])];
|
|
117
|
+
result = spawnSync(executable, commandArgs, {
|
|
189
118
|
cwd: opts.cwd,
|
|
190
119
|
env,
|
|
191
120
|
stdio,
|