@indigoai-us/hq-cli 5.345.155 → 5.345.159
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 +289 -0
- package/assets/scaffold/core/scripts/rebuild-company-knowledge-index.sh +110 -2
- package/assets/scaffold/core/scripts/rebuild-projects-index.sh +101 -5
- package/assets/scaffold/core/scripts/worktree.sh +4 -0
- package/dist/command-catalog.generated.d.ts +536 -7
- package/dist/command-catalog.generated.js +676 -24
- package/dist/command-registration-plan.d.ts +18 -0
- package/dist/command-registration-plan.js +4 -0
- package/dist/commands/_patterns.d.ts +17 -0
- package/dist/commands/_patterns.js +22 -0
- package/dist/commands/agent-inbox.js +2 -2
- package/dist/commands/agent-kit.js +8 -6
- package/dist/commands/bot.js +7 -4
- package/dist/commands/channel-suggest.d.ts +56 -0
- package/dist/commands/channel-suggest.js +170 -0
- package/dist/commands/channels.js +7 -1
- package/dist/commands/cloud.d.ts +45 -0
- package/dist/commands/cloud.js +150 -10
- package/dist/commands/company.js +4 -3
- package/dist/commands/core-checkpoint-native.d.ts +18 -0
- package/dist/commands/core-checkpoint-native.js +67 -2
- package/dist/commands/core-pipeline-driver-native.d.ts +12 -1
- package/dist/commands/core-pipeline-driver-native.js +131 -5
- package/dist/commands/core.js +13 -1
- package/dist/commands/crm.js +83 -6
- package/dist/commands/deploy-access.d.ts +33 -0
- package/dist/commands/deploy-access.js +322 -0
- package/dist/commands/dm-attach.d.ts +106 -0
- package/dist/commands/dm-attach.js +184 -0
- package/dist/commands/dm.d.ts +37 -0
- package/dist/commands/dm.js +172 -10
- package/dist/commands/feedback-show.d.ts +40 -0
- package/dist/commands/feedback-show.js +261 -0
- package/dist/commands/feedback.d.ts +21 -0
- package/dist/commands/feedback.js +152 -2
- package/dist/commands/files-browse.d.ts +16 -0
- package/dist/commands/files-browse.js +52 -1
- package/dist/commands/files.d.ts +24 -0
- package/dist/commands/files.js +52 -1
- package/dist/commands/integrations-connect.js +7 -3
- package/dist/commands/integrations-core.d.ts +13 -1
- package/dist/commands/integrations-core.js +49 -4
- package/dist/commands/integrations.js +4 -2
- package/dist/commands/lanes-accounts.js +16 -1
- package/dist/commands/lanes-loop.js +14 -4
- package/dist/commands/lanes-message.js +6 -4
- package/dist/commands/lanes-resume.js +36 -13
- package/dist/commands/lanes-shared.d.ts +5 -0
- package/dist/commands/lanes-shared.js +13 -3
- package/dist/commands/lanes-watch.js +1 -0
- package/dist/commands/lanes.d.ts +20 -1
- package/dist/commands/lanes.js +201 -51
- package/dist/commands/mesh.js +149 -1
- package/dist/commands/monitor.d.ts +1 -0
- package/dist/commands/monitor.js +4 -1
- package/dist/commands/repo.d.ts +41 -0
- package/dist/commands/repo.js +331 -0
- package/dist/commands/scan.d.ts +3 -1
- package/dist/commands/scan.js +7 -3
- package/dist/commands/secrets.d.ts +37 -0
- package/dist/commands/secrets.js +185 -16
- package/dist/commands/signals.js +17 -5
- package/dist/commands/staff/listings.d.ts +41 -0
- package/dist/commands/staff/listings.js +160 -0
- package/dist/commands/staff/reset-test-account.d.ts +3 -2
- package/dist/commands/staff/reset-test-account.js +4 -3
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2629 -1036
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/agent-kit/inbox-state.d.ts +7 -0
- package/dist/lib/agent-kit/inbox-state.js +5 -1
- package/dist/lib/agent-kit/mcp/tools.js +2 -1
- package/dist/lib/agent-kit/run/inbox.js +2 -1
- package/dist/lib/agent-kit/wake-secrets.d.ts +7 -0
- package/dist/lib/agent-kit/wake-secrets.js +32 -0
- package/dist/lib/agent-kit/wake.d.ts +4 -0
- package/dist/lib/agent-kit/wake.js +49 -3
- package/dist/lib/bot/inflight.d.ts +7 -1
- package/dist/lib/bot/inflight.js +10 -2
- package/dist/lib/bot/prompt.js +3 -0
- package/dist/lib/bot/run.js +72 -20
- package/dist/lib/bot/session.d.ts +14 -0
- package/dist/lib/bot/session.js +70 -0
- package/dist/lib/capacity/collectors/claude-oauth.d.ts +6 -0
- package/dist/lib/capacity/collectors/claude-oauth.js +12 -0
- package/dist/lib/capacity/lane-login.d.ts +6 -0
- package/dist/lib/capacity/lane-login.js +5 -0
- package/dist/lib/core-utils/worktree.js +2 -0
- package/dist/lib/daemon/exit-cause.d.ts +43 -0
- package/dist/lib/daemon/exit-cause.js +88 -0
- package/dist/lib/daemon/handoff.d.ts +2 -0
- package/dist/lib/daemon/handoff.js +6 -1
- package/dist/lib/daemon/hq-child-bootstrap.mjs +20 -3
- package/dist/lib/daemon/paths.d.ts +2 -0
- package/dist/lib/daemon/paths.js +1 -0
- package/dist/lib/daemon/run.js +13 -0
- package/dist/lib/daemon/supervisor.d.ts +54 -1
- package/dist/lib/daemon/supervisor.js +297 -21
- package/dist/lib/daemon/unit-holds.d.ts +22 -0
- package/dist/lib/daemon/unit-holds.js +86 -0
- package/dist/lib/doctor/checks/sync-health.d.ts +9 -0
- package/dist/lib/doctor/checks/sync-health.js +66 -1
- package/dist/lib/doctor/company-resolution-state.d.ts +30 -0
- package/dist/lib/doctor/company-resolution-state.js +89 -0
- package/dist/lib/doctor/fix/apply.d.ts +36 -6
- package/dist/lib/doctor/fix/apply.js +128 -12
- package/dist/lib/doctor/types.d.ts +1 -1
- package/dist/lib/doctor/types.js +1 -0
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/hook-engine/resolve.js +6 -2
- package/dist/lib/index-render/company-knowledge.d.ts +1 -1
- package/dist/lib/index-render/company-knowledge.js +4 -5
- package/dist/lib/index-render/index.js +44 -3
- package/dist/lib/index-render/projects.d.ts +1 -1
- package/dist/lib/index-render/projects.js +5 -5
- package/dist/lib/lanes/account-company.d.ts +28 -0
- package/dist/lib/lanes/account-company.js +53 -0
- package/dist/lib/lanes/accounts.d.ts +22 -3
- package/dist/lib/lanes/accounts.js +38 -2
- package/dist/lib/lanes/claude-balance.d.ts +6 -1
- package/dist/lib/lanes/claude-balance.js +6 -1
- package/dist/lib/lanes/claude-credential.d.ts +78 -0
- package/dist/lib/lanes/claude-credential.js +163 -0
- package/dist/lib/lanes/compaction.d.ts +2 -0
- package/dist/lib/lanes/compaction.js +9 -0
- package/dist/lib/lanes/config.js +6 -0
- package/dist/lib/lanes/envelope-repair-dispatch.js +2 -1
- package/dist/lib/lanes/hooks-install.js +2 -1
- package/dist/lib/lanes/host-capacity.d.ts +2 -0
- package/dist/lib/lanes/host-capacity.js +1 -0
- package/dist/lib/lanes/keep-alive.d.ts +1 -1
- package/dist/lib/lanes/keep-alive.js +3 -1
- package/dist/lib/lanes/links.js +10 -4
- package/dist/lib/lanes/loop.js +16 -1
- package/dist/lib/lanes/node-binary.d.ts +17 -0
- package/dist/lib/lanes/node-binary.js +30 -0
- package/dist/lib/lanes/outpost-dispatch.d.ts +2 -1
- package/dist/lib/lanes/outpost-dispatch.js +1 -0
- package/dist/lib/lanes/reconcile.js +4 -0
- package/dist/lib/lanes/remote-control-lane.js +2 -0
- package/dist/lib/lanes/spawn.d.ts +3 -0
- package/dist/lib/lanes/spawn.js +119 -9
- package/dist/lib/lanes/store.d.ts +6 -0
- package/dist/lib/lanes/store.js +107 -18
- package/dist/lib/lanes/types.d.ts +2 -0
- package/dist/lib/lanes/watch.js +28 -5
- package/dist/lib/lanes/watcher-main.js +4 -3
- package/dist/lib/lanes/watcher.d.ts +4 -0
- package/dist/lib/lanes/watcher.js +110 -21
- package/dist/lib/mesh/api.d.ts +32 -0
- package/dist/lib/mesh/api.js +75 -0
- package/dist/lib/secret-scan/detect.js +9 -0
- package/dist/lib/secret-scan/repo-config.d.ts +41 -0
- package/dist/lib/secret-scan/repo-config.js +237 -0
- package/dist/lib/secret-scan/rules.d.ts +7 -1
- package/dist/lib/secret-scan/rules.js +3 -1
- package/dist/lib/secret-scan/staged.d.ts +5 -0
- package/dist/lib/secret-scan/staged.js +19 -2
- package/dist/lib/secret-scan/types.d.ts +2 -0
- package/dist/lib/sync/server-owned-local-only.d.ts +29 -0
- package/dist/lib/sync/server-owned-local-only.js +75 -0
- package/dist/sentry-before-send.js +1 -0
- package/dist/utils/access-denied-hint.js +6 -0
- package/dist/utils/client-health.d.ts +34 -0
- package/dist/utils/client-health.js +96 -3
- package/dist/utils/package-use-lease.js +27 -9
- package/dist/utils/version-gate.d.ts +7 -0
- package/dist/utils/version-gate.js +66 -0
- package/package.json +8 -8
|
@@ -825,7 +825,7 @@ async function settleManagerStatusGate(hqRoot, lane, manifestTurnId, at, deps, l
|
|
|
825
825
|
written = false;
|
|
826
826
|
}
|
|
827
827
|
if (written) {
|
|
828
|
-
return
|
|
828
|
+
return updateLaneFollowup(hqRoot, lane.lane_id, deps, (current) => {
|
|
829
829
|
if (current.status_gate?.turn_id !== gate.turn_id || current.status_gate.consecutive_misses === 0)
|
|
830
830
|
return current;
|
|
831
831
|
return { ...current, status_gate: { ...current.status_gate, consecutive_misses: 0 } };
|
|
@@ -837,7 +837,7 @@ async function settleManagerStatusGate(hqRoot, lane, manifestTurnId, at, deps, l
|
|
|
837
837
|
path: gate.status_path,
|
|
838
838
|
at,
|
|
839
839
|
identified_by: target.identified_by ?? "juniors",
|
|
840
|
-
}, resumeText);
|
|
840
|
+
}, resumeText, (current) => followupRunMoved(hqRoot, lane.lane_id, deps, current));
|
|
841
841
|
const miss = updated.status_gate_history?.find((entry) => entry.event === "status_missed" && entry.turn_id === gate.turn_id);
|
|
842
842
|
if (miss) {
|
|
843
843
|
logDecision({
|
|
@@ -876,7 +876,7 @@ async function settleBlockerFreshness(hqRoot, lane, manifestTurnId, at, deps, me
|
|
|
876
876
|
if (resolution.stale.length === 0) {
|
|
877
877
|
if (lane.blocker_freshness_state === undefined)
|
|
878
878
|
return lane;
|
|
879
|
-
return
|
|
879
|
+
return updateLaneFollowup(hqRoot, lane.lane_id, deps, (current) => current.blocker_freshness_state === undefined
|
|
880
880
|
? current
|
|
881
881
|
: { ...current, blocker_freshness_state: undefined });
|
|
882
882
|
}
|
|
@@ -895,7 +895,7 @@ async function settleBlockerFreshness(hqRoot, lane, manifestTurnId, at, deps, me
|
|
|
895
895
|
? findBlockerFreshnessSessionInbox(hqRoot, lane.senior.id)
|
|
896
896
|
: { reason: "The session senior has no session id" }
|
|
897
897
|
: undefined;
|
|
898
|
-
let updated =
|
|
898
|
+
let updated = updateLaneFollowup(hqRoot, lane.lane_id, deps, (current) => {
|
|
899
899
|
deliverEscalation = false;
|
|
900
900
|
deliverFallback = false;
|
|
901
901
|
retryUndeliverable = false;
|
|
@@ -1014,7 +1014,7 @@ async function settleBlockerFreshness(hqRoot, lane, manifestTurnId, at, deps, me
|
|
|
1014
1014
|
kind: "blocker_stale_escalation_undeliverable",
|
|
1015
1015
|
text: `${escalationText} The session senior inbox is unavailable (${reason}); this escalation is recorded in the lane inbox for its next turn.`,
|
|
1016
1016
|
});
|
|
1017
|
-
updated =
|
|
1017
|
+
updated = updateLaneFollowup(hqRoot, lane.lane_id, deps, (current) => {
|
|
1018
1018
|
const state = current.blocker_freshness_state;
|
|
1019
1019
|
if (state === undefined)
|
|
1020
1020
|
return current;
|
|
@@ -1037,7 +1037,7 @@ async function settleBlockerFreshness(hqRoot, lane, manifestTurnId, at, deps, me
|
|
|
1037
1037
|
const message = error instanceof Error ? error.message : String(error);
|
|
1038
1038
|
process.stderr.write(`hq lanes watcher: blocker freshness fallback delivery failed for ${lane.lane_id}: ${message}\n`);
|
|
1039
1039
|
logDecision({ event: "blocker_stale_escalation_fallback_failed", at, turn_id: turnId, fingerprint, error: message });
|
|
1040
|
-
updated =
|
|
1040
|
+
updated = updateLaneFollowup(hqRoot, lane.lane_id, deps, (current) => {
|
|
1041
1041
|
const state = current.blocker_freshness_state;
|
|
1042
1042
|
const failureReason = `${undeliverableReason ?? `Session ${lane.senior.id ?? "unknown"} has no monitor-session inbox`}; lane inbox fallback failed: ${message}`;
|
|
1043
1043
|
return {
|
|
@@ -1080,7 +1080,7 @@ async function settleBlockerFreshness(hqRoot, lane, manifestTurnId, at, deps, me
|
|
|
1080
1080
|
}
|
|
1081
1081
|
logDecision({ event: "blocker_stale_escalation_delivered", at, turn_id: turnId, senior_kind: lane.senior.kind });
|
|
1082
1082
|
if (retryUndeliverable) {
|
|
1083
|
-
updated =
|
|
1083
|
+
updated = updateLaneFollowup(hqRoot, lane.lane_id, deps, (current) => {
|
|
1084
1084
|
const state = current.blocker_freshness_state;
|
|
1085
1085
|
const failedFallbackStillOpen = (current.status_gate_history ?? []).some((entry) => entry.event === "blocker_stale_escalation_undeliverable" &&
|
|
1086
1086
|
entry.fingerprint === fingerprint && entry.delivery_failed === true &&
|
|
@@ -1104,7 +1104,7 @@ async function settleBlockerFreshness(hqRoot, lane, manifestTurnId, at, deps, me
|
|
|
1104
1104
|
const message = error instanceof Error ? error.message : String(error);
|
|
1105
1105
|
process.stderr.write(`hq lanes watcher: blocker freshness escalation delivery failed for ${lane.lane_id}: ${message}\n`);
|
|
1106
1106
|
logDecision({ event: "blocker_stale_escalation_delivery_failed", at, turn_id: turnId, error: message });
|
|
1107
|
-
updated =
|
|
1107
|
+
updated = updateLaneFollowup(hqRoot, lane.lane_id, deps, (current) => {
|
|
1108
1108
|
const state = current.blocker_freshness_state;
|
|
1109
1109
|
if (state?.escalated_fingerprint !== fingerprint)
|
|
1110
1110
|
return current;
|
|
@@ -1151,7 +1151,7 @@ async function settleKrFreshness(hqRoot, lane, manifestTurnId, at, deps, logDeci
|
|
|
1151
1151
|
if (stalled.length === 0) {
|
|
1152
1152
|
if (lane.kr_freshness_state === undefined)
|
|
1153
1153
|
return lane;
|
|
1154
|
-
return
|
|
1154
|
+
return updateLaneFollowup(hqRoot, lane.lane_id, deps, (current) => current.kr_freshness_state === undefined
|
|
1155
1155
|
? current
|
|
1156
1156
|
: { ...current, kr_freshness_state: undefined });
|
|
1157
1157
|
}
|
|
@@ -1159,7 +1159,7 @@ async function settleKrFreshness(hqRoot, lane, manifestTurnId, at, deps, logDeci
|
|
|
1159
1159
|
const names = stalled.map((kpi) => kpi.name);
|
|
1160
1160
|
const turnId = manifestTurnId ?? lane.status_gate?.turn_id ?? `unknown-${at}`;
|
|
1161
1161
|
const resumeText = `These KPIs in ${resolution.statusPath} were not re-measured in the last ${resolution.windowHours}h: ${names.join(", ")}. Measure each one now, update value and measured_at, then end the turn.`;
|
|
1162
|
-
return
|
|
1162
|
+
return updateLaneFollowup(hqRoot, lane.lane_id, deps, (current) => {
|
|
1163
1163
|
const history = current.status_gate_history ?? [];
|
|
1164
1164
|
if (history.some((entry) => entry.event === "kr_stalled" && entry.turn_id === turnId))
|
|
1165
1165
|
return current;
|
|
@@ -1297,6 +1297,38 @@ function defaultSleep(ms) {
|
|
|
1297
1297
|
});
|
|
1298
1298
|
}
|
|
1299
1299
|
const WATCHER_LOOP_OWNER_FILE = "watcher-loop.pid";
|
|
1300
|
+
/** Run identity (spawn manifest envelope path) of the current claimant. */
|
|
1301
|
+
const WATCHER_LOOP_RUN_FILE = "watcher-loop.run";
|
|
1302
|
+
/** How long a new phase watcher waits for an older phase's watcher to let go. */
|
|
1303
|
+
export const WATCHER_CLAIM_WAIT_MS = 60_000;
|
|
1304
|
+
const WATCHER_CLAIM_RETRY_MS = 250;
|
|
1305
|
+
/** Identifies one worker run of a lane; changes on every resume. */
|
|
1306
|
+
function watcherRunId(runDir) {
|
|
1307
|
+
try {
|
|
1308
|
+
return readSpawnManifest(runDir).envelope_path ?? "";
|
|
1309
|
+
}
|
|
1310
|
+
catch {
|
|
1311
|
+
return "";
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
/** True when the lane has moved on to a run this watcher does not own. */
|
|
1315
|
+
function followupRunMoved(hqRoot, laneId, deps, current) {
|
|
1316
|
+
if (deps.followupRun === undefined)
|
|
1317
|
+
return false;
|
|
1318
|
+
return current.state === "running" || watcherRunId(laneRunDir(hqRoot, laneId)) !== deps.followupRun;
|
|
1319
|
+
}
|
|
1320
|
+
/** Watcher lane write that is a no-op once the lane belongs to a newer run. */
|
|
1321
|
+
function updateLaneFollowup(hqRoot, laneId, deps, updater) {
|
|
1322
|
+
return retryUpdateLane(hqRoot, laneId, "watcher", (current) => followupRunMoved(hqRoot, laneId, deps, current) ? current : updater(current));
|
|
1323
|
+
}
|
|
1324
|
+
function readClaimRunId(runDir) {
|
|
1325
|
+
try {
|
|
1326
|
+
return fs.readFileSync(path.join(runDir, WATCHER_LOOP_RUN_FILE), "utf8").trim();
|
|
1327
|
+
}
|
|
1328
|
+
catch {
|
|
1329
|
+
return null;
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1300
1332
|
function claimWatcherLoop(hqRoot, laneId, deps) {
|
|
1301
1333
|
const runDir = laneRunDir(hqRoot, laneId);
|
|
1302
1334
|
const ownerPath = path.join(runDir, WATCHER_LOOP_OWNER_FILE);
|
|
@@ -1310,6 +1342,7 @@ function claimWatcherLoop(hqRoot, laneId, deps) {
|
|
|
1310
1342
|
}
|
|
1311
1343
|
if (existing !== null)
|
|
1312
1344
|
fs.unlinkSync(ownerPath);
|
|
1345
|
+
fs.writeFileSync(path.join(runDir, WATCHER_LOOP_RUN_FILE), `${watcherRunId(runDir)}\n`, "utf8");
|
|
1313
1346
|
writePidRecord(ownerPath, {
|
|
1314
1347
|
pid: process.pid,
|
|
1315
1348
|
starttime: readStarttime(process.pid) ?? readProcStarttime(process.pid) ?? undefined,
|
|
@@ -1319,7 +1352,11 @@ function claimWatcherLoop(hqRoot, laneId, deps) {
|
|
|
1319
1352
|
});
|
|
1320
1353
|
if (owner === null)
|
|
1321
1354
|
return null;
|
|
1355
|
+
let released = false;
|
|
1322
1356
|
return () => {
|
|
1357
|
+
if (released)
|
|
1358
|
+
return;
|
|
1359
|
+
released = true;
|
|
1323
1360
|
withLaneHireLocks(hqRoot, [laneId], () => {
|
|
1324
1361
|
try {
|
|
1325
1362
|
const current = fs.statSync(ownerPath);
|
|
@@ -1391,7 +1428,7 @@ async function maybeContinueAfterMissingEnvelopeRepair(hqRoot, lane, envelope, e
|
|
|
1391
1428
|
}
|
|
1392
1429
|
if (!enabled)
|
|
1393
1430
|
return lane;
|
|
1394
|
-
const queued =
|
|
1431
|
+
const queued = updateLaneFollowup(hqRoot, lane.lane_id, deps, (current) => {
|
|
1395
1432
|
if (current.version !== lane.version ||
|
|
1396
1433
|
(current.state !== "done" && current.state !== "blocked") ||
|
|
1397
1434
|
current.last_envelope?.ref !== envelopeRef ||
|
|
@@ -1421,17 +1458,59 @@ async function maybeContinueAfterMissingEnvelopeRepair(hqRoot, lane, envelope, e
|
|
|
1421
1458
|
}
|
|
1422
1459
|
/** Detached and synchronous classifiers share one process-identity claim. */
|
|
1423
1460
|
export async function runWatcherLoop(hqRoot, laneId, deps = {}) {
|
|
1424
|
-
|
|
1425
|
-
if (release === null)
|
|
1426
|
-
|
|
1461
|
+
let release = claimWatcherLoop(hqRoot, laneId, deps);
|
|
1462
|
+
if (release === null) {
|
|
1463
|
+
// A live claimant from an older run (e.g. the previous phase's watcher
|
|
1464
|
+
// still finishing its post-terminal mesh call) lets go soon. Wait for it
|
|
1465
|
+
// instead of leaving this run unwatched. A claimant on the same run is a
|
|
1466
|
+
// true duplicate and this watcher exits at once.
|
|
1467
|
+
const sleep = deps.sleep ?? defaultSleep;
|
|
1468
|
+
const now = deps.now ?? (() => Date.now());
|
|
1469
|
+
const runDir = laneRunDir(hqRoot, laneId);
|
|
1470
|
+
const myRun = watcherRunId(runDir);
|
|
1471
|
+
const waitMs = deps.claimWaitMs ?? WATCHER_CLAIM_WAIT_MS;
|
|
1472
|
+
const started = now();
|
|
1473
|
+
let claimantRun = readClaimRunId(runDir);
|
|
1474
|
+
while (release === null && claimantRun !== myRun && now() - started < waitMs) {
|
|
1475
|
+
await sleep(WATCHER_CLAIM_RETRY_MS);
|
|
1476
|
+
release = claimWatcherLoop(hqRoot, laneId, deps);
|
|
1477
|
+
if (release === null)
|
|
1478
|
+
claimantRun = readClaimRunId(runDir);
|
|
1479
|
+
}
|
|
1480
|
+
if (release === null) {
|
|
1481
|
+
let claimantPid = null;
|
|
1482
|
+
try {
|
|
1483
|
+
claimantPid = readPidRecord(path.join(runDir, WATCHER_LOOP_OWNER_FILE))?.pid ?? null;
|
|
1484
|
+
}
|
|
1485
|
+
catch {
|
|
1486
|
+
claimantPid = null;
|
|
1487
|
+
}
|
|
1488
|
+
const record = {
|
|
1489
|
+
event: "watcher_claim_lost",
|
|
1490
|
+
at: new Date(now()).toISOString(),
|
|
1491
|
+
claimant_pid: claimantPid,
|
|
1492
|
+
claimant_run: claimantRun,
|
|
1493
|
+
run: myRun,
|
|
1494
|
+
waited_ms: now() - started,
|
|
1495
|
+
};
|
|
1496
|
+
try {
|
|
1497
|
+
writeWatcherDecision(deps.watcherOutPath ?? watcherOutLogPath(hqRoot, laneId), record, deps.watcherOutMaxBytes ?? WATCHER_OUT_MAX_BYTES);
|
|
1498
|
+
}
|
|
1499
|
+
catch {
|
|
1500
|
+
// Logging is best effort; stderr below still records the loss.
|
|
1501
|
+
}
|
|
1502
|
+
process.stderr.write(`hq lanes watcher: claim lost for ${laneId} to pid ${claimantPid ?? "unknown"}\n`);
|
|
1503
|
+
return;
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1427
1506
|
try {
|
|
1428
|
-
await runWatcherLoopOwned(hqRoot, laneId, deps);
|
|
1507
|
+
await runWatcherLoopOwned(hqRoot, laneId, deps, release);
|
|
1429
1508
|
}
|
|
1430
1509
|
finally {
|
|
1431
1510
|
release();
|
|
1432
1511
|
}
|
|
1433
1512
|
}
|
|
1434
|
-
async function runWatcherLoopOwned(hqRoot, laneId, deps) {
|
|
1513
|
+
async function runWatcherLoopOwned(hqRoot, laneId, deps, releaseClaim = () => { }) {
|
|
1435
1514
|
const sleep = deps.sleep ?? defaultSleep;
|
|
1436
1515
|
const now = deps.now ?? (() => Date.now());
|
|
1437
1516
|
const isAlive = deps.isAlive ?? isPidAlive;
|
|
@@ -1459,6 +1538,7 @@ async function runWatcherLoopOwned(hqRoot, laneId, deps) {
|
|
|
1459
1538
|
const mesh = deps.mesh === undefined ? createLaneMesh({ interactive: false }) : deps.mesh;
|
|
1460
1539
|
const storyStatusSuppressions = new Set();
|
|
1461
1540
|
const meshRequestTimeoutMs = deps.meshRequestTimeoutMs ?? 10_000;
|
|
1541
|
+
const ownRun = watcherRunId(runDir);
|
|
1462
1542
|
const initialLane = readLaneSoft(hqRoot, laneId);
|
|
1463
1543
|
if (mesh && initialLane) {
|
|
1464
1544
|
// Every armed watcher refreshes the server descriptor. This migrates old
|
|
@@ -1889,6 +1969,12 @@ async function runWatcherLoopOwned(hqRoot, laneId, deps) {
|
|
|
1889
1969
|
state: updated.state,
|
|
1890
1970
|
version: updated.version,
|
|
1891
1971
|
});
|
|
1972
|
+
// The lane is classified and this watcher stops polling after this
|
|
1973
|
+
// tick. Let go of the single-owner claim before the slow mesh and
|
|
1974
|
+
// follow-up awaits, so a watcher armed for the next phase can claim.
|
|
1975
|
+
if (tick.done)
|
|
1976
|
+
releaseClaim();
|
|
1977
|
+
const followupDeps = tick.done ? { ...deps, followupRun: ownRun } : deps;
|
|
1892
1978
|
// Spec §8 terminal rows: done → lane_done + story review; blocked →
|
|
1893
1979
|
// lane_blocked; ask → lane_ask with the queue ids. After the state
|
|
1894
1980
|
// write, so a mesh failure can never leave the lane unclassified.
|
|
@@ -1914,9 +2000,9 @@ async function runWatcherLoopOwned(hqRoot, laneId, deps) {
|
|
|
1914
2000
|
}
|
|
1915
2001
|
}
|
|
1916
2002
|
if (terminalWrite.changed) {
|
|
1917
|
-
updated = await settleManagerStatusGate(hqRoot, updated, manifest.turn_id, tick.emit.at,
|
|
1918
|
-
updated = await settleBlockerFreshness(hqRoot, updated, manifest.turn_id, tick.emit.at,
|
|
1919
|
-
updated = await settleKrFreshness(hqRoot, updated, manifest.turn_id, tick.emit.at,
|
|
2003
|
+
updated = await settleManagerStatusGate(hqRoot, updated, manifest.turn_id, tick.emit.at, followupDeps, logDecision);
|
|
2004
|
+
updated = await settleBlockerFreshness(hqRoot, updated, manifest.turn_id, tick.emit.at, followupDeps, mesh, logDecision);
|
|
2005
|
+
updated = await settleKrFreshness(hqRoot, updated, manifest.turn_id, tick.emit.at, followupDeps, logDecision);
|
|
1920
2006
|
}
|
|
1921
2007
|
const managerStatusFollowupQueued = manifest.turn_id !== undefined &&
|
|
1922
2008
|
updated.status_gate_history?.some((entry) => "turn_id" in entry && entry.turn_id === manifest.turn_id &&
|
|
@@ -1924,11 +2010,14 @@ async function runWatcherLoopOwned(hqRoot, laneId, deps) {
|
|
|
1924
2010
|
(entry.event === "blocker_stale" && entry.resume_queued === true) ||
|
|
1925
2011
|
(entry.event === "kr_stalled" && entry.resume_queued === true))) === true;
|
|
1926
2012
|
if (terminalWrite.changed && !managerStatusFollowupQueued) {
|
|
1927
|
-
updated = await maybeContinueAfterMissingEnvelopeRepair(hqRoot, updated, effectiveEnvelope ?? null, latest?.ref, tick.emit.at,
|
|
2013
|
+
updated = await maybeContinueAfterMissingEnvelopeRepair(hqRoot, updated, effectiveEnvelope ?? null, latest?.ref, tick.emit.at, followupDeps, logDecision);
|
|
1928
2014
|
}
|
|
1929
2015
|
if (terminalWrite.changed && deps.recoverQueuedMessages && !managerStatusFollowupQueued && updated.state !== "queued") {
|
|
1930
2016
|
try {
|
|
1931
|
-
if (
|
|
2017
|
+
if (followupRunMoved(hqRoot, laneId, followupDeps, readLane(hqRoot, laneId))) {
|
|
2018
|
+
logDecision({ event: "message_recovery_skipped", at: tick.emit.at, reason: "newer_run" });
|
|
2019
|
+
}
|
|
2020
|
+
else if (dropboxInboxHasMessages(hqRoot, laneId)) {
|
|
1932
2021
|
await deps.recoverQueuedMessages(updated);
|
|
1933
2022
|
logDecision({
|
|
1934
2023
|
event: "message_recovery",
|
package/dist/lib/mesh/api.d.ts
CHANGED
|
@@ -176,6 +176,38 @@ export declare function putProjectView(token: string, companyUid: string, projec
|
|
|
176
176
|
}): Promise<MeshProjectView>;
|
|
177
177
|
/** Register the project genesis thread and deterministic project channel. */
|
|
178
178
|
export declare function registerMeshProject(token: string, companyUid: string, projectId: string, channelId?: string): Promise<MeshProjectRegistration>;
|
|
179
|
+
export declare const PROJECT_LIFECYCLE_STATUSES: readonly ["active", "completed", "archived"];
|
|
180
|
+
export type MeshProjectLifecycleStatus = (typeof PROJECT_LIFECYCLE_STATUSES)[number];
|
|
181
|
+
/** One row of the server's PROJECT_LIFECYCLE record for a project. */
|
|
182
|
+
export interface MeshProjectLifecycle {
|
|
183
|
+
projectId: string;
|
|
184
|
+
status: MeshProjectLifecycleStatus;
|
|
185
|
+
/** OCC version of the lifecycle row (not the Board version). */
|
|
186
|
+
version: number;
|
|
187
|
+
updatedAt: string;
|
|
188
|
+
updatedBy: string;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* List every lifecycle row for a company (follows the 25-row server pages).
|
|
192
|
+
* A project with no row has never left `active` and is not listed.
|
|
193
|
+
*/
|
|
194
|
+
export declare function listProjectLifecycles(token: string, companyUid: string): Promise<MeshProjectLifecycle[]>;
|
|
195
|
+
/** The lifecycle row for one project, or null when it has never been changed. */
|
|
196
|
+
export declare function getProjectLifecycle(token: string, companyUid: string, projectId: string): Promise<MeshProjectLifecycle | null>;
|
|
197
|
+
/**
|
|
198
|
+
* PATCH /v1/work-mesh/projects/{id}/lifecycle. `expectedVersion` is the
|
|
199
|
+
* current lifecycle row version (0 when the project has no row yet). The
|
|
200
|
+
* server answers 409 BOARD_NONEMPTY when archiving a Board that still has
|
|
201
|
+
* stories unless `confirmNonempty` is set, and a plain 409 on a version
|
|
202
|
+
* mismatch. Nothing is deleted: stories, folder, channel, members and
|
|
203
|
+
* history stay; the project only leaves the active list and the organize
|
|
204
|
+
* candidates.
|
|
205
|
+
*/
|
|
206
|
+
export declare function setProjectLifecycle(token: string, companyUid: string, projectId: string, change: {
|
|
207
|
+
status: MeshProjectLifecycleStatus;
|
|
208
|
+
expectedVersion: number;
|
|
209
|
+
confirmNonempty?: boolean;
|
|
210
|
+
}): Promise<MeshProjectLifecycle>;
|
|
179
211
|
export declare function pairDmUidsFromInbox(inbox: unknown): string[];
|
|
180
212
|
export interface WarmCacheResult {
|
|
181
213
|
directory: boolean;
|
package/dist/lib/mesh/api.js
CHANGED
|
@@ -515,6 +515,81 @@ export async function registerMeshProject(token, companyUid, projectId, channelI
|
|
|
515
515
|
channelCreated: result?.channelCreated === true,
|
|
516
516
|
};
|
|
517
517
|
}
|
|
518
|
+
// ---------------------------------------------------------------------------
|
|
519
|
+
// Project lifecycle (active | completed | archived)
|
|
520
|
+
// ---------------------------------------------------------------------------
|
|
521
|
+
export const PROJECT_LIFECYCLE_STATUSES = ["active", "completed", "archived"];
|
|
522
|
+
function lifecycleFromRow(row) {
|
|
523
|
+
const rec = asRecord(row);
|
|
524
|
+
const projectId = String(rec?.projectId || "").trim();
|
|
525
|
+
const status = String(rec?.status || "").trim();
|
|
526
|
+
const version = Number(rec?.version);
|
|
527
|
+
if (!projectId || !Number.isSafeInteger(version) || version < 0)
|
|
528
|
+
return null;
|
|
529
|
+
if (!PROJECT_LIFECYCLE_STATUSES.includes(status))
|
|
530
|
+
return null;
|
|
531
|
+
return {
|
|
532
|
+
projectId,
|
|
533
|
+
status: status,
|
|
534
|
+
version,
|
|
535
|
+
updatedAt: String(rec?.updatedAt || ""),
|
|
536
|
+
updatedBy: String(rec?.updatedBy || ""),
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* List every lifecycle row for a company (follows the 25-row server pages).
|
|
541
|
+
* A project with no row has never left `active` and is not listed.
|
|
542
|
+
*/
|
|
543
|
+
export async function listProjectLifecycles(token, companyUid) {
|
|
544
|
+
const rows = [];
|
|
545
|
+
let cursor;
|
|
546
|
+
// Bounded so a server that keeps returning the same cursor cannot loop forever.
|
|
547
|
+
for (let page = 0; page < 200; page += 1) {
|
|
548
|
+
const data = asRecord(await meshJson(token, "/v1/work-mesh/project-lifecycle", {
|
|
549
|
+
query: { companyUid, ...(cursor ? { cursor } : {}) },
|
|
550
|
+
}));
|
|
551
|
+
const projects = Array.isArray(data?.projects) ? data.projects : [];
|
|
552
|
+
for (const row of projects) {
|
|
553
|
+
const parsed = lifecycleFromRow(row);
|
|
554
|
+
if (parsed)
|
|
555
|
+
rows.push(parsed);
|
|
556
|
+
}
|
|
557
|
+
const next = typeof data?.nextCursor === "string" ? data.nextCursor.trim() : "";
|
|
558
|
+
if (!next || next === cursor)
|
|
559
|
+
break;
|
|
560
|
+
cursor = next;
|
|
561
|
+
}
|
|
562
|
+
return rows;
|
|
563
|
+
}
|
|
564
|
+
/** The lifecycle row for one project, or null when it has never been changed. */
|
|
565
|
+
export async function getProjectLifecycle(token, companyUid, projectId) {
|
|
566
|
+
const rows = await listProjectLifecycles(token, companyUid);
|
|
567
|
+
return rows.find((row) => row.projectId === projectId) ?? null;
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* PATCH /v1/work-mesh/projects/{id}/lifecycle. `expectedVersion` is the
|
|
571
|
+
* current lifecycle row version (0 when the project has no row yet). The
|
|
572
|
+
* server answers 409 BOARD_NONEMPTY when archiving a Board that still has
|
|
573
|
+
* stories unless `confirmNonempty` is set, and a plain 409 on a version
|
|
574
|
+
* mismatch. Nothing is deleted: stories, folder, channel, members and
|
|
575
|
+
* history stay; the project only leaves the active list and the organize
|
|
576
|
+
* candidates.
|
|
577
|
+
*/
|
|
578
|
+
export async function setProjectLifecycle(token, companyUid, projectId, change) {
|
|
579
|
+
const data = await meshJson(token, `/v1/work-mesh/projects/${encodeURIComponent(projectId)}/lifecycle`, {
|
|
580
|
+
method: "PATCH",
|
|
581
|
+
query: { companyUid },
|
|
582
|
+
body: {
|
|
583
|
+
status: change.status,
|
|
584
|
+
expectedVersion: change.expectedVersion,
|
|
585
|
+
...(change.confirmNonempty ? { confirmNonempty: true } : {}),
|
|
586
|
+
},
|
|
587
|
+
});
|
|
588
|
+
const parsed = lifecycleFromRow(data);
|
|
589
|
+
if (!parsed)
|
|
590
|
+
throw new Error("Work mesh returned an invalid project lifecycle row");
|
|
591
|
+
return parsed;
|
|
592
|
+
}
|
|
518
593
|
export function pairDmUidsFromInbox(inbox) {
|
|
519
594
|
const rec = asRecord(inbox);
|
|
520
595
|
const ids = new Set();
|
|
@@ -36,6 +36,8 @@ function allowed(al, c) {
|
|
|
36
36
|
const lower = c.secret.toLowerCase();
|
|
37
37
|
checks.push(al.stopwords.some((s) => lower.includes(s)));
|
|
38
38
|
}
|
|
39
|
+
if (al.commits.length)
|
|
40
|
+
checks.push(c.commit !== undefined && al.commits.includes(c.commit.toLowerCase()));
|
|
39
41
|
if (!checks.length)
|
|
40
42
|
return false;
|
|
41
43
|
return al.condition === "AND" ? checks.every(Boolean) : checks.some(Boolean);
|
|
@@ -50,6 +52,10 @@ function lineBounds(text, start, end) {
|
|
|
50
52
|
offset++;
|
|
51
53
|
return { line: text.slice(from, to), offset };
|
|
52
54
|
}
|
|
55
|
+
/** A repo allowlist applies to this rule (no targetRules means every rule). */
|
|
56
|
+
function repoAllowed(rules, ruleId, c) {
|
|
57
|
+
return (rules.repo ?? []).some((al) => (!al.targetRules.length || al.targetRules.includes(ruleId)) && allowed(al, c));
|
|
58
|
+
}
|
|
53
59
|
/** True when the global allowlist excludes this path from scanning. */
|
|
54
60
|
export function pathIgnored(file, rules = compiledRules()) {
|
|
55
61
|
return rules.global.paths.some((p) => p.test(file));
|
|
@@ -60,6 +66,7 @@ export function scanPath(file, rules = compiledRules()) {
|
|
|
60
66
|
return [];
|
|
61
67
|
return rules.rules
|
|
62
68
|
.filter((r) => !r.regex && r.path?.test(file))
|
|
69
|
+
.filter((r) => !repoAllowed(rules, r.id, { file, secret: "", match: "", line: "" }))
|
|
63
70
|
.map((r) => ({ ruleId: r.id, description: r.description, file, line: 1 }));
|
|
64
71
|
}
|
|
65
72
|
export function scanFragment(frag, rules = compiledRules()) {
|
|
@@ -103,6 +110,8 @@ export function scanFragment(frag, rules = compiledRules()) {
|
|
|
103
110
|
continue;
|
|
104
111
|
if (rule.allowlists.some((al) => allowed(al, cand)))
|
|
105
112
|
continue;
|
|
113
|
+
if (repoAllowed(rules, rule.id, cand))
|
|
114
|
+
continue;
|
|
106
115
|
const lineNo = startLine + offset;
|
|
107
116
|
const key = `${rule.id}:${lineNo}`;
|
|
108
117
|
if (seen.has(key))
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repo-level allowlists: `[[allowlists]]` (and the legacy `[allowlist]`) from
|
|
3
|
+
* the scanned repo's own `.gitleaks.toml`, with gitleaks semantics for
|
|
4
|
+
* `paths`, `regexes`, `regexTarget`, `stopwords`, `commits`, `targetRules` and
|
|
5
|
+
* `condition`. They apply on top of the built-in rules and allowlists, to that
|
|
6
|
+
* repo only. Repo `[[rules]]`, `[extend]` and the rest of the file are ignored:
|
|
7
|
+
* a repo can allow known false positives, not change what is detected.
|
|
8
|
+
*
|
|
9
|
+
* The file is read from git, never the working tree:
|
|
10
|
+
* - The staged (index) copy applies only when it is identical to the copy
|
|
11
|
+
* already committed at HEAD (or at MERGE_HEAD during a merge). A commit
|
|
12
|
+
* that changes the allowlist is scanned with the HEAD allowlist, so
|
|
13
|
+
* widening the allowlist and adding the secret cannot happen in one commit.
|
|
14
|
+
* - A changed staged copy is still validated, so a broken file fails now.
|
|
15
|
+
* - A symlinked .gitleaks.toml is rejected.
|
|
16
|
+
*
|
|
17
|
+
* Narrow by construction: every allowlist needs `paths`, each anchored with a
|
|
18
|
+
* literal prefix (`^golden/...`); `regexes` and `stopwords` need
|
|
19
|
+
* `condition = "AND"`, so they never reach files outside those paths.
|
|
20
|
+
*
|
|
21
|
+
* Fails closed: anything not acceptable is a ScanConfigError, and the scan
|
|
22
|
+
* exits 2 rather than running with a guess.
|
|
23
|
+
*/
|
|
24
|
+
import { type CompiledAllowlist } from "./rules.js";
|
|
25
|
+
export declare class ScanConfigError extends Error {
|
|
26
|
+
}
|
|
27
|
+
export declare const REPO_CONFIG_FILE = ".gitleaks.toml";
|
|
28
|
+
/**
|
|
29
|
+
* Length of the literal text right after a leading `^`: letters, digits,
|
|
30
|
+
* `/ _ - @ ~` and backslash-escaped punctuation. A quantifier on the last
|
|
31
|
+
* literal makes it optional, so it does not count.
|
|
32
|
+
*/
|
|
33
|
+
export declare function literalPathPrefix(src: string): number;
|
|
34
|
+
/** Validated allowlists from the text of a .gitleaks.toml. */
|
|
35
|
+
export declare function parseRepoAllowlists(text: string, file?: string): CompiledAllowlist[];
|
|
36
|
+
/**
|
|
37
|
+
* Compiled allowlists for a staged scan of the repo at `repoRoot`, read from
|
|
38
|
+
* git as described at the top of this file. [] when the repo has none.
|
|
39
|
+
*/
|
|
40
|
+
export declare function loadRepoAllowlists(repoRoot: string): CompiledAllowlist[];
|
|
41
|
+
//# sourceMappingURL=repo-config.d.ts.map
|