@indigoai-us/hq-cli 5.345.198 → 5.345.200
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 +71 -0
- package/assets/scaffold/core/scripts/lib/session-scope-capability.sh +6 -4
- package/dist/command-catalog.generated.d.ts +150 -6
- package/dist/command-catalog.generated.js +182 -6
- package/dist/commands/bot-events.d.ts +19 -0
- package/dist/commands/bot-events.js +46 -0
- package/dist/commands/bot-jobs.d.ts +2 -0
- package/dist/commands/bot-jobs.js +43 -2
- package/dist/commands/bot-runtime.d.ts +35 -0
- package/dist/commands/bot-runtime.js +115 -0
- package/dist/commands/bot.d.ts +11 -0
- package/dist/commands/bot.js +72 -11
- package/dist/commands/creators.js +2 -1
- package/dist/commands/lanes-report.d.ts +2 -1
- package/dist/commands/lanes-report.js +44 -1
- package/dist/commands/lanes.d.ts +9 -0
- package/dist/commands/lanes.js +95 -9
- package/dist/commands/library-remove.js +2 -1
- package/dist/commands/library.js +5 -4
- package/dist/commands/meetings.js +1 -21
- package/dist/commands/mesh.d.ts +0 -7
- package/dist/commands/mesh.js +5 -1
- package/dist/commands/signals-run.d.ts +2 -0
- package/dist/commands/signals-run.js +6 -1
- package/dist/commands/sync-held.js +2 -2
- package/dist/lanes-drain-hook.js +9 -0
- package/dist/lib/bot/api.d.ts +5 -0
- package/dist/lib/bot/claude-login.d.ts +31 -0
- package/dist/lib/bot/claude-login.js +160 -0
- package/dist/lib/bot/events.d.ts +18 -0
- package/dist/lib/bot/events.js +160 -0
- package/dist/lib/bot/executive-settings.d.ts +10 -0
- package/dist/lib/bot/executive-settings.js +43 -0
- package/dist/lib/bot/fleet-cron.js +4 -0
- package/dist/lib/bot/fleet-download.d.ts +27 -0
- package/dist/lib/bot/fleet-download.js +162 -0
- package/dist/lib/bot/fleet-local.d.ts +51 -2
- package/dist/lib/bot/fleet-local.js +179 -76
- package/dist/lib/bot/fleet-python.d.ts +2 -0
- package/dist/lib/bot/fleet-python.js +29 -0
- package/dist/lib/bot/job-run.js +1 -1
- package/dist/lib/bot/jobs/hot-signal-sync.d.ts +8 -0
- package/dist/lib/bot/jobs/hot-signal-sync.js +2 -0
- package/dist/lib/bot/jobs/meeting-detect.d.ts +28 -0
- package/dist/lib/bot/jobs/meeting-detect.js +176 -0
- package/dist/lib/bot/jobs/meeting-rider.d.ts +53 -0
- package/dist/lib/bot/jobs/meeting-rider.js +284 -0
- package/dist/lib/bot/jobs/signal-to-lane.d.ts +32 -0
- package/dist/lib/bot/jobs/signal-to-lane.js +169 -0
- package/dist/lib/bot/local-jobs.d.ts +15 -2
- package/dist/lib/bot/local-jobs.js +24 -7
- package/dist/lib/bot/run.d.ts +7 -49
- package/dist/lib/bot/run.js +100 -4
- package/dist/lib/bot/status.d.ts +21 -0
- package/dist/lib/doctor/checks/bots.d.ts +6 -0
- package/dist/lib/doctor/checks/bots.js +47 -1
- package/dist/lib/hot-channels/adapters/meetings.d.ts +1 -0
- package/dist/lib/hot-channels/adapters/meetings.js +3 -1
- package/dist/lib/hot-channels/meeting-source-ref.d.ts +3 -0
- package/dist/lib/hot-channels/meeting-source-ref.js +7 -0
- package/dist/lib/hot-channels/schedule.js +2 -2
- package/dist/lib/hot-channels/signals-pass.d.ts +9 -32
- package/dist/lib/hot-channels/signals-pass.js +51 -17
- package/dist/lib/lanes/admission.d.ts +2 -0
- package/dist/lib/lanes/admission.js +4 -0
- package/dist/lib/lanes/approval-gate.js +4 -1
- package/dist/lib/lanes/links.d.ts +8 -1
- package/dist/lib/lanes/links.js +84 -3
- package/dist/lib/lanes/mesh-events.js +3 -1
- package/dist/lib/lanes/outcome.d.ts +154 -0
- package/dist/lib/lanes/outcome.js +151 -0
- package/dist/lib/lanes/reconcile.js +1 -0
- package/dist/lib/lanes/signal-brief-template.d.ts +7 -0
- package/dist/lib/lanes/signal-brief-template.js +40 -0
- package/dist/lib/lanes/signal-lane.d.ts +24 -0
- package/dist/lib/lanes/signal-lane.js +99 -0
- package/dist/lib/lanes/source-report.d.ts +37 -0
- package/dist/lib/lanes/source-report.js +66 -0
- package/dist/lib/lanes/spawn.d.ts +6 -0
- package/dist/lib/lanes/spawn.js +21 -0
- package/dist/lib/lanes/store.d.ts +3 -0
- package/dist/lib/lanes/store.js +64 -0
- package/dist/lib/lanes/story-ledger.d.ts +3 -0
- package/dist/lib/lanes/story-ledger.js +1 -0
- package/dist/lib/lanes/types.d.ts +137 -0
- package/dist/lib/lanes/types.js +17 -2
- package/dist/lib/lanes/watcher.js +4 -0
- package/dist/lib/live-meetings.d.ts +19 -0
- package/dist/lib/live-meetings.js +23 -0
- package/dist/lib/mesh/api.js +2 -1
- package/dist/lib/mesh/live/daemon/doctor.js +6 -7
- package/dist/lib/mesh/live/daemon/run.d.ts +0 -10
- package/dist/lib/mesh/live/daemon/run.js +3 -0
- package/dist/lib/mesh/live/emit.d.ts +2 -11
- package/dist/lib/mesh/live/emit.js +20 -2
- package/dist/lib/mesh/live/flush.d.ts +2 -0
- package/dist/lib/mesh/live/flush.js +34 -3
- package/dist/lib/mesh/live/reporting-privacy.d.ts +7 -0
- package/dist/lib/mesh/live/reporting-privacy.js +47 -0
- package/dist/lib/mesh/live/spool.d.ts +8 -0
- package/dist/lib/mesh/live/spool.js +59 -40
- package/dist/lib/plan-limit-nag.d.ts +30 -4
- package/dist/lib/plan-limit-nag.js +68 -17
- package/dist/lib/signals/engine.d.ts +4 -0
- package/dist/lib/signals/engine.js +25 -5
- package/dist/lib/signals/incremental.d.ts +42 -0
- package/dist/lib/signals/incremental.js +191 -0
- package/dist/lib/signals/prior-signals.d.ts +11 -0
- package/dist/lib/signals/prior-signals.js +44 -0
- package/dist/lib/signals/reply.js +6 -1
- package/dist/lib/signals/settings.d.ts +5 -0
- package/dist/lib/signals/settings.js +17 -0
- package/dist/lib/signals/steps/extract.d.ts +23 -2
- package/dist/lib/signals/steps/extract.js +142 -9
- package/dist/lib/signals/steps/types.d.ts +1 -0
- package/dist/lib/signals/write-guard.js +7 -0
- package/dist/lib/work-context/company.d.ts +2 -3
- package/dist/lib/work-context/company.js +4 -22
- package/dist/secrets-env-action.js +5 -2
- package/dist/utils/cognito-callback-port.d.ts +17 -0
- package/dist/utils/cognito-callback-port.js +56 -0
- package/dist/utils/cognito-config.js +7 -3
- package/dist/utils/cognito-session.d.ts +7 -6
- package/dist/utils/cognito-session.js +19 -14
- package/dist/utils/interactive-caller.d.ts +23 -0
- package/dist/utils/interactive-caller.js +80 -0
- package/dist/utils/login-browser.js +5 -2
- package/dist/utils/resident-package-preload.js +4 -0
- package/package.json +7 -7
|
@@ -7,7 +7,8 @@ import * as fs from "node:fs";
|
|
|
7
7
|
import * as os from "node:os";
|
|
8
8
|
import * as path from "node:path";
|
|
9
9
|
import { botsRoot, botDir, launchdLabel } from "../../bot/paths.js";
|
|
10
|
-
import { readBotConfig } from "../../bot/config.js";
|
|
10
|
+
import { HQ_FLEET_RUNTIME_ENGINE_ID, readBotConfig } from "../../bot/config.js";
|
|
11
|
+
import { checkClaudeCodeLogin, claudeLoginFix, describeClaudeLoginSources, } from "../../bot/claude-login.js";
|
|
11
12
|
import { isPidAlive, readBotStatus } from "../../bot/status.js";
|
|
12
13
|
import { nodeBinaryProblem } from "../../node-binary.js";
|
|
13
14
|
import { launchdPlistProgramArguments, parseLaunchdCommand } from "../../service-manager/launchd.js";
|
|
@@ -17,6 +18,20 @@ const DEFAULT_DEPS = {
|
|
|
17
18
|
home: () => os.homedir(),
|
|
18
19
|
root: (home) => botsRoot(home ?? os.homedir()),
|
|
19
20
|
};
|
|
21
|
+
function isClaudeFleetBot(config) {
|
|
22
|
+
return config.engine?.id === HQ_FLEET_RUNTIME_ENGINE_ID && config.runtime === "claude";
|
|
23
|
+
}
|
|
24
|
+
/** The last reported failure, unless a later turn worked. */
|
|
25
|
+
export function unresolvedLastFailure(status) {
|
|
26
|
+
const failure = status?.lastFailure;
|
|
27
|
+
if (!failure)
|
|
28
|
+
return null;
|
|
29
|
+
const failedAt = Date.parse(failure.at);
|
|
30
|
+
const okAt = status?.lastTurnOkAt ? Date.parse(status.lastTurnOkAt) : Number.NaN;
|
|
31
|
+
if (Number.isFinite(failedAt) && Number.isFinite(okAt) && okAt > failedAt)
|
|
32
|
+
return null;
|
|
33
|
+
return failure;
|
|
34
|
+
}
|
|
20
35
|
export const botsFamily = {
|
|
21
36
|
id: BOTS_FAMILY_ID,
|
|
22
37
|
title: BOTS_FAMILY_TITLE,
|
|
@@ -66,6 +81,7 @@ function runChecks(deps) {
|
|
|
66
81
|
];
|
|
67
82
|
}
|
|
68
83
|
const results = [];
|
|
84
|
+
let claudeLogin = null;
|
|
69
85
|
for (const name of names) {
|
|
70
86
|
const dir = botDir(name, root);
|
|
71
87
|
const config = readBotConfig(dir);
|
|
@@ -73,6 +89,36 @@ function runChecks(deps) {
|
|
|
73
89
|
continue;
|
|
74
90
|
const status = readBotStatus(dir);
|
|
75
91
|
const alive = !!status && status.state === "running" && isPidAlive(status.pid);
|
|
92
|
+
if (isClaudeFleetBot(config)) {
|
|
93
|
+
claudeLogin ??= (deps.claudeLogin ?? (() => checkClaudeCodeLogin({ home })))();
|
|
94
|
+
if (claudeLogin.signedIn) {
|
|
95
|
+
results.push({
|
|
96
|
+
status: "PASS",
|
|
97
|
+
checkId: `bots.${name}.claude-login`,
|
|
98
|
+
target: dir,
|
|
99
|
+
message: `${name} can read a Claude Code login here (${claudeLogin.via}).`,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
results.push({
|
|
104
|
+
status: "FAIL",
|
|
105
|
+
checkId: `bots.${name}.claude-login`,
|
|
106
|
+
target: dir,
|
|
107
|
+
message: `Claude Code is not signed in on this computer, so ${name} cannot answer. ${describeClaudeLoginSources(claudeLogin)}`,
|
|
108
|
+
remediation: claudeLoginFix(name),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const lastFailure = unresolvedLastFailure(status);
|
|
113
|
+
if (lastFailure) {
|
|
114
|
+
results.push({
|
|
115
|
+
status: "FAIL",
|
|
116
|
+
checkId: `bots.${name}.last-failure`,
|
|
117
|
+
target: dir,
|
|
118
|
+
message: `${name} last failed at ${lastFailure.at} (${lastFailure.class}): ${lastFailure.cause}`,
|
|
119
|
+
remediation: `${lastFailure.fix} Details: hq bot status ${name}`,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
76
122
|
if (alive && status.lastHeartbeatOk === false) {
|
|
77
123
|
const heartbeatMessage = status.lastHeartbeatReason === "identity_mismatch"
|
|
78
124
|
? `${name} heartbeat stopped: identity mismatch (configured agent ${status.lastHeartbeatConfiguredAgentUid ?? config.agentUid}; token agent ${status.lastHeartbeatTokenAgentUid ?? "unknown"}).`
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
* watermark; the vault and other providers still land.
|
|
43
43
|
*/
|
|
44
44
|
import { type PollHotChannelAdapter } from "../contract.js";
|
|
45
|
+
export { meetingSignalSourceRef } from "../meeting-source-ref.js";
|
|
45
46
|
export declare const MEETINGS_ADAPTER_ID = "meetings";
|
|
46
47
|
export declare const MEETINGS_CHANNEL = "meeting-events";
|
|
47
48
|
export declare const MEETINGS_PROVIDER = "hq-vault";
|
|
@@ -49,6 +49,8 @@ import { isAuthError } from "../../../utils/auth-error.js";
|
|
|
49
49
|
import { MAX_CATCH_UP_WINDOW_MS } from "../contract.js";
|
|
50
50
|
import { assertSafeSegment, HOT_SIGNAL_ORIGIN, writeFileAtomic } from "../landing.js";
|
|
51
51
|
import { SyncLockLostError } from "../sync-lock.js";
|
|
52
|
+
import { meetingSignalSourceRef } from "../meeting-source-ref.js";
|
|
53
|
+
export { meetingSignalSourceRef } from "../meeting-source-ref.js";
|
|
52
54
|
import { MEETING_NOTE_PROVIDERS, MEETING_PROVIDERS, sameMeeting, } from "./meeting-providers.js";
|
|
53
55
|
export const MEETINGS_ADAPTER_ID = "meetings";
|
|
54
56
|
export const MEETINGS_CHANNEL = "meeting-events";
|
|
@@ -217,7 +219,7 @@ function toEvent(t) {
|
|
|
217
219
|
title: t.title,
|
|
218
220
|
body,
|
|
219
221
|
payloadRef,
|
|
220
|
-
frontmatter: { audience_source: t.audienceSource, provider: MEETINGS_VAULT_SOURCE },
|
|
222
|
+
frontmatter: { audience_source: t.audienceSource, provider: MEETINGS_VAULT_SOURCE, signal_source_ref: meetingSignalSourceRef(nonEmptyString(t.frontmatter.recall_bot_id) ?? t.transcriptId) },
|
|
221
223
|
raw: { transcript: payloadRef, frontmatter: t.frontmatter, audience: [...t.audience], audience_source: t.audienceSource },
|
|
222
224
|
};
|
|
223
225
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { assertSafeSegment } from "./landing.js";
|
|
2
|
+
/** Independent of transcript revisions and the adapter's versioned pointer filename. */
|
|
3
|
+
export function meetingSignalSourceRef(id) {
|
|
4
|
+
assertSafeSegment(id, "meeting id");
|
|
5
|
+
return `meeting:${id}`;
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=meeting-source-ref.js.map
|
|
@@ -76,9 +76,9 @@ export function toJobView(found, now) {
|
|
|
76
76
|
return {
|
|
77
77
|
botName: found.botName,
|
|
78
78
|
jobId: found.job.jobId,
|
|
79
|
-
cron: found.job.cron,
|
|
79
|
+
cron: found.job.cron ?? "",
|
|
80
80
|
timezone: found.job.timezone,
|
|
81
|
-
dailyAt: dailyAtOf(found.job.cron),
|
|
81
|
+
dailyAt: dailyAtOf(found.job.cron ?? ""),
|
|
82
82
|
nextRunAt: next ? next.toISOString() : null,
|
|
83
83
|
lastRunAt: found.job.lastRunAt ?? null,
|
|
84
84
|
};
|
|
@@ -1,35 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The local signals pass (US-011). After each hot-channel adapter run, the
|
|
3
|
-
* pass takes the adapter's channel through the ontology worklist, asks
|
|
4
|
-
* `claude -p` for signals one document at a time, and writes those signals to
|
|
5
|
-
* `companies/{co}/signals/`. It returns the count for the run ledger.
|
|
6
|
-
*
|
|
7
|
-
* Work comes from the worklist, not only from this run's landings: when the
|
|
8
|
-
* worklist script exits 0, the pass takes the channel's unprocessed items in
|
|
9
|
-
* the worklist's order (newest first) up to the run's document cap. Items the
|
|
10
|
-
* cap leaves over stay unmarked, and a later run picks them up, including a
|
|
11
|
-
* run that landed nothing (a bounded backlog drain: at most
|
|
12
|
-
* DEFAULT_MAX_SIGNAL_DOCS claude calls per run, and no claude call at all when
|
|
13
|
-
* the backlog is empty). A channel with no source.yaml (worklist exit 2, the
|
|
14
|
-
* usual case for a hot channel such as `meeting-events`) has no worklist, so
|
|
15
|
-
* the pass keeps its own backlog: landed documents it has not processed yet
|
|
16
|
-
* are listed in `sources/_signals-pending/{channel}.json` and drained, newest
|
|
17
|
-
* first by `when` (else `created_at`), on later runs under the same cap. A
|
|
18
|
-
* cloud-run source (exit 4) is skipped.
|
|
19
|
-
*
|
|
20
|
-
* A landed document with both `payload_ref` and `raw_ref` is a pointer (the
|
|
21
|
-
* meetings, Slack and Attio adapters land these). For it, claude gets the
|
|
22
|
-
* pointer followed by the document `payload_ref` names, capped at
|
|
23
|
-
* MAX_PAYLOAD_BYTES. A payload outside `sources/`, missing or unreadable falls
|
|
24
|
-
* back to the pointer alone, and the reason is warned and kept on the signal.
|
|
25
|
-
*
|
|
26
|
-
* Billing: policy hq-no-headless-print-subscription-billing forbids headless
|
|
27
|
-
* `claude -p` in general. This is its allowed case, per the owner decision of
|
|
28
|
-
* 2026-10-21: the owner's own `claude` login, on their own machine, in a
|
|
29
|
-
* bounded single-turn call. Cloud Claude is not the bot brain, and no model is
|
|
30
|
-
* called from the poller itself. Do not reuse this pass for a shared, fleet or
|
|
31
|
-
* hosted identity.
|
|
32
|
-
*/
|
|
33
1
|
import type { HotChannelRunResult, WriteGuard } from "./loop.js";
|
|
34
2
|
/** Ledger `error` values a failed pass records. */
|
|
35
3
|
export type SignalsPassErrorCode = "claude-logged-out" | "claude-timeout" | "claude-bad-output";
|
|
@@ -128,6 +96,14 @@ interface ParsedSignal {
|
|
|
128
96
|
type: SignalType;
|
|
129
97
|
title: string;
|
|
130
98
|
body: string;
|
|
99
|
+
update?: string;
|
|
100
|
+
}
|
|
101
|
+
export interface IndexRow {
|
|
102
|
+
signal_id: string;
|
|
103
|
+
type: string;
|
|
104
|
+
source_ref: string;
|
|
105
|
+
key: string;
|
|
106
|
+
written_at: string;
|
|
131
107
|
}
|
|
132
108
|
export declare function defaultPromptFile(): string;
|
|
133
109
|
export declare const CLAUDE_LOGGED_OUT_RE: RegExp;
|
|
@@ -172,6 +148,7 @@ export declare function signalId(sourceRef: string, type: string, title: string)
|
|
|
172
148
|
export declare function signalTypeDir(key: string, type: SignalType): string;
|
|
173
149
|
/** Write `signals/@{key}/_audience.yaml` once, when the partition is first created. */
|
|
174
150
|
export declare function ensureAudienceFile(vaultRoot: string, item: Pick<WorkItem, "audienceKey" | "principals" | "audienceSource">): Promise<void>;
|
|
151
|
+
export declare function appendSignalIndex(vaultRoot: string, day: string, rows: readonly IndexRow[]): Promise<void>;
|
|
175
152
|
/**
|
|
176
153
|
* Build the claude signals pass for one company. The pass keeps state for one
|
|
177
154
|
* run: the auth preflight runs once, and the document cap counts across every
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { prepareSignalPublication, flushSignalPublications } from "../bot/jobs/signal-to-lane.js";
|
|
1
2
|
/**
|
|
2
3
|
* The local signals pass (US-011). After each hot-channel adapter run, the
|
|
3
4
|
* pass takes the adapter's channel through the ontology worklist, asks
|
|
@@ -30,6 +31,7 @@
|
|
|
30
31
|
* called from the poller itself. Do not reuse this pass for a shared, fleet or
|
|
31
32
|
* hosted identity.
|
|
32
33
|
*/
|
|
34
|
+
import { acquireSyncLock } from "./sync-lock.js";
|
|
33
35
|
import { spawn } from "node:child_process";
|
|
34
36
|
import { createHash } from "node:crypto";
|
|
35
37
|
import * as fs from "node:fs/promises";
|
|
@@ -390,7 +392,10 @@ export function parseClaudeSignals(stdout) {
|
|
|
390
392
|
typeof s.body !== "string") {
|
|
391
393
|
throw new SignalsPassError("claude-bad-output", `claude signal ${i} has the wrong shape`);
|
|
392
394
|
}
|
|
393
|
-
|
|
395
|
+
if (s.update !== undefined && (typeof s.update !== "string" || !/^[a-f0-9]{64}$/.test(s.update))) {
|
|
396
|
+
throw new SignalsPassError("claude-bad-output", `claude signal ${i} has an invalid update id`);
|
|
397
|
+
}
|
|
398
|
+
return { type: s.type, title: s.title.trim(), body: s.body.trim(), ...(s.update ? { update: s.update } : {}) };
|
|
394
399
|
});
|
|
395
400
|
}
|
|
396
401
|
/** `sha256(source_ref \n type \n title)`: the same signal from the same source keeps its id. */
|
|
@@ -441,25 +446,40 @@ export async function ensureAudienceFile(vaultRoot, item) {
|
|
|
441
446
|
throw err;
|
|
442
447
|
}
|
|
443
448
|
}
|
|
444
|
-
async function appendSignalIndex(vaultRoot, day, rows) {
|
|
449
|
+
export async function appendSignalIndex(vaultRoot, day, rows) {
|
|
445
450
|
if (rows.length === 0)
|
|
446
451
|
return;
|
|
447
|
-
|
|
448
|
-
let
|
|
452
|
+
assertSafeSegment(day, "index day");
|
|
453
|
+
let lock = await acquireSyncLock(vaultRoot, `signal-index-${day}`);
|
|
454
|
+
for (let attempt = 0; !lock && attempt < 100; attempt += 1) {
|
|
455
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
456
|
+
lock = await acquireSyncLock(vaultRoot, `signal-index-${day}`);
|
|
457
|
+
}
|
|
458
|
+
if (!lock)
|
|
459
|
+
throw new Error("Signal index is busy; retry this extraction");
|
|
449
460
|
try {
|
|
450
|
-
const
|
|
451
|
-
|
|
461
|
+
const file = path.join(vaultRoot, "signals", "_index", `${day}.json`);
|
|
462
|
+
let existing = [];
|
|
463
|
+
try {
|
|
464
|
+
const parsed = JSON.parse(await fs.readFile(file, "utf8"));
|
|
465
|
+
if (!Array.isArray(parsed))
|
|
466
|
+
throw new Error("Invalid daily signal index");
|
|
452
467
|
existing = parsed;
|
|
468
|
+
}
|
|
469
|
+
catch (err) {
|
|
470
|
+
if (err.code !== "ENOENT")
|
|
471
|
+
throw err;
|
|
472
|
+
}
|
|
473
|
+
const seen = new Set(existing.map((row) => row.signal_id));
|
|
474
|
+
const added = rows.filter((row) => !seen.has(row.signal_id));
|
|
475
|
+
if (added.length === 0)
|
|
476
|
+
return;
|
|
477
|
+
await lock.assertHeld();
|
|
478
|
+
await writeFileAtomic(vaultRoot, file, `${JSON.stringify([...existing, ...added], null, 2)}\n`);
|
|
453
479
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
throw err;
|
|
480
|
+
finally {
|
|
481
|
+
await lock.release();
|
|
457
482
|
}
|
|
458
|
-
const seen = new Set(existing.map((row) => row.signal_id));
|
|
459
|
-
const added = rows.filter((row) => !seen.has(row.signal_id));
|
|
460
|
-
if (added.length === 0)
|
|
461
|
-
return;
|
|
462
|
-
await writeFileAtomic(vaultRoot, file, `${JSON.stringify([...existing, ...added], null, 2)}\n`);
|
|
463
483
|
}
|
|
464
484
|
/**
|
|
465
485
|
* Build the claude signals pass for one company. The pass keeps state for one
|
|
@@ -610,17 +630,20 @@ export function createClaudeSignalsPass(options) {
|
|
|
610
630
|
await beforeWrite();
|
|
611
631
|
await ensureAudienceFile(vaultRoot, item);
|
|
612
632
|
}
|
|
633
|
+
const pointer = splitFrontmatter(await fs.readFile(path.join(vaultRoot, item.file), "utf8")).frontmatter;
|
|
634
|
+
const sourceRef = typeof pointer.signal_source_ref === "string" && /^meeting:[a-zA-Z0-9._-]+$/.test(pointer.signal_source_ref)
|
|
635
|
+
? pointer.signal_source_ref : item.file;
|
|
613
636
|
for (const signal of signals) {
|
|
614
|
-
const id = signalId(
|
|
637
|
+
const id = signalId(sourceRef, signal.type, signal.title);
|
|
615
638
|
const key = `${signalTypeDir(item.audienceKey, signal.type)}/${id}.md`;
|
|
616
|
-
rows.push({ signal_id: id, type: signal.type, source_ref:
|
|
639
|
+
rows.push({ signal_id: id, type: signal.type, source_ref: sourceRef, key, written_at: at });
|
|
617
640
|
const file = path.join(vaultRoot, key);
|
|
618
641
|
if (await exists(file))
|
|
619
642
|
continue;
|
|
620
643
|
const frontmatter = {
|
|
621
644
|
signal_id: id,
|
|
622
645
|
type: signal.type,
|
|
623
|
-
source_ref:
|
|
646
|
+
source_ref: sourceRef,
|
|
624
647
|
canonical_content: signal.title,
|
|
625
648
|
audience_key: item.audienceKey,
|
|
626
649
|
origin: SIGNAL_ORIGIN,
|
|
@@ -632,15 +655,26 @@ export function createClaudeSignalsPass(options) {
|
|
|
632
655
|
frontmatter.payload_fallback = input.fallback;
|
|
633
656
|
const text = `---\n${yaml.dump(frontmatter, { lineWidth: -1, noRefs: true }).trimEnd()}\n---\n## ${heading(signal.type)}\n\n${signal.body || signal.title}\n`;
|
|
634
657
|
await beforeWrite();
|
|
658
|
+
// Cloud-extracted signals arriving by upload/garden are out of scope.
|
|
659
|
+
prepareSignalPublication(options.hqRoot, options.company, {
|
|
660
|
+
signal_id: id, type: signal.type, source_ref: sourceRef,
|
|
661
|
+
audience_key: item.audienceKey, origin: SIGNAL_ORIGIN,
|
|
662
|
+
});
|
|
635
663
|
await writeFileAtomic(vaultRoot, file, text);
|
|
664
|
+
// Publish the index before the event consumer looks up this new signal.
|
|
665
|
+
await appendSignalIndex(vaultRoot, at.slice(0, 10), [rows[rows.length - 1]]);
|
|
666
|
+
flushSignalPublications(options.hqRoot, options.company);
|
|
636
667
|
written += 1;
|
|
637
668
|
}
|
|
638
669
|
await beforeWrite();
|
|
639
670
|
await appendSignalIndex(vaultRoot, at.slice(0, 10), rows);
|
|
671
|
+
flushSignalPublications(options.hqRoot, options.company);
|
|
640
672
|
return written;
|
|
641
673
|
};
|
|
642
674
|
return async (result, guard) => {
|
|
643
675
|
const beforeWrite = guard?.beforeWrite ?? (async () => undefined);
|
|
676
|
+
await beforeWrite();
|
|
677
|
+
flushSignalPublications(options.hqRoot, options.company);
|
|
644
678
|
const byChannel = new Map();
|
|
645
679
|
// The adapter's own channel is listed even when this run landed nothing
|
|
646
680
|
// there, so items an earlier run left over the cap still get processed.
|
|
@@ -10,6 +10,8 @@ export interface AdmissionHost {
|
|
|
10
10
|
}
|
|
11
11
|
export type AdmissionCapReason = "provider_cap" | "account_cap" | "profile_cap";
|
|
12
12
|
export declare const ADMISSION_LOCK_STALE_MS = 30000;
|
|
13
|
+
/** Serialize signal lookup and persistence separately from provider admission. */
|
|
14
|
+
export declare function withSignalCreationLock<T>(hqRoot: string, key: string, fn: () => Promise<T>): Promise<T>;
|
|
13
15
|
export declare function withLaneAdmissionLock<T>(hqRoot: string, provider: LaneRecord["provider"], fn: () => Promise<T>, sleep?: (ms: number) => Promise<void>): Promise<T>;
|
|
14
16
|
export declare function withAllLaneAdmissionLocks<T>(hqRoot: string, fn: () => Promise<T>, sleep?: (ms: number) => Promise<void>): Promise<T>;
|
|
15
17
|
export declare function laneRunsOnHost(lane: LaneRecord, host: AdmissionHost): boolean;
|
|
@@ -88,6 +88,10 @@ async function withAdmissionLock(lockPath, fn, sleep) {
|
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
|
+
/** Serialize signal lookup and persistence separately from provider admission. */
|
|
92
|
+
export function withSignalCreationLock(hqRoot, key, fn) {
|
|
93
|
+
return withAdmissionLock(path.join(hqRoot, "workspace", "lanes-runs", ".signal-locks", key + ".lock"), fn, defaultAdmissionSleep);
|
|
94
|
+
}
|
|
91
95
|
export function withLaneAdmissionLock(hqRoot, provider, fn, sleep = defaultAdmissionSleep) {
|
|
92
96
|
return withAdmissionLock(admissionLockPath(hqRoot, provider), fn, sleep);
|
|
93
97
|
}
|
|
@@ -73,9 +73,12 @@ export function enforceRegistryApprovalGate(lane, envelope) {
|
|
|
73
73
|
if (missing.length === 0) {
|
|
74
74
|
return { envelope, approvedIdsToConsume: used, violation: false };
|
|
75
75
|
}
|
|
76
|
+
// The worker's outcome block described the action the gate just refused,
|
|
77
|
+
// so it does not carry over onto the blocked envelope.
|
|
78
|
+
const { outcome: _refusedOutcome, ...envelopeWithoutOutcome } = envelope;
|
|
76
79
|
return {
|
|
77
80
|
envelope: {
|
|
78
|
-
...
|
|
81
|
+
...envelopeWithoutOutcome,
|
|
79
82
|
decision: "blocked",
|
|
80
83
|
summary: `External action requires senior approval at checkpoint: ${missing.join(", ")}.`,
|
|
81
84
|
story_status: "review",
|
|
@@ -30,10 +30,17 @@ export interface LinkResult {
|
|
|
30
30
|
exitCode?: number;
|
|
31
31
|
}
|
|
32
32
|
export declare function resolveLinkSessionId(root: string, explicit: string | undefined, env?: NodeJS.ProcessEnv): string;
|
|
33
|
+
/** Resolve only the caller's link, never another arbitrary open conductor. */
|
|
34
|
+
export declare function resolveSignalConductor(root: string, env: NodeJS.ProcessEnv, explicitLink?: string, senior?: string): {
|
|
35
|
+
senior: string;
|
|
36
|
+
link?: string;
|
|
37
|
+
};
|
|
33
38
|
/** Cheap hook guard used before remote flag lookup or queue access. */
|
|
34
39
|
export declare function linkedSessionIdFromHookInput(input: string): string;
|
|
35
|
-
export declare function isLinkedSession(root: string, sid: string): boolean;
|
|
40
|
+
export declare function isLinkedSession(root: string, sid: string, env?: NodeJS.ProcessEnv): boolean;
|
|
36
41
|
export declare function drainLinkQueue(inbox: string): string;
|
|
42
|
+
/** Called by the installed lane drain hook before core's pointer-file guard. */
|
|
43
|
+
export declare function bindSignalLaneHook(root: string, input: string, env: NodeJS.ProcessEnv): void;
|
|
37
44
|
export declare function runLaneLink(subcommand: string, options: LinkOptions): Promise<LinkResult>;
|
|
38
45
|
/** Apply the default-on feature gate contract at the command boundary. */
|
|
39
46
|
export declare function runLaneLinkWithFlag(subcommand: string, options: LinkOptions, enabled: boolean): Promise<LinkResult>;
|
package/dist/lib/lanes/links.js
CHANGED
|
@@ -2,7 +2,7 @@ import { randomBytes } from "node:crypto";
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { appendDropboxMessageAtPath } from "./dropbox.js";
|
|
5
|
-
import { withLaneRecordFileLock } from "./store.js";
|
|
5
|
+
import { readLane, withLaneRecordFileLock } from "./store.js";
|
|
6
6
|
import { LanesError } from "./errors.js";
|
|
7
7
|
export const LANE_SESSION_LINKS_FLAG = "lanes.session-links";
|
|
8
8
|
const ID_PATTERN = /^[A-Za-z0-9._-]+$/;
|
|
@@ -58,6 +58,33 @@ function readPointer(root, sid) {
|
|
|
58
58
|
throw error;
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
|
+
/** Resolve only the caller's link, never another arbitrary open conductor. */
|
|
62
|
+
export function resolveSignalConductor(root, env, explicitLink, senior) {
|
|
63
|
+
if (senior === "user" && explicitLink === undefined)
|
|
64
|
+
return { senior: "user" };
|
|
65
|
+
let link = explicitLink;
|
|
66
|
+
if (link === undefined) {
|
|
67
|
+
try {
|
|
68
|
+
link = readPointer(root, resolveLinkSessionId(root, undefined, env))?.link;
|
|
69
|
+
}
|
|
70
|
+
catch { /* A missing caller pointer is handled below. */ }
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const dir = linkPath(root, link);
|
|
74
|
+
const meta = readJson(path.join(dir, "meta.json"));
|
|
75
|
+
if (typeof meta.conductor_session !== "string" || !safeId(meta.conductor_session))
|
|
76
|
+
throw new Error("invalid conductor");
|
|
77
|
+
const resolved = `session:${meta.conductor_session}`;
|
|
78
|
+
if (senior !== undefined && senior !== resolved)
|
|
79
|
+
throw new LanesError("signal_senior_mismatch", "Signal lane senior must match its conductor link");
|
|
80
|
+
return { senior: resolved, link };
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
if (error instanceof LanesError)
|
|
84
|
+
throw error;
|
|
85
|
+
throw new LanesError("no_conductor_link", "No open conductor link for this session; pass --link or --senior user");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
61
88
|
function atomicJson(file, value) {
|
|
62
89
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
63
90
|
const tmp = `${file}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
|
|
@@ -95,7 +122,44 @@ export function linkedSessionIdFromHookInput(input) {
|
|
|
95
122
|
return "";
|
|
96
123
|
}
|
|
97
124
|
}
|
|
98
|
-
|
|
125
|
+
/** A fresh provider session is unknown until its first hook. Bind that session
|
|
126
|
+
* from the persisted lane, rather than inheriting the creator's pointer. */
|
|
127
|
+
function bindSignalLaneSession(root, sid, env) {
|
|
128
|
+
if (!env.HQ_LANE_SIGNAL_LINK)
|
|
129
|
+
return;
|
|
130
|
+
const laneId = env.HQ_LANE_ID;
|
|
131
|
+
if (!safeId(sid) || !safeId(laneId) || readPointer(root, sid))
|
|
132
|
+
return;
|
|
133
|
+
let lane;
|
|
134
|
+
try {
|
|
135
|
+
lane = readLane(root, laneId);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
process.stderr.write("hq lanes signal link: lane record unavailable\n");
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (!lane.signal || !lane.signal_link || lane.signal_link !== env.HQ_LANE_SIGNAL_LINK)
|
|
142
|
+
return;
|
|
143
|
+
const dir = linkPath(root, lane.signal_link);
|
|
144
|
+
const meta = readJson(path.join(dir, "meta.json"));
|
|
145
|
+
if (lane.senior.kind !== "session" || lane.senior.id !== meta.conductor_session || sid === meta.conductor_session)
|
|
146
|
+
return;
|
|
147
|
+
const child = lane.lane_id;
|
|
148
|
+
const at = new Date().toISOString();
|
|
149
|
+
const childDir = path.join(dir, "children", child);
|
|
150
|
+
fs.mkdirSync(path.join(childDir, "inbox", "pending"), { recursive: true });
|
|
151
|
+
atomicJson(path.join(childDir, "meta.json"), { name: child, session_id: sid, engine: lane.provider, company: lane.company?.slug ?? "", title: lane.signal.title, state: "working", joined: at, updated: at });
|
|
152
|
+
fs.mkdirSync(path.dirname(pointerPath(root, sid)), { recursive: true });
|
|
153
|
+
fs.writeFileSync(pointerPath(root, sid), `child ${lane.signal_link} ${child}\n`, { flag: "wx", mode: 0o600 });
|
|
154
|
+
}
|
|
155
|
+
export function isLinkedSession(root, sid, env = process.env) {
|
|
156
|
+
try {
|
|
157
|
+
bindSignalLaneSession(root, sid, env);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
process.stderr.write("hq lanes signal link: could not bind worker session\n");
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
99
163
|
return Boolean(safeId(sid) && readPointer(root, sid));
|
|
100
164
|
}
|
|
101
165
|
function activeChild(root, link, name) {
|
|
@@ -478,6 +542,18 @@ function isChildHookPayload(payload) {
|
|
|
478
542
|
const transcript = payload.transcript_path ?? payload.transcriptPath;
|
|
479
543
|
return typeof transcript === "string" && /[/\\]subagents[/\\][^/\\]+\.jsonl$/.test(transcript);
|
|
480
544
|
}
|
|
545
|
+
/** Called by the installed lane drain hook before core's pointer-file guard. */
|
|
546
|
+
export function bindSignalLaneHook(root, input, env) {
|
|
547
|
+
const payload = JSON.parse(input);
|
|
548
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload))
|
|
549
|
+
return;
|
|
550
|
+
const record = payload;
|
|
551
|
+
if (isChildHookPayload(record))
|
|
552
|
+
return;
|
|
553
|
+
const sid = linkedSessionIdFromHookInput(input);
|
|
554
|
+
if (sid)
|
|
555
|
+
bindSignalLaneSession(root, sid, env);
|
|
556
|
+
}
|
|
481
557
|
export async function runLaneLink(subcommand, options) {
|
|
482
558
|
const now = options.now ?? (() => new Date());
|
|
483
559
|
const env = options.env ?? process.env;
|
|
@@ -498,13 +574,18 @@ export async function runLaneLink(subcommand, options) {
|
|
|
498
574
|
childContext = isChildHookPayload(payload);
|
|
499
575
|
stopHookActive = payload.stop_hook_active === true || payload.stopHookActive === true;
|
|
500
576
|
}
|
|
501
|
-
const requireSid = () =>
|
|
577
|
+
const requireSid = () => {
|
|
578
|
+
sid ||= resolveLinkSessionId(root, options.sessionId, env);
|
|
579
|
+
bindSignalLaneSession(root, sid, env);
|
|
580
|
+
return sid;
|
|
581
|
+
};
|
|
502
582
|
const links = rootDir(root);
|
|
503
583
|
if (subcommand === "_deliver") {
|
|
504
584
|
// Subagent hook events carry the parent's session_id. Delivering there would
|
|
505
585
|
// drain the root's queue into the subagent, so leave the message queued.
|
|
506
586
|
if (!sid || childContext || !["PostToolUse", "Stop"].includes(options.event ?? ""))
|
|
507
587
|
return {};
|
|
588
|
+
bindSignalLaneSession(root, sid, env);
|
|
508
589
|
if (options.event === "Stop" && (stopHookActive || env.HQ_STOP_DECISION_DELIVERABLE === "0"))
|
|
509
590
|
return {};
|
|
510
591
|
const pointer = readPointer(root, sid);
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
import { appendThreadEvent, clamp, ensureLaneMeshThread, ensureProjectThread, getProjectView, patchStoryStatus, putProjectView, requireToken, registerMeshProject, resolveMeshCompany, } from "../mesh/api.js";
|
|
46
46
|
import { LanesError } from "./errors.js";
|
|
47
47
|
import { terminalEnvelopeStoryStatus } from "./stories.js";
|
|
48
|
+
import { gatedInteractiveLogin } from "../../utils/interactive-caller.js";
|
|
48
49
|
export const LANE_MESH_EVENTS = [
|
|
49
50
|
"lane_started",
|
|
50
51
|
"lane_progress",
|
|
@@ -309,7 +310,8 @@ async function repairProjectView(input) {
|
|
|
309
310
|
}
|
|
310
311
|
export function createLaneMesh(opts = {}) {
|
|
311
312
|
const interactive = opts.interactive ?? true;
|
|
312
|
-
|
|
313
|
+
// Gated per call: the same mesh can serve a terminal and a detached watcher.
|
|
314
|
+
const token = opts.token ?? (() => requireToken({ interactive: gatedInteractiveLogin(interactive) }));
|
|
313
315
|
const repair = opts.repairProjectView ?? repairProjectView;
|
|
314
316
|
return {
|
|
315
317
|
async ensureProjectThread(company, projectId) {
|