@indigoai-us/hq-cli 5.141.0 → 5.143.0

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.
Files changed (36) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/assets/scaffold/core/scripts/checkpoint-stop-gate.sh +77 -2
  3. package/dist/command-catalog.generated.d.ts +7 -1
  4. package/dist/command-catalog.generated.js +9 -1
  5. package/dist/commands/agents.d.ts +1 -0
  6. package/dist/commands/agents.js +4 -8
  7. package/dist/commands/dm.js +1 -5
  8. package/dist/commands/lanes.js +125 -26
  9. package/dist/commands/mesh.js +25 -2
  10. package/dist/commands/report.d.ts +3 -1
  11. package/dist/commands/report.js +69 -15
  12. package/dist/lib/agent-kit/mcp/tools.js +2 -1
  13. package/dist/lib/bot/api.d.ts +4 -0
  14. package/dist/lib/bot/api.js +13 -1
  15. package/dist/lib/lanes/effort.d.ts +3 -2
  16. package/dist/lib/lanes/effort.js +5 -1
  17. package/dist/lib/lanes/errors.d.ts +1 -1
  18. package/dist/lib/lanes/index.d.ts +2 -0
  19. package/dist/lib/lanes/index.js +1 -0
  20. package/dist/lib/lanes/mesh-events.d.ts +4 -5
  21. package/dist/lib/lanes/mesh-events.js +14 -4
  22. package/dist/lib/lanes/profile.d.ts +48 -0
  23. package/dist/lib/lanes/profile.js +405 -0
  24. package/dist/lib/lanes/reconcile.js +25 -3
  25. package/dist/lib/lanes/registry.d.ts +3 -0
  26. package/dist/lib/lanes/registry.js +45 -1
  27. package/dist/lib/lanes/spawn.d.ts +14 -2
  28. package/dist/lib/lanes/spawn.js +238 -14
  29. package/dist/lib/lanes/store.js +10 -0
  30. package/dist/lib/lanes/types.d.ts +11 -1
  31. package/dist/lib/lanes/types.js +3 -3
  32. package/dist/lib/notify-recipient.d.ts +7 -0
  33. package/dist/lib/notify-recipient.js +11 -0
  34. package/dist/lib/report-publish.d.ts +2 -2
  35. package/dist/lib/report-publish.js +9 -8
  36. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,37 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.143.0] — 2026-09-23
6
+
7
+ ### Fixed
8
+
9
+ - `hq dm` and report commands now reject non-recipient and non-company channel
10
+ requests locally, and report updates retry one fresh revision after a
11
+ conflict.
12
+ - `hq mesh project register` now verifies the company's Board view before
13
+ registering and reports an actionable local error when the Board is missing.
14
+
15
+ ## [5.142.0] — 2026-09-23
16
+
17
+ ### Added
18
+
19
+ - Added reusable personal worker profiles for `hq lanes create --worker`, with
20
+ strict `meta.yaml` validation, content hashes, profile-default precedence,
21
+ provider-specific prompt/settings carriers, and reapplication on every fresh,
22
+ resumed, moved, repaired, and compaction spawn.
23
+ - Personal lanes can now use explicit `--mesh-company` attribution so their
24
+ project thread, presence, and lifecycle events appear in Work Mesh after
25
+ membership validation.
26
+
27
+ ### Fixed
28
+
29
+ - Account login now reuses a registered account home when `--home` is omitted.
30
+ Codex account login also accepts `--device-auth` for remote boxes without a
31
+ localhost browser callback.
32
+ - Checkpoint Stop-gate reply detection now uses the Stop payload when the
33
+ transcript has not flushed yet, and fails open with an ambiguity log when
34
+ neither reply source is reliable.
35
+
5
36
  ## [5.141.0] — 2026-09-23
6
37
 
7
38
  - `hq bot jobs` now checks the bot's configured agent ID against the machine credential before calling the jobs API. A mismatch reports both IDs and explains how to fix the bot configuration or re-mint its credentials.
@@ -138,6 +138,19 @@ exec 3>&2
138
138
  }
139
139
 
140
140
  input="$(cat 2>/dev/null || printf '{}')"
141
+ input_reply_evidence="$(printf '%s' "$input" | jq -r '
142
+ if has("last_assistant_message") then
143
+ if (.last_assistant_message | type) == "string" then
144
+ "present:" + ((.last_assistant_message | gsub("\\s+"; "") | length) | tostring)
145
+ elif (.last_assistant_message | type) == "array" then
146
+ "present:" + (([.last_assistant_message[]? | (.text? // "")] | join("") | gsub("\\s+"; "") | length) | tostring)
147
+ else
148
+ "invalid"
149
+ end
150
+ else
151
+ "absent"
152
+ end
153
+ ' 2>/dev/null || printf 'invalid')"
141
154
  input_fields="$(printf '%s' "$input" | jq -r '[.transcript_path // "", .session_id // ""] | @tsv' 2>/dev/null)" || exit 0
142
155
  IFS=$'\t' read -r transcript_path session_id <<<"$input_fields" || exit 0
143
156
 
@@ -288,6 +301,15 @@ exec 3>&2
288
301
  session_key="$(hq_hook_safe_session_key "$session_id")"
289
302
  [ -n "$session_key" ] || exit 0
290
303
 
304
+ reply_ambiguity_log="$state_dir/checkpoint-stop-gate-ambiguity.log"
305
+ log_reply_ambiguity() {
306
+ local reason="$1"
307
+ local timestamp
308
+ timestamp="$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || printf 'unknown-time')"
309
+ printf '%s\t%s\treply-evidence-ambiguous\t%s\n' "$timestamp" "$session_key" "$reason" \
310
+ >>"$reply_ambiguity_log" 2>/dev/null || true
311
+ }
312
+
291
313
  # Parse only the recent JSONL tail. Claude and Codex persist different row
292
314
  # shapes, so normalize both to the same user/tool boundary before checking
293
315
  # whether the final action was the checkpoint command.
@@ -469,7 +491,12 @@ exec 3>&2
469
491
  else (if $seg_reply_chars > 0 then "0-replied" else "0-unreplied" end)
470
492
  end
471
493
  end
472
- ' 2>/dev/null)" || exit 0
494
+ ' 2>/dev/null)"
495
+ parsed_status=$?
496
+ if [ "$parsed_status" -ne 0 ]; then
497
+ log_reply_ambiguity "transcript-parse-failed"
498
+ exit 0
499
+ fi
473
500
  # A Claude success verdict carries the satisfying checkpoint's tool id, the
474
501
  # count of non-whitespace assistant text characters delivered after the
475
502
  # genuine turn's last work tool (its user-facing reply, wherever it sits
@@ -494,6 +521,53 @@ exec 3>&2
494
521
  *) exit 0 ;;
495
522
  esac
496
523
 
524
+ # Claude's Stop payload carries the final assistant message independently of
525
+ # the transcript writer. Use the stronger of the two sources: the transcript
526
+ # can already contain a reply while the payload is empty, and the payload can
527
+ # arrive before the transcript writer flushes. If neither source can prove a
528
+ # reply, the evidence is ambiguous rather than proof that no reply exists.
529
+ # Fail open for the reply demand and record the ambiguity so a host-side
530
+ # flush regression remains observable without forcing a duplicate.
531
+ reply_evidence_ambiguous=0
532
+ case "$reply_chars" in
533
+ ''|*[!0-9]*) reply_chars=0 ;;
534
+ esac
535
+ case "$input_reply_evidence" in
536
+ present:*)
537
+ payload_reply_chars="${input_reply_evidence#present:}"
538
+ case "$payload_reply_chars" in
539
+ ''|*[!0-9]*) reply_evidence_ambiguous=1 ;;
540
+ *)
541
+ if [ "$runtime" = "claude" ]; then
542
+ if [ "$satisfied" = "1" ]; then
543
+ if [ "$payload_reply_chars" -gt "$reply_chars" ]; then
544
+ reply_chars="$payload_reply_chars"
545
+ fi
546
+ elif [ "$satisfied" = "0" ] && [ "$payload_reply_chars" -gt 0 ]; then
547
+ replied=1
548
+ fi
549
+ fi
550
+ ;;
551
+ esac
552
+ ;;
553
+ absent|invalid)
554
+ if [ "$runtime" = "claude" ] && [ "$reply_chars" -eq 0 ]; then
555
+ if [ "$satisfied" = "1" ] && [ "$reply_required" = "1" ]; then
556
+ reply_evidence_ambiguous=1
557
+ elif [ "$satisfied" = "0" ]; then
558
+ # A missing checkpoint plus an unflushed reply is also ambiguous;
559
+ # let the already-replied branch record the turn without asking the
560
+ # host to repeat text that may already be visible.
561
+ reply_evidence_ambiguous=1
562
+ replied=1
563
+ fi
564
+ fi
565
+ ;;
566
+ esac
567
+ if [ "$reply_evidence_ambiguous" = "1" ]; then
568
+ log_reply_ambiguity "payload=$input_reply_evidence;transcript-reply-chars=$reply_chars"
569
+ fi
570
+
497
571
  # Codex tool-result envelopes do not reliably expose a nested shell exit
498
572
  # status. The CLI writes this session-scoped stamp only after a successful
499
573
  # checkpoint, so require a fresh stamp in addition to the final command.
@@ -562,7 +636,8 @@ exec 3>&2
562
636
  # own delivery contract. HQ_CHECKPOINT_REPLY_MIN overrides the minimum
563
637
  # non-whitespace character threshold; 0 disables the enforcement.
564
638
  if [ "$satisfied" = "1" ] && [ "$runtime" = "claude" ] \
565
- && [ "$reply_required" = "1" ] && [ -n "$reply_tool_id" ]; then
639
+ && [ "$reply_required" = "1" ] && [ -n "$reply_tool_id" ] \
640
+ && [ "$reply_evidence_ambiguous" != "1" ]; then
566
641
  reply_min="${HQ_CHECKPOINT_REPLY_MIN:-80}"
567
642
  case "$reply_min" in ''|*[!0-9]*) reply_min=80 ;; esac
568
643
  case "$reply_chars" in ''|*[!0-9]*) reply_chars=0 ;; esac
@@ -5740,7 +5740,7 @@ export declare const COMMAND_CATALOG: readonly [{
5740
5740
  readonly subcommands: readonly [];
5741
5741
  }, {
5742
5742
  readonly name: "register";
5743
- readonly description: "POST project registration to create or bind its project channel and Board";
5743
+ readonly description: "Register an existing Board and bind its project channel; use project ensure to create or repair a Board";
5744
5744
  readonly aliases: readonly [];
5745
5745
  readonly hidden: false;
5746
5746
  readonly usage: "[options] <projectId>";
@@ -6743,6 +6743,9 @@ export declare const COMMAND_CATALOG: readonly [{
6743
6743
  readonly options: readonly [{
6744
6744
  readonly flags: "--company <slug|uid>";
6745
6745
  readonly description: "Company slug or cloud uid; omit for a core/personal lane";
6746
+ }, {
6747
+ readonly flags: "--mesh-company <slug|uid>";
6748
+ readonly description: "Explicit Work Mesh company for a personal lane; membership is verified";
6746
6749
  }, {
6747
6750
  readonly flags: "--project <slug>";
6748
6751
  readonly description: "HQ project slug";
@@ -7175,6 +7178,9 @@ export declare const COMMAND_CATALOG: readonly [{
7175
7178
  readonly options: readonly [{
7176
7179
  readonly flags: "--home <path>";
7177
7180
  readonly description: "Account home directory";
7181
+ }, {
7182
+ readonly flags: "--device-auth";
7183
+ readonly description: "Use Codex device authentication instead of localhost callback";
7178
7184
  }, {
7179
7185
  readonly flags: "--json";
7180
7186
  readonly description: "JSON on stdout (always)";
@@ -7409,7 +7409,7 @@ export const COMMAND_CATALOG = [
7409
7409
  },
7410
7410
  {
7411
7411
  "name": "register",
7412
- "description": "POST project registration to create or bind its project channel and Board",
7412
+ "description": "Register an existing Board and bind its project channel; use project ensure to create or repair a Board",
7413
7413
  "aliases": [],
7414
7414
  "hidden": false,
7415
7415
  "usage": "[options] <projectId>",
@@ -8714,6 +8714,10 @@ export const COMMAND_CATALOG = [
8714
8714
  "flags": "--company <slug|uid>",
8715
8715
  "description": "Company slug or cloud uid; omit for a core/personal lane"
8716
8716
  },
8717
+ {
8718
+ "flags": "--mesh-company <slug|uid>",
8719
+ "description": "Explicit Work Mesh company for a personal lane; membership is verified"
8720
+ },
8717
8721
  {
8718
8722
  "flags": "--project <slug>",
8719
8723
  "description": "HQ project slug"
@@ -9278,6 +9282,10 @@ export const COMMAND_CATALOG = [
9278
9282
  "flags": "--home <path>",
9279
9283
  "description": "Account home directory"
9280
9284
  },
9285
+ {
9286
+ "flags": "--device-auth",
9287
+ "description": "Use Codex device authentication instead of localhost callback"
9288
+ },
9281
9289
  {
9282
9290
  "flags": "--json",
9283
9291
  "description": "JSON on stdout (always)"
@@ -336,6 +336,7 @@ export declare function formatJobsTable(jobs: AgentJobView[]): string;
336
336
  export declare function formatPauseResult(result: PauseJobResult): string;
337
337
  /** Human-readable "hot-applied to the running box" vs "saved for next launch". */
338
338
  export declare function appliedHint(applied: boolean): string;
339
+ /** Agent uid shape (`agt_…`). */
339
340
  /** One message in a DM thread, as returned by `GET /v1/notify/thread`. */
340
341
  export interface DmThreadMessage {
341
342
  eventId: string;
@@ -36,6 +36,7 @@ import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
36
36
  import { launchSessionManagerPlugin, SessionManagerPluginLaunchError, TerminalSessionTimeoutError, waitForTerminalSession, } from "@indigoai-us/hq-cloud/outposts/node";
37
37
  import { peekIdToken } from "../utils/id-token.js";
38
38
  import { isPlanGateError } from "../utils/plan-gate-error.js";
39
+ import { AGENT_UID_PATTERN, isAgentUid } from "../lib/notify-recipient.js";
39
40
  import { confirmChargeOrExit, formatUsd, parseBillingPayload, surfaceBillingBlocked, } from "../utils/billing-gate.js";
40
41
  /** Reasoning-effort values hq-pro accepts on `runtime-config`. */
41
42
  export const VALID_EFFORTS = new Set([
@@ -807,14 +808,6 @@ export function appliedHint(applied) {
807
808
  ? "applied to the running box now"
808
809
  : "saved — takes effect on the agent's next launch";
809
810
  }
810
- // ---------------------------------------------------------------------------
811
- // Talking to agents (DM). Agents are members addressable over HQ's DM system:
812
- // a message rides `POST /v1/notify/dm` (the wire contract accepts `agt_*`), the
813
- // agent's box picks it up and replies over DM (a first-class agent reply
814
- // channel), and the two-way conversation is read back via `GET /v1/notify/thread`.
815
- // ---------------------------------------------------------------------------
816
- /** Agent uid shape (`agt_…`). */
817
- const AGENT_UID_PATTERN = /^agt_[A-Za-z0-9_-]+$/;
818
811
  /**
819
812
  * Resolve an agent reference to its `agt_` uid. An `agt_…` value is used
820
813
  * directly; anything else is treated as a slug or display name and resolved
@@ -909,6 +902,9 @@ async function resolveJobsTargetOrExit(ref, company) {
909
902
  }
910
903
  /** Send a DM to an agent. Returns nothing meaningful beyond success. */
911
904
  export async function sendAgentDm(token, agentUid, message) {
905
+ if (!isAgentUid(agentUid)) {
906
+ throw new Error(`Invalid agent recipient '${agentUid}': expected an agt_ uid.`);
907
+ }
912
908
  await agentsRequest({
913
909
  token,
914
910
  path: "/v1/notify/dm",
@@ -4,11 +4,7 @@ import { ensureCognitoToken } from "../utils/cognito-session.js";
4
4
  import { vaultApiFetch } from "../utils/vault-api.js";
5
5
  import { isMachineIdentity } from "@indigoai-us/hq-cloud";
6
6
  import { peekIdToken } from "../utils/id-token.js";
7
- const EMAIL_PATTERN = /^[^\s]+@[^\s]+$/;
8
- // People (prs_) and agents (agt_) are both first-class DM participants; the
9
- // server applies the same membership-overlap gate to either and delivers
10
- // agt_* recipients into the agent's durable box inbox.
11
- const RECIPIENT_UID_PATTERN = /^(prs|agt)_[A-Za-z0-9_-]+$/;
7
+ import { EMAIL_PATTERN, RECIPIENT_UID_PATTERN } from "../lib/notify-recipient.js";
12
8
  /**
13
9
  * Classify a recipient arg as an email or a person/agent uid. Mirrors the
14
10
  * email/uid heuristic used by `hq members`. Returns null for neither.
@@ -18,7 +18,7 @@ import { vaultApiFetch } from "../utils/vault-api.js";
18
18
  import { openMeshTransport } from "../lib/mesh/client.js";
19
19
  import { createLaneMesh, emitLaneDecisionEvents, } from "../lib/lanes/mesh-events.js";
20
20
  import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
21
- import { AccountResolutionError, coveredLaneIds, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS, LANE_STATES, PROVIDER_HOME_ENV, QUESTION_STATUSES, SENIOR_KINDS, accountMaxRunning, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installDrainHook, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, listAccountStatuses, listLaneIds, listQuestionIds, listSessionLiveness, loadLanesConfig, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, MONITOR_ENGINES, MONITOR_MAX_TIMEOUT_MS, MONITOR_UNCOVERED_EXIT_CODE, MAX_WATCHED_LANES, monitorReminderText, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneState, parseLivenessState, parseQuestionStatus, parseSeniorKind, readDaemonPresenceConnected, readLane, retryUpdateLane, readQuestion, resolveAccount, resolveLaneWorker, isLoggedOut, runReconcilePass, runLaneWatch, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, writeLanesConfig, appendDropboxMessage, answerDropboxMessage, deliveryRefusalReason, resumeCommandRefusal, dropboxDir, dropboxInboxPath, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneDeliveryMechanism, hasRecordedSessionId, providerSupportsSessionResume, laneIdForAnswerDelivery, operatorMessageRefusalReason, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "../lib/lanes/index.js";
21
+ import { AccountResolutionError, coveredLaneIds, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS, LANE_STATES, PROVIDER_HOME_ENV, QUESTION_STATUSES, SENIOR_KINDS, accountMaxRunning, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installDrainHook, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, listAccountStatuses, listLaneIds, listQuestionIds, listSessionLiveness, loadLanesConfig, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, MONITOR_ENGINES, MONITOR_MAX_TIMEOUT_MS, MONITOR_UNCOVERED_EXIT_CODE, MAX_WATCHED_LANES, monitorReminderText, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneState, parseLivenessState, parseQuestionStatus, parseSeniorKind, readDaemonPresenceConnected, readLane, retryUpdateLane, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, isLoggedOut, runReconcilePass, runLaneWatch, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, writeLanesConfig, appendDropboxMessage, answerDropboxMessage, deliveryRefusalReason, resumeCommandRefusal, dropboxDir, dropboxInboxPath, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneDeliveryMechanism, hasRecordedSessionId, providerSupportsSessionResume, laneIdForAnswerDelivery, operatorMessageRefusalReason, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "../lib/lanes/index.js";
22
22
  import { questionBodyAbsPath } from "../lib/lanes/questions.js";
23
23
  import { compactionAfterUsage, } from "../lib/lanes/compaction.js";
24
24
  import { providerCompactAfterMs } from "../lib/lanes/config.js";
@@ -735,10 +735,13 @@ function assertSameTenant(laneCompany, senior, seniorCompany, extra = {}) {
735
735
  ...extra,
736
736
  });
737
737
  }
738
- function recordMatchesCompany(recordCompany, filter) {
739
- if (!recordCompany)
740
- return false;
741
- return recordCompany.uid === filter.uid || recordCompany.slug === filter.slug;
738
+ function recordMatchesCompany(lane, filter) {
739
+ return companyRefMatches(lane.company, filter) || companyRefMatches(lane.mesh_company, filter);
740
+ }
741
+ function companyRefMatches(recordCompany, filter) {
742
+ return (recordCompany !== null &&
743
+ recordCompany !== undefined &&
744
+ (recordCompany.uid === filter.uid || recordCompany.slug === filter.slug));
742
745
  }
743
746
  function walkHitsLane(hqRoot, start, laneId, ctx) {
744
747
  const seen = new Set();
@@ -828,7 +831,7 @@ function compareQueueOrder(a, b) {
828
831
  return a.created_at.localeCompare(b.created_at);
829
832
  }
830
833
  function admitCreate(input) {
831
- const { hqRoot, provider, pinnedAccount, priority, createdAt, config } = input;
834
+ const { hqRoot, provider, pinnedAccount, profileId, profileMaxConcurrent, priority, createdAt, config, } = input;
832
835
  const providerCfg = config.providers[provider];
833
836
  const lanes = loadAllLanes(hqRoot).filter((lane) => lane.lane_id !== input.excludeLaneId);
834
837
  const runningProvider = lanes.filter((lane) => lane.provider === provider && lane.state === "running");
@@ -845,9 +848,13 @@ function admitCreate(input) {
845
848
  const providerRunning = runningProvider.length;
846
849
  const queuedCount = queuedSame.length;
847
850
  const maxQueued = providerCfg.max_queued;
851
+ const profileRunning = profileId === undefined
852
+ ? 0
853
+ : lanes.filter((lane) => lane.worker_profile === profileId && lane.state === "running").length;
848
854
  const atCap = providerRunning >= providerCfg.max_running ||
849
855
  (pinnedAccount !== undefined &&
850
- runningCount >= accountMaxRunning(providerCfg, pinnedAccount));
856
+ runningCount >= accountMaxRunning(providerCfg, pinnedAccount)) ||
857
+ (profileMaxConcurrent !== undefined && profileRunning >= profileMaxConcurrent);
851
858
  const synthetic = {
852
859
  schema_version: "hq-lane.v1",
853
860
  lane_id: "__incoming__",
@@ -1116,6 +1123,15 @@ export function composeWorkerBrief(worker, taskBrief) {
1116
1123
  "",
1117
1124
  "## Knowledge",
1118
1125
  formatUnknownValue(yaml.knowledge) || "(none)",
1126
+ ...(worker.profile
1127
+ ? [
1128
+ "",
1129
+ "## Reusable worker profile",
1130
+ `Profile id: ${worker.profile.id}`,
1131
+ `Profile content hash: ${worker.profile.contentHash}`,
1132
+ "The profile is re-read and applied for every fresh, resumed, moved, and repair spawn.",
1133
+ ]
1134
+ : []),
1119
1135
  "",
1120
1136
  "## Verification checkpoints",
1121
1137
  checkpoints || "(none)",
@@ -1356,6 +1372,7 @@ function mintMessageId() {
1356
1372
  async function runCreate(ctx, opts) {
1357
1373
  const hqRoot = ctx.hqRoot();
1358
1374
  const companyFlag = opts.company?.trim();
1375
+ const meshCompanyFlag = opts.meshCompany?.trim();
1359
1376
  const project = opts.project?.trim();
1360
1377
  const story = opts.story?.trim();
1361
1378
  const briefFile = opts.briefFile?.trim();
@@ -1368,9 +1385,9 @@ async function runCreate(ctx, opts) {
1368
1385
  if (!briefFile) {
1369
1386
  throw new LanesError("missing_brief_file", "--brief-file is required", {});
1370
1387
  }
1371
- const provider = opts.provider
1388
+ const explicitProvider = opts.provider
1372
1389
  ? parseLaneProvider(opts.provider)
1373
- : parseLaneProvider("codex");
1390
+ : undefined;
1374
1391
  if (opts.outpost !== undefined && opts.outpost !== "") {
1375
1392
  const requested = opts.outpost.trim();
1376
1393
  let known;
@@ -1396,12 +1413,29 @@ async function runCreate(ctx, opts) {
1396
1413
  const company = companyFlag
1397
1414
  ? await ctx.resolveCompany(companyFlag)
1398
1415
  : null;
1416
+ let meshCompany = company;
1417
+ if (meshCompanyFlag) {
1418
+ const resolvedMeshCompany = await ctx.resolveCompany(meshCompanyFlag);
1419
+ if (company &&
1420
+ (company.uid !== resolvedMeshCompany.uid || company.slug !== resolvedMeshCompany.slug)) {
1421
+ throw new LanesError("mesh_company_mismatch", "--mesh-company must match --company when both are provided", {
1422
+ company: company.slug,
1423
+ mesh_company: resolvedMeshCompany.slug,
1424
+ });
1425
+ }
1426
+ meshCompany = resolvedMeshCompany;
1427
+ }
1399
1428
  const seniorCompany = companyOfSenior(hqRoot, senior, ctx);
1400
1429
  assertSameTenant(company, senior, seniorCompany);
1401
1430
  let resolvedWorker;
1402
1431
  if (opts.worker) {
1403
1432
  resolvedWorker = resolveLaneWorker(hqRoot, opts.worker, company?.slug);
1404
1433
  }
1434
+ const profile = resolvedWorker?.profile;
1435
+ const provider = explicitProvider ??
1436
+ profile?.meta.provider ??
1437
+ parseLaneProvider("codex");
1438
+ const profileDefaultsApply = profile !== undefined && profile.meta.provider === provider;
1405
1439
  const briefAbs = resolveCallerPath(ctx.callerCwd(), briefFile);
1406
1440
  if (!ctx.existsSync(briefAbs)) {
1407
1441
  throw new LanesError("brief_not_found", `Brief file not found: ${briefAbs}`, {
@@ -1423,12 +1457,19 @@ async function runCreate(ctx, opts) {
1423
1457
  const cliModel = parseCliModel(opts.model);
1424
1458
  // Validated here because `claude --effort <unknown>` only warns and runs.
1425
1459
  const cliEffort = parseCliEffort(provider, opts.effort);
1460
+ const profileModel = profileDefaultsApply ? profile?.meta.model : undefined;
1461
+ const profileEffort = profileDefaultsApply ? profile?.meta.effort : undefined;
1462
+ const profileAccount = profileDefaultsApply ? profile?.meta.account : undefined;
1463
+ const profileAccounts = profileDefaultsApply ? profile?.meta.accounts : undefined;
1426
1464
  let pinnedAccount;
1427
1465
  const accountWasExplicit = Boolean(opts.account && opts.account.trim() !== "");
1428
1466
  if (outpostId) {
1429
1467
  if (accountWasExplicit) {
1430
1468
  pinnedAccount = opts.account.trim();
1431
1469
  }
1470
+ else if (profileAccount !== undefined && profileAccounts === undefined) {
1471
+ pinnedAccount = profileAccount;
1472
+ }
1432
1473
  }
1433
1474
  else if (opts.account !== undefined) {
1434
1475
  const requested = opts.account.trim();
@@ -1438,9 +1479,27 @@ async function runCreate(ctx, opts) {
1438
1479
  const resolved = resolveAccount(provider, requested, config, {
1439
1480
  hqRoot,
1440
1481
  cliModel,
1482
+ model: cliModel ?? profileModel,
1441
1483
  });
1442
1484
  pinnedAccount = resolved.id;
1443
1485
  }
1486
+ else if (profileAccount !== undefined && profileAccounts === undefined) {
1487
+ const resolved = resolveAccount(provider, profileAccount, config, {
1488
+ hqRoot,
1489
+ cliModel,
1490
+ model: cliModel ?? profileModel,
1491
+ });
1492
+ pinnedAccount = resolved.id;
1493
+ }
1494
+ if (outpostId !== undefined && profile !== undefined) {
1495
+ throw new LanesError("worker_profile_outpost_unsupported", `Worker profile ${JSON.stringify(profile.id)} cannot be used with --outpost; personal profiles are local-only`, {
1496
+ worker: profile.id,
1497
+ outpost: outpostId,
1498
+ profile_account: profileAccount ?? null,
1499
+ profile_accounts: profileAccounts ?? null,
1500
+ pinned_account: pinnedAccount ?? null,
1501
+ });
1502
+ }
1444
1503
  const priority = parsePriority(opts.priority);
1445
1504
  let timeoutSecs = parseOptionalInt(opts.timeout, "--timeout");
1446
1505
  if (timeoutSecs === undefined && resolvedWorker) {
@@ -1478,6 +1537,9 @@ async function runCreate(ctx, opts) {
1478
1537
  hqRoot,
1479
1538
  existsSync: ctx.existsSync,
1480
1539
  cliModel,
1540
+ ...(profileAccounts !== undefined
1541
+ ? { allowedAccounts: profileAccounts }
1542
+ : {}),
1481
1543
  }).id;
1482
1544
  const toValidate = pinnedAccount !== undefined
1483
1545
  ? [candidate]
@@ -1486,6 +1548,7 @@ async function runCreate(ctx, opts) {
1486
1548
  ...Object.keys(accounts)
1487
1549
  .filter((id) => id !== candidate &&
1488
1550
  accounts[id]?.enabled &&
1551
+ (profileAccounts === undefined || profileAccounts.includes(id)) &&
1489
1552
  accountStatus(provider, id, config, { existsSync: ctx.existsSync }).logged_in === true)
1490
1553
  .sort(),
1491
1554
  ];
@@ -1548,8 +1611,9 @@ async function runCreate(ctx, opts) {
1548
1611
  });
1549
1612
  }
1550
1613
  }
1551
- const threadId = company
1552
- ? await ctx.mesh.ensureProjectThread(company, project)
1614
+ const allowedAccounts = validatedAccounts.size > 0 ? [...validatedAccounts] : profileAccounts;
1615
+ const threadId = meshCompany
1616
+ ? await ctx.mesh.ensureProjectThread(meshCompany, project)
1553
1617
  : undefined;
1554
1618
  const persist = async () => {
1555
1619
  let admission;
@@ -1559,7 +1623,7 @@ async function runCreate(ctx, opts) {
1559
1623
  hqRoot,
1560
1624
  existsSync: ctx.existsSync,
1561
1625
  cliModel,
1562
- ...(validatedAccounts.size > 0 ? { allowedAccounts: [...validatedAccounts] } : {}),
1626
+ ...(allowedAccounts !== undefined ? { allowedAccounts } : {}),
1563
1627
  }).id;
1564
1628
  }
1565
1629
  const accountCfg = pinnedAccount !== undefined
@@ -1567,6 +1631,7 @@ async function runCreate(ctx, opts) {
1567
1631
  : undefined;
1568
1632
  const model = resolveLaneModel({
1569
1633
  cliModel,
1634
+ profileModel,
1570
1635
  accountModel: accountCfg?.model,
1571
1636
  providerModel: config.providers[provider].model,
1572
1637
  });
@@ -1574,6 +1639,7 @@ async function runCreate(ctx, opts) {
1574
1639
  // every source except `cli`.
1575
1640
  const effort = resolveLaneEffort({
1576
1641
  cliEffort,
1642
+ profileEffort,
1577
1643
  accountEffort: accountCfg?.effort,
1578
1644
  providerEffort: config.providers[provider].effort,
1579
1645
  model,
@@ -1587,6 +1653,10 @@ async function runCreate(ctx, opts) {
1587
1653
  hqRoot,
1588
1654
  provider,
1589
1655
  pinnedAccount,
1656
+ ...(profile !== undefined ? { profileId: profile.id } : {}),
1657
+ ...(profile?.meta.max_concurrent !== undefined
1658
+ ? { profileMaxConcurrent: profile.meta.max_concurrent }
1659
+ : {}),
1590
1660
  priority,
1591
1661
  createdAt,
1592
1662
  config,
@@ -1605,6 +1675,9 @@ async function runCreate(ctx, opts) {
1605
1675
  const queuedReason = admission.action === "queue" ? admission.reason : undefined;
1606
1676
  const lane = createLane(hqRoot, {
1607
1677
  company,
1678
+ ...(company === null && meshCompany
1679
+ ? { mesh_company: meshCompany }
1680
+ : {}),
1608
1681
  project_id: project,
1609
1682
  story_id: story,
1610
1683
  thread_id: threadId,
@@ -1617,9 +1690,25 @@ async function runCreate(ctx, opts) {
1617
1690
  worker: resolvedWorker?.id ?? opts.worker,
1618
1691
  ...(resolvedWorker ? { worker_path: resolvedWorker.path } : {}),
1619
1692
  ...(resolvedWorker?.pack ? { worker_pack: resolvedWorker.pack } : {}),
1693
+ ...(profile
1694
+ ? {
1695
+ worker_profile: profile.id,
1696
+ worker_profile_hash: profile.contentHash,
1697
+ worker_profile_ignored: ignoredWorkerProfileFields(provider),
1698
+ }
1699
+ : {}),
1620
1700
  provider,
1621
1701
  account: pinnedAccount,
1622
1702
  ...(model !== undefined ? { model } : {}),
1703
+ model_source: cliModel !== undefined
1704
+ ? "cli"
1705
+ : profileModel !== undefined
1706
+ ? "profile"
1707
+ : accountCfg?.model !== undefined
1708
+ ? "account"
1709
+ : config.providers[provider].model !== undefined
1710
+ ? "provider"
1711
+ : "none",
1623
1712
  ...(effort.effort !== undefined ? { effort: effort.effort } : {}),
1624
1713
  effort_source: effort.source,
1625
1714
  host,
@@ -1668,13 +1757,13 @@ async function runCreate(ctx, opts) {
1668
1757
  runCompaction: ctx.runCompaction,
1669
1758
  locateTranscript: ctx.locateTranscript,
1670
1759
  });
1671
- if (company) {
1672
- await emitLaneMeshEvent(ctx, company, threadId, "lane_started", {
1760
+ if (meshCompany) {
1761
+ await emitLaneMeshEvent(ctx, meshCompany, threadId, "lane_started", {
1673
1762
  lane_id: started.lane_id,
1674
1763
  project_id: project,
1675
1764
  story_id: story,
1676
1765
  });
1677
- await ctx.mesh.patchStoryStatus(company, project, story, "in_progress");
1766
+ await ctx.mesh.patchStoryStatus(meshCompany, project, story, "in_progress");
1678
1767
  }
1679
1768
  }
1680
1769
  else if (outpostId) {
@@ -2012,7 +2101,7 @@ async function runList(ctx, opts) {
2012
2101
  return false;
2013
2102
  if (opts.story && lane.story_id !== opts.story)
2014
2103
  return false;
2015
- if (company && !recordMatchesCompany(lane.company, company)) {
2104
+ if (company && !recordMatchesCompany(lane, company)) {
2016
2105
  return false;
2017
2106
  }
2018
2107
  if (senior && seniorKey(lane.senior) !== seniorKey(senior))
@@ -2195,7 +2284,9 @@ async function prepareAccountMove(ctx, lane, input) {
2195
2284
  const next = {
2196
2285
  ...current,
2197
2286
  account: target,
2198
- ...(input.cliModel !== undefined ? { model: input.cliModel } : {}),
2287
+ ...(input.cliModel !== undefined
2288
+ ? { model: input.cliModel, model_source: "cli" }
2289
+ : {}),
2199
2290
  account_history: appendCappedHistory(current.account_history ?? [], {
2200
2291
  from,
2201
2292
  to: target,
@@ -2751,14 +2842,14 @@ async function runAsk(ctx, opts) {
2751
2842
  questionId,
2752
2843
  });
2753
2844
  questionId = withBody.question_id;
2754
- await emitLaneMeshEvent(ctx, lane.company, lane.thread_id, "lane_question", {
2845
+ await emitLaneMeshEvent(ctx, lane.mesh_company ?? lane.company, lane.thread_id, "lane_question", {
2755
2846
  lane_id: lane.lane_id,
2756
2847
  question_id: withBody.question_id,
2757
2848
  hop: withBody.hop,
2758
2849
  });
2759
2850
  const audience = resolveAudience(hqRoot, withBody, ctx);
2760
2851
  if (audience.kind === "user") {
2761
- await emitLaneMeshEvent(ctx, lane.company, lane.thread_id, "lane_question_at_user", {
2852
+ await emitLaneMeshEvent(ctx, lane.mesh_company ?? lane.company, lane.thread_id, "lane_question_at_user", {
2762
2853
  lane_id: lane.lane_id,
2763
2854
  question_id: withBody.question_id,
2764
2855
  });
@@ -2818,7 +2909,7 @@ async function runQuestionsList(ctx, opts) {
2818
2909
  return false;
2819
2910
  if (opts.story && q.story_id !== opts.story)
2820
2911
  return false;
2821
- if (company && !recordMatchesCompany(q.company, company)) {
2912
+ if (company && !companyRefMatches(q.company, company)) {
2822
2913
  return false;
2823
2914
  }
2824
2915
  if (forFilter) {
@@ -2984,7 +3075,7 @@ async function runQuestionsEscalate(ctx, questionId, opts) {
2984
3075
  const nextAudience = resolveAudience(hqRoot, updated, ctx);
2985
3076
  if (nextAudience.kind === "user") {
2986
3077
  const lane = readLane(hqRoot, updated.lane_id);
2987
- await emitLaneMeshEvent(ctx, lane.company, lane.thread_id, "lane_question_at_user", { lane_id: lane.lane_id, question_id: updated.question_id });
3078
+ await emitLaneMeshEvent(ctx, lane.mesh_company ?? lane.company, lane.thread_id, "lane_question_at_user", { lane_id: lane.lane_id, question_id: updated.question_id });
2988
3079
  }
2989
3080
  printJson({ ok: true, action: "questions.escalate", question: updated });
2990
3081
  }
@@ -3063,10 +3154,13 @@ async function runAccountsLogin(ctx, providerRaw, accountId, opts) {
3063
3154
  if (!id) {
3064
3155
  throw new LanesError("invalid_account", "account-id must be non-empty", {});
3065
3156
  }
3157
+ if (opts.deviceAuth && provider !== "codex") {
3158
+ throw new LanesError("device_auth_unsupported", "--device-auth is supported only for codex account login", { provider });
3159
+ }
3160
+ const registeredAccount = config.providers[provider].accounts[id];
3066
3161
  const homeRaw = opts.home?.trim() ||
3067
- (id === "a"
3068
- ? DEFAULT_ACCOUNT_HOMES[provider]
3069
- : `~/.${provider}-${id}`);
3162
+ registeredAccount?.home ||
3163
+ (id === "a" ? DEFAULT_ACCOUNT_HOMES[provider] : `~/.${provider}-${id}`);
3070
3164
  const homeResolved = homeRaw.startsWith("~") || path.isAbsolute(homeRaw)
3071
3165
  ? homeRaw
3072
3166
  : resolveCallerPath(callerCwd, homeRaw);
@@ -3098,14 +3192,17 @@ async function runAccountsLogin(ctx, providerRaw, accountId, opts) {
3098
3192
  installDrainHook(provider, home);
3099
3193
  const envVar = PROVIDER_HOME_ENV[provider];
3100
3194
  const login = VENDOR_LOGIN[provider];
3195
+ const loginArgs = provider === "codex" && opts.deviceAuth
3196
+ ? [...login.args, "--device-auth"]
3197
+ : login.args;
3101
3198
  const result = await ctx.spawnLogin({
3102
3199
  command: login.command,
3103
- args: login.args,
3200
+ args: loginArgs,
3104
3201
  env: { ...ctx.env, [envVar]: home },
3105
3202
  cwd: home,
3106
3203
  });
3107
3204
  if (result.status !== 0) {
3108
- throw new LanesError("login_failed", `${login.command} ${login.args.join(" ")} exited ${result.status}`, { provider, account: id, status: result.status });
3205
+ throw new LanesError("login_failed", `${login.command} ${loginArgs.join(" ")} exited ${result.status}`, { provider, account: id, status: result.status });
3109
3206
  }
3110
3207
  const grokNeedsRails = provider === "grok";
3111
3208
  if ((grokNeedsRails || copied) && !railsPresent(home)) {
@@ -3300,6 +3397,7 @@ export function registerLanesCommand(parent, deps = {}) {
3300
3397
  .command("create")
3301
3398
  .description("Register a lane, run admission, and return immediately")
3302
3399
  .option("--company <slug|uid>", "Company slug or cloud uid; omit for a core/personal lane")
3400
+ .option("--mesh-company <slug|uid>", "Explicit Work Mesh company for a personal lane; membership is verified")
3303
3401
  .requiredOption("--project <slug>", "HQ project slug")
3304
3402
  .requiredOption("--story <id>", "Story id from the project's prd.json")
3305
3403
  .requiredOption("--brief-file <path>", "Path to the lane brief")
@@ -3472,6 +3570,7 @@ export function registerLanesCommand(parent, deps = {}) {
3472
3570
  .argument("<provider>", "claude | codex | grok")
3473
3571
  .argument("<account-id>", "Open-ended account id")
3474
3572
  .option("--home <path>", "Account home directory")
3573
+ .option("--device-auth", "Use Codex device authentication instead of localhost callback")
3475
3574
  .action(wrap(async (provider, accountId, opts) => {
3476
3575
  await runAccountsLogin(ctx, provider, accountId, opts);
3477
3576
  }));