@indigoai-us/hq-cli 5.145.0 → 5.147.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.
- package/CHANGELOG.md +20 -0
- package/dist/command-catalog.generated.d.ts +18 -0
- package/dist/command-catalog.generated.js +23 -0
- package/dist/commands/mesh.js +132 -1
- package/dist/commands/report.d.ts +6 -0
- package/dist/commands/report.js +23 -5
- package/dist/lib/mesh/api.d.ts +1 -1
- package/dist/lib/mesh/api.js +6 -2
- package/dist/lib/mesh/live/daemon/credentials.d.ts +37 -0
- package/dist/lib/mesh/live/daemon/credentials.js +141 -10
- package/dist/lib/mesh/live/daemon/doctor.js +4 -3
- package/dist/lib/mesh/live/daemon/presence.d.ts +15 -0
- package/dist/lib/mesh/live/daemon/presence.js +69 -1
- package/dist/lib/mesh/live/daemon/run.js +59 -3
- package/dist/lib/mesh/live/flush.d.ts +8 -0
- package/dist/lib/mesh/live/flush.js +71 -9
- package/dist/lib/mesh/live/session-seq.d.ts +4 -0
- package/dist/lib/mesh/live/session-seq.js +33 -0
- package/dist/lib/mesh/project-board.d.ts +14 -0
- package/dist/lib/mesh/project-board.js +32 -1
- package/dist/lib/work-context/bind-best.d.ts +2 -0
- package/dist/lib/work-context/bind-best.js +34 -1
- package/dist/lib/work-context/bind-calling-session.d.ts +26 -0
- package/dist/lib/work-context/bind-calling-session.js +63 -0
- package/dist/lib/work-context/prd-sync.d.ts +34 -0
- package/dist/lib/work-context/prd-sync.js +120 -0
- package/dist/lib/work-context/prompt-rebind.d.ts +25 -0
- package/dist/lib/work-context/prompt-rebind.js +85 -0
- package/dist/lib/work-context/reconcile.d.ts +2 -0
- package/dist/lib/work-context/reconcile.js +31 -0
- package/dist/lib/work-context/state.d.ts +2 -0
- package/dist/lib/work-context/state.js +8 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.147.0] — 2026-09-23
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- `hq mesh context prd-sync` now moves the Board. Its task_status events carry the session's company and project, and take sequence numbers from the session's shared counter (the same file the hooks use), so the server no longer rejects them as duplicates. Rejections print their codes.
|
|
10
|
+
- The mesh daemon no longer treats a missing company slug, a membership timeout, or a lookup error as proof that the device owner is not a member. A device-default session that was cleared to `needs_company` is checked once against the configured default company and, when that membership is active, the company is put back and held events flush. The same cleared session is not counted as a new denial on every later flush.
|
|
11
|
+
- `hq mesh project ensure` no longer fails when the server refuses to close the extra genesis thread; it leaves that thread open and prints one line. Registration and the calling session's binding complete.
|
|
12
|
+
- The mesh daemon re-checks a `needs_project` or unresolved session when it flushes that session's events. If the project Board is now an exact or clear catalog match, the batch is posted with that project and the session file is bound. A tie or near miss stays `needs_project`. The check runs at most once a minute per session.
|
|
13
|
+
|
|
14
|
+
## [5.146.0] — 2026-09-23
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- The mesh daemon asks for presence in the device-default company and the companies with a session updated in the last day (at most five), and asks again within a minute when that set changes. `hq mesh doctor` says when a company was left out because presence is off or because one credential cannot cover every company.
|
|
19
|
+
- `hq mesh project ensure` closes a genesis thread only when that same call created it and register returns a different server `proj-` thread. An already-open project thread stays open on a re-run. The register route does not accept a client thread id.
|
|
20
|
+
- `hq mesh project ensure` on a new project closes the client genesis thread when register returns the server `proj-` thread, so the project has one open thread. The register route does not accept a client thread id.
|
|
21
|
+
- A session that registers or creates a project is bound to it. `hq mesh project ensure` and `hq mesh project set --create` bind the calling session when it is the same company and still `needs_project` or unresolved, and they leave a session that is already on a different project alone. The next prompt re-checks `needs_project` against the current catalog. `hq mesh context prd-sync --session <sid> --file <prd.json>` emits `task_status` when a bound session's story `passes` or `status` changes (`passes: true` is done, `in_progress` is in progress) and does not call the network when nothing changed.
|
|
22
|
+
- `hq report publish` now fills a report's missing company from its channel and keeps it across updates. Before, a report saved without a company was published under a personal access policy and the deploy refused it.
|
|
23
|
+
|
|
5
24
|
## [5.145.0] — 2026-09-23
|
|
6
25
|
|
|
7
26
|
- `hq files` now preserves a trailing-slash private-folder pattern such as
|
|
@@ -58,6 +77,7 @@
|
|
|
58
77
|
### Fixed
|
|
59
78
|
|
|
60
79
|
- `hq lanes monitor-check` now recognizes a live Claude Monitor task launched through the CLI's shell/node process wrapper, so active lanes are not reported as uncovered.
|
|
80
|
+
|
|
61
81
|
- A session hint now binds to the best existing project. An exact Board is used as-is. An exact local PRD gets a Board through the same genesis-thread, view, then register path as `project ensure`, and is bound. A single clear match is bound with a recorded reason. When nothing matches, or two candidates are close, the session stays `needs_project` and the start hook lists the candidates. `hq mesh context bind-project` applies the answer. A flush HTTP 404 downgrades that session, holds its events, and logs one line instead of dead-lettering them. `hq mesh doctor` lists `needs_project` sessions.
|
|
62
82
|
|
|
63
83
|
## [5.139.0] — 2026-09-23
|
|
@@ -5882,6 +5882,24 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
5882
5882
|
readonly description: "Print machine-readable JSON";
|
|
5883
5883
|
}];
|
|
5884
5884
|
readonly subcommands: readonly [];
|
|
5885
|
+
}, {
|
|
5886
|
+
readonly name: "prd-sync";
|
|
5887
|
+
readonly description: "Emit task_status for PRD story passes/status changes on a bound session";
|
|
5888
|
+
readonly aliases: readonly [];
|
|
5889
|
+
readonly hidden: false;
|
|
5890
|
+
readonly usage: "[options]";
|
|
5891
|
+
readonly arguments: readonly [];
|
|
5892
|
+
readonly options: readonly [{
|
|
5893
|
+
readonly flags: "--session <sid>";
|
|
5894
|
+
readonly description: "Canonical session id";
|
|
5895
|
+
}, {
|
|
5896
|
+
readonly flags: "--file <path>";
|
|
5897
|
+
readonly description: "prd.json written by the bound session";
|
|
5898
|
+
}, {
|
|
5899
|
+
readonly flags: "--json";
|
|
5900
|
+
readonly description: "Print machine-readable JSON";
|
|
5901
|
+
}];
|
|
5902
|
+
readonly subcommands: readonly [];
|
|
5885
5903
|
}, {
|
|
5886
5904
|
readonly name: "organize";
|
|
5887
5905
|
readonly description: "List or submit one-time project/task decisions (US-007B / US-005B)";
|
|
@@ -7593,6 +7593,29 @@ export const COMMAND_CATALOG = [
|
|
|
7593
7593
|
],
|
|
7594
7594
|
"subcommands": []
|
|
7595
7595
|
},
|
|
7596
|
+
{
|
|
7597
|
+
"name": "prd-sync",
|
|
7598
|
+
"description": "Emit task_status for PRD story passes/status changes on a bound session",
|
|
7599
|
+
"aliases": [],
|
|
7600
|
+
"hidden": false,
|
|
7601
|
+
"usage": "[options]",
|
|
7602
|
+
"arguments": [],
|
|
7603
|
+
"options": [
|
|
7604
|
+
{
|
|
7605
|
+
"flags": "--session <sid>",
|
|
7606
|
+
"description": "Canonical session id"
|
|
7607
|
+
},
|
|
7608
|
+
{
|
|
7609
|
+
"flags": "--file <path>",
|
|
7610
|
+
"description": "prd.json written by the bound session"
|
|
7611
|
+
},
|
|
7612
|
+
{
|
|
7613
|
+
"flags": "--json",
|
|
7614
|
+
"description": "Print machine-readable JSON"
|
|
7615
|
+
}
|
|
7616
|
+
],
|
|
7617
|
+
"subcommands": []
|
|
7618
|
+
},
|
|
7596
7619
|
{
|
|
7597
7620
|
"name": "organize",
|
|
7598
7621
|
"description": "List or submit one-time project/task decisions (US-007B / US-005B)",
|
package/dist/commands/mesh.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* require hq-pack-work-mesh on disk and does not start MQTT listen.
|
|
6
6
|
* Distinct from `hq doctor` (hook guardrails).
|
|
7
7
|
*/
|
|
8
|
+
import { resolve as resolvePath } from "node:path";
|
|
8
9
|
import * as os from "node:os";
|
|
9
10
|
import { readFileSync } from "node:fs";
|
|
10
11
|
import { findHqRoot } from "../utils/manifest.js";
|
|
@@ -12,7 +13,7 @@ import chalk from "chalk";
|
|
|
12
13
|
import { loadCachedTokens, isMachineIdentity } from "../utils/cognito-session.js";
|
|
13
14
|
import * as readline from "node:readline/promises";
|
|
14
15
|
import { STORY_STATUSES, appendThreadEvent, ensureProjectThread, eventPayload, getProjectView, listActiveMembershipCompanies, listActiveThreads, patchStory, putProjectView, registerMeshProject, resolveActiveMembershipCompany, resolveMeshPrincipalUid, warmMeshConversationCache, } from "../lib/mesh/api.js";
|
|
15
|
-
import { ensureProjectBoard } from "../lib/mesh/project-board.js";
|
|
16
|
+
import { closeSupersededProjectThread, ensureProjectBoard } from "../lib/mesh/project-board.js";
|
|
16
17
|
import { buildPreflightFixtures } from "../lib/work-context/preflight-contract.js";
|
|
17
18
|
import { createCandidatesFetcher, createMigratePoster, createOrganizePoster, createWorkSessionDeliverer, fetchCompanyLive, formatCompanyLiveTable, openMeshTransport, probeMigrationCapabilityForMemberships, requireToken, } from "../lib/mesh/client.js";
|
|
18
19
|
import { clearDefaultCompany, isDefaultCompanyDisabled, readDeviceConfig, recordMigrationCapabilitySnapshot, setRepairHeldWithDefault, setDefaultCompany, } from "../lib/work-context/config.js";
|
|
@@ -39,6 +40,10 @@ import { formatMigrateConfirmation, submitSessionMigration, } from "../lib/work-
|
|
|
39
40
|
import { formatOrganizeList, prepareOrganizeDecision, settleOrganizeAskWithoutBind, submitOrganizeDecision, } from "../lib/work-context/organize.js";
|
|
40
41
|
import { readSessionState, updateSessionState } from "../lib/work-context/state.js";
|
|
41
42
|
import { findLocalPrd, listNeedsProjectSessions } from "../lib/work-context/bind-best.js";
|
|
43
|
+
import { bindCallingSession, formatSessionBoundLine } from "../lib/work-context/bind-calling-session.js";
|
|
44
|
+
import { rebindNeedsProjectOnPrompt } from "../lib/work-context/prompt-rebind.js";
|
|
45
|
+
import { planPrdSync } from "../lib/work-context/prd-sync.js";
|
|
46
|
+
import { reserveSessionSeq } from "../lib/mesh/live/session-seq.js";
|
|
42
47
|
import { WORK_CONTEXT_CONTRACT_VERSION } from "../lib/work-context/contract.js";
|
|
43
48
|
import { loadObservationFromFile, markUntracked, parseObservationJson, reconcileObservation, } from "../lib/work-context/reconcile.js";
|
|
44
49
|
import { resolveCompany } from "../lib/work-context/company.js";
|
|
@@ -388,6 +393,9 @@ async function runProjectSet(projectId, opts) {
|
|
|
388
393
|
throw err;
|
|
389
394
|
}
|
|
390
395
|
}
|
|
396
|
+
if (!current && opts.create) {
|
|
397
|
+
noteCallingSessionBound(view.projectId, company);
|
|
398
|
+
}
|
|
391
399
|
if (opts.json) {
|
|
392
400
|
console.log(JSON.stringify({ ok: true, action: "project.set", principalUid, ...view }, null, 2));
|
|
393
401
|
return;
|
|
@@ -488,6 +496,13 @@ async function runProjectEnsure(projectId, opts) {
|
|
|
488
496
|
}
|
|
489
497
|
}
|
|
490
498
|
registration = await registerMeshProject(token, company.companyUid, projectId);
|
|
499
|
+
await closeSupersededProjectThread({
|
|
500
|
+
token,
|
|
501
|
+
companyUid: company.companyUid,
|
|
502
|
+
openedThreadId: genesis.threadId,
|
|
503
|
+
canonicalThreadId: registration.threadId,
|
|
504
|
+
created: genesis.created,
|
|
505
|
+
});
|
|
491
506
|
if (!registration.threadId || !registration.channelId) {
|
|
492
507
|
fail("Work mesh registration did not return a threadId and channelId.");
|
|
493
508
|
}
|
|
@@ -519,12 +534,106 @@ async function runProjectEnsure(projectId, opts) {
|
|
|
519
534
|
version: verified.version,
|
|
520
535
|
stories: verified.stories,
|
|
521
536
|
};
|
|
537
|
+
noteCallingSessionBound(payload.projectId, company);
|
|
522
538
|
if (opts.json) {
|
|
523
539
|
console.log(JSON.stringify(payload));
|
|
524
540
|
return;
|
|
525
541
|
}
|
|
526
542
|
console.log(`Work mesh: project ${payload.projectId} ensured (thread ${payload.threadId}, channel ${payload.channelId}, ${payload.stories.length} stories)`);
|
|
527
543
|
}
|
|
544
|
+
async function runPrdSync(opts) {
|
|
545
|
+
const sessionId = opts.session.trim();
|
|
546
|
+
const file = opts.file.trim();
|
|
547
|
+
if (!sessionId || !file)
|
|
548
|
+
fail("Usage: hq mesh context prd-sync --session <sid> --file <prd.json>");
|
|
549
|
+
const root = workContextHomeRoot();
|
|
550
|
+
const plan = planPrdSync({ sessionId, file, root });
|
|
551
|
+
if (!plan.changed) {
|
|
552
|
+
const payload = { ok: true, action: "context.prd-sync", changed: 0, ...(plan.skip ? { skip: plan.skip } : {}) };
|
|
553
|
+
if (opts.json)
|
|
554
|
+
console.log(JSON.stringify(payload));
|
|
555
|
+
else
|
|
556
|
+
console.log(plan.skip ? `Work mesh: prd-sync skipped (${plan.skip})` : "Work mesh: prd-sync unchanged");
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
let token;
|
|
560
|
+
try {
|
|
561
|
+
token = await requireToken();
|
|
562
|
+
}
|
|
563
|
+
catch {
|
|
564
|
+
token = null;
|
|
565
|
+
}
|
|
566
|
+
if (!token) {
|
|
567
|
+
console.error("work-mesh: prd-sync has changes but no token; snapshot not advanced");
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
// Attribution evidence: without company and project the server files these
|
|
571
|
+
// as unassigned and the Board never moves.
|
|
572
|
+
const bound = readSessionState(sessionId, root);
|
|
573
|
+
const evidenceBase = {
|
|
574
|
+
...(bound?.companySlug ? { companySlug: bound.companySlug } : {}),
|
|
575
|
+
...(bound?.projectId ? { project: bound.projectId } : {}),
|
|
576
|
+
touchedPaths: [resolvePath(file)],
|
|
577
|
+
};
|
|
578
|
+
const seqFirst = reserveSessionSeq(sessionId, plan.events.length);
|
|
579
|
+
const events = plan.events.map((event, index) => buildMeshEmitEvent({
|
|
580
|
+
eventId: generateUlid(Date.now() + index),
|
|
581
|
+
kind: "task_status",
|
|
582
|
+
sessionId,
|
|
583
|
+
harness: "hq-sessions",
|
|
584
|
+
adapterVersion: "1.0.0",
|
|
585
|
+
at: new Date().toISOString(),
|
|
586
|
+
seq: seqFirst + index,
|
|
587
|
+
source: "hooks",
|
|
588
|
+
taskId: event.taskId,
|
|
589
|
+
status: event.status,
|
|
590
|
+
summary: event.status === "done" ? `${event.taskId} passes` : `${event.taskId} in progress`,
|
|
591
|
+
evidence: { ...evidenceBase, task: event.taskId },
|
|
592
|
+
}));
|
|
593
|
+
const poster = createEmitPoster({ token, baseUrl: resolveVaultApiBase(process.env) });
|
|
594
|
+
const emitted = await emitEvents({
|
|
595
|
+
workMeshRoot: workMeshHomeRoot(),
|
|
596
|
+
poster,
|
|
597
|
+
newEvents: events,
|
|
598
|
+
});
|
|
599
|
+
if (emitted.unassigned > 0 || emitted.rejected > 0) {
|
|
600
|
+
console.error(`work-mesh: prd-sync sent ${events.length}; unassigned=${emitted.unassigned} rejected=${emitted.rejected}`);
|
|
601
|
+
for (const line of formatEmitRejectionLines(emitted.rejections))
|
|
602
|
+
console.error(line);
|
|
603
|
+
}
|
|
604
|
+
plan.commit();
|
|
605
|
+
if (plan.taskId) {
|
|
606
|
+
updateSessionState(sessionId, root, (current) => {
|
|
607
|
+
if (!current)
|
|
608
|
+
return null;
|
|
609
|
+
return {
|
|
610
|
+
...current,
|
|
611
|
+
taskId: plan.taskId,
|
|
612
|
+
contextStatus: current.projectId ? "bound" : current.contextStatus,
|
|
613
|
+
updatedAt: new Date().toISOString(),
|
|
614
|
+
};
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
if (opts.json) {
|
|
618
|
+
console.log(JSON.stringify({ ok: true, action: "context.prd-sync", changed: plan.events.length, taskId: plan.taskId }));
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
console.log(`Work mesh: prd-sync emitted ${plan.events.length} task_status`);
|
|
622
|
+
}
|
|
623
|
+
function noteCallingSessionBound(projectId, company) {
|
|
624
|
+
try {
|
|
625
|
+
const bound = bindCallingSession({
|
|
626
|
+
root: workContextHomeRoot(),
|
|
627
|
+
projectId,
|
|
628
|
+
company,
|
|
629
|
+
});
|
|
630
|
+
if (bound.bound)
|
|
631
|
+
console.error(formatSessionBoundLine(bound.sessionId, bound.projectId));
|
|
632
|
+
}
|
|
633
|
+
catch {
|
|
634
|
+
/* session bind is local and must not fail project ensure */
|
|
635
|
+
}
|
|
636
|
+
}
|
|
528
637
|
async function runDoctor(opts) {
|
|
529
638
|
if (opts.apply) {
|
|
530
639
|
fail("`hq mesh doctor --apply` (paced Board PUTs from local prd.json) is not in the CLI yet.\n" +
|
|
@@ -1327,6 +1436,21 @@ async function runSessionEnqueue(cliKind, opts) {
|
|
|
1327
1436
|
const sessionId = resolveEnqueueSessionId(opts.sessionId, process.env);
|
|
1328
1437
|
if (!sessionId)
|
|
1329
1438
|
fail("sessionId required (--session-id or HQ_SESSION_ID)");
|
|
1439
|
+
if (kind === "turn_start") {
|
|
1440
|
+
try {
|
|
1441
|
+
rebindNeedsProjectOnPrompt({
|
|
1442
|
+
sessionId,
|
|
1443
|
+
root: workContextHomeRoot(),
|
|
1444
|
+
hqRoot: opts.hqRoot?.trim() || process.env.HQ_ROOT,
|
|
1445
|
+
cwd: opts.cwd,
|
|
1446
|
+
companySlug: opts.companySlug,
|
|
1447
|
+
env: process.env,
|
|
1448
|
+
});
|
|
1449
|
+
}
|
|
1450
|
+
catch {
|
|
1451
|
+
/* prompt rebind is fail-soft */
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1330
1454
|
const evidence = buildEmitEvidence({
|
|
1331
1455
|
sessionId: sessionId,
|
|
1332
1456
|
hqRoot: opts.hqRoot,
|
|
@@ -1674,6 +1798,13 @@ export function registerMeshCommand(program) {
|
|
|
1674
1798
|
.option("--company <slug|uid>", "Company override when creating a Board")
|
|
1675
1799
|
.option("--json", "Print machine-readable JSON")
|
|
1676
1800
|
.action((target, slug, opts) => wrap(() => runBindProject(target, { ...opts, slug }))());
|
|
1801
|
+
context
|
|
1802
|
+
.command("prd-sync")
|
|
1803
|
+
.description("Emit task_status for PRD story passes/status changes on a bound session")
|
|
1804
|
+
.requiredOption("--session <sid>", "Canonical session id")
|
|
1805
|
+
.requiredOption("--file <path>", "prd.json written by the bound session")
|
|
1806
|
+
.option("--json", "Print machine-readable JSON")
|
|
1807
|
+
.action((opts) => wrap(() => runPrdSync(opts))());
|
|
1677
1808
|
context
|
|
1678
1809
|
.command("organize")
|
|
1679
1810
|
.description("List or submit one-time project/task decisions (US-007B / US-005B)")
|
|
@@ -8,6 +8,12 @@ type ChannelRow = {
|
|
|
8
8
|
type ReportPatchInput = ReportPatch | ((current: ChannelReport) => ReportPatch);
|
|
9
9
|
/** Any company-owned channel (company or project scope) can carry reports; personal channels cannot. */
|
|
10
10
|
export declare function isCompanyReportChannel(channel: ChannelRow): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Records created before hq-pro stamped companyUid carry null. The channel the
|
|
13
|
+
* caller can see knows its company, and publish needs it to pick the deploy
|
|
14
|
+
* org, so fill it from there rather than failing every publish.
|
|
15
|
+
*/
|
|
16
|
+
export declare function withChannelCompany(report: ChannelReport, channels: ChannelRow[]): ChannelReport;
|
|
11
17
|
/** Explicit context has priority; discovery is the authenticated fallback for normal CLI use. */
|
|
12
18
|
export declare function resolveReport(token: string, reportId: string, requestedChannelId?: string): Promise<ChannelReport>;
|
|
13
19
|
export declare function patchReport(token: string, report: ChannelReport, patch: ReportPatchInput): Promise<ChannelReport>;
|
package/dist/commands/report.js
CHANGED
|
@@ -117,6 +117,17 @@ async function readReportInChannel(token, channelId, reportId) {
|
|
|
117
117
|
throw new ReportPublishError("Report response was incomplete.");
|
|
118
118
|
return body.report;
|
|
119
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* Records created before hq-pro stamped companyUid carry null. The channel the
|
|
122
|
+
* caller can see knows its company, and publish needs it to pick the deploy
|
|
123
|
+
* org, so fill it from there rather than failing every publish.
|
|
124
|
+
*/
|
|
125
|
+
export function withChannelCompany(report, channels) {
|
|
126
|
+
if (report.companyUid)
|
|
127
|
+
return report;
|
|
128
|
+
const companyUid = channels.find((row) => row.channelId === report.channelId)?.companyUid;
|
|
129
|
+
return companyUid ? { ...report, companyUid } : report;
|
|
130
|
+
}
|
|
120
131
|
/** Explicit context has priority; discovery is the authenticated fallback for normal CLI use. */
|
|
121
132
|
export async function resolveReport(token, reportId, requestedChannelId) {
|
|
122
133
|
const explicit = requestedChannelId ?? process.env.HQ_REPORT_CHANNEL_ID;
|
|
@@ -129,14 +140,14 @@ export async function resolveReport(token, reportId, requestedChannelId) {
|
|
|
129
140
|
const body = await res.json();
|
|
130
141
|
if (!body.report)
|
|
131
142
|
throw new ReportPublishError("Report response was incomplete.");
|
|
132
|
-
return body.report;
|
|
143
|
+
return withChannelCompany(body.report, channels);
|
|
133
144
|
}
|
|
134
145
|
const recordedChannel = locallyRecordedChannel(reportId);
|
|
135
146
|
if (recordedChannel) {
|
|
136
147
|
requireCompanyReportChannel(channels, recordedChannel);
|
|
137
148
|
const recorded = await readReportInChannel(token, recordedChannel, reportId);
|
|
138
149
|
if (recorded)
|
|
139
|
-
return recorded;
|
|
150
|
+
return withChannelCompany(recorded, channels);
|
|
140
151
|
}
|
|
141
152
|
// The report API is intentionally channel-scoped. Discover only among the
|
|
142
153
|
// caller's existing channels, using list endpoints so missing reports never
|
|
@@ -157,13 +168,13 @@ export async function resolveReport(token, reportId, requestedChannelId) {
|
|
|
157
168
|
const reports = Array.isArray(body.reports) ? body.reports : [];
|
|
158
169
|
const match = reports.find((report) => report.reportId === reportId);
|
|
159
170
|
if (match)
|
|
160
|
-
return match;
|
|
171
|
+
return withChannelCompany(match, channels);
|
|
161
172
|
}
|
|
162
173
|
throw new ReportPublishError(notFoundMessage(scannedChannels));
|
|
163
174
|
}
|
|
164
175
|
export async function patchReport(token, report, patch) {
|
|
165
176
|
if (!report.companyUid)
|
|
166
|
-
throw new ReportPublishError("Channel reports require a company channel.");
|
|
177
|
+
throw new ReportPublishError("Channel reports require a company channel.", "source-invalid");
|
|
167
178
|
const patchOnce = (current) => vaultApiFetch({
|
|
168
179
|
token,
|
|
169
180
|
path: reportPath(current.channelId, current.reportId),
|
|
@@ -279,7 +290,14 @@ export async function deployReportStatic(token, input) {
|
|
|
279
290
|
}
|
|
280
291
|
async function publishWithOrg(token, report, directory, draft) {
|
|
281
292
|
const published = await publishReport(report, directory, draft, {
|
|
282
|
-
|
|
293
|
+
// The server echoes the stored record, whose companyUid may still be null
|
|
294
|
+
// on reports created before hq-pro stamped it. Keep the company resolved
|
|
295
|
+
// from the channel so the access policy and deploy org stay correct.
|
|
296
|
+
patch: async (current, patch) => {
|
|
297
|
+
const next = await patchReport(token, current, patch);
|
|
298
|
+
const companyUid = next.companyUid ?? current.companyUid ?? report.companyUid;
|
|
299
|
+
return companyUid ? { ...next, companyUid } : next;
|
|
300
|
+
},
|
|
283
301
|
deploy: async (input) => {
|
|
284
302
|
const org = await orgFor(token, report);
|
|
285
303
|
return deployReportStatic(token, { ...input, org });
|
package/dist/lib/mesh/api.d.ts
CHANGED
|
@@ -78,7 +78,7 @@ export declare function listActiveMembershipCompanies(token: string): Promise<Me
|
|
|
78
78
|
* Returns null when the caller is not an active member. Throws on transport /
|
|
79
79
|
* auth failures so the caller can surface DEFAULT_COMPANY_UNAVAILABLE.
|
|
80
80
|
*/
|
|
81
|
-
export declare function resolveActiveMembershipCompany(token: string, slug: string): Promise<MeshCompany | null>;
|
|
81
|
+
export declare function resolveActiveMembershipCompany(token: string, slug: string, signal?: AbortSignal): Promise<MeshCompany | null>;
|
|
82
82
|
export interface RequireTokenOptions {
|
|
83
83
|
/**
|
|
84
84
|
* False from detached or unattended callers (the lane watcher). A person
|
package/dist/lib/mesh/api.js
CHANGED
|
@@ -113,11 +113,15 @@ export async function listActiveMembershipCompanies(token) {
|
|
|
113
113
|
* Returns null when the caller is not an active member. Throws on transport /
|
|
114
114
|
* auth failures so the caller can surface DEFAULT_COMPANY_UNAVAILABLE.
|
|
115
115
|
*/
|
|
116
|
-
export async function resolveActiveMembershipCompany(token, slug) {
|
|
116
|
+
export async function resolveActiveMembershipCompany(token, slug, signal) {
|
|
117
117
|
const trimmed = slug.trim();
|
|
118
118
|
if (!trimmed)
|
|
119
119
|
return null;
|
|
120
|
-
|
|
120
|
+
// An aborted lookup is not "not a member". Throw so callers defer.
|
|
121
|
+
if (signal?.aborted) {
|
|
122
|
+
throw new Error("membership lookup aborted");
|
|
123
|
+
}
|
|
124
|
+
const payload = asRecord(await meshJson(token, "/membership/me", { signal }));
|
|
121
125
|
if (!payload || !Array.isArray(payload.memberships)) {
|
|
122
126
|
throw new Error("Malformed membership response: expected memberships array");
|
|
123
127
|
}
|
|
@@ -18,6 +18,12 @@ export declare const REFUSED_RETRY_ENV = "HQ_MESH_DAEMON_REFUSED_RETRY_MS";
|
|
|
18
18
|
export declare const MAX_TIMER_DELAY_MS = 2147483647;
|
|
19
19
|
/** Cap any single retry so a bogus Retry-After cannot park the daemon. */
|
|
20
20
|
export declare const MAX_RETRY_DELAY_MS: number;
|
|
21
|
+
/** Contract-3 vend accepts at most this many preferred company uids. */
|
|
22
|
+
export declare const PREFERRED_COMPANY_LIMIT = 5;
|
|
23
|
+
/** Session files older than this do not count as companies in use. */
|
|
24
|
+
export declare const PREFERRED_SESSION_WINDOW_MS: number;
|
|
25
|
+
/** Preferred-set changes re-vend at most this often. */
|
|
26
|
+
export declare const PREFERRED_REVEND_MIN_MS = 60000;
|
|
21
27
|
export type CredentialVendFailureKind = "refused" | "transient";
|
|
22
28
|
/**
|
|
23
29
|
* Typed vend failure. `refused` = server will not vend right now
|
|
@@ -51,6 +57,8 @@ export interface PresenceCompany {
|
|
|
51
57
|
}
|
|
52
58
|
export interface DroppedPresenceCompany {
|
|
53
59
|
companyUid: string;
|
|
60
|
+
/** Short name for doctor text when the server sent one. */
|
|
61
|
+
label?: string;
|
|
54
62
|
/** Server-supplied cause when available; otherwise a deliberately neutral label. */
|
|
55
63
|
reason: string;
|
|
56
64
|
}
|
|
@@ -98,6 +106,30 @@ export declare function normalizeContract3Bundle(raw: unknown): Contract3Bundle;
|
|
|
98
106
|
* No identity file → return the bundle unchanged (person laptop).
|
|
99
107
|
*/
|
|
100
108
|
export declare function scopeBundleToAgentIdentity(bundle: Contract3Bundle, env?: NodeJS.ProcessEnv): Contract3Bundle;
|
|
109
|
+
/**
|
|
110
|
+
* Device default first, then distinct company uids from session files updated
|
|
111
|
+
* in the last 24 hours, newest first, max 5. Any read error returns undefined
|
|
112
|
+
* so the vend body omits the field.
|
|
113
|
+
*/
|
|
114
|
+
export declare function readPreferredCompanyUids(input: {
|
|
115
|
+
workContextRoot: string;
|
|
116
|
+
nowMs?: number;
|
|
117
|
+
}): string[] | undefined;
|
|
118
|
+
export declare function preferredCompanyKey(uids: readonly string[] | undefined): string;
|
|
119
|
+
/** True when a new preferred set should force a vend, at most once per minute. */
|
|
120
|
+
export declare function shouldRevendPreferredCompanies(input: {
|
|
121
|
+
previous: readonly string[] | undefined;
|
|
122
|
+
next: readonly string[] | undefined;
|
|
123
|
+
lastVendMs: number;
|
|
124
|
+
nowMs: number;
|
|
125
|
+
minIntervalMs?: number;
|
|
126
|
+
}): boolean;
|
|
127
|
+
export declare function contract3VendBody(preferredCompanyUids: readonly string[] | undefined): {
|
|
128
|
+
contractVersion: 3;
|
|
129
|
+
preferredCompanyUids?: string[];
|
|
130
|
+
};
|
|
131
|
+
/** Plain doctor line for one company the vend left out. */
|
|
132
|
+
export declare function formatDroppedPresenceCompany(company: DroppedPresenceCompany): string;
|
|
101
133
|
export type CredentialVendPostResult = {
|
|
102
134
|
status: number;
|
|
103
135
|
body: unknown;
|
|
@@ -110,6 +142,8 @@ export declare function createContract3Fetcher(opts: {
|
|
|
110
142
|
post?: (path: string, body: unknown) => Promise<CredentialVendPostResult>;
|
|
111
143
|
/** Process env for identity-file scoping (tests). */
|
|
112
144
|
env?: NodeJS.ProcessEnv;
|
|
145
|
+
/** Companies to ask for. Undefined (read error) omits the field. */
|
|
146
|
+
readPreferredCompanyUids?: () => string[] | undefined;
|
|
113
147
|
}): CredentialsFetcher;
|
|
114
148
|
export interface CredentialRenewalErrorInfo {
|
|
115
149
|
/** Absolute ISO timestamp when the renewal retry is scheduled to fire. */
|
|
@@ -130,8 +164,11 @@ export declare class CredentialRenewalManager {
|
|
|
130
164
|
private readonly random;
|
|
131
165
|
private handle;
|
|
132
166
|
private stopped;
|
|
167
|
+
private inflight;
|
|
133
168
|
constructor(fetchCredentials: CredentialsFetcher, onRenewed: (bundle: Contract3Bundle) => void, onError?: (err: unknown, info?: CredentialRenewalErrorInfo) => void, timers?: TimerHost, retryDelayMs?: number, refusedRetryMs?: number, random?: () => number);
|
|
134
169
|
schedule(bundle: Contract3Bundle): void;
|
|
170
|
+
/** Vend now (preferred companies changed). No-op while a vend is in flight. */
|
|
171
|
+
renewNow(): void;
|
|
135
172
|
private renew;
|
|
136
173
|
stop(): void;
|
|
137
174
|
private clear;
|