@indigoai-us/hq-cli 5.345.77 → 5.345.79
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 +10 -0
- package/dist/lib/doctor/fix/apply.d.ts +6 -3
- package/dist/lib/doctor/fix/apply.js +43 -17
- package/dist/lib/lanes/spawn.js +1 -0
- package/dist/lib/lanes/store.js +41 -25
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.345.79] — 2026-10-11
|
|
6
|
+
- `hq lanes` no longer fails every list, message, or admission command when
|
|
7
|
+
one lane still has leftover queue details from an older worker-profile wait.
|
|
8
|
+
Those leftover details are dropped on read, cleared when the queue reason
|
|
9
|
+
changes, and one unreadable lane is skipped so the rest of the host stays
|
|
10
|
+
usable.
|
|
11
|
+
- `hq doctor --fix` still refreshes a stale company sync journal when
|
|
12
|
+
`.claude/`, `.codex/`, or `.grok/` has uncommitted changes. Those dirty
|
|
13
|
+
files still block hook-file repairs unless you pass `--force`.
|
|
14
|
+
|
|
5
15
|
## [5.345.77] — 2026-10-10
|
|
6
16
|
- Local bots no longer fail a turn when the installed Codex is too old for the
|
|
7
17
|
model it was asked to use (often the model a newer Codex app set in your own
|
|
@@ -5,9 +5,12 @@
|
|
|
5
5
|
* wrapped in guardrails that are NOT polish — each is a PRD acceptance criterion:
|
|
6
6
|
*
|
|
7
7
|
* 1. Dirty-tree refusal. If the working tree has uncommitted changes under
|
|
8
|
-
* `.claude/`, `.codex/`, or `.grok/`, `--fix` refuses
|
|
9
|
-
* unless `--force`, so
|
|
10
|
-
* in-flight edits to the security layer.
|
|
8
|
+
* `.claude/`, `.codex/`, or `.grok/`, `--fix` refuses hook-file repairs
|
|
9
|
+
* and exits non-zero unless `--force`, so those writes can never be
|
|
10
|
+
* tangled up with unrelated in-flight edits to the security layer.
|
|
11
|
+
* Repairs that do not write those trees (a stale-company `refresh-sync`
|
|
12
|
+
* pull) still run: the SessionStart health hook never passes `--force`,
|
|
13
|
+
* and blocking the pull left `sync.journal.<slug>` WARN after `--fix`.
|
|
11
14
|
* 2. Allowlist only. It repairs the classified safe classes — restore an
|
|
12
15
|
* execute bit, add a hook id to the gate profiles it is missing from,
|
|
13
16
|
* re-register a script present on disk, or pull a company whose local
|
|
@@ -5,9 +5,12 @@
|
|
|
5
5
|
* wrapped in guardrails that are NOT polish — each is a PRD acceptance criterion:
|
|
6
6
|
*
|
|
7
7
|
* 1. Dirty-tree refusal. If the working tree has uncommitted changes under
|
|
8
|
-
* `.claude/`, `.codex/`, or `.grok/`, `--fix` refuses
|
|
9
|
-
* unless `--force`, so
|
|
10
|
-
* in-flight edits to the security layer.
|
|
8
|
+
* `.claude/`, `.codex/`, or `.grok/`, `--fix` refuses hook-file repairs
|
|
9
|
+
* and exits non-zero unless `--force`, so those writes can never be
|
|
10
|
+
* tangled up with unrelated in-flight edits to the security layer.
|
|
11
|
+
* Repairs that do not write those trees (a stale-company `refresh-sync`
|
|
12
|
+
* pull) still run: the SessionStart health hook never passes `--force`,
|
|
13
|
+
* and blocking the pull left `sync.journal.<slug>` WARN after `--fix`.
|
|
11
14
|
* 2. Allowlist only. It repairs the classified safe classes — restore an
|
|
12
15
|
* execute bit, add a hook id to the gate profiles it is missing from,
|
|
13
16
|
* re-register a script present on disk, or pull a company whose local
|
|
@@ -107,20 +110,34 @@ export async function applyFixes(options) {
|
|
|
107
110
|
...over,
|
|
108
111
|
});
|
|
109
112
|
// 1. Dirty-tree refusal (AC8). Never write over uncommitted security-layer
|
|
110
|
-
// edits unless the user forces it.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
113
|
+
// edits unless the user forces it. Probe once; hook-file plans are
|
|
114
|
+
// dropped per pass so a stale-journal pull can still run.
|
|
115
|
+
const dirty = options.force
|
|
116
|
+
? []
|
|
117
|
+
: (options.dirtyCheck ?? uncommittedHookConfigChanges)(hqRoot);
|
|
118
|
+
const refuseDirtyHookWrites = (plans) => {
|
|
119
|
+
if (dirty.length === 0)
|
|
120
|
+
return plans;
|
|
121
|
+
const allowed = plans.filter((plan) => plan.relpath.length === 0);
|
|
122
|
+
if (allowed.length === plans.length)
|
|
123
|
+
return plans;
|
|
124
|
+
if (allowed.length === 0)
|
|
125
|
+
return "refuse";
|
|
126
|
+
writeErr(`hq doctor --fix: skipping hook-file repairs — uncommitted changes under ` +
|
|
127
|
+
`${HOOK_CONFIG_DIRS.join(", ")}. Refreshing sync journals still runs; ` +
|
|
128
|
+
`re-run with --force to repair hook files.\n`);
|
|
129
|
+
return allowed;
|
|
130
|
+
};
|
|
131
|
+
const refuseDirtyTree = () => {
|
|
132
|
+
writeErr(`hq doctor --fix: refusing to run — uncommitted changes under ` +
|
|
133
|
+
`${HOOK_CONFIG_DIRS.join(", ")}:\n` +
|
|
134
|
+
dirty.map((line) => ` ${line}`).join("\n") +
|
|
135
|
+
`\nCommit or stash them, or re-run with --force.\n`);
|
|
136
|
+
return empty({ exitCode: 1, refused: "dirty-tree" });
|
|
137
|
+
};
|
|
121
138
|
// Resolve the customer-visible manual-only output gate once per fix run.
|
|
122
|
-
//
|
|
123
|
-
//
|
|
139
|
+
// Fail open to the pre-change output if the identity is absent or the
|
|
140
|
+
// registry lookup fails.
|
|
124
141
|
let showManualOnlyRemediations = false;
|
|
125
142
|
try {
|
|
126
143
|
showManualOnlyRemediations =
|
|
@@ -172,7 +189,16 @@ export async function applyFixes(options) {
|
|
|
172
189
|
];
|
|
173
190
|
};
|
|
174
191
|
for (let pass = 1; pass <= MAX_FIX_PASSES; pass += 1) {
|
|
175
|
-
const
|
|
192
|
+
const plannedThisPass = refuseDirtyHookWrites(buildPlans(findings));
|
|
193
|
+
if (plannedThisPass === "refuse") {
|
|
194
|
+
if (planned.length === 0)
|
|
195
|
+
return refuseDirtyTree();
|
|
196
|
+
write(`Pass ${pass}/${MAX_FIX_PASSES}: remaining repairs write hook files; ` +
|
|
197
|
+
`stopped because the tree is dirty.\n`);
|
|
198
|
+
refused = "dirty-tree";
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
const plans = plannedThisPass;
|
|
176
202
|
if (plans.length === 0)
|
|
177
203
|
break;
|
|
178
204
|
fixableCount += plans.length;
|
package/dist/lib/lanes/spawn.js
CHANGED
|
@@ -677,6 +677,7 @@ export async function retryLaneOnAlternateAccount(hqRoot, lane, reason, writer =
|
|
|
677
677
|
...lane,
|
|
678
678
|
state: "queued",
|
|
679
679
|
queued_reason: "account_retry",
|
|
680
|
+
queued_details: undefined,
|
|
680
681
|
queued_at: at,
|
|
681
682
|
terminal_reason: undefined,
|
|
682
683
|
terminal_details: undefined,
|
package/dist/lib/lanes/store.js
CHANGED
|
@@ -386,6 +386,28 @@ function parseLaneStories(raw, filePath) {
|
|
|
386
386
|
}
|
|
387
387
|
}
|
|
388
388
|
}
|
|
389
|
+
/** Profile, disk, and admit details are only valid for their matching queue reason. */
|
|
390
|
+
function queuedDetailsAreStale(state, queuedReason, detailsReason) {
|
|
391
|
+
if (detailsReason === "worker_profile_changed") {
|
|
392
|
+
return queuedReason !== "worker_profile_changed";
|
|
393
|
+
}
|
|
394
|
+
if (detailsReason === "low_disk") {
|
|
395
|
+
return state !== "queued" || queuedReason !== "low_disk";
|
|
396
|
+
}
|
|
397
|
+
if (detailsReason === "admit_failed") {
|
|
398
|
+
return state !== "queued" || queuedReason !== "admit_failed";
|
|
399
|
+
}
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
function withoutStaleQueuedDetails(record) {
|
|
403
|
+
const details = record.queued_details;
|
|
404
|
+
if (details === undefined)
|
|
405
|
+
return record;
|
|
406
|
+
if (!queuedDetailsAreStale(record.state, record.queued_reason, details.reason)) {
|
|
407
|
+
return record;
|
|
408
|
+
}
|
|
409
|
+
return { ...record, queued_details: undefined };
|
|
410
|
+
}
|
|
389
411
|
export function parseLaneRecord(raw, filePath) {
|
|
390
412
|
if (!isPlainObject(raw)) {
|
|
391
413
|
throw new LaneSchemaError("Lane record must be a JSON object", {
|
|
@@ -638,7 +660,11 @@ export function parseLaneRecord(raw, filePath) {
|
|
|
638
660
|
});
|
|
639
661
|
}
|
|
640
662
|
const detailsRecord = details;
|
|
641
|
-
if (detailsRecord.reason
|
|
663
|
+
if (queuedDetailsAreStale(raw.state, raw.queued_reason, detailsRecord.reason)) {
|
|
664
|
+
normalized = { ...raw };
|
|
665
|
+
delete normalized.queued_details;
|
|
666
|
+
}
|
|
667
|
+
else if (detailsRecord.reason === "worker_profile_changed") {
|
|
642
668
|
if ((detailsRecord.expected_hash !== null && typeof detailsRecord.expected_hash !== "string") ||
|
|
643
669
|
typeof detailsRecord.actual_hash !== "string" || detailsRecord.actual_hash === "" ||
|
|
644
670
|
typeof detailsRecord.recovery_command !== "string" || detailsRecord.recovery_command.trim() === "") {
|
|
@@ -647,17 +673,6 @@ export function parseLaneRecord(raw, filePath) {
|
|
|
647
673
|
field: "queued_details",
|
|
648
674
|
});
|
|
649
675
|
}
|
|
650
|
-
if (raw.queued_reason !== "worker_profile_changed") {
|
|
651
|
-
if (raw.queued_reason === undefined) {
|
|
652
|
-
throw new LaneSchemaError("queued_details requires queued_reason worker_profile_changed", {
|
|
653
|
-
path: filePath,
|
|
654
|
-
field: "queued_details",
|
|
655
|
-
queued_reason: raw.queued_reason,
|
|
656
|
-
});
|
|
657
|
-
}
|
|
658
|
-
normalized = { ...raw };
|
|
659
|
-
delete normalized.queued_details;
|
|
660
|
-
}
|
|
661
676
|
}
|
|
662
677
|
else if (detailsRecord.reason === "outpost_dispatch_unknown") {
|
|
663
678
|
if (raw.state !== "queued" ||
|
|
@@ -1505,10 +1520,10 @@ export function readAllLanes(hqRoot, deps = defaultStoreDeps()) {
|
|
|
1505
1520
|
const identity = indexedDeps.outpostIdentity();
|
|
1506
1521
|
const isRemoteMirror = outpostId !== undefined && (identity.kind === "not_outpost" ||
|
|
1507
1522
|
(identity.kind === "id" && identity.id !== outpostId));
|
|
1508
|
-
if (!isRemoteMirror)
|
|
1509
|
-
throw err;
|
|
1510
1523
|
const field = typeof err.details.field === "string" ? err.details.field : "unknown_field";
|
|
1511
|
-
process.stderr.write(
|
|
1524
|
+
process.stderr.write(isRemoteMirror
|
|
1525
|
+
? `lanes store: skipping malformed mirrored lane ${laneId} field ${field}: ${err.message}\n`
|
|
1526
|
+
: `lanes store: skipping unreadable lane ${laneId} field ${field}: ${err.message}\n`);
|
|
1512
1527
|
}
|
|
1513
1528
|
}
|
|
1514
1529
|
return records.sort((a, b) => Date.parse(a.created_at) - Date.parse(b.created_at) ||
|
|
@@ -1631,13 +1646,14 @@ export function createLane(hqRoot, init, deps = defaultStoreDeps()) {
|
|
|
1631
1646
|
senior_history: init.senior_history ?? [],
|
|
1632
1647
|
attribution_history: init.attribution_history ?? [],
|
|
1633
1648
|
};
|
|
1634
|
-
|
|
1649
|
+
const createdRecord = withoutStaleQueuedDetails(record);
|
|
1650
|
+
parseLaneRecord(createdRecord, filePath);
|
|
1635
1651
|
const created = withRecordLock(filePath, deps, () => {
|
|
1636
1652
|
if (deps.existsSync(filePath)) {
|
|
1637
1653
|
throw new LanesError("lane_exists", `Lane ${lane_id} already exists`, { lane_id });
|
|
1638
1654
|
}
|
|
1639
|
-
atomicWriteJson(filePath,
|
|
1640
|
-
return
|
|
1655
|
+
atomicWriteJson(filePath, createdRecord, deps);
|
|
1656
|
+
return createdRecord;
|
|
1641
1657
|
});
|
|
1642
1658
|
return syncStoryLedgerForLane(hqRoot, created, deps);
|
|
1643
1659
|
}
|
|
@@ -1689,30 +1705,30 @@ export function updateLane(hqRoot, laneId, expectedVersion, writer, updater, dep
|
|
|
1689
1705
|
}
|
|
1690
1706
|
if (!isTerminalLaneState(current.state) && isTerminalLaneState(updated.state)) {
|
|
1691
1707
|
const releasedGrant = updated.hire_grant ?? current.hire_grant ?? 0;
|
|
1692
|
-
return {
|
|
1708
|
+
return withoutStaleQueuedDetails({
|
|
1693
1709
|
...updated,
|
|
1694
1710
|
released_hire_grant: releasedGrant,
|
|
1695
1711
|
hire_grant: 0,
|
|
1696
|
-
};
|
|
1712
|
+
});
|
|
1697
1713
|
}
|
|
1698
1714
|
if (isTerminalLaneState(current.state) && !isTerminalLaneState(updated.state) &&
|
|
1699
1715
|
current.released_hire_grant !== undefined) {
|
|
1700
|
-
return {
|
|
1716
|
+
return withoutStaleQueuedDetails({
|
|
1701
1717
|
...updated,
|
|
1702
1718
|
hire_grant: current.released_hire_grant,
|
|
1703
1719
|
released_hire_grant: undefined,
|
|
1704
|
-
};
|
|
1720
|
+
});
|
|
1705
1721
|
}
|
|
1706
1722
|
if (isTerminalLaneState(updated.state) &&
|
|
1707
1723
|
(updated.hire_grant !== current.hire_grant ||
|
|
1708
1724
|
updated.released_hire_grant !== current.released_hire_grant)) {
|
|
1709
|
-
return {
|
|
1725
|
+
return withoutStaleQueuedDetails({
|
|
1710
1726
|
...updated,
|
|
1711
1727
|
hire_grant: 0,
|
|
1712
1728
|
released_hire_grant: updated.released_hire_grant ?? updated.hire_grant ?? 0,
|
|
1713
|
-
};
|
|
1729
|
+
});
|
|
1714
1730
|
}
|
|
1715
|
-
return updated;
|
|
1731
|
+
return withoutStaleQueuedDetails(updated);
|
|
1716
1732
|
}, deps, {
|
|
1717
1733
|
beforeWrite: (current) => assertLaneStoreWriteAllowed(current, laneId, writer),
|
|
1718
1734
|
stamp: (current) => ({ written_by_version: newestLaneStoreVersion(current.written_by_version) }),
|