@indigoai-us/hq-cli 5.345.169 → 5.345.171
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 +69 -0
- package/assets/scaffold/core/scripts/hq-delegate-send.sh +1 -1
- package/dist/command-catalog.generated.d.ts +181 -9
- package/dist/command-catalog.generated.js +224 -10
- package/dist/commands/access.d.ts +32 -1
- package/dist/commands/access.js +119 -5
- package/dist/commands/agent-kit.d.ts +1 -1
- package/dist/commands/agent-kit.js +3 -1
- package/dist/commands/agents.js +2 -1
- package/dist/commands/bot.d.ts +3 -0
- package/dist/commands/bot.js +141 -14
- package/dist/commands/cloud.d.ts +78 -1
- package/dist/commands/cloud.js +274 -9
- package/dist/commands/core-company-index-scope-flag.d.ts +8 -1
- package/dist/commands/core-company-index-scope-flag.js +9 -2
- package/dist/commands/core-delegate-w1b.js +5 -2
- package/dist/commands/core-dm-bind-native.js +19 -4
- package/dist/commands/core-rebuild-index-fast.js +24 -6
- package/dist/commands/core.js +18 -6
- package/dist/commands/dm.d.ts +17 -3
- package/dist/commands/dm.js +121 -26
- package/dist/commands/feedback-triage.d.ts +43 -1
- package/dist/commands/feedback-triage.js +123 -23
- package/dist/commands/feedback.d.ts +40 -1
- package/dist/commands/feedback.js +184 -4
- package/dist/commands/files.d.ts +42 -0
- package/dist/commands/files.js +223 -30
- package/dist/commands/integrations-api.d.ts +4 -0
- package/dist/commands/integrations-api.js +18 -2
- package/dist/commands/integrations-connect.d.ts +10 -3
- package/dist/commands/integrations-connect.js +70 -6
- package/dist/commands/integrations-core.d.ts +23 -0
- package/dist/commands/integrations-core.js +100 -0
- package/dist/commands/integrations-manage.js +8 -1
- package/dist/commands/integrations.js +49 -5
- package/dist/commands/lanes-loop.d.ts +20 -0
- package/dist/commands/lanes-loop.js +69 -11
- package/dist/commands/lanes.js +9 -1
- package/dist/commands/mesh.js +6 -0
- package/dist/commands/rescue.d.ts +9 -0
- package/dist/commands/rescue.js +31 -0
- package/dist/commands/run.js +115 -40
- package/dist/commands/scan.d.ts +5 -1
- package/dist/commands/scan.js +16 -4
- package/dist/commands/secrets.js +50 -9
- package/dist/commands/sync-manifest.d.ts +16 -0
- package/dist/commands/sync-manifest.js +17 -1
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2367 -2109
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/agent-kit/inbox-state.d.ts +17 -0
- package/dist/lib/agent-kit/inbox-state.js +41 -20
- package/dist/lib/agent-kit/kit-config.d.ts +2 -0
- package/dist/lib/agent-kit/kit-config.js +6 -0
- package/dist/lib/agent-kit/mcp/tools.js +12 -5
- package/dist/lib/agent-kit/run/heartbeat.d.ts +6 -3
- package/dist/lib/agent-kit/run/heartbeat.js +6 -3
- package/dist/lib/agent-kit/run/inbox.d.ts +8 -3
- package/dist/lib/agent-kit/run/inbox.js +42 -10
- package/dist/lib/agent-kit/run/request-timeout.d.ts +26 -0
- package/dist/lib/agent-kit/run/request-timeout.js +51 -0
- package/dist/lib/bot/api.d.ts +1 -1
- package/dist/lib/bot/config.d.ts +10 -0
- package/dist/lib/bot/config.js +17 -1
- package/dist/lib/bot/decisions.d.ts +62 -0
- package/dist/lib/bot/decisions.js +228 -0
- package/dist/lib/bot/fleet-local.d.ts +127 -0
- package/dist/lib/bot/fleet-local.js +638 -0
- package/dist/lib/bot/index.d.ts +2 -0
- package/dist/lib/bot/index.js +2 -0
- package/dist/lib/bot/local-config.d.ts +3 -2
- package/dist/lib/bot/local-config.js +5 -2
- package/dist/lib/bot/promote.d.ts +10 -0
- package/dist/lib/bot/promote.js +3 -2
- package/dist/lib/bot/run.d.ts +7 -0
- package/dist/lib/bot/run.js +104 -1
- package/dist/lib/bot/runtime/index.js +8 -4
- package/dist/lib/bot/runtime/scratch-dir.d.ts +24 -0
- package/dist/lib/bot/runtime/scratch-dir.js +41 -0
- package/dist/lib/caller-principal.d.ts +4 -0
- package/dist/lib/caller-principal.js +40 -0
- package/dist/lib/capacity/collectors/grok.js +8 -2
- package/dist/lib/cli-flag-keys.d.ts +2 -0
- package/dist/lib/cli-flag-keys.js +2 -0
- package/dist/lib/daemon/catalog.d.ts +4 -0
- package/dist/lib/daemon/catalog.js +63 -29
- package/dist/lib/daemon/config.d.ts +7 -0
- package/dist/lib/daemon/config.js +32 -0
- package/dist/lib/daemon/run.d.ts +3 -2
- package/dist/lib/daemon/run.js +23 -3
- package/dist/lib/daemon/supervisor.d.ts +1 -0
- package/dist/lib/daemon/supervisor.js +4 -0
- package/dist/lib/daemon/sync-core-canary.d.ts +12 -0
- package/dist/lib/daemon/sync-core-canary.js +61 -0
- package/dist/lib/doctor/checks/release-links.d.ts +62 -1
- package/dist/lib/doctor/checks/release-links.js +188 -2
- package/dist/lib/doctor/checks/sync-health.d.ts +32 -0
- package/dist/lib/doctor/checks/sync-health.js +54 -1
- package/dist/lib/doctor/fix/apply.d.ts +26 -4
- package/dist/lib/doctor/fix/apply.js +149 -11
- package/dist/lib/doctor/fix/remediation.d.ts +8 -1
- package/dist/lib/doctor/fix/remediation.js +22 -0
- package/dist/lib/doctor/types.d.ts +1 -1
- package/dist/lib/doctor/types.js +1 -0
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/index-render/index.d.ts +9 -0
- package/dist/lib/index-render/index.js +65 -5
- package/dist/lib/index-render/session-scope.d.ts +69 -0
- package/dist/lib/index-render/session-scope.js +279 -0
- package/dist/lib/lanes/admission.js +7 -20
- package/dist/lib/lanes/reconcile.js +13 -15
- package/dist/lib/lanes/stale-lock.d.ts +50 -0
- package/dist/lib/lanes/stale-lock.js +105 -0
- package/dist/lib/lanes/store.js +11 -76
- package/dist/lib/mesh/live/daemon/work-subscribe.js +31 -0
- package/dist/lib/notify-recipient.d.ts +10 -4
- package/dist/lib/notify-recipient.js +16 -4
- package/dist/lib/secret-input-link.d.ts +42 -0
- package/dist/lib/secret-input-link.js +93 -0
- package/dist/lib/secret-scan/range.d.ts +57 -0
- package/dist/lib/secret-scan/range.js +266 -0
- package/dist/lib/secret-scan/repo-config.d.ts +18 -3
- package/dist/lib/secret-scan/repo-config.js +135 -2
- package/dist/lib/work-context/organize.d.ts +7 -0
- package/dist/lib/work-context/organize.js +21 -1
- package/dist/run/script-bound-schema.d.ts +27 -0
- package/dist/run/script-bound-schema.js +97 -0
- package/dist/secrets-env-action.d.ts +2 -0
- package/dist/secrets-env-action.js +7 -2
- package/dist/sentry-before-send.js +1 -0
- package/dist/utils/cognito-session.d.ts +3 -30
- package/dist/utils/cognito-session.js +2 -100
- package/dist/utils/machine-principal-state.d.ts +38 -0
- package/dist/utils/machine-principal-state.js +110 -0
- package/dist/utils/safe-link.js +9 -0
- package/dist/utils/script-bound-exec.d.ts +158 -0
- package/dist/utils/script-bound-exec.js +568 -0
- package/dist/utils/sync-watchdog.d.ts +92 -0
- package/dist/utils/sync-watchdog.js +250 -0
- package/package.json +8 -8
package/dist/commands/dm.js
CHANGED
|
@@ -5,7 +5,9 @@ import { resolveDestinationCompanyUid, vaultApiFetch } from "../utils/vault-api.
|
|
|
5
5
|
import { isMachineIdentity } from "@indigoai-us/hq-cloud";
|
|
6
6
|
import { peekIdToken } from "../utils/id-token.js";
|
|
7
7
|
import { redactAgentInboxItem as redactInboxCredentials } from "../lib/agent-kit/inbox-redact.js";
|
|
8
|
-
import {
|
|
8
|
+
import { classifyAgentInboxBody } from "../lib/agent-kit/inbox-state.js";
|
|
9
|
+
import { EMAIL_PATTERN, RECIPIENT_UID_PATTERN, PERSON_AGENT_AUDIENCE_MESSAGE, PERSON_CANNOT_SEND_AGENT_CODE, callerMayUseAudience, defaultAudienceFor, recipientKindOf, } from "../lib/notify-recipient.js";
|
|
10
|
+
import { callerIsMachinePrincipal } from "../lib/caller-principal.js";
|
|
9
11
|
export { defaultAudienceFor, recipientKindOf };
|
|
10
12
|
import { dmForwardCliEnabled } from "./dm-forward-flag.js";
|
|
11
13
|
import { attachmentScopeForChannel, formatUploadedAttachments, personalScopeForCaller, uploadDmAttachments, validateAttachmentPaths, } from "./dm-attach.js";
|
|
@@ -120,29 +122,45 @@ export function matchChannelsByName(channels, name) {
|
|
|
120
122
|
* eba06f59). Returns null when nothing or more than one contact matches. Pure.
|
|
121
123
|
*/
|
|
122
124
|
export function matchContactByName(contacts, name) {
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
+
const unique = matchContactsByName(contacts, name);
|
|
126
|
+
return unique.length === 1 ? unique[0] : null;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Every distinct contact (person or bot) whose display name matches `name`,
|
|
130
|
+
* using the same rule as matchContactByName. A leading '@' is the explicit
|
|
131
|
+
* person/bot selector and is ignored for matching. Pure.
|
|
132
|
+
*/
|
|
133
|
+
export function matchContactsByName(contacts, name) {
|
|
134
|
+
const bare = name.trim().replace(/^@/, "");
|
|
135
|
+
const target = bare.toLowerCase();
|
|
136
|
+
const targetSlug = channelSlug(bare);
|
|
125
137
|
if (!target)
|
|
126
|
-
return
|
|
138
|
+
return [];
|
|
127
139
|
const hits = contacts.filter((c) => {
|
|
128
140
|
if (!c.personUid)
|
|
129
141
|
return false;
|
|
130
142
|
const dn = c.displayName?.trim().toLowerCase();
|
|
131
143
|
return Boolean(dn && (dn === target || (targetSlug && channelSlug(dn) === targetSlug)));
|
|
132
144
|
});
|
|
133
|
-
|
|
134
|
-
|
|
145
|
+
return [...new Map(hits.map((c) => [c.personUid, c])).values()];
|
|
146
|
+
}
|
|
147
|
+
/** One line per ambiguous contact match, for a refusal message. Pure. */
|
|
148
|
+
export function formatContactMatches(matches) {
|
|
149
|
+
return matches
|
|
150
|
+
.map((c) => ` ${c.displayName || "(no name)"}${c.email ? ` <${c.email}>` : ""} ${c.personUid}`)
|
|
151
|
+
.join("\n");
|
|
135
152
|
}
|
|
153
|
+
/** Contacts, or null when the lookup failed (so callers can say so). */
|
|
136
154
|
async function fetchContacts(token) {
|
|
137
155
|
try {
|
|
138
156
|
const res = await vaultApiFetch({ token, path: "/v1/notify/contacts" });
|
|
139
157
|
if (!res.ok)
|
|
140
|
-
return
|
|
158
|
+
return null;
|
|
141
159
|
const data = (await res.json());
|
|
142
|
-
return Array.isArray(data.contacts) ? data.contacts :
|
|
160
|
+
return Array.isArray(data.contacts) ? data.contacts : null;
|
|
143
161
|
}
|
|
144
162
|
catch {
|
|
145
|
-
return
|
|
163
|
+
return null;
|
|
146
164
|
}
|
|
147
165
|
}
|
|
148
166
|
/**
|
|
@@ -427,6 +445,8 @@ export async function resolveDmPeerUid(token, recipient) {
|
|
|
427
445
|
function friendlyDmError(status, code, fallback) {
|
|
428
446
|
if (status === 401)
|
|
429
447
|
return "Not authenticated — run `hq login` and try again.";
|
|
448
|
+
if (code === PERSON_CANNOT_SEND_AGENT_CODE)
|
|
449
|
+
return fallback?.trim() || PERSON_AGENT_AUDIENCE_MESSAGE;
|
|
430
450
|
if (status === 404 || code === "RECIPIENT_NOT_FOUND") {
|
|
431
451
|
return "Recipient not found or not reachable — you can only DM someone you share an active company with.";
|
|
432
452
|
}
|
|
@@ -818,29 +838,47 @@ function explicitAudience(opts) {
|
|
|
818
838
|
}
|
|
819
839
|
return explicit;
|
|
820
840
|
}
|
|
841
|
+
/** An explicit agent/both audience from a person login. */
|
|
842
|
+
export class PersonAudienceError extends Error {
|
|
843
|
+
constructor() {
|
|
844
|
+
super(PERSON_AGENT_AUDIENCE_MESSAGE);
|
|
845
|
+
this.name = "PersonAudienceError";
|
|
846
|
+
}
|
|
847
|
+
}
|
|
821
848
|
/**
|
|
822
849
|
* Resolve the audience for a send. The default comes from the recipient kind
|
|
823
|
-
* (`defaultAudienceFor`
|
|
824
|
-
* `--
|
|
850
|
+
* and the caller (`defaultAudienceFor`: always `human` for a person login);
|
|
851
|
+
* `--for-human` maps to `human`; an explicit `--audience` wins over
|
|
852
|
+
* `--for-human` (with a warning) and is validated. A person login passing
|
|
853
|
+
* `--audience agent|both` throws PersonAudienceError.
|
|
825
854
|
*/
|
|
826
|
-
export function resolveSendAudience(opts, recipientKind, warn = (m) => console.error(chalk.yellow(m))) {
|
|
855
|
+
export function resolveSendAudience(opts, recipientKind, warn = (m) => console.error(chalk.yellow(m)), machineCaller = callerIsMachinePrincipal()) {
|
|
827
856
|
const explicit = explicitAudience(opts);
|
|
857
|
+
if (!callerMayUseAudience(explicit, machineCaller))
|
|
858
|
+
throw new PersonAudienceError();
|
|
828
859
|
if (explicit !== undefined) {
|
|
829
860
|
if (opts.forHuman) {
|
|
830
861
|
warn("Both --audience " + explicit + " and --for-human were passed; using --audience " + explicit + ".");
|
|
831
862
|
}
|
|
832
863
|
return explicit;
|
|
833
864
|
}
|
|
834
|
-
return opts.forHuman ? "human" : defaultAudienceFor(recipientKind);
|
|
865
|
+
return opts.forHuman ? "human" : defaultAudienceFor(recipientKind, machineCaller);
|
|
835
866
|
}
|
|
836
867
|
async function runDmSend(recipient, message, rawOpts) {
|
|
837
868
|
// Validate an explicit --audience before any network call; the default is
|
|
838
869
|
// applied once the recipient kind is known.
|
|
839
870
|
explicitAudience(rawOpts);
|
|
871
|
+
// People never send agent-tagged messages: refuse before any network call.
|
|
872
|
+
const machineCaller = callerIsMachinePrincipal();
|
|
873
|
+
if (!callerMayUseAudience(rawOpts.audience, machineCaller)) {
|
|
874
|
+
console.error(chalk.red(PERSON_AGENT_AUDIENCE_MESSAGE));
|
|
875
|
+
process.exit(1);
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
840
878
|
const opts = { ...rawOpts };
|
|
841
879
|
const withAudience = (kind) => ({
|
|
842
880
|
...rawOpts,
|
|
843
|
-
audience: resolveSendAudience(rawOpts, kind),
|
|
881
|
+
audience: resolveSendAudience(rawOpts, kind, undefined, machineCaller),
|
|
844
882
|
});
|
|
845
883
|
// --channel <name> is an explicit channel target; the positional carries the
|
|
846
884
|
// message (recipient slot), e.g. `hq dm --channel vyg-dev "hello"`.
|
|
@@ -874,16 +912,46 @@ async function runDmSend(recipient, message, rawOpts) {
|
|
|
874
912
|
// A bare name or #hash addresses a named DM channel.
|
|
875
913
|
const channelName = parseChannelName(recipient);
|
|
876
914
|
if (channelName !== null) {
|
|
877
|
-
// A bare word
|
|
878
|
-
// exact person and say so; '#name' or --channel always means the
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
915
|
+
// A bare word may name a person or bot as well as a channel. Prefer the
|
|
916
|
+
// exact person/bot and say so; '#name' or --channel always means the
|
|
917
|
+
// channel, '@name' always means a person/bot. Several same-named
|
|
918
|
+
// contacts are ambiguous: refuse with the list rather than fall through
|
|
919
|
+
// to a channel of that name (feedback eba06f59).
|
|
920
|
+
const typed = recipient.trim();
|
|
921
|
+
const explicitPerson = typed.startsWith("@");
|
|
922
|
+
const bare = channelName.replace(/^@/, "");
|
|
923
|
+
let person = null;
|
|
924
|
+
if (!typed.startsWith("#")) {
|
|
925
|
+
const contacts = await fetchContacts(await ensureCognitoToken());
|
|
926
|
+
if (contacts === null) {
|
|
927
|
+
// Without contacts we cannot tell a person/bot from a channel, so a
|
|
928
|
+
// bare name must not guess the channel either.
|
|
929
|
+
console.error(chalk.red(`Could not load your contacts to resolve '${typed}'; nothing was sent. ` +
|
|
930
|
+
`Use their email or prs_/agt_ uid for a person or bot, or '#${bare}' or --channel ${bare} to post to the channel.`));
|
|
931
|
+
process.exit(1);
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
else {
|
|
935
|
+
const matches = matchContactsByName(contacts, bare);
|
|
936
|
+
if (matches.length > 1) {
|
|
937
|
+
console.error(chalk.red(`'${bare}' matches ${matches.length} people or bots; nothing was sent. Use an email or uid:\n${formatContactMatches(matches)}\n` +
|
|
938
|
+
`Use '#${bare}' or --channel ${bare} for a channel.`));
|
|
939
|
+
process.exit(1);
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
person = matches[0] ?? null;
|
|
943
|
+
}
|
|
944
|
+
if (!person && explicitPerson) {
|
|
945
|
+
console.error(chalk.red(`No person or bot named '${bare}' in your contacts; nothing was sent. Use '#${bare}' for a channel.`));
|
|
946
|
+
process.exit(1);
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
882
950
|
if (!person?.personUid) {
|
|
883
951
|
await runChannelSend(channelName, message, withAudience("channel"));
|
|
884
952
|
return;
|
|
885
953
|
}
|
|
886
|
-
console.log(chalk.dim(`Resolved '${
|
|
954
|
+
console.log(chalk.dim(`Resolved '${bare}' to person ${person.displayName || person.email || person.personUid} (${person.personUid}). Use '#${bare}' or --channel ${bare} to post to a channel instead.`));
|
|
887
955
|
recipient = person.personUid;
|
|
888
956
|
}
|
|
889
957
|
Object.assign(opts, withAudience(recipientKindOf(recipient)));
|
|
@@ -1492,23 +1560,46 @@ async function runAgentInbox(token, agentUid, opts) {
|
|
|
1492
1560
|
console.error(chalk.red(`GET ${route} → ${res.status}`));
|
|
1493
1561
|
process.exit(1);
|
|
1494
1562
|
}
|
|
1495
|
-
|
|
1496
|
-
|
|
1563
|
+
// Same guard as `hq agent inbox` (hq-cli #2155): a 200 alone does not
|
|
1564
|
+
// certify the read. A decode failure stays undefined for the classifier;
|
|
1565
|
+
// parser errors and response bytes are never echoed.
|
|
1566
|
+
const classified = classifyAgentInboxBody(await res.json().catch(() => undefined));
|
|
1567
|
+
if (classified.status === "invalid") {
|
|
1568
|
+
console.error(chalk.red(`GET ${route} returned an invalid response; the inbox could not be read, and nothing was acknowledged.`));
|
|
1569
|
+
process.exit(1);
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
const partial = classified.status === "partial";
|
|
1573
|
+
if (partial) {
|
|
1574
|
+
console.error(chalk.yellow("The server inbox contained invalid messages; showing the valid ones only, which may be incomplete."));
|
|
1575
|
+
}
|
|
1576
|
+
let items = classified.items.map(redactAgentInboxItem);
|
|
1497
1577
|
if (opts.limit && Number(opts.limit) > 0)
|
|
1498
1578
|
items = items.slice(0, Number(opts.limit));
|
|
1499
1579
|
if (opts.json) {
|
|
1500
1580
|
console.log(JSON.stringify(items, null, 2));
|
|
1501
1581
|
}
|
|
1502
1582
|
else if (items.length === 0) {
|
|
1503
|
-
console.log(chalk.dim(
|
|
1583
|
+
console.log(chalk.dim(partial
|
|
1584
|
+
? "No pending inbox items can be confirmed; the server inbox could not be read completely."
|
|
1585
|
+
: "No pending inbox items."));
|
|
1504
1586
|
}
|
|
1505
1587
|
else {
|
|
1506
1588
|
console.log(chalk.green(`${items.length} pending inbox item${items.length === 1 ? "" : "s"}:`));
|
|
1507
1589
|
for (const item of items)
|
|
1508
1590
|
console.log(`\n${formatAgentInboxItem(item)}`);
|
|
1509
1591
|
}
|
|
1510
|
-
if (opts.markRead) {
|
|
1592
|
+
if (opts.markRead && partial) {
|
|
1593
|
+
// An incomplete read cannot show what --mark-read would hide, so ack nothing.
|
|
1594
|
+
const n = items.length;
|
|
1595
|
+
const ids = items.map((item) => item.id).join(", ");
|
|
1596
|
+
console.error(chalk.yellow(`--mark-read skipped: the inbox read was incomplete, so ${n} ${n === 1 ? "item was" : "items were"} not acked` +
|
|
1597
|
+
`${opts.limit ? " (count reflects --limit)" : ""}` +
|
|
1598
|
+
`${n > 0 ? `: ${ids}. Ack individually with \`hq agent inbox done <id>\`.` : "."}`));
|
|
1599
|
+
}
|
|
1600
|
+
if (opts.markRead && !partial) {
|
|
1511
1601
|
let acked = 0;
|
|
1602
|
+
let failed = 0;
|
|
1512
1603
|
for (const item of items) {
|
|
1513
1604
|
const id = typeof item.messageId === "string" ? item.messageId : typeof item.id === "string" ? item.id : null;
|
|
1514
1605
|
if (!id)
|
|
@@ -1516,9 +1607,13 @@ async function runAgentInbox(token, agentUid, opts) {
|
|
|
1516
1607
|
const ack = await vaultApiFetch({ token, path: `${route}/${encodeURIComponent(id)}/ack`, method: "POST" });
|
|
1517
1608
|
if (ack.ok)
|
|
1518
1609
|
acked += 1;
|
|
1610
|
+
else
|
|
1611
|
+
failed += 1;
|
|
1519
1612
|
}
|
|
1520
1613
|
if (!opts.json && acked > 0)
|
|
1521
1614
|
console.log(chalk.dim(`\nAcked ${acked}.`));
|
|
1615
|
+
if (failed > 0)
|
|
1616
|
+
console.error(chalk.yellow(`${failed} item${failed === 1 ? "" : "s"} could not be acked.`));
|
|
1522
1617
|
}
|
|
1523
1618
|
}
|
|
1524
1619
|
async function runDmInbox(opts) {
|
|
@@ -1729,7 +1824,7 @@ async function runDmChannel(target, opts) {
|
|
|
1729
1824
|
export function registerDmCommand(program) {
|
|
1730
1825
|
const dm = program
|
|
1731
1826
|
.command("dm")
|
|
1732
|
-
.description("Send and read direct messages, and manage connection requests. Messages
|
|
1827
|
+
.description("Send and read direct messages, and manage connection requests. Messages sent from a person login are always tagged for people. When an agent or bot sends, messages to an agent, a channel or a group are tagged for agents and folded away from people by default. Pass --for-human or --audience human to tag a message for people; --audience agent|both is for agents and bots only.");
|
|
1733
1828
|
dm
|
|
1734
1829
|
.command("send [recipient] [message]", { isDefault: true, hidden: true })
|
|
1735
1830
|
.description('Send a direct message. RECIPIENT can be a person (email, personUid, or agentUid), a GROUP DM (comma-separated: "a@x.com,b@y.com"), or one of your DM CHANNELS by name — bare (hq dm vyg-dev "hi"), hash form (hq dm "#vyg-dev" "hi"), or via --channel (hq dm --channel vyg-dev "hi"). A person receives a DM as an HQ Sync notification; an agent receives it in its durable box inbox. If you aren\'t connected yet, it sends a connection request that holds your message. See your channels with `hq channels`.\n\nCHANNEL MENTIONS: HQ mentions are structured — the server never reads `@name` out of your text, so a channel message has to carry the participants alongside the body. When you write `@name` in a channel message, this command looks the name up in the channel roster (case-insensitive on display name and slug/handle; quote multi-word names as @"Jacob Posel") and sends it as a real mention, which notifies people and wakes agents. If a name is unknown or matches more than one member, the send FAILS with the candidates instead of posting something that mentions nobody. A bare "@" or an email address in your prose is never treated as a mention. Pass --no-mentions to post the text literally.\n\nCHANNEL THREADS: `hq dm <channel> --thread <eventId> "…"` replies under an existing root (the request carries rootEventId). `--thread last` replies to your most recent top-level message in that channel. Copy a root eventId from `hq dm channel <name>`, which prints each root\'s id and indents replies underneath.')
|
|
@@ -1742,7 +1837,7 @@ export function registerDmCommand(program) {
|
|
|
1742
1837
|
.option("--details-file <path>", "Read the details from a file (1:1 DMs; channel sends also supported)")
|
|
1743
1838
|
.option("--at <iso>", "Schedule delivery at an ISO8601 time (store-and-forward; delivered within ~60s of the time; 1:1 DMs only)")
|
|
1744
1839
|
.option("--in <duration>", "Schedule delivery after a relative delay: 30s, 10m, 2h, 1d (1:1 DMs only)")
|
|
1745
|
-
.option("--audience <human|agent|both>", "Tag who this message is for: human (a person must read it), agent (progress/status, hidden from humans by default), or both. Default: human for a person, agent for an agent, a channel or a group. Wins over --for-human.")
|
|
1840
|
+
.option("--audience <human|agent|both>", "Tag who this message is for: human (a person must read it), agent (progress/status, hidden from humans by default), or both. agent and both are for agents and bots only; a person login always sends human. Default for agents and bots: human for a person, agent for an agent, a channel or a group. Wins over --for-human.")
|
|
1746
1841
|
.option("--for-human", "Tag the message for people (alias for --audience human).")
|
|
1747
1842
|
.option("--attach <path>", "Upload a local file and attach it to the message (repeatable, up to 5 files, 25 MB each). 1:1 DMs and named channels; the file lands in the conversation's vault scope and the recipient can open it. Not available for group DMs or scheduled sends.", (value, previous = []) => [...previous, value])
|
|
1748
1843
|
.action(async (recipient, message, opts) => {
|
|
@@ -9,6 +9,18 @@ import { Command } from "commander";
|
|
|
9
9
|
*/
|
|
10
10
|
export declare const TRIAGE_KINDS: readonly ["bug", "feature", "question"];
|
|
11
11
|
export type TriageKind = (typeof TRIAGE_KINDS)[number];
|
|
12
|
+
/**
|
|
13
|
+
* Internal feedback row statuses `hq feedback list --status` accepts.
|
|
14
|
+
* `stalled` is open work that started and then stopped: a row is in_progress
|
|
15
|
+
* only while it has a linked open PR or a live lane, and moves to stalled after
|
|
16
|
+
* 48h with no activity. It is listed apart from `new` (not started).
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Statuses shown in `list --status` help. Help text only: the CLI does not
|
|
20
|
+
* validate against this list, because hq-pro owns the set and returns a 400
|
|
21
|
+
* for an unknown value.
|
|
22
|
+
*/
|
|
23
|
+
export declare const FEEDBACK_LIST_STATUSES: readonly ["new", "triaging", "accepted", "wont_fix", "in_progress", "resolved", "waiting_on_customer", "reopened", "stalled", "needs_info"];
|
|
12
24
|
export declare const MAX_TRIAGE_SUMMARY = 2000;
|
|
13
25
|
export declare const DEFAULT_LIST_LIMIT = 50;
|
|
14
26
|
export interface FeedbackListRow {
|
|
@@ -24,6 +36,14 @@ export type FeedbackListSelector = {
|
|
|
24
36
|
} | {
|
|
25
37
|
kind: "assignee";
|
|
26
38
|
assignee: string;
|
|
39
|
+
}
|
|
40
|
+
/** Every visible row, untriaged or not. Used for `--status` with no other selector. */
|
|
41
|
+
| {
|
|
42
|
+
kind: "any";
|
|
43
|
+
}
|
|
44
|
+
/** Every row not in a terminal status, untriaged or not (`--open`). */
|
|
45
|
+
| {
|
|
46
|
+
kind: "open";
|
|
27
47
|
};
|
|
28
48
|
/** Email claim of the caller's staff token, used by `list --mine`. */
|
|
29
49
|
export declare function emailFromToken(token: string): string | null;
|
|
@@ -36,14 +56,36 @@ export declare function formatFeedbackAge(iso: string | undefined, now?: number)
|
|
|
36
56
|
* back as `?cursor=`.
|
|
37
57
|
*/
|
|
38
58
|
export declare function cursorFrom(data: unknown): string | null;
|
|
59
|
+
/**
|
|
60
|
+
* Lifecycle states hq-pro treats as closed (`TERMINAL_STATUSES` in
|
|
61
|
+
* feedback-status.ts). `--open` lists every row outside this set; a row with
|
|
62
|
+
* no status counts as open.
|
|
63
|
+
*/
|
|
64
|
+
export declare const TERMINAL_FEEDBACK_STATUSES: readonly string[];
|
|
65
|
+
/** Why a list stopped before the server ran out of rows, if it did. */
|
|
66
|
+
export type FeedbackListTruncation = "limit" | "page-cap" | "server";
|
|
67
|
+
export interface FeedbackListResult {
|
|
68
|
+
items: FeedbackListRow[];
|
|
69
|
+
/** Set when matching rows may exist beyond `items`. */
|
|
70
|
+
truncated?: FeedbackListTruncation;
|
|
71
|
+
}
|
|
39
72
|
/** Page through `GET /v1/feedback` until `limit` rows (or every row with `limit: null`). */
|
|
40
73
|
export declare function listFeedback(opts: {
|
|
41
74
|
token: string;
|
|
42
75
|
selector: FeedbackListSelector;
|
|
43
76
|
status?: string;
|
|
44
77
|
limit: number | null;
|
|
45
|
-
}): Promise<
|
|
78
|
+
}): Promise<FeedbackListResult>;
|
|
79
|
+
/** One-line stderr notice for a list that stopped before the end. */
|
|
80
|
+
export declare function feedbackTruncationNotice(reason: FeedbackListTruncation, shown: number): string;
|
|
46
81
|
export declare function formatFeedbackTable(rows: FeedbackListRow[], now?: number): string;
|
|
82
|
+
/** Status cell for the list table. A stalled row is flagged so it does not read as not started. */
|
|
83
|
+
export declare function formatFeedbackStatus(status: unknown): string;
|
|
84
|
+
/**
|
|
85
|
+
* Normalize `list --status`; accepts `in-progress` as an alias of `in_progress`.
|
|
86
|
+
* Any non-empty value goes to the server, which validates it.
|
|
87
|
+
*/
|
|
88
|
+
export declare function parseListStatus(raw: string): string;
|
|
47
89
|
export declare function parseListLimit(raw: string): number;
|
|
48
90
|
export declare function buildTriagePatch(opts: {
|
|
49
91
|
id: string;
|
|
@@ -11,6 +11,29 @@ import { assertFeedbackId, normalizeFeedbackAssignee, readFeedbackError, resolve
|
|
|
11
11
|
* send them.
|
|
12
12
|
*/
|
|
13
13
|
export const TRIAGE_KINDS = ["bug", "feature", "question"];
|
|
14
|
+
/**
|
|
15
|
+
* Internal feedback row statuses `hq feedback list --status` accepts.
|
|
16
|
+
* `stalled` is open work that started and then stopped: a row is in_progress
|
|
17
|
+
* only while it has a linked open PR or a live lane, and moves to stalled after
|
|
18
|
+
* 48h with no activity. It is listed apart from `new` (not started).
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Statuses shown in `list --status` help. Help text only: the CLI does not
|
|
22
|
+
* validate against this list, because hq-pro owns the set and returns a 400
|
|
23
|
+
* for an unknown value.
|
|
24
|
+
*/
|
|
25
|
+
export const FEEDBACK_LIST_STATUSES = [
|
|
26
|
+
"new",
|
|
27
|
+
"triaging",
|
|
28
|
+
"accepted",
|
|
29
|
+
"wont_fix",
|
|
30
|
+
"in_progress",
|
|
31
|
+
"resolved",
|
|
32
|
+
"waiting_on_customer",
|
|
33
|
+
"reopened",
|
|
34
|
+
"stalled",
|
|
35
|
+
"needs_info",
|
|
36
|
+
];
|
|
14
37
|
export const MAX_TRIAGE_SUMMARY = 2000;
|
|
15
38
|
export const DEFAULT_LIST_LIMIT = 50;
|
|
16
39
|
/** Safety stop for --all so a server that never ends its cursor can't loop forever. */
|
|
@@ -64,49 +87,114 @@ export function cursorFrom(data) {
|
|
|
64
87
|
}
|
|
65
88
|
return null;
|
|
66
89
|
}
|
|
90
|
+
function isTruncated(data) {
|
|
91
|
+
return (!!data &&
|
|
92
|
+
typeof data === "object" &&
|
|
93
|
+
!Array.isArray(data) &&
|
|
94
|
+
data.truncated === true);
|
|
95
|
+
}
|
|
67
96
|
/** Server page cap (FEEDBACK_LIST_PAGE_MAX). The plain list path rejects a cursor without a limit. */
|
|
68
97
|
const LIST_PAGE_SIZE = 200;
|
|
98
|
+
/**
|
|
99
|
+
* Lifecycle states hq-pro treats as closed (`TERMINAL_STATUSES` in
|
|
100
|
+
* feedback-status.ts). `--open` lists every row outside this set; a row with
|
|
101
|
+
* no status counts as open.
|
|
102
|
+
*/
|
|
103
|
+
export const TERMINAL_FEEDBACK_STATUSES = ["resolved", "wont_fix", "accepted"];
|
|
104
|
+
function isOpenRow(row) {
|
|
105
|
+
const status = row.status;
|
|
106
|
+
return typeof status !== "string" || !TERMINAL_FEEDBACK_STATUSES.includes(status);
|
|
107
|
+
}
|
|
69
108
|
/** Page through `GET /v1/feedback` until `limit` rows (or every row with `limit: null`). */
|
|
70
109
|
export async function listFeedback(opts) {
|
|
71
110
|
const base = opts.selector.kind === "untriaged"
|
|
72
111
|
? { triage: "untriaged" }
|
|
73
|
-
:
|
|
112
|
+
: opts.selector.kind === "assignee"
|
|
113
|
+
? { assignee: opts.selector.assignee }
|
|
114
|
+
: {};
|
|
74
115
|
if (opts.status)
|
|
75
116
|
base.status = opts.status;
|
|
117
|
+
const keep = opts.selector.kind === "open" ? isOpenRow : () => true;
|
|
76
118
|
const rows = [];
|
|
77
119
|
let cursor = null;
|
|
120
|
+
let serverTruncated = false;
|
|
78
121
|
for (let page = 0; page < MAX_LIST_PAGES; page++) {
|
|
79
|
-
|
|
122
|
+
// The untriaged queue applies `limit` after filtering, so ask for exactly
|
|
123
|
+
// what is still needed. The plain list path applies `limit` to the
|
|
124
|
+
// DynamoDB read BEFORE its filters, so a small page returns few or no
|
|
125
|
+
// matches per request and burns the page budget: always ask for a full
|
|
126
|
+
// page there and trim to the limit here.
|
|
127
|
+
const pageSize = opts.limit === null || opts.selector.kind !== "untriaged"
|
|
128
|
+
? LIST_PAGE_SIZE
|
|
129
|
+
: Math.min(LIST_PAGE_SIZE, opts.limit - rows.length);
|
|
80
130
|
const query = { ...base, limit: String(pageSize) };
|
|
81
131
|
if (cursor)
|
|
82
132
|
query.cursor = cursor;
|
|
83
133
|
const res = await vaultApiFetch({ token: opts.token, path: "/v1/feedback", query });
|
|
84
134
|
if (!res.ok) {
|
|
85
|
-
throw new Error(
|
|
135
|
+
throw new Error("Failed to list feedback: " + (await readFeedbackError(res)));
|
|
86
136
|
}
|
|
87
137
|
const data = (await res.json());
|
|
88
|
-
rows.push(...rowsFrom(data));
|
|
89
|
-
if (
|
|
90
|
-
|
|
138
|
+
rows.push(...rowsFrom(data).filter(keep));
|
|
139
|
+
if (isTruncated(data))
|
|
140
|
+
serverTruncated = true;
|
|
91
141
|
cursor = cursorFrom(data);
|
|
92
|
-
if (
|
|
93
|
-
|
|
142
|
+
if (opts.limit !== null && rows.length >= opts.limit) {
|
|
143
|
+
// More rows exist when this page overflowed the limit or the server
|
|
144
|
+
// has another page.
|
|
145
|
+
const more = rows.length > opts.limit || cursor !== null || serverTruncated;
|
|
146
|
+
return {
|
|
147
|
+
items: rows.slice(0, opts.limit),
|
|
148
|
+
...(more ? { truncated: serverTruncated ? "server" : "limit" } : {}),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (!cursor) {
|
|
152
|
+
return { items: rows, ...(serverTruncated ? { truncated: "server" } : {}) };
|
|
153
|
+
}
|
|
94
154
|
}
|
|
95
|
-
return rows;
|
|
155
|
+
return { items: rows, truncated: "page-cap" };
|
|
156
|
+
}
|
|
157
|
+
/** One-line stderr notice for a list that stopped before the end. */
|
|
158
|
+
export function feedbackTruncationNotice(reason, shown) {
|
|
159
|
+
if (reason === "limit") {
|
|
160
|
+
return "Showing the first " + String(shown) + " items; more may match. Use --all or a larger --limit.";
|
|
161
|
+
}
|
|
162
|
+
if (reason === "page-cap") {
|
|
163
|
+
return ("Stopped after " + String(MAX_LIST_PAGES) + " pages (" + String(shown) +
|
|
164
|
+
" items); more may match. Narrow the query, for example with --status or --assignee instead of --open.");
|
|
165
|
+
}
|
|
166
|
+
return "The server cut this read short; some matching items may be missing.";
|
|
96
167
|
}
|
|
97
168
|
export function formatFeedbackTable(rows, now = Date.now()) {
|
|
98
|
-
const header = ["ID", "AGE", "PLAN", "TITLE"];
|
|
169
|
+
const header = ["ID", "AGE", "STATUS", "PLAN", "TITLE"];
|
|
99
170
|
const body = rows.map((row) => [
|
|
100
171
|
row.id,
|
|
101
172
|
formatFeedbackAge(row.submittedAt ?? row.createdAt, now),
|
|
173
|
+
formatFeedbackStatus(row.status),
|
|
102
174
|
typeof row.plan === "string" && row.plan.length > 0 ? row.plan : "-",
|
|
103
175
|
typeof row.title === "string" ? row.title : "",
|
|
104
176
|
]);
|
|
105
|
-
const widths = [0, 1, 2].map((col) => Math.max(header[col].length, ...body.map((cells) => cells[col].length)));
|
|
177
|
+
const widths = [0, 1, 2, 3].map((col) => Math.max(header[col].length, ...body.map((cells) => cells[col].length)));
|
|
106
178
|
return [header, ...body]
|
|
107
|
-
.map((cells) => cells.map((cell, col) => (col <
|
|
179
|
+
.map((cells) => cells.map((cell, col) => (col < 4 ? cell.padEnd(widths[col]) : cell)).join(" ").trimEnd())
|
|
108
180
|
.join("\n");
|
|
109
181
|
}
|
|
182
|
+
/** Status cell for the list table. A stalled row is flagged so it does not read as not started. */
|
|
183
|
+
export function formatFeedbackStatus(status) {
|
|
184
|
+
if (typeof status !== "string" || status.length === 0)
|
|
185
|
+
return "-";
|
|
186
|
+
return status === "stalled" ? "stalled!" : status;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Normalize `list --status`; accepts `in-progress` as an alias of `in_progress`.
|
|
190
|
+
* Any non-empty value goes to the server, which validates it.
|
|
191
|
+
*/
|
|
192
|
+
export function parseListStatus(raw) {
|
|
193
|
+
const value = raw.trim().toLowerCase().replace(/-/g, "_");
|
|
194
|
+
if (!value)
|
|
195
|
+
throw new Error("--status must not be empty.");
|
|
196
|
+
return value;
|
|
197
|
+
}
|
|
110
198
|
export function parseListLimit(raw) {
|
|
111
199
|
const n = Number(raw);
|
|
112
200
|
if (!Number.isInteger(n) || n < 1) {
|
|
@@ -167,31 +255,40 @@ function registerListSubcommand(feedbackCmd) {
|
|
|
167
255
|
.command("list")
|
|
168
256
|
.description("List feedback: untriaged (default), yours, or one person's")
|
|
169
257
|
.option("--untriaged", "Items nobody has triaged yet (the default)")
|
|
258
|
+
.option("--open", "Every item not resolved, won't-fix or accepted, triaged or not")
|
|
170
259
|
.option("--mine", "Items assigned to you")
|
|
171
260
|
.option("--assignee <person>", "Items assigned to this email address or prs_ uid")
|
|
172
|
-
.option("--status <status>",
|
|
261
|
+
.option("--status <status>", `Only items with this status (${FEEDBACK_LIST_STATUSES.join(", ")}). Without --untriaged, --mine or --assignee it lists every item in that status`)
|
|
173
262
|
.option("--limit <n>", `Stop after this many items (default ${DEFAULT_LIST_LIMIT})`)
|
|
174
263
|
.option("--all", "Fetch every page")
|
|
175
|
-
.option("--json", "Print { items, count } as JSON")
|
|
264
|
+
.option("--json", "Print { items, count } as JSON, plus truncated: true when more items match")
|
|
176
265
|
.action(async (opts) => {
|
|
177
266
|
try {
|
|
178
|
-
const picked = [opts.untriaged, opts.mine, opts.assignee !== undefined].filter(Boolean);
|
|
267
|
+
const picked = [opts.untriaged, opts.open, opts.mine, opts.assignee !== undefined].filter(Boolean);
|
|
179
268
|
if (picked.length > 1) {
|
|
180
|
-
throw new Error("Use only one of --untriaged, --mine and --assignee.");
|
|
269
|
+
throw new Error("Use only one of --untriaged, --open, --mine and --assignee.");
|
|
270
|
+
}
|
|
271
|
+
if (opts.open && opts.status !== undefined) {
|
|
272
|
+
throw new Error("--open already selects every open status; use --status on its own instead.");
|
|
181
273
|
}
|
|
182
274
|
if (opts.all && opts.limit !== undefined) {
|
|
183
275
|
throw new Error("Use --limit or --all, not both.");
|
|
184
276
|
}
|
|
185
|
-
|
|
186
|
-
throw new Error("--status must not be empty.");
|
|
187
|
-
}
|
|
277
|
+
const status = opts.status === undefined ? undefined : parseListStatus(opts.status);
|
|
188
278
|
const limit = opts.all
|
|
189
279
|
? null
|
|
190
280
|
: opts.limit === undefined
|
|
191
281
|
? DEFAULT_LIST_LIMIT
|
|
192
282
|
: parseListLimit(opts.limit);
|
|
193
283
|
const token = await resolveStaffToken();
|
|
194
|
-
|
|
284
|
+
// No selector means the untriaged queue, except with --status: a
|
|
285
|
+
// status filter alone lists every row in that status. The queue only
|
|
286
|
+
// holds untriaged rows, so `--status accepted` there was nearly empty.
|
|
287
|
+
let selector = opts.open
|
|
288
|
+
? { kind: "open" }
|
|
289
|
+
: opts.status !== undefined && !opts.untriaged
|
|
290
|
+
? { kind: "any" }
|
|
291
|
+
: { kind: "untriaged" };
|
|
195
292
|
if (opts.mine) {
|
|
196
293
|
const email = emailFromToken(token);
|
|
197
294
|
if (!email) {
|
|
@@ -206,14 +303,17 @@ function registerListSubcommand(feedbackCmd) {
|
|
|
206
303
|
};
|
|
207
304
|
}
|
|
208
305
|
assertVaultApiRequestSupported({ token, path: "/v1/feedback" });
|
|
209
|
-
const items = await listFeedback({
|
|
306
|
+
const { items, truncated } = await listFeedback({
|
|
210
307
|
token,
|
|
211
308
|
selector,
|
|
212
|
-
status
|
|
309
|
+
status,
|
|
213
310
|
limit,
|
|
214
311
|
});
|
|
312
|
+
if (truncated) {
|
|
313
|
+
console.error(chalk.yellow("Note:"), feedbackTruncationNotice(truncated, items.length));
|
|
314
|
+
}
|
|
215
315
|
if (opts.json) {
|
|
216
|
-
console.log(JSON.stringify({ items, count: items.length }));
|
|
316
|
+
console.log(JSON.stringify({ items, count: items.length, ...(truncated ? { truncated: true } : {}) }));
|
|
217
317
|
}
|
|
218
318
|
else if (items.length === 0) {
|
|
219
319
|
console.log("No feedback items match.");
|
|
@@ -73,7 +73,7 @@ export declare function attachDebugLogs(requestBody: Record<string, unknown>, di
|
|
|
73
73
|
}): void;
|
|
74
74
|
export declare function submitFeedback(opts: FeedbackSubmitOptions): Promise<FeedbackResult>;
|
|
75
75
|
/** Customer-visible states accepted by `hq feedback update --state`. */
|
|
76
|
-
export declare const PUBLIC_TICKET_STATES: readonly ["triaged", "in-progress", "shipped", "verified", "resolved"];
|
|
76
|
+
export declare const PUBLIC_TICKET_STATES: readonly ["triaged", "in-progress", "stalled", "shipped", "verified", "resolved"];
|
|
77
77
|
export type PublicTicketState = (typeof PUBLIC_TICKET_STATES)[number];
|
|
78
78
|
export declare function assertFeedbackId(id: string): void;
|
|
79
79
|
/**
|
|
@@ -88,6 +88,45 @@ export declare function fetchFeedbackLink(opts: {
|
|
|
88
88
|
id: string;
|
|
89
89
|
token: string;
|
|
90
90
|
}): Promise<string>;
|
|
91
|
+
/**
|
|
92
|
+
* Work links for an in-progress update. A feedback row is in_progress only
|
|
93
|
+
* while it has a linked open PR or a live lane, so `--state in-progress`
|
|
94
|
+
* requires one of them. Field names match the hq-pro PATCH body.
|
|
95
|
+
*/
|
|
96
|
+
export interface FeedbackWorkLink {
|
|
97
|
+
prUrl?: string;
|
|
98
|
+
laneId?: string;
|
|
99
|
+
}
|
|
100
|
+
/** Accept `in_progress` as an alias of the public `in-progress` state. */
|
|
101
|
+
export declare function normalizePublicState(raw: string): string;
|
|
102
|
+
/** Validate --pr/--lane for a state; in-progress needs at least one, other states take none. */
|
|
103
|
+
export declare function buildWorkLink(state: string, opts: {
|
|
104
|
+
pr?: string;
|
|
105
|
+
lane?: string;
|
|
106
|
+
}): FeedbackWorkLink | null;
|
|
107
|
+
/** Read a feedback row with the staff GET. Returns null when it cannot be read. */
|
|
108
|
+
export declare function fetchFeedbackRow(opts: {
|
|
109
|
+
id: string;
|
|
110
|
+
token: string;
|
|
111
|
+
}): Promise<Record<string, unknown> | null>;
|
|
112
|
+
/** Delays between read-back attempts: 3 reads over about 1s. */
|
|
113
|
+
export declare const LINK_READBACK_DELAYS_MS: readonly number[];
|
|
114
|
+
/** Overridable in tests so retries do not wait on real timers. */
|
|
115
|
+
export declare const linkReadbackTimer: {
|
|
116
|
+
sleep: (ms: number) => Promise<void>;
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* Store the PR or lane link before any state change, then confirm it was
|
|
120
|
+
* saved by reading the row back. The PATCH response does not echo prUrl or
|
|
121
|
+
* laneId, and an older server ignores unknown fields, so only the GET shows
|
|
122
|
+
* the link is stored. A link-only PATCH can itself change the row (hq-pro
|
|
123
|
+
* resumes a stalled row), so errors report the row's actual state.
|
|
124
|
+
*/
|
|
125
|
+
export declare function storeFeedbackWorkLink(opts: {
|
|
126
|
+
id: string;
|
|
127
|
+
token: string;
|
|
128
|
+
link: FeedbackWorkLink;
|
|
129
|
+
}): Promise<void>;
|
|
91
130
|
/** Post a customer-visible update; the server also mirrors it into #hq-feedback. */
|
|
92
131
|
export declare function postFeedbackUpdate(opts: {
|
|
93
132
|
id: string;
|