@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.
Files changed (129) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/assets/scaffold/core/scripts/lib/session-scope-capability.sh +6 -4
  3. package/dist/command-catalog.generated.d.ts +150 -6
  4. package/dist/command-catalog.generated.js +182 -6
  5. package/dist/commands/bot-events.d.ts +19 -0
  6. package/dist/commands/bot-events.js +46 -0
  7. package/dist/commands/bot-jobs.d.ts +2 -0
  8. package/dist/commands/bot-jobs.js +43 -2
  9. package/dist/commands/bot-runtime.d.ts +35 -0
  10. package/dist/commands/bot-runtime.js +115 -0
  11. package/dist/commands/bot.d.ts +11 -0
  12. package/dist/commands/bot.js +72 -11
  13. package/dist/commands/creators.js +2 -1
  14. package/dist/commands/lanes-report.d.ts +2 -1
  15. package/dist/commands/lanes-report.js +44 -1
  16. package/dist/commands/lanes.d.ts +9 -0
  17. package/dist/commands/lanes.js +95 -9
  18. package/dist/commands/library-remove.js +2 -1
  19. package/dist/commands/library.js +5 -4
  20. package/dist/commands/meetings.js +1 -21
  21. package/dist/commands/mesh.d.ts +0 -7
  22. package/dist/commands/mesh.js +5 -1
  23. package/dist/commands/signals-run.d.ts +2 -0
  24. package/dist/commands/signals-run.js +6 -1
  25. package/dist/commands/sync-held.js +2 -2
  26. package/dist/lanes-drain-hook.js +9 -0
  27. package/dist/lib/bot/api.d.ts +5 -0
  28. package/dist/lib/bot/claude-login.d.ts +31 -0
  29. package/dist/lib/bot/claude-login.js +160 -0
  30. package/dist/lib/bot/events.d.ts +18 -0
  31. package/dist/lib/bot/events.js +160 -0
  32. package/dist/lib/bot/executive-settings.d.ts +10 -0
  33. package/dist/lib/bot/executive-settings.js +43 -0
  34. package/dist/lib/bot/fleet-cron.js +4 -0
  35. package/dist/lib/bot/fleet-download.d.ts +27 -0
  36. package/dist/lib/bot/fleet-download.js +162 -0
  37. package/dist/lib/bot/fleet-local.d.ts +51 -2
  38. package/dist/lib/bot/fleet-local.js +179 -76
  39. package/dist/lib/bot/fleet-python.d.ts +2 -0
  40. package/dist/lib/bot/fleet-python.js +29 -0
  41. package/dist/lib/bot/job-run.js +1 -1
  42. package/dist/lib/bot/jobs/hot-signal-sync.d.ts +8 -0
  43. package/dist/lib/bot/jobs/hot-signal-sync.js +2 -0
  44. package/dist/lib/bot/jobs/meeting-detect.d.ts +28 -0
  45. package/dist/lib/bot/jobs/meeting-detect.js +176 -0
  46. package/dist/lib/bot/jobs/meeting-rider.d.ts +53 -0
  47. package/dist/lib/bot/jobs/meeting-rider.js +284 -0
  48. package/dist/lib/bot/jobs/signal-to-lane.d.ts +32 -0
  49. package/dist/lib/bot/jobs/signal-to-lane.js +169 -0
  50. package/dist/lib/bot/local-jobs.d.ts +15 -2
  51. package/dist/lib/bot/local-jobs.js +24 -7
  52. package/dist/lib/bot/run.d.ts +7 -49
  53. package/dist/lib/bot/run.js +100 -4
  54. package/dist/lib/bot/status.d.ts +21 -0
  55. package/dist/lib/doctor/checks/bots.d.ts +6 -0
  56. package/dist/lib/doctor/checks/bots.js +47 -1
  57. package/dist/lib/hot-channels/adapters/meetings.d.ts +1 -0
  58. package/dist/lib/hot-channels/adapters/meetings.js +3 -1
  59. package/dist/lib/hot-channels/meeting-source-ref.d.ts +3 -0
  60. package/dist/lib/hot-channels/meeting-source-ref.js +7 -0
  61. package/dist/lib/hot-channels/schedule.js +2 -2
  62. package/dist/lib/hot-channels/signals-pass.d.ts +9 -32
  63. package/dist/lib/hot-channels/signals-pass.js +51 -17
  64. package/dist/lib/lanes/admission.d.ts +2 -0
  65. package/dist/lib/lanes/admission.js +4 -0
  66. package/dist/lib/lanes/approval-gate.js +4 -1
  67. package/dist/lib/lanes/links.d.ts +8 -1
  68. package/dist/lib/lanes/links.js +84 -3
  69. package/dist/lib/lanes/mesh-events.js +3 -1
  70. package/dist/lib/lanes/outcome.d.ts +154 -0
  71. package/dist/lib/lanes/outcome.js +151 -0
  72. package/dist/lib/lanes/reconcile.js +1 -0
  73. package/dist/lib/lanes/signal-brief-template.d.ts +7 -0
  74. package/dist/lib/lanes/signal-brief-template.js +40 -0
  75. package/dist/lib/lanes/signal-lane.d.ts +24 -0
  76. package/dist/lib/lanes/signal-lane.js +99 -0
  77. package/dist/lib/lanes/source-report.d.ts +37 -0
  78. package/dist/lib/lanes/source-report.js +66 -0
  79. package/dist/lib/lanes/spawn.d.ts +6 -0
  80. package/dist/lib/lanes/spawn.js +21 -0
  81. package/dist/lib/lanes/store.d.ts +3 -0
  82. package/dist/lib/lanes/store.js +64 -0
  83. package/dist/lib/lanes/story-ledger.d.ts +3 -0
  84. package/dist/lib/lanes/story-ledger.js +1 -0
  85. package/dist/lib/lanes/types.d.ts +137 -0
  86. package/dist/lib/lanes/types.js +17 -2
  87. package/dist/lib/lanes/watcher.js +4 -0
  88. package/dist/lib/live-meetings.d.ts +19 -0
  89. package/dist/lib/live-meetings.js +23 -0
  90. package/dist/lib/mesh/api.js +2 -1
  91. package/dist/lib/mesh/live/daemon/doctor.js +6 -7
  92. package/dist/lib/mesh/live/daemon/run.d.ts +0 -10
  93. package/dist/lib/mesh/live/daemon/run.js +3 -0
  94. package/dist/lib/mesh/live/emit.d.ts +2 -11
  95. package/dist/lib/mesh/live/emit.js +20 -2
  96. package/dist/lib/mesh/live/flush.d.ts +2 -0
  97. package/dist/lib/mesh/live/flush.js +34 -3
  98. package/dist/lib/mesh/live/reporting-privacy.d.ts +7 -0
  99. package/dist/lib/mesh/live/reporting-privacy.js +47 -0
  100. package/dist/lib/mesh/live/spool.d.ts +8 -0
  101. package/dist/lib/mesh/live/spool.js +59 -40
  102. package/dist/lib/plan-limit-nag.d.ts +30 -4
  103. package/dist/lib/plan-limit-nag.js +68 -17
  104. package/dist/lib/signals/engine.d.ts +4 -0
  105. package/dist/lib/signals/engine.js +25 -5
  106. package/dist/lib/signals/incremental.d.ts +42 -0
  107. package/dist/lib/signals/incremental.js +191 -0
  108. package/dist/lib/signals/prior-signals.d.ts +11 -0
  109. package/dist/lib/signals/prior-signals.js +44 -0
  110. package/dist/lib/signals/reply.js +6 -1
  111. package/dist/lib/signals/settings.d.ts +5 -0
  112. package/dist/lib/signals/settings.js +17 -0
  113. package/dist/lib/signals/steps/extract.d.ts +23 -2
  114. package/dist/lib/signals/steps/extract.js +142 -9
  115. package/dist/lib/signals/steps/types.d.ts +1 -0
  116. package/dist/lib/signals/write-guard.js +7 -0
  117. package/dist/lib/work-context/company.d.ts +2 -3
  118. package/dist/lib/work-context/company.js +4 -22
  119. package/dist/secrets-env-action.js +5 -2
  120. package/dist/utils/cognito-callback-port.d.ts +17 -0
  121. package/dist/utils/cognito-callback-port.js +56 -0
  122. package/dist/utils/cognito-config.js +7 -3
  123. package/dist/utils/cognito-session.d.ts +7 -6
  124. package/dist/utils/cognito-session.js +19 -14
  125. package/dist/utils/interactive-caller.d.ts +23 -0
  126. package/dist/utils/interactive-caller.js +80 -0
  127. package/dist/utils/login-browser.js +5 -2
  128. package/dist/utils/resident-package-preload.js +4 -0
  129. package/package.json +7 -7
@@ -1541,6 +1541,16 @@ function recoverRawJsonArrayField(rawText, field) {
1541
1541
  }
1542
1542
  return undefined;
1543
1543
  }
1544
+ /**
1545
+ * Repair-turn guidance for the optional outcome block. Every repair prompt
1546
+ * names the block, its three states, and the consistency rules so a model
1547
+ * that wrote a malformed outcome can fix it in the single repair turn.
1548
+ */
1549
+ export const LANE_OUTCOME_REPAIR_GUIDANCE = "Optional outcome block: if you include \"outcome\", it is an object with exactly the keys state, outcome, gathered, next_steps, and optional decision_id. " +
1550
+ "state is one of done, needs_decision, partial. outcome is a non-empty string; gathered is an array of {label, text, ref?}; next_steps is an array of non-empty strings. " +
1551
+ "state needs_decision requires a non-empty outcome.decision_id or a non-empty questions[] (with decision ask). " +
1552
+ "state partial requires at least one next_steps entry. state done must not be combined with decision blocked. " +
1553
+ "If you cannot satisfy these rules, remove the outcome block; the envelope is valid without it.";
1544
1554
  export function envelopeRepairPrompt(input) {
1545
1555
  const maxRawTextChars = 24_000;
1546
1556
  const rawText = input.rawText.length <= maxRawTextChars
@@ -1553,6 +1563,7 @@ export function envelopeRepairPrompt(input) {
1553
1563
  "Printing an envelope in the conversation or writing it to last-message.txt is not enough; write the JSON object directly to the expected file.",
1554
1564
  "If the task is unfinished, write decision: done, story_complete: false, and set story_status and story_complete_reason to describe the work that remains. The repair turn itself is never a reason for blocked.",
1555
1565
  "This repair turn only writes the missing envelope; do not resume the task.",
1566
+ LANE_OUTCOME_REPAIR_GUIDANCE,
1556
1567
  "Schema:",
1557
1568
  JSON.stringify(LANE_ENVELOPE_JSON_SCHEMA, null, 2),
1558
1569
  `Write the corrected JSON object to ${input.envelopePath}, overwriting it. ` +
@@ -1564,6 +1575,7 @@ export function envelopeRepairPrompt(input) {
1564
1575
  `The file you wrote at ${input.envelopePath} is not a valid hq-lane-envelope.v1. This is the single repair turn: rewrite that file so it validates, and do nothing else.`,
1565
1576
  `Parser error: ${input.error}`,
1566
1577
  "For an ask question, include non-empty id, question, and context fields.",
1578
+ LANE_OUTCOME_REPAIR_GUIDANCE,
1567
1579
  "Schema:", JSON.stringify(LANE_ENVELOPE_JSON_SCHEMA, null, 2),
1568
1580
  "Your previous output was:", rawText,
1569
1581
  `Write the corrected JSON object to ${input.envelopePath}, overwriting it. Output only that JSON. Do not add keys the schema does not name, and do not resume the task.`,
@@ -1645,6 +1657,7 @@ export function envelopeRepairPrompt(input) {
1645
1657
  ? "This is a JSON syntax error. Fix the exact reported line and column and remove the offending token or delimiter. Do not add trailing commas."
1646
1658
  : "This is a schema validation error. Correct the named field path and use only the allowed value(s) stated in the error.",
1647
1659
  "Keep every supplied value that is already valid. Do not invent next_check_at; it is optional. For decision=ask include non-empty id, question, and context fields; for decision=blocked include blocked_reason.",
1660
+ LANE_OUTCOME_REPAIR_GUIDANCE,
1648
1661
  "Minimal valid envelope shape (retain your original decision and values when valid):",
1649
1662
  JSON.stringify(minimalEnvelope, null, 2),
1650
1663
  "Schema:",
@@ -2675,6 +2688,10 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
2675
2688
  : undefined;
2676
2689
  const childEnv = workerEnv(env, { envVar: resolved.envVar, home: remoteControlHome ?? resolved.home }, lane.lane_id, hqRoot, autoCompactTokens, lane.mesh_company ?? lane.company ?? undefined, workerPolicyDir, lane.registry_worker, undefined, laneWorkerSessionEnv(existing, plan.mode === "resume" ? plan.session_id : remoteSessionId));
2677
2690
  const laneTurnId = randomUUID();
2691
+ if (lane.signal_link)
2692
+ childEnv.HQ_LANE_SIGNAL_LINK = lane.signal_link;
2693
+ else
2694
+ delete childEnv.HQ_LANE_SIGNAL_LINK;
2678
2695
  childEnv.HQ_LANE_TURN_ID = laneTurnId;
2679
2696
  const fingerprint = (deps.buildFingerprint ?? laneStartFingerprint)(hqRoot, childEnv);
2680
2697
  const model = resolveLaneModel({
@@ -3322,6 +3339,10 @@ export function spawnEnvelopeRepair(hqRoot, lane, input, deps = {}) {
3322
3339
  ? providerAutoCompactTokens(config.providers.claude)
3323
3340
  : undefined;
3324
3341
  const childEnv = workerEnv(env, { envVar: resolved.envVar, home: resolved.home }, lane.lane_id, hqRoot, autoCompactTokens, lane.mesh_company ?? lane.company ?? undefined, workerPolicyDir, lane.registry_worker, undefined, laneWorkerSessionEnv(lane, mode === "resume" && sessionId !== null ? sessionId : undefined));
3342
+ if (lane.signal_link)
3343
+ childEnv.HQ_LANE_SIGNAL_LINK = lane.signal_link;
3344
+ else
3345
+ delete childEnv.HQ_LANE_SIGNAL_LINK;
3325
3346
  const accountCfg = config.providers[lane.provider].accounts[resolved.id];
3326
3347
  const model = resolveLaneModel({
3327
3348
  cliModel: pinnedModel,
@@ -1,6 +1,9 @@
1
1
  import * as fs from "node:fs";
2
2
  import { type OutpostIdentity } from "./admission-host.js";
3
3
  import { type AttributionHistoryEntry, type LaneRecord, type LaneWriter, type QuestionRecord, type RoutingHistoryEntry, type SeniorHistoryEntry } from "./types.js";
4
+ /** Signal identity is company-scoped and survives terminal lane states. */
5
+ export declare function findLaneBySignal(company: string, signalId: string, hqRoot?: string): LaneRecord | undefined;
6
+ export declare function listLanesBySource(company: string, sourceRef: string, hqRoot?: string): LaneRecord[];
4
7
  export declare const LANES_STORE_REL: string;
5
8
  export declare const LANE_RECORDS_DIR = "lanes";
6
9
  export declare const QUESTION_RECORDS_DIR = "questions";
@@ -19,8 +19,52 @@ import { readOutpostIdentity } from "./admission-host.js";
19
19
  import { LaneNotFoundError, LaneSchemaError, LaneVersionConflict, LanesError, isLaneVersionConflict, } from "./errors.js";
20
20
  import { HISTORY_CAP, HOST_KINDS, LANE_STORY_STATUSES, LANE_SCHEMA_VERSION, QUESTION_SCHEMA_VERSION, SENIOR_KINDS, isHostKind, isLaneProvider, isQueuedReason, isLaneState, isQuestionStatus, isSeniorKind, isTerminalLaneState, parseMajorSchema, REQUIRED_LANE_RECORD_FIELDS, LANE_FIELD_WRITERS, LANE_RUNNERS, LANE_WRITERS, } from "./types.js";
21
21
  import { laneStories } from "./stories.js";
22
+ import { parseLaneOutcomeShape } from "./outcome.js";
22
23
  import { laneStoryLedgerEnabled, syncLaneToProjectStoryLedger, validateProjectStories, } from "./story-ledger.js";
23
24
  import { liveOwnerMaxMs, reclaimStaleLock } from "./stale-lock.js";
25
+ /** Signal identity is company-scoped and survives terminal lane states. */
26
+ export function findLaneBySignal(company, signalId, hqRoot = resolveStoreHqRoot()) {
27
+ return readAllLanes(hqRoot).find(lane => lane.company?.slug === company && lane.signal?.signal_id === signalId);
28
+ }
29
+ export function listLanesBySource(company, sourceRef, hqRoot = resolveStoreHqRoot()) {
30
+ if (!/^[a-zA-Z0-9_-]+$/.test(company))
31
+ throw new LaneSchemaError("Invalid company slug");
32
+ const lanes = readAllLanes(hqRoot).filter(lane => lane.company?.slug === company && lane.signal?.source_ref === sourceRef);
33
+ if (!lanes.length)
34
+ return lanes;
35
+ // Daily indexes preserve extraction order, which can differ from dispatch order.
36
+ const index = path.join(hqRoot, "companies", company, "signals", "_index");
37
+ const order = new Map();
38
+ let files;
39
+ try {
40
+ files = fs.readdirSync(index).filter(name => name.endsWith(".json")).sort();
41
+ }
42
+ catch (error) {
43
+ if (error.code !== "ENOENT")
44
+ throw error;
45
+ files = [];
46
+ }
47
+ if (files.length) {
48
+ const expected = path.join(fs.realpathSync(hqRoot), "companies", company, "signals", "_index");
49
+ if (fs.realpathSync(index) !== expected)
50
+ throw new LaneSchemaError("Signal index must stay within this company");
51
+ for (const file of files) {
52
+ const filePath = path.join(index, file);
53
+ if (fs.realpathSync(filePath) !== path.join(expected, file))
54
+ throw new LaneSchemaError("Signal index file must stay within this company");
55
+ const rows = JSON.parse(fs.readFileSync(filePath, "utf8"));
56
+ if (!Array.isArray(rows))
57
+ throw new LaneSchemaError("Signal index must contain an array");
58
+ for (const row of rows) {
59
+ if (row && typeof row === "object" && row.source_ref === sourceRef && typeof row.signal_id === "string" && !order.has(row.signal_id)) {
60
+ order.set(row.signal_id, order.size);
61
+ }
62
+ }
63
+ }
64
+ }
65
+ return lanes.sort((a, b) => (order.get(a.signal.signal_id) ?? Number.MAX_SAFE_INTEGER) - (order.get(b.signal.signal_id) ?? Number.MAX_SAFE_INTEGER)
66
+ || a.created_at.localeCompare(b.created_at) || a.lane_id.localeCompare(b.lane_id));
67
+ }
24
68
  export const LANES_STORE_REL = path.join("workspace", "lanes");
25
69
  export const LANE_RECORDS_DIR = "lanes";
26
70
  export const QUESTION_RECORDS_DIR = "questions";
@@ -451,6 +495,17 @@ export function parseLaneRecord(raw, filePath) {
451
495
  const schema_version = requireString(raw.schema_version, "schema_version");
452
496
  parseMajorSchema(schema_version, "hq-lane", 1);
453
497
  requireString(raw.lane_id, "lane_id");
498
+ if (raw.signal !== undefined) {
499
+ if (!raw.signal || typeof raw.signal !== "object" || Array.isArray(raw.signal)) {
500
+ throw new LaneSchemaError("signal must be an object", { field: "signal" });
501
+ }
502
+ const signal = raw.signal;
503
+ for (const field of ["signal_id", "type", "source_ref", "title"])
504
+ requireString(signal[field], "signal." + field);
505
+ }
506
+ if (raw.signal_link !== undefined && (typeof raw.signal_link !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(raw.signal_link) || !raw.signal)) {
507
+ throw new LaneSchemaError("signal_link requires a signal and a valid link id", { field: "signal_link" });
508
+ }
454
509
  if (raw.remote_control !== undefined && (raw.remote_control !== true || raw.provider !== "claude")) {
455
510
  throw new LaneSchemaError("remote_control requires a Claude lane", {
456
511
  path: filePath,
@@ -666,6 +721,15 @@ export function parseLaneRecord(raw, filePath) {
666
721
  raw.envelope_validation_errors.some((error) => typeof error !== "string" || error.trim() === ""))) {
667
722
  throw new LaneSchemaError("envelope_validation_errors must be a non-empty array of non-empty strings", { path: filePath, field: "envelope_validation_errors" });
668
723
  }
724
+ if (raw.outcome !== undefined) {
725
+ try {
726
+ parseLaneOutcomeShape(raw.outcome);
727
+ }
728
+ catch (error) {
729
+ const message = error instanceof Error ? error.message : String(error);
730
+ throw new LaneSchemaError(`outcome is malformed: ${message}`, { path: filePath, field: "outcome" });
731
+ }
732
+ }
669
733
  if (raw.fallback_question_cancellation_failures !== undefined) {
670
734
  if (!Array.isArray(raw.fallback_question_cancellation_failures)) {
671
735
  throw new LaneSchemaError("fallback_question_cancellation_failures must be an array", {
@@ -1,3 +1,4 @@
1
+ import type { LaneOutcome } from "./outcome.js";
1
2
  import { type LaneRecord, type LaneState } from "./types.js";
2
3
  export declare const LANE_STORY_LEDGER_FLAG = "lanes.story-ledger";
3
4
  declare const STORY_STATES: readonly ["todo", "in_progress", "review", "waiting", "blocked", "done", "dropped"];
@@ -55,6 +56,8 @@ export interface LaneStatusSnapshot {
55
56
  state: LaneState;
56
57
  queued_reason: string | null;
57
58
  fingerprint: string | null;
59
+ /** The last envelope's outcome block, or null when it carried none. */
60
+ outcome: LaneOutcome | null;
58
61
  }>;
59
62
  prs: Array<{
60
63
  story_id: string;
@@ -813,6 +813,7 @@ export function buildLaneStatusSnapshot(input) {
813
813
  state: lane.state,
814
814
  queued_reason: lane.queued_reason ?? null,
815
815
  fingerprint: lane.fingerprint ?? null,
816
+ outcome: lane.outcome ?? null,
816
817
  }));
817
818
  const projectPrs = [];
818
819
  const blockers = [];
@@ -3,6 +3,7 @@
3
3
  * §4, §5.5, §6.2). Finite sets are string-literal unions plus `const` arrays
4
4
  * so the command layer can validate flags explicitly.
5
5
  */
6
+ import { type LaneOutcome } from "./outcome.js";
6
7
  export declare const LANE_SCHEMA_VERSION: "hq-lane.v1";
7
8
  export declare const QUESTION_SCHEMA_VERSION: "hq-lane-question.v1";
8
9
  export declare const ENVELOPE_SCHEMA_VERSION: "hq-lane-envelope.v1";
@@ -85,6 +86,76 @@ export declare const LANE_ENVELOPE_JSON_SCHEMA: {
85
86
  readonly type: readonly ["string", "null"];
86
87
  readonly description: "Optional worker fingerprint (hq fingerprint self); null when it could not be built. Older readers ignore it.";
87
88
  };
89
+ readonly outcome: {
90
+ readonly type: "object";
91
+ readonly additionalProperties: false;
92
+ readonly required: readonly ["state", "outcome", "gathered", "next_steps"];
93
+ readonly properties: {
94
+ readonly state: {
95
+ readonly enum: readonly ["done", "needs_decision", "partial"];
96
+ };
97
+ readonly outcome: {
98
+ readonly type: "string";
99
+ readonly minLength: 1;
100
+ readonly pattern: "\\S";
101
+ };
102
+ readonly gathered: {
103
+ readonly type: "array";
104
+ readonly items: {
105
+ readonly type: "object";
106
+ readonly additionalProperties: false;
107
+ readonly required: readonly ["label", "text"];
108
+ readonly properties: {
109
+ readonly label: {
110
+ readonly type: "string";
111
+ readonly minLength: 1;
112
+ readonly pattern: "\\S";
113
+ };
114
+ readonly text: {
115
+ readonly type: "string";
116
+ readonly minLength: 1;
117
+ readonly pattern: "\\S";
118
+ };
119
+ readonly ref: {
120
+ readonly type: "string";
121
+ readonly minLength: 1;
122
+ readonly pattern: "\\S";
123
+ };
124
+ };
125
+ };
126
+ };
127
+ readonly next_steps: {
128
+ readonly type: "array";
129
+ readonly items: {
130
+ readonly type: "string";
131
+ readonly minLength: 1;
132
+ readonly pattern: "\\S";
133
+ };
134
+ };
135
+ readonly decision_id: {
136
+ readonly type: "string";
137
+ readonly minLength: 1;
138
+ readonly pattern: "\\S";
139
+ };
140
+ };
141
+ readonly allOf: readonly [{
142
+ readonly if: {
143
+ readonly properties: {
144
+ readonly state: {
145
+ readonly const: "partial";
146
+ };
147
+ };
148
+ };
149
+ readonly then: {
150
+ readonly properties: {
151
+ readonly next_steps: {
152
+ readonly minItems: 1;
153
+ };
154
+ };
155
+ };
156
+ }];
157
+ readonly description: string;
158
+ };
88
159
  };
89
160
  readonly allOf: readonly [{
90
161
  readonly if: {
@@ -171,6 +242,57 @@ export declare const LANE_ENVELOPE_JSON_SCHEMA: {
171
242
  };
172
243
  };
173
244
  };
245
+ }, {
246
+ readonly if: {
247
+ readonly required: readonly ["outcome"];
248
+ readonly properties: {
249
+ readonly outcome: {
250
+ readonly properties: {
251
+ readonly state: {
252
+ readonly const: "needs_decision";
253
+ };
254
+ };
255
+ };
256
+ };
257
+ };
258
+ readonly then: {
259
+ readonly anyOf: readonly [{
260
+ readonly properties: {
261
+ readonly outcome: {
262
+ readonly required: readonly ["decision_id"];
263
+ };
264
+ };
265
+ }, {
266
+ readonly required: readonly ["questions"];
267
+ readonly properties: {
268
+ readonly questions: {
269
+ readonly minItems: 1;
270
+ };
271
+ };
272
+ }];
273
+ };
274
+ }, {
275
+ readonly if: {
276
+ readonly required: readonly ["outcome"];
277
+ readonly properties: {
278
+ readonly decision: {
279
+ readonly const: "blocked";
280
+ };
281
+ };
282
+ };
283
+ readonly then: {
284
+ readonly properties: {
285
+ readonly outcome: {
286
+ readonly properties: {
287
+ readonly state: {
288
+ readonly not: {
289
+ readonly const: "done";
290
+ };
291
+ };
292
+ };
293
+ };
294
+ };
295
+ };
174
296
  }];
175
297
  readonly description: string;
176
298
  };
@@ -523,7 +645,15 @@ export interface LaneStory {
523
645
  verified_at?: string;
524
646
  verified_by?: SeniorRef;
525
647
  }
648
+ export interface LaneSignal {
649
+ signal_id: string;
650
+ type: string;
651
+ source_ref: string;
652
+ title: string;
653
+ }
526
654
  export interface LaneRecord {
655
+ signal?: LaneSignal;
656
+ signal_link?: string;
527
657
  schema_version: typeof LANE_SCHEMA_VERSION | string;
528
658
  lane_id: string;
529
659
  /** CLI release that most recently wrote this lane record. Absent on legacy rows. */
@@ -729,6 +859,11 @@ export interface LaneRecord {
729
859
  story_status?: StoryStatus;
730
860
  /** Envelope parser failures retained when a usable ask is recovered. */
731
861
  envelope_validation_errors?: string[];
862
+ /**
863
+ * Copied from the last ingested envelope's optional `outcome` block.
864
+ * Cleared when a later envelope carries no outcome.
865
+ */
866
+ outcome?: LaneOutcome;
732
867
  /** Set by the reconcile pass when `--timeout` has passed (spec §5.7). */
733
868
  overdue?: boolean;
734
869
  overdue_at?: string;
@@ -1027,6 +1162,8 @@ export interface LaneEnvelope {
1027
1162
  story_complete_reason?: string;
1028
1163
  /** The worker's fingerprint, or null when it could not be built. Optional. */
1029
1164
  fingerprint?: string | null;
1165
+ /** Optional structured result block (see outcome.ts). */
1166
+ outcome?: LaneOutcome;
1030
1167
  }
1031
1168
  export interface AccountConfig {
1032
1169
  /** Company slug or UID owning this account. Untagged accounts are personal. */
@@ -4,6 +4,7 @@
4
4
  * so the command layer can validate flags explicitly.
5
5
  */
6
6
  import { LanesError, LaneSchemaError } from "./errors.js";
7
+ import { LANE_OUTCOME_ENVELOPE_RULES, LANE_OUTCOME_JSON_SCHEMA, parseLaneOutcome, } from "./outcome.js";
7
8
  export const LANE_SCHEMA_VERSION = "hq-lane.v1";
8
9
  export const QUESTION_SCHEMA_VERSION = "hq-lane-question.v1";
9
10
  export const ENVELOPE_SCHEMA_VERSION = "hq-lane-envelope.v1";
@@ -53,6 +54,7 @@ export const LANE_ENVELOPE_JSON_SCHEMA = {
53
54
  type: ["string", "null"],
54
55
  description: "Optional worker fingerprint (hq fingerprint self); null when it could not be built. Older readers ignore it.",
55
56
  },
57
+ outcome: LANE_OUTCOME_JSON_SCHEMA,
56
58
  },
57
59
  allOf: [
58
60
  {
@@ -80,11 +82,16 @@ export const LANE_ENVELOPE_JSON_SCHEMA = {
80
82
  else: { not: { required: ["story_complete_reason"] } },
81
83
  },
82
84
  },
85
+ ...LANE_OUTCOME_ENVELOPE_RULES,
83
86
  ],
84
87
  description: "questions is present and non-empty exactly when decision is ask. " +
85
88
  "blocked_reason is present exactly when decision is blocked. " +
86
89
  "story_complete true requires story_status done; story_complete false requires story_status other than done; " +
87
- "story_complete_reason is required whenever story_complete is present.",
90
+ "story_complete_reason is required whenever story_complete is present. " +
91
+ "outcome is optional; outcome.state is done, needs_decision or partial. " +
92
+ "needs_decision requires outcome.decision_id or a non-empty questions[]; " +
93
+ "partial requires at least one outcome.next_steps entry; " +
94
+ "outcome.state done is rejected when decision is blocked.",
88
95
  };
89
96
  export const LANE_STATES = [
90
97
  "queued",
@@ -247,6 +254,7 @@ export const REQUIRED_LANE_RECORD_FIELDS = [
247
254
  export const LANE_WRITERS = ["creator", "command", "watcher", "reconcile", "spawn", "story-ledger"];
248
255
  /** Field ownership at the durable-record mutation boundary (spec §10.1). */
249
256
  export const LANE_FIELD_WRITERS = {
257
+ signal: ["creator"], signal_link: ["creator"],
250
258
  schema_version: ["creator"], lane_id: ["creator"], written_by_version: [], owner_written_by_version: [], company: ["creator", "command"], company_history: ["command"],
251
259
  project_id: ["command"], story_id: ["command"], no_story_reason: ["creator", "command"], focus_overrides: ["command"], ledger_sync_error: ["story-ledger"], pending_question_ids: ["story-ledger"], stories: ["creator", "command", "watcher", "reconcile", "spawn"], thread_id: ["creator", "command", "spawn"], mesh_company: ["creator", "command"],
252
260
  depth: ["command"], senior: ["command"], senior_history: ["command"],
@@ -260,7 +268,7 @@ export const LANE_FIELD_WRITERS = {
260
268
  provider: ["creator", "command"], runner: ["creator"], remote_control: ["creator"], remote_control_title: ["creator"], provider_source: ["creator", "command"], account: ["creator", "spawn", "command", "watcher", "reconcile"], account_source: ["creator", "command"], profile_settings_pinned: ["command"], account_selection: ["creator", "command"], account_pick: ["creator"], admission_accounts: ["creator", "command"], account_history: ["command", "spawn", "watcher", "reconcile"], failed_resume_history: ["command"], fallback_question_cancellation_failures: ["command"], pending_resume: ["command", "spawn", "watcher", "reconcile"], placement_default: ["creator"], host: ["creator", "command"], origin_host: ["creator"], origin_host_source: ["creator"],
261
269
  version: ["creator"], created_at: ["creator"], updated_at: ["creator"], ended_at: ["reconcile"], envelope_summary: ["reconcile"], attribution_history: ["command", "watcher", "reconcile"], story_switch_pending: ["command", "watcher", "reconcile"], story_switch_unacknowledged: ["command", "watcher", "reconcile"],
262
270
  last_envelope: ["watcher", "reconcile"], model: ["creator", "spawn", "command"], model_source: ["creator", "spawn", "command"], effort: ["creator", "spawn", "command"], effort_source: ["creator", "spawn", "command"], fast_mode: ["spawn"],
263
- last_message_at: ["creator", "command"], status_gate: ["spawn", "watcher"], status_gate_history: ["watcher", "reconcile"], blocker_freshness_state: ["watcher"], kr_freshness_state: ["watcher"], last_compaction: ["spawn"], story_complete: ["watcher", "reconcile"], story_complete_reason: ["watcher", "reconcile"], story_status: ["watcher", "reconcile"],
271
+ last_message_at: ["creator", "command"], status_gate: ["spawn", "watcher"], status_gate_history: ["watcher", "reconcile"], blocker_freshness_state: ["watcher"], kr_freshness_state: ["watcher"], last_compaction: ["spawn"], story_complete: ["watcher", "reconcile"], story_complete_reason: ["watcher", "reconcile"], story_status: ["watcher", "reconcile"], outcome: ["watcher", "reconcile"],
264
272
  overdue: ["reconcile"], overdue_at: ["reconcile"], last_host_report_at: ["watcher", "spawn", "reconcile"], stalled: ["watcher"], stalled_at: ["watcher"], stalled_reason: ["watcher"],
265
273
  admitted_reason: ["command", "reconcile"], terminal_reason: ["command", "watcher", "spawn", "reconcile"], stop_history: ["command", "reconcile"], settings_history: ["command"], pending_settings: ["command", "spawn"], interrupted_at: ["command", "watcher", "reconcile", "spawn"], terminal_details: ["command", "spawn", "watcher", "reconcile"],
266
274
  quota_reset_at: ["watcher", "reconcile", "spawn", "command"], model_quota_marker: ["watcher", "reconcile"], outpost_command_id: ["command"],
@@ -322,6 +330,7 @@ export function parseLaneEnvelope(raw, options = {}) {
322
330
  "story_complete",
323
331
  "story_complete_reason",
324
332
  "fingerprint",
333
+ "outcome",
325
334
  ].includes(key));
326
335
  if (extra.length > 0) {
327
336
  throw new LaneSchemaError(`Lane envelope rejects unknown properties: ${extra.join(", ")}`, { extra });
@@ -453,6 +462,12 @@ export function parseLaneEnvelope(raw, options = {}) {
453
462
  if (typeof raw.blocked_reason === "string") {
454
463
  envelope.blocked_reason = raw.blocked_reason;
455
464
  }
465
+ if (raw.outcome !== undefined) {
466
+ envelope.outcome = parseLaneOutcome(raw.outcome, {
467
+ decision: envelope.decision,
468
+ questionCount: envelope.questions?.length ?? 0,
469
+ });
470
+ }
456
471
  return envelope;
457
472
  }
458
473
  //# sourceMappingURL=types.js.map
@@ -425,6 +425,8 @@ function applyTerminalToLaneWithResult(hqRoot, laneId, classification, opts = {}
425
425
  },
426
426
  ...(envelope.decision !== "blocked" ? { keep_alive_blocked_resume_streak: 0 } : {}),
427
427
  envelope_validation_errors: opts.envelopeValidationErrors,
428
+ // The last envelope's outcome block; undefined clears a stale one.
429
+ outcome: envelope.outcome,
428
430
  };
429
431
  if (!approvalGate.violation && approvalGate.approvedIdsToConsume.length > 0) {
430
432
  next.registry_worker_approval_history = approvalGate.approvedIdsToConsume.reduce((history, approvalId) => appendCappedHistory(history, {
@@ -509,6 +511,8 @@ function applyTerminalToLaneWithResult(hqRoot, laneId, classification, opts = {}
509
511
  const storySwitch = applyStorySwitchEnvelope(current, envelope, at, envelopeRef);
510
512
  if (envelope.decision !== "blocked")
511
513
  next.keep_alive_blocked_resume_streak = 0;
514
+ // The last envelope's outcome block; undefined clears a stale one.
515
+ next.outcome = envelope.outcome;
512
516
  next.last_envelope = {
513
517
  ref: envelopeRef,
514
518
  decision: envelope.decision,
@@ -0,0 +1,19 @@
1
+ export interface LiveMeetingBot {
2
+ meetingId?: string;
3
+ meetingUrl?: string;
4
+ recallBotId?: string;
5
+ botId?: string;
6
+ companyId?: string;
7
+ meetingTitle?: string;
8
+ title?: string;
9
+ status?: string;
10
+ scheduledStartTime?: string;
11
+ createdAt?: string;
12
+ linesSoFar?: number;
13
+ transcriptLineCount?: number;
14
+ sourceLanded?: boolean;
15
+ savedTranscriptPath?: string;
16
+ }
17
+ /** The attended list used by meetings live and the Executive's poll producer. */
18
+ export declare function fetchAttendedLiveBots(token: string, companyId: string, strict?: boolean, signal?: AbortSignal): Promise<LiveMeetingBot[]>;
19
+ //# sourceMappingURL=live-meetings.d.ts.map
@@ -0,0 +1,23 @@
1
+ import { vaultApiFetch } from "../utils/vault-api.js";
2
+ /** The attended list used by meetings live and the Executive's poll producer. */
3
+ export async function fetchAttendedLiveBots(token, companyId, strict = false, signal) {
4
+ try {
5
+ const res = await vaultApiFetch({ token, path: "/v1/meetings", query: { companyId, view: "live" }, ...(signal ? { signal } : {}) });
6
+ if (!res.ok) {
7
+ if (strict)
8
+ throw new Error("Could not list attended meetings");
9
+ return [];
10
+ }
11
+ const data = (await res.json());
12
+ if (strict && !Array.isArray(data.bots))
13
+ throw new Error("Invalid attended meetings response");
14
+ return Array.isArray(data.bots) ? data.bots : [];
15
+ }
16
+ catch (err) {
17
+ if (strict)
18
+ throw err;
19
+ // Older servers and offline callers retain the CLI's own-notetaker list.
20
+ return [];
21
+ }
22
+ }
23
+ //# sourceMappingURL=live-meetings.js.map
@@ -9,6 +9,7 @@ import { ensureCognitoToken, isMachineIdentity } from "../../utils/cognito-sessi
9
9
  import { peekIdToken } from "../../utils/id-token.js";
10
10
  import { getCompanyUid, vaultApiFetch } from "../../utils/vault-api.js";
11
11
  import { meshCacheRoot, removeSessionFallbackFiles, writeMeshCacheFile, } from "./cache.js";
12
+ import { gatedInteractiveLogin } from "../../utils/interactive-caller.js";
12
13
  const PRINCIPAL_UID = /^(prs|agt)_/;
13
14
  export const STORY_STATUSES = new Set([
14
15
  "queued",
@@ -183,7 +184,7 @@ export async function requireToken(opts = {}) {
183
184
  // hard policy indigo-agent-caller-resolution-id-token, agent claims ride
184
185
  // the ID token only; the server is correct to require it. Humans keep the
185
186
  // person access token (unchanged).
186
- const interactive = opts.interactive ?? true;
187
+ const interactive = gatedInteractiveLogin(opts.interactive ?? true);
187
188
  if (isMachineIdentity()) {
188
189
  return ensureCognitoToken({ tokenSource: "machine", interactive });
189
190
  }
@@ -12,7 +12,7 @@ import { workContextRoot } from "../../../work-context/paths.js";
12
12
  import { outboxQuarantineStatusEnabled } from "../../../work-context/outbox-quarantine-status-flag.js";
13
13
  import { formatDroppedPresenceCompany } from "./credentials.js";
14
14
  import { formatOwnerPushStatus, ownerPushStatusForAccount, readOwnerInboxPushState, } from "./owner-inbox-push.js";
15
- import { countDistinctHeldEvents, countJsonlLines, countUnattributedEvents, } from "../spool.js";
15
+ import { countDistinctHeldEvents, countJsonlLines, countUnattributedEvents, forEachJsonlLine, } from "../spool.js";
16
16
  import { workMeshDeadLetterPath, workMeshHeldPath, workMeshRoot, workMeshSpoolPath, } from "../paths.js";
17
17
  import { daemonDir, daemonPidPath } from "./paths.js";
18
18
  import { readEmitState } from "../emit.js";
@@ -31,11 +31,10 @@ function oldestJsonlAgeMs(filePath, nowMs) {
31
31
  if (!fs.existsSync(filePath))
32
32
  return undefined;
33
33
  try {
34
- const text = fs.readFileSync(filePath, "utf8");
35
34
  let oldest;
36
- for (const line of text.split("\n")) {
35
+ forEachJsonlLine(filePath, (line) => {
37
36
  if (!line.trim())
38
- continue;
37
+ return;
39
38
  try {
40
39
  const obj = JSON.parse(line);
41
40
  const at = (typeof obj.at === "string" && obj.at) ||
@@ -43,10 +42,10 @@ function oldestJsonlAgeMs(filePath, nowMs) {
43
42
  (obj.event && typeof obj.event.at === "string" && obj.event.at) ||
44
43
  undefined;
45
44
  if (!at)
46
- continue;
45
+ return;
47
46
  const t = Date.parse(at);
48
47
  if (!Number.isFinite(t))
49
- continue;
48
+ return;
50
49
  const age = nowMs - t;
51
50
  if (oldest === undefined || age > oldest)
52
51
  oldest = age;
@@ -54,7 +53,7 @@ function oldestJsonlAgeMs(filePath, nowMs) {
54
53
  catch {
55
54
  /* skip */
56
55
  }
57
- }
56
+ });
58
57
  return oldest;
59
58
  }
60
59
  catch {
@@ -1,13 +1,3 @@
1
- /**
2
- * hq mesh daemon run — single-instance resident process.
3
- *
4
- * - pid lock at ~/.hq/work-mesh/daemon/pid
5
- * - fs.watch spool + 2s debounce; flush at least every 10s (US-008 path)
6
- * - replay context outbox
7
- * - board.md refresh for bound sessions ≤ every 60s
8
- * - MQTT presence (contract-3)
9
- * - SIGTERM/SIGINT: flush once more, MQTT DISCONNECT (server derives offline), exit
10
- */
11
1
  import { type DeliverFn } from "../../../work-context/outbox.js";
12
2
  import { type SessionEventsPoster } from "../session-events-client.js";
13
3
  import { type FlushSummary } from "../flush.js";
@@ -1,3 +1,4 @@
1
+ import { reportingRuntimeAllowed } from "../reporting-privacy.js";
1
2
  /**
2
3
  * hq mesh daemon run — single-instance resident process.
3
4
  *
@@ -380,6 +381,8 @@ export async function runMeshDaemon(deps = {}) {
380
381
  baseUrl: resolveVaultApiBase(env),
381
382
  });
382
383
  return flushSessionEvents({
384
+ env,
385
+ reportingAllowed: () => reportingRuntimeAllowed(machineIdentity, env),
383
386
  workMeshRoot: meshRoot,
384
387
  workContextRoot: ctxRoot,
385
388
  poster,
@@ -1,14 +1,3 @@
1
- /**
2
- * Direct-emit orchestration (owner decision 2026-09-08). One invocation:
3
- * 1. drains the local retry file (prior failures),
4
- * 2. appends the new event(s),
5
- * 3. POSTs /v1/mesh/events in batches with a short bounded retry,
6
- * 4. keeps only network/5xx/429-failed or unaccounted events for next time,
7
- * 5. records lastPostAt + per-event status counts for `hq mesh doctor`.
8
- *
9
- * accepted / unassigned / rejected are all terminal (rejected is dropped with a
10
- * count — never re-posted). No long-lived process, spool, or held queue.
11
- */
12
1
  import { type EmitPoster, type EmitEventStatus } from "./emit-client.js";
13
2
  import { type RandomFn, type SleepFn } from "./backoff.js";
14
3
  export interface EmitStateFile {
@@ -23,6 +12,8 @@ export interface EmitStateFile {
23
12
  export declare function emitStatePath(workMeshRoot: string): string;
24
13
  export declare function readEmitState(workMeshRoot: string): EmitStateFile | null;
25
14
  export interface EmitEventsDeps {
15
+ env?: NodeJS.ProcessEnv;
16
+ reportingAllowed?: () => Promise<boolean>;
26
17
  workMeshRoot: string;
27
18
  poster: EmitPoster;
28
19
  /** New events to emit this invocation (may be empty — just drain retry). */
@@ -1,3 +1,4 @@
1
+ import { reportingLocallyDisabled, mayEmitConversation } from "./reporting-privacy.js";
1
2
  /**
2
3
  * Direct-emit orchestration (owner decision 2026-09-08). One invocation:
3
4
  * 1. drains the local retry file (prior failures),
@@ -90,10 +91,27 @@ export async function emitEvents(deps) {
90
91
  statuses: { accepted: 0, unassigned: 0, rejected: 0 },
91
92
  rejections: [],
92
93
  };
94
+ const env = deps.env ?? process.env;
95
+ if (reportingLocallyDisabled(env) || (deps.reportingAllowed && !(await deps.reportingAllowed()))) {
96
+ const written = writeEmitRetry(deps.workMeshRoot, combined, retryMax);
97
+ return { ...summary, attempted: 0, retained: written.written,
98
+ retryDepth: written.written, droppedOverflow: written.dropped };
99
+ }
100
+ const allowed = combined.filter(event => {
101
+ if (mayEmitConversation(event, env))
102
+ return true;
103
+ summary.rejected += 1;
104
+ summary.statuses.rejected += 1;
105
+ const eventId = eventIdOf(event);
106
+ if (eventId)
107
+ summary.rejections.push({ eventId, code: "PERSONAL_OR_UNATTRIBUTED" });
108
+ return false;
109
+ });
110
+ summary.attempted = allowed.length;
93
111
  const keep = [];
94
112
  let anyPosted = false;
95
- for (let i = 0; i < combined.length; i += MESH_EVENTS_BATCH_MAX) {
96
- const chunk = combined.slice(i, i + MESH_EVENTS_BATCH_MAX);
113
+ for (let i = 0; i < allowed.length; i += MESH_EVENTS_BATCH_MAX) {
114
+ const chunk = allowed.slice(i, i + MESH_EVENTS_BATCH_MAX);
97
115
  summary.batches += 1;
98
116
  let result = await deps.poster(chunk);
99
117
  for (let attempt = 1; attempt < maxAttempts && !result.ok && result.retryable; attempt += 1) {